Release v0.4.8: develop -> main - #42
Merged
Merged
Conversation
Roughness was global per-grating; a single RoughnessSpec.sigma_nm was applied to every interface. This adds per-layer roughness, where each layer carries the roughness of its top interface, falling back to the grating-level sigma when unset (backward compatible). - LayerSpec gains roughness_sigma_nm; SingleLayerStack/MultilayerStack/ CustomStack expose per-layer/per-material roughness arguments. - BaseStack.interface_roughness_sigmas_bottom_up() / has_per_layer_roughness() build a per-interface sigma array used by both roughness kinds. - gratings._roughness_interface_offset() perturbs each interface by its own sigma; the underresolved warning uses the max per-interface sigma. - Per-layer debye-waller sigmas combine in quadrature into an effective scalar; a bare RoughnessSpec is unchanged. - Fixed-angle roughness example uses the per-layer system (uniform sigma across layers); tutorial + CHANGELOG updated; new test_stacks.py plus per-layer/quadrature tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEdvY6MWmJDeEm22pwbTRA
Add per-interface roughness fields to the grating form and a run-time roughness-kind dropdown, wiring the existing per-layer roughness core through the Flask app. - stacks: add BaseStack.substrate_roughness_sigma_nm so the substrate boundary (interface 0) can be set explicitly; wire it into interface_roughness_sigmas_bottom_up() / has_per_layer_roughness() and the build_* helpers. - persistence: serialize/deserialize substrate, layer/material, and top cap roughness sigmas; single-layer gratings now carry a coating stack so the sigmas round-trip. Bump SCHEMA_VERSION to 2; old specs without roughness keys still load (default None). - grating form: roughness sigma input for substrate, single layer, material A, material B, and top cap; parsed via _optional_float. - run form: None / Debye-Waller / Random interface dropdown. Attached to the grating in _execute_run_job via _attach_roughness(), covering every workflow and resume (run resolution clones preserve .roughness). - tests: substrate-override stacks tests; persistence round-trip, backward-compat load, and _attach_roughness web tests; schema v2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEdvY6MWmJDeEm22pwbTRA
Add a geometry-only example that builds one laminar grating with an extreme random-interface roughness (sigma = 100 nm) on the internal Cr/C interface and exports a close-up image of the distorted profile. The internal interface is used (rather than the top surface) so the full excursion is visible instead of clipping at the top of the z-grid. Also restrict the fixed_angle_roughness close-up export to the geometry-distorting random-interface runs; Debye-Waller close-ups (which do not change the geometry) are skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEdvY6MWmJDeEm22pwbTRA
Set substrate_roughness_sigma_nm to the same per-layer sigma so the substrate/coating boundary (interface 0) is roughened too, instead of falling back to the flat default. Previously only each layer's top interface carried roughness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEdvY6MWmJDeEm22pwbTRA
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEdvY6MWmJDeEm22pwbTRA
Replace the per-sample white-noise interface with a periodic Gaussian random field with a Gaussian autocorrelation, characterised by a lateral correlation length. White noise put power at every spatial frequency up to the grid Nyquist, giving unphysical per-pixel spikes; real metrology shows a clear characteristic lateral scale. - RoughnessSpec.correlation_length_nm: autocorrelation length in nm. None -> period/10 (visible per-period structure at any pitch); 0.0 -> legacy white noise; >0 -> explicit length. - gratings: synthesise the offset from a Gaussian power spectrum via rFFT on the periodic domain, then normalise to sigma (per-layer sigma semantics unchanged). Deterministic per seed+interface. - Tests: correlated field is far smoother than white noise, default correlation length tracks the period, 0.0 reproduces white noise, plus validation. Docs + changelog updated, incl. the ~10 um metrology reference and the note that long correlations suit debye-waller. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEdvY6MWmJDeEm22pwbTRA
…amples Library: RoughnessSpec gains num_supercells so random-interface roughness can span multiple grating periods as one continuous correlated field; the RCWA solver's fundamental period scales accordingly and diffraction orders are reported in physical (possibly fractional) units end to end. Examples: split the fixed-angle roughness example into a roughness-kind comparison and a roughness-correlation-length comparison, each with their own results directory, whole-grating (not close-up) geometry plots, and a bash runner. The correlation-length script now also sweeps num_supercells=5 and overlays it on the same comparison plot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parallel BatchSimulationRunner path serializes case results through JSON records; deserialization cast the orders array back with dtype=int, truncating fractional supercell orders (e.g. -1.2, -1.4, -1.6, -1.8 all collapsed onto -1). The all-orders CSV then contained several near-zero rows labeled order -1 per energy alongside the real one, which made the comparison plot zigzag to zero between energy points for supercell runs. Deserialize orders as float, apply the integer-preserving CSV order formatting to the theta-search sweep writer too, and align the empty placeholder arrays. Adds a round-trip regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
roughness_kind_comparison.py now saves, for each run, a diffraction-order-vs- intensity plot at the energy closest to the middle of the sweep, showing the full order spectrum (specular plus any diffuse satellite orders from supercell roughness). Across both roughness example scripts, all figure outputs (grating previews, order-spectrum plots, comparison plots) now live in plots_roughness_* folders instead of being mixed into results_roughness_*, which now hold only CSV data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both roughness_kind_comparison.py and roughness_correlation.py accept
--family {baseline,debye-waller,random-interface} to run only that family
instead of the full sweep. The final comparison plot now scans the results
directory for whatever CSVs already exist on disk (rather than only this
invocation's runs), so running families one at a time still builds up a
complete comparison once all of them have been run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rent config Library: RoughnessSpec gains num_realizations (default 8 for random-interface, forced to 1 for debye-waller) to average incoherent disorder realizations per simulated point, and seed now accepts None to draw real entropy once at construction time. run_simulation transparently averages efficiencies across realizations while keeping orders/angles (which don't depend on the specific roughness draw) from a single solve. Examples: both roughness scripts stop hardcoding a fixed seed (None draws real, non-reproducible randomness) and expose ROUGHNESS_NUM_REALIZATIONS. The roughness-kind comparison plot (inline and standalone) now only includes CSVs matching the script's current run configuration instead of globbing every matching file in the results directory, so stale sigma/supercell sweeps left over from earlier configs no longer leak into the plot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- _refresh_interactive_figure only calls plt.show() once per figure instead of on every redraw, so the macosx backend stops re-raising the window and the live-plot window can be minimized/backgrounded during a run. - Force the spawn multiprocessing start method on macOS (fork is unsafe once BLAS/Numba/Cocoa native state is initialized) and size auto worker counts from each solve's peak RSS high-water mark, not just steady-state RSS. - Re-run the roughness-kind comparison example with supercells=10 and let its comparison plot always try supercell counts 1/3/5/10 regardless of the script's current sweep config. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add per-layer roughness support and fix macOS live-plot/worker-memory bugs
Failed trials use a large sentinel loss, which stretched the log-scale y-axis and hid the real convergence trend. Cap the top at 10x the first trial's loss whenever an outlier exceeds that ceiling. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
The 1D solver lived in one flat module, which left no place for a second solver to reuse its numerics. Split it into a package: - solvers/common.py: shared types, res0/res1, the Fourier machinery, the layer field operators (including the Li/FFF rules for TM), the interface-response cascade, and the efficiency extraction - solvers/rcwa.py: the modal layer block and res2, unchanged numerically The tail of _solve_te_stack and _solve_tm_stack was identical apart from the layer operator and the semi-infinite admittances, so it is now one shared solve_stack_from_layer_blocks() parameterized by a per-layer block builder. A second solver only has to supply that builder. grax.rcwa_1d stays as a re-export shim so existing imports keep working. Because a shim cannot be monkeypatched through, the cascade-parity test now patches grax.solvers.common, where the name is actually resolved; patching the alias silently left the optimized cascade in place and made that test compare a run against itself. Verified bit-identical all-order efficiencies and angles across eight laminar/blazed/multilayer/sinusoidal cases in both polarizations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second, independent electromagnetic engine. Instead of eigen-decomposing a z-invariant layer operator, it integrates the coupled first-order system d/dz [F; G] = [[0, A], [B, 0]] [F; G] with fourth-order Runge-Kutta. A and B come from the shared layer_field_operators(), so the differential method inherits the Li/fast-Fourier-factorization inverse rule for TM rather than re-deriving it, and integrates exactly the same truncated system RCWA solves. Agreement with RCWA is therefore a property of the construction, not a coincidence: measured max|dR| is ~1e-10 at the default step and drops to ~1e-14 as step_phase shrinks. Step and sub-block sizes are set in units of optical phase, not nanometers, so one setting behaves consistently across photon energies, grazing angles, and truncation orders. The per-layer bound on |q| comes from the operator's row norm, which is tight for the low-contrast permittivities of X-ray optics. Stability: a transfer matrix is only ever formed across a sub-block whose optical thickness is capped by block_phase, and sub-blocks are combined with the existing interface-response cascade. That cascade is an R-matrix propagation, so deep gratings and strongly evanescent orders never build up a growing exponential. Two permittivity sampling modes: - "textures" (default) integrates the same z-sliced permittivity RCWA uses, which is what makes the two solvers directly comparable - "continuous" re-expands the permittivity from the true profile at each sub-block, dropping the staircase approximation entirely. Its result is independent of z_resolution and already closer to the z-converged limit than RCWA at z_resolution = 0.1 nm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the existing backend= / default_polarization= pattern: - run_simulation and RCWASimulation take solver= and neviere_options= - BatchSimulationRunner takes default_solver / default_neviere_options, and cases can override both per case - SingleSimulationResult and CaseExecutionResult record which solver produced them, round-tripped through checkpoints so a resumed sweep keeps that provenance Every default is "rcwa", so existing callers, saved checkpoints, examples and the web UI behave exactly as before. The multilayer theta-search workflow keeps its own payload and is unchanged. The one-line-per-solve INFO log now names the solver, which is why the logging test's expected string moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unit tests cover solver parity against RCWA on laminar, blazed, sinusoidal and multilayer gratings in both polarizations, and check that the residual behaves like fourth-order truncation error: halving step_phase cuts it by more than eight. Two anchors do not lean on RCWA at all: - a zero-depth grating must return the analytic Fresnel reflectivity of a plain vacuum/substrate interface in order zero and nothing anywhere else, which pins the absolute normalization in TE and TM - a lossless dielectric grating must put its propagating reflected and transmitted orders at exactly one The smoke tests use the published RETICOLO exemple1_1D values already referenced elsewhere in the suite: a deep, high-contrast lamellar grating at normal incidence, which is a far harder convergence case than the shallow X-ray gratings this project normally runs. The differential method reaches both the TE and the TM reference value. That geometry also exposed a limit worth recording: the modal solver forms q/sinh(q d) across a whole layer and overflows above roughly seven wavelengths of depth, while the differential method stays finite and energy-conserving at 167 wavelengths. The deep test asserts the modal solver still fails there, so the claim gets revisited rather than silently kept if that limit is ever lifted. Also covers continuous z-sampling: it is now bit-identical across z_resolution_nm, because the integration depth comes from the grating geometry instead of the sliced profile's row count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- docs/developer/neviere-theory.md: the coupled system, why the TM operators use the inverse rule, why the propagation is an R-matrix cascade rather than a transfer-matrix product, and how the phase-based step settings work. Written as a delta against the RCWA theory page rather than repeating it. - docs/tutorials/choosing-a-solver.md: when to reach for each solver, the batch-runner and per-case forms, and what continuous z-sampling buys. - docs/validation/solver-comparison.md: how to reproduce the side-by-side runs and where the two solvers would be expected to diverge. - examples/simulation/neviere_solver: one grating through both solvers, showing that tightening step_phase drives the residual towards zero and that continuous sampling matches a much finer staircase than the one it was run against. Registered with the smoke suite. - References for Nevière/Vincent/Petit 1974, Nevière 1994, Nevière & Popov 2003, and Popov & Nevière 2000 on the TM equations. The docs build copies the solver-comparison figures only when present, so a docs build does not require running the sweeps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Roughness is applied around the solve rather than inside it, but the supercell path also changes the period, the order grid and the realization averaging, so it is worth asserting the differential method threads through all of it unchanged rather than assuming it does. Covers solver-level Debye-Waller, grating-level Debye-Waller, and random-interface supercell roughness with realization averaging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comparison sweeps write checkpoints_neviere/ and checkpoints_rerun/ alongside the tracked checkpoints/ baseline. Those are scratch for a rerun, not artifacts worth committing, and a partial one from an interrupted sweep is actively misleading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every validation case run through both solvers on the same revision, with
per-order deviation tables and side-by-side figures.
points max |dE| max/peak
laminar 400 l/mm 601 5.6e-11 3.1e-10
blazed 600 l/mm 195 4.2e-12 1.7e-11
laminar 150 l/mm 496 3.9e-12 1.2e-11
blazed 2400 l/mm multilayer 173 8.7e-11 1.3e-10
All p polarization at production resolution; the multilayer case is
strided to 173 of its 1727 energy-angle pairs, spanning 500 to 6000 eV.
The differences are floating-point noise: in the difference panels they
form a structureless band rather than tracking the spectral features, and
they shrink as the fourth power of step_phase.
The brief anticipated a few percent. It is far better than that because
the two solvers are independent propagators for one shared model, not
independent models, which is worth being explicit about: the comparison
is a sharp test of the propagation and a weak test of everything upstream
of it. The analytic Fresnel and energy-conservation tests, the published
RETICOLO values, and the external-code pages cover the shared parts.
Each *_rerun.csv is a current-code RCWA baseline written alongside the
checked-in artifacts rather than over them, because those artifacts no
longer reproduce (2-4%, and 22% in the 12-28 eV tail of the 150 l/mm
case). That drift predates this work and applies equally to both current
solvers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fills in the per-order deviation table for all four cases, the drift between the checked-in artifacts and current code, and a section on why the two solvers agree as closely as they do and what that comparison therefore does and does not test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each case folder now has a dedicated entry point sitting next to its RCWA sweep, and its comparison script overlays the differential-method curve alongside the external codes. The entry points delegate to the existing sweep script with --solver neviere rather than restating the setup. Duplicating a grating definition across two files is how the two runs silently stop being comparable: one gets a depth or resolution change and the other does not, and the resulting plot looks like a solver disagreement. Extra arguments are forwarded, so --stride and --quick still work. The comparison scripts read the RCWA curve from a same-revision *_rerun.csv when one exists, falling back to the checked-in artifact. That matters here: the committed CSVs predate several solver changes, so pairing a fresh differential-method run against them would render that 2-4% drift as though it were a difference between the two methods. Each script now prints which file every curve came from. The two solvers agree to ~1e-11, so the second curve would completely hide the first. The differential-method curve is dashed over the solid RCWA one, otherwise the plot reads as if a curve failed to load. A missing solver run is skipped with a note rather than raising, so the comparison scripts still work before the differential-method sweep has been run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every validation folder now has the same three files: grating_definition.py grating, materials, sweep grid, truncation run_rcwa.py runs it with the modal solver run_neviere.py runs it with the differential method Both runners import the definition and nothing else shared, so they cannot drift apart in geometry, energy grid or Fourier truncation. That is the failure this structure is built against: two runners that each carry their own copy of the setup eventually disagree on a depth or a resolution, and the comparison plot then shows a solver disagreement that is really a mismatched sweep. Replaces the previous single sweep script with a --solver flag, and the thin *_neviere.py entry points over it. Each comparison script is now standalone as well, with no imports from outside its folder. It plots whichever solvers have been run, reading *_rcwa.csv and *_neviere.csv and falling back to the older checked-in artifact only when no fresh RCWA run exists. The differential-method curve is dashed over the solid RCWA one: they agree to ~1e-11, so drawn solid the second curve hides the first and the plot reads as though a curve failed to load. The solver cross-check moved to tools/solver_comparison/ and out of the docs. It is a developer tool for quantifying drift between the two solvers, not part of the validation workflow. Verified per case, against the pre-restructure runs: the restructured RCWA runner reproduces them exactly (max |dE| = 0), and the differential method still tracks it to ~1e-12. Fixes a bug found by that check: the multilayer stack identifies top_material by matching it against material_a or material_b, so loading the same material twice returned two DataFrames and raised. The optical-constants loaders are now cached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The batch runner builds a separate payload for the theta-search workflow.
That payload carried backend but not solver, so
BatchSimulationRunner(default_solver="neviere").run_cases(
multilayer_theta_search_cases(...))
computed with RCWA. No error, no warning. The result was not mislabelled
-- it reported the solver that actually ran -- but the caller's request
was silently dropped.
run_multilayer_theta_search and run_multilayer_theta_search_sweep now
take solver and neviere_options next to backend, threaded through all
three stages (rough scan, precise scan, final solve) so the selected
angle and the final efficiency come from the same solver.
The root cause was two hand-built copies of the runner-settings mapping:
one in BatchSimulationRunner, one in the theta-search sweep. A setting
added to one was silently missed by the other. Both now go through
batch.runner_settings(), which owns the key list and rejects unknown
names, so the next setting cannot drift the same way.
Regression test asserts a neviere theta-search case reports "neviere";
it fails with assert ['rcwa'] == ['neviere'] before this change. A second
test pins that both solvers select the same angle and efficiency.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
run_parameter_study now takes solver and neviere_options. It also gains backend, which it previously hardcoded to "numba" internally -- it was the one public entrypoint that silently fixed its own backend, so a convergence study could not be reproduced against the backend it ran on. Defaults preserve existing behaviour exactly. Results also record solver_options alongside solver. The name alone does not pin a differential-method result: the same "neviere" label covers every step_phase and both sampling modes, so a checkpoint saying "neviere" was not reproducible. The field is None for RCWA, populated from the existing NeviereOptions.to_dict(), and round-trips through checkpoint serialization -- a restored record rebuilds an equal NeviereOptions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MeasurementFitConfig gains solver and neviere_options, validated beside the existing backend check, readable from a spec mapping, and recorded in the run metadata next to backend_requested / backend_effective. The objective reads them off the config rather than taking them as arguments, the same way it already reads diffraction_order and fourier_orders. backend stays an argument because the caller resolves it from "auto"; solver has no "auto" -- the two solvers are different methods, not interchangeable implementations of one, so picking between them automatically would be wrong. This is where the differential method pays off most: it runs 2-5x faster than RCWA at production resolution, and a fit is thousands of solves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The run form gets a solver dropdown beside Polarization, validated by _normalized_solver() in the same shape as _normalized_polarization(). It reaches all four workflows: the batch runner covers fixed-angle, monochromator and multilayer theta search, and the parameter-study branch passes it through separately. The choice is persisted in the run input and recorded in the manifest, so a saved run says which solver produced it, and the run detail page shows it. Without that a stored run was ambiguous the moment a second solver existed. This depends on the theta-search payload fix: the form offers that workflow, so before it the dropdown would have been wrong for one of the four. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Breaking change, no alias. The class drives whichever solver solver= selects, so the RCWA-specific name described only one of the two things it can do. Arguments and behaviour are unchanged; call sites need only the new spelling. Also documents where solver selection now reaches -- every workflow entrypoint, with a table in the choosing-a-solver tutorial -- and how a result reports the solver and, for the differential method, the integration settings that produced it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four renames, no behaviour change. default_ prefix dropped from the BatchSimulationRunner settings. The prefix was accurate -- each of these really is the value a case inherits when it omits its own key, and backend, the one setting with no per-case override, correctly had no prefix -- but it made default_fourier_orders: int = 25 read as the default of a default. The runner's arguments now mirror run_simulation's exactly, which is a stronger consistency story than a prefix encoding an override relationship most callers never use. Each docstring now states that relationship explicitly, since the name no longer carries it. neviere_options -> solver_options everywhere. The input named a specific solver while the result field it corresponds to was already called solver_options; one name now covers both directions, and a third solver would need no new parameter. Validation stays solver-aware, and the NeviereOptions class keeps its name. min_efficiency -> min_reflected_efficiency. It was half of a min/max pair whose other half was already max_reflected_efficiency, and the batch runner had to translate between the two spellings. The progress bar printed "RCWA batch" regardless of which solver was running; it now names the solver. Docstrings on SingleSimulationResult, BatchSimulationRunner, gratings.py and run_parameter_study no longer describe solver-agnostic machinery as RCWA-specific. Left alone deliberately: grax.rcwa_1d (documented compatibility shim), res0/res1/res2 (RETICOLO-derived, deliberately preserved), and coerce_neviere_options / NeviereOptions, which are genuinely Neviere-specific. Verified: golden RCWA output bit-identical across 8 cases, and validation/laminar/run_rcwa.py still reproduces its committed CSV exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every example that solves now takes --solver rcwa|neviere, defaulting to rcwa. Seventeen scripts: each folder's entry script plus the five polarization_comparison.py siblings. Excluded are the helpers_*.py and comparison_*.py files in fixed_angle_roughness (they only plot) and neviere_solver.py (it deliberately runs both). One flag rather than a second copy of each example. The two solvers agree to ~1e-11, so an example ending in an overlay of two identical curves would teach nothing; a flag shows the thing worth showing, which is that swapping solver is a one-argument change. Solver-dependent outputs are suffixed, so an rcwa run and a neviere run sit side by side instead of clobbering each other. Geometry artifacts (*_profile.png, multilayer_stack_schematic.png) stay unsuffixed because they do not depend on the solver -- the same split validation/ already uses. Fourteen committed artifacts were renamed to *_rcwa.*; the moves are pure renames and the tutorial snippets naming those paths follow. Also fixes a bug unrelated to solver choice: multilayer_theta_search.py and blazed_multilayer_memory_comparison.py use max_workers, and on macOS the runner spawns workers that re-import the script by path. With no __main__ guard each worker re-ran the whole example and recursively spawned more. multilayer_theta_search.py was confirmed failing with BrokenProcessPool before the fix and completes after it. The other two max_workers examples already had main() guards and needed nothing. That bug survived because the "example" smoke tests only compile the scripts and check their structure -- nothing executes them. Worth closing separately; noted rather than fixed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Duplicating each existing example per solver would have produced ten plots of two indistinguishable curves. These three cover the cases where the choice actually changes something, so each plot has a result to show. deep_grating_limits sweeps groove depth on the published RETICOLO exemple1_1D lamellar grating. The modal solver evaluates q/sinh(q d) across a whole layer and stops at 8.4 wavelengths; the differential method caps the optical thickness of any transfer matrix it forms and reaches 167. Where both work they agree to 3.3e-12, and the differential method holds its energy balance within 4e-11 of one throughout. continuous_vs_staircase sweeps z_resolution_nm on a sinusoidal profile. Continuous sampling is bit-identical at every resolution -- it never sees a staircase -- while a staircase run carries 2.7e-3 of discretization error at 2 nm, falling to 1.3e-5 at 0.05 nm. The two staircase curves track each other to 6e-13, which is what makes the solvers comparable elsewhere. solver_runtime times both across three gratings. The docstring initially claimed the gap was largest on the multilayer; the measurements did not support that, so it now reports what was actually observed: the gap is set by resolution, 1.2-1.4x reduced and 2.4-3.0x at production, because coarse runs have too few distinct layers for the eigensolve to dominate. Every timing is printed next to the max efficiency difference so the speed number can be read against the accuracy it was obtained at. Two plotting bugs found while checking the output: invert_xaxis() called on both axes of a sharex pair cancels out, which had the convergence axis labelled backwards; and the continuous-sampling error is exactly zero, which a log axis silently drops rather than draws, so it is now pinned to a labelled floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The examples can all be run with --solver now, but the tests only exercised a subset of what they call. Audited the grax API surface the examples use against what the both-solver tests actually cover, and filled the gaps: monochromator_cases 7 example files, 0 tests energy_angle_cases 2 example files, 0 tests run_parameter_study 1 example file, 0 tests run_multilayer_theta_search_sweep 1 example file, 0 tests assemble_custom_stack / LayerSpec 2 example files, 0 tests AFMGrating 2 example files, 0 tests write_all_orders_csv and friends 14 example files, 0 tests Each now has a test asserting the two solvers agree, so swapping --solver on any example lands on a tested path. run_parameter_study is worth singling out: it drives GratingSimulation rather than the batch runner, so it reaches the solver by a path nothing else takes. The theta-search sweep test failed on the first run and found a real bug. That sweep builds its own CaseExecutionResult and copied only the diagnostics across, so solver and solver_options fell back to dataclass defaults: a neviere sweep reported itself as rcwa. The computation was right -- the two sweeps differ by 5e-14, so neviere genuinely ran -- but a saved sweep recorded the wrong provenance. Now copied from the underlying result. While checking that, polarization looked like the same bug and is not: the multilayer theta-search workflow never passes polarization to run_simulation at all, so it is s-only and reporting "s" was correct. Noted in the test rather than "fixed". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library spoke two vocabularies for one concept. Internally it is TE/TM -- res0's docstring says "1D TE/TM-style entry only", the solve dispatches to _solve_te_stack or _solve_tm_stack -- while the public API took only s/p. Anyone reading the theory docs or the Neviere and Li papers had to already know the two were the same before they could call the API. s/p/TE/TM are now all accepted, case-insensitively, and canonicalize to s/p so results, CSVs and checkpoints still carry only the two canonical spellings and nothing downstream learns a second name. TE resolves before any physics happens, and the tests assert bit-identical output rather than merely close, since anything else would mean the spellings took different paths. Validation was copy-pasted with an identical message in core.py, batch.py and web/app.py, and run_parameter_study did not validate at all -- it failed several frames down inside the simulation wrapper. All four now go through one normalize_polarization, exported from grax so callers building case dicts can canonicalize the same way. The docstring records why the alias is exact: s and TE name the same state only in classical mounting. This solver is 1D classical, so the equivalence holds, but it would stop holding if conical mounting were ever added, and that constraint should sit where someone would break it. The audit also found the multilayer theta-search workflow had no polarization argument anywhere, so every search ran the default s -- awkward, given all four validation cases run p. It now threads through all three stages, the case builder, the batch payload for that workflow (which carried zero polarization keys, the same gap that hid the solver bug) and the web form. Default stays s. The test that matters asserts p selects a different angle and efficiency from s (14.5260 vs 14.5849 degrees); asserting only that p runs would have passed even if the value were stored and ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add Nevière solver and align GraxPy workflows
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>
Joint multi-measurement fitting, optimizer resume, and a runnable example
… case generator Three fixes for the multilayer theta search: - The `Requested theta half-width ... reaches near/into 0 deg` notice leaked to the terminal from the spawned batch workers, which re-import grax but never call setup_logging, so the record fell through to logging.lastResort. Attach a NullHandler to the `grax` logger at import; setup_logging now also sets propagate=False and de-duplicates its file handler. The notice itself drops from WARNING to INFO and is emitted once per distinct (center, half-width) instead of on every rough/fine re-centring attempt. - multilayer_theta_search_cases gained `solver` / `solver_options`, matching run_multilayer_theta_search. The batch runner already honoured a per-case "solver" key; the generator just could not set one. - The multilayer_theta_search example takes --polarization alongside --solver and writes each run under results/<solver>_<polarization>/ so s and p runs no longer overwrite each other. Polarization and solver were already threaded through run_multilayer_theta_search(_sweep) and the web run form on develop; this closes the remaining gaps on that branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix theta-search solver and polarization handling
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.
Release promotion
developtomain.v0.4.8