Skip to content

Add per-layer roughness support and fix macOS live-plot/worker-memory bugs - #38

Merged
simonevadi merged 15 commits into
developfrom
feature/roughness_per_layer
Jul 22, 2026
Merged

Add per-layer roughness support and fix macOS live-plot/worker-memory bugs#38
simonevadi merged 15 commits into
developfrom
feature/roughness_per_layer

Conversation

@simonevadi

Copy link
Copy Markdown
Contributor

Summary

  • Adds per-layer roughness support (grating/layer/substrate sigma), a correlated-Gaussian random-interface roughness kind, and corresponding Web UI fields.
  • Fixes a macOS bug where live_plot batch runs kept re-raising/focusing the plot window on every point, making it impossible to minimize.
  • Fixes macOS multiprocessing safety (forces spawn instead of fork) and improves auto worker-count memory calibration to use each solve's peak RSS instead of only steady-state RSS.
  • Updates the fixed-angle roughness example/comparison script and regenerates its plots/results.
  • Updates CHANGELOG.md (Unreleased section) with all of the above.

Test plan

  • pytest tests/unit -q — 319 passed
  • Manual verification that _refresh_interactive_figure only calls plt.show() once per figure across repeated refreshes

🤖 Generated with Claude Code

simonevadi and others added 15 commits July 15, 2026 09:47
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>
@simonevadi
simonevadi merged commit 7f4d304 into develop Jul 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant