Skip to content

fix(photonics2d): consistent simulate/optimize (v1)#264

Open
mkeeler43 wants to merge 6 commits into
mainfrom
fix-photonics
Open

fix(photonics2d): consistent simulate/optimize (v1)#264
mkeeler43 wants to merge 6 commits into
mainfrom
fix-photonics

Conversation

@mkeeler43

@mkeeler43 mkeeler43 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

simulate and optimize disagree in Photonics2D v0: the objective optimize reports for a design
is not reproduced when you simulate the design it returned. On the published v0 dataset,
re-simulating a stored optimal_design differs from its stored objective by a median of ~130% and
up to 37×
. This PR fixes it by adding Photonics2D v1; v0 is unchanged and still importable.

Photonics background (what's specific to this problem)

Unlike the structural/thermal problems, a photonics design is a continuous density field
rho ∈ [0, 1] that is converted to a permittivity map and scored by an FDFD solve
(frequency-domain Maxwell). The device is a wavelength demultiplexer, and the objective
("total overlap") measures how well two wavelengths reach their respective output ports.

Gradient-based topology optimization here uses two operators (from the ceviche tutorial this problem
is refactored from), applied to rho before it becomes permittivity:

  • blur (operator_blur) — a convolution imposing a minimum feature size; without it the optimizer
    produces unmanufacturable single-pixel features.
  • projection (operator_proj) — a tanh smoothed-Heaviside that pushes rho toward 0/1 so the
    final design is near-binary. Its sharpness is beta; a continuation schedule ramps beta from
    soft to hard over the run so early-iteration gradients stay informative.

Both are optimization machinery — they only make sense while searching for a design, not while
scoring a finished one.

The bug (v0)

v0 ran blur + projection inside the shared epsr_parameterization, which was called by both
optimize and simulate — and simulate ran it at beta = max_beta while optimize ramped
beta. Two consequences:

  • simulate(design) silently re-blurs and re-projects the design before scoring it, so it does not
    score the design it was handed.
  • optimize returned a design that was projected once and then binarized with np.rint, which
    differs again from what its final recorded objective was computed on.

So the stored/returned design is not the one that produced the reported number, and the gap grows
with blur_radius (more projection → larger mismatch) — up to 37× on the dataset.

The fix (v1)

Blur + projection live only inside optimize; simulate scores designs as given.

  • A design is a finished physical density rho ∈ [0, 1] (blur + projection already applied).
  • simulate(design) validates rho ∈ [0, 1] (check_constraints) and converts it to permittivity by
    scaling only (design_to_epsr) — no blur, no projection — then runs FDFD.
  • optimize(start):
    • Step 0 evaluates the raw start, so history[0] == simulate(start).
    • Steps 1..N apply blur + projection with beta ramping initial_beta → max_beta in an explicit
      Adam loop, clipping rho to [0, 1] each step. (The tanh projection only maps [0,1]→[0,1], so
      keeping rho bounded is what makes it well-behaved — this is why the projection does not need to
      be a sigmoid.)
    • Returns the projected density at max_beta, so simulate(optimize(x)[0]) == history[-1].
  • epsr_parameterization is split into design_to_epsr (scale only, used by simulate) and
    filter_and_project (blur + projection, used by optimize). Both are additive in backend.py;
    no v0 function is modified.
  • Optimizer knobs (penalty_weight, step_size, eta, initial_beta, max_beta) are now validated
    Config fields. The trailing np.rint binarization and the beta-scheduling adam_optimize
    callback are gone — the explicit loop updates beta directly.

Fixes # (simulate vs optimization-history inconsistency issue — please link)

Reverting to v0

  • Default: from engibench.problems.photonics2d import Photonics2D → v1 (…_v1 dataset).
  • Legacy: from engibench.problems.photonics2d.v0 import Photonics2D → v0 (…_v0, unchanged).

Dataset

Regenerated on Euler, reusing v0's exact conditions and train/val/test split (1594 / 199 / 200) so the
two are directly comparable; all 1993 optimizations succeeded. Published to
IDEALLab/photonics_2d_120_120_v1. Warm start is fixed across all rows
(random_design(noise=0.001), seed 0), documented on the problem page.

Validation

Re-simulate each stored optimal_design and compare to its stored objective:

Dataset median mean max
v0 130% 430% 37×
v1 ~3e-10 ~1e-8 ~2e-7

Type of change

  • Bug fix, shipped as a new problem version (v1); v0 behavior preserved
  • This change requires a documentation update (done — docs/problems/photonics2d.md)

The default Photonics2D now returns different numbers than before (it is v1), so the default import
is behavior-changing; handled by the version bump, with v0 kept importable for reproducibility.

Checklist

  • ruff check and ruff format
  • mypy
  • Commented code (docstrings explain the photonics-specific blur/projection/continuation)
  • Documentation updated
  • Tests added (tests/test_photonics2d.py) plus reference values
  • New and existing unit tests pass, including the dataset-backed generic tests

mkeeler43 and others added 6 commits June 12, 2026 16:31
v0 applied the density projection (blur + tanh smoothed-Heaviside) inside
both simulate and optimize at differing strengths, so an optimized design
did not reproduce its reported objective when re-simulated (the gap grew
with blur_radius, up to ~80%). v1 confines filtering/projection to optimize
and evaluates designs as-is everywhere else, so simulate(optimize(x)[0]) ==
history[-1] and history[0] == simulate(x).

- backend.py: add design_to_epsr (scale-only) and filter_and_project
  (blur+project), purely additive; v0 functions unchanged and frozen.
- v1.py: subclass of v0 with an explicit Adam loop + beta continuation,
  rho clipped to [0,1] each step; optimizer knobs promoted to validated
  Config fields; bounds checked via check_constraints.
- __init__: export v1 as the default Photonics2D.
- dataset_slurm_v1.py: SLURM dataset generator that collects results and
  pushes to IDEALLab/photonics_2d_120_120_v1.
- tests/test_photonics2d.py: round-trip, step-0, bounds, dtype, backend units.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The intermediate job/result files defaulted into a temp dir under $SCRATCH,
which HPC clusters purge on a rolling basis; a run left un-assembled for
weeks loses its results. --work-dir lets the intermediate files be written
to persistent storage so a delayed run is recoverable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a Versions section and v1 dataset fields to the problem docs, including
  how to revert to v0 (`from engibench.problems.photonics2d.v0 import Photonics2D`).
- Remove the one-off SLURM dataset-generation script from the branch; the v1
  dataset is already published and the script remains in git history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lity

- Merge origin/main (adds the reference-value regression test).
- Add tests/reference/simulate/problems.photonics2d.v1.Photonics2D.json and drop
  the orphaned v0 reference (v0 is no longer the exported/tested class).
- v1.py: drop the autograd `ArrayBox` from type annotations (it evaluated against a
  Mock during the docs/doctest builds and crashed them; it is also autograd-internal
  jargon), and rewrite the module docstring in plain language so blur/projection/beta
  continuation are understandable without prior v0 knowledge.
- docs: document the fixed warm-start used to generate the v1 dataset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mkeeler43 mkeeler43 requested a review from g-braeunlich July 15, 2026 09:15
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