Skip to content

rcwa: a polarization word takes the pure 1D TE/TM path - #142

Merged
yonghakim merged 2 commits into
mainfrom
claude/rcwa-pure-1d-path
Nov 3, 2026
Merged

yonghakim merged 2 commits into
mainfrom
claude/rcwa-pure-1d-path

Conversation

@yonghakim

Copy link
Copy Markdown
Member

What changes

A public rcwa.run of a one-row raster grating lit by PlaneWave(polarization='s') or 'p' now solves on the engine's pure 1D TE/TM path (grating_type 0) instead of the conical one. No public field is added.

The engine's routing principle, "the declaration selects the path, and nothing else does" (_assign_grating_type), stays. What counts as a declaration widens:

  • A polarization word, 's' or 'p', is a declaration: the polarization is fixed, not a parameter anyone sweeps or differentiates.
  • A Jones pair is a value, and keeps the conical path, (1, 0) included.

The word takes the pure path only where that path answers the same light. Every test is structural, so it is decided the same way under jax.jit and vmap:

  • every layer is a one-row scalar raster on an axis-aligned cell (the routing's own one_dimensional test);
  • phi and ky are absent, or a zero that nothing traces or differentiates;
  • the formulation is not first_order, and there is no tensor.

Everywhere else the word solves bit for bit as the pair it names, silently. Unlike tetm it refuses nothing.

Outputs(scattering=True) keeps the conical path: the public matrix is 2N x 2N with channel 'sp', and the pure path's is N x N, 'te'/'tm'. That one fact the engine cannot see, so the adapter hands the pair over when a scattering matrix is requested (_adapt.plane_wave_polarization). Everything else is decided in the engine (_routed_grating_type, _polarization_word_holds).

The chosen path is now recorded:

  • CheckReport.resolved['solve_path'], planned before any solve;
  • Result.diagnostics['solve_path'], one entry per wavelength, as taken.

Values are '1d_te', '1d_tm', '1d_conical' or '2d'. Before this change nothing recorded the path. Coherent-source runs report '2d', the layout their side-resolved first-order cascade is written on.

phi, kx and ky are stored under the solver's _eager(), as the material setters already are, so a stated phi=0. stays a concrete zero inside jax.jit. Without that the jitted run of an eligible problem took the conical path (measured).

Why

Before this change a one-row raster grating always solved on the conical path. The results are identical, but the conical path is 2.8–5.8x slower and needs 5–14x more memory. No decision record ever dropped tetm from the public lifecycle. The documented workaround pointed at wavebit.compat, which was retired on 2026-10-15.

Measurements

Public rcwa.run, NumPy, a 10-core laptop, 2026-11-03. The scene is a 1000 nm cell with a 560 nm bar of permittivity 12.25 in air, 200 nm thick, at 1363 nm and normal incidence, on a 50-pixel raster with fourier_coefficients='integrated'. Each 'p' run is measured against the pair (0, 1), one process per run. Times are the best of 7/3/1 runs; memory is the peak footprint above the process baseline.

orders 'p' (pure TM) (0, 1) (conical) time ratio peak memory, pure / conical sum(R) apart
(50, 0) 0.017 s 0.045 s 2.7x 1.8 / 26 MB 5e-13
(200, 0) 0.29 s 1.22 s 4.1x 31 / 251 MB 8e-12
(800, 0) 10.8 s 57.8 s 5.3x 0.39 / 5.9 GB 4.3e-10

These agree with an earlier out-of-tree prototype measured on the same machine: 0.017 / 0.28 / 10.5 s, and 25 → 5 MB, 258 → 48 MB, 5.98 → 0.39 GB.

Verification

  • New tests/api/test_unified_rcwa_pure_1d.py, 66 tests, all passing locally.
    • The regression matrix runs each feature with the word (pure) and with the pair (conical) and compares every observable to 1e-11 of its family's scale. Measured worst over the matrix: 1.9e-13. Features: both words, theta 0/±0.3, both sides, a stated phi=0, the kx spelling, raster mu, a lossy bar, half-space merge, a spectrum, ETM, detuning, nvf, laurent+integrated, explicit second_order, magnetic half-spaces, a negative-index substrate, a Rayleigh point and evanescent incidence; fields and boundary fields on both sides.
    • JAX and PyTorch backends; single precision to 1e-4.
    • jax.grad with respect to thickness, theta and permittivity; torch autograd.
    • prepare then jit, vmap and jit(vmap(grad)), with a kernel call counter confirming that the pure kernel ran.
    • Fallback pins, word against pair bit for bit: a skewed cell (NVF), a nonzero phi, a nonzero ky, first_order and scattering=True (2N x 2N, 'sp'). A Jones pair stays conical. A traced or differentiated azimuth stays conical under jit, grad, jit(grad), vmap and torch requires_grad, bit for bit with the pair. A traced run reports the plan '1d_te' and records '1d_conical'.
    • A coherent-source run reports '2d', planned and taken.
  • Old against new on the fallbacks. The same script ran on git archive origin/main (93cf7cb) and on this branch. All 184 arrays are bit-identical. The arrays cover amplitudes, efficiencies, powers, fields, boundary fields, S blocks, jit/grad/vmap values, torch gradients and single precision.
  • Existing tests.
    • pytree, polarization, tetm and precision tests: 242 passed.
    • direct wavevector and oblique lattice tests: 314 passed.
    • 36 related files (26 that run the public lifecycle, 10 engine jit, condition and field-state files), default tier (not slow and not research, 4 workers): 2011 passed, 1 failed. The failure is test_torch_compile.py::test_canary_inductor_compiles_the_eye_write_in_order, caused by a stale Inductor precompiled header in this machine's temp directory (mtime changed). With a clean TMPDIR, all 20 torch-compile tests pass. The 3 slow tests in those files were not run locally.
    • tools/run_verification.py --profile quick: 16 passed, 1 failed. The failure is test_installed_metadata_matches_the_checkout_distribution_contract, which needs installed package metadata; this run used PYTHONPATH on src.
    • Docs: sphinx-build -W -b html docs builds clean, and tests/api/test_docs_contract.py passes (166).
    • Environment: conda env wavebit, Python 3.12, numpy 2.5.1, jax/jaxlib 0.11.0, torch 2.14.1, macOS arm64.

Found, not fixed here

  • jax.jit of a public run whose Jones pair is exactly (0, 1) aborts the Python process. It happens on main as well, with jax/jaxlib 0.11.0: XLA's algebraic simplifier reads the runtime guard's complex constant [0, 1] as an iota and rewrites its abs to a value of the wrong type. (1, 0) and the word 'p' are unaffected. The new test writes that pair as (0, 2), which normalizes to exactly (0, 1).
  • migration.py:81 still calls .name on the normalized tetm string, so migrating a v10 document that declares tetm raises AttributeError.

🤖 Generated with Claude Code

yonghakim and others added 2 commits November 4, 2026 00:08
A public rcwa.run of a one-row raster grating lit by PlaneWave(polarization='s')
or 'p' now solves on the engine's pure TE/TM path (grating_type 0) instead of the
conical one. No public field is added.

The routing principle stays -- the declaration selects the path, and nothing else
does -- and what counts as a declaration widens. The words 's' / 'p' declare the
polarization fixed. A Jones pair, (1, 0) included, is a value and stays conical.
The word takes the pure path only where that path answers the same light. Every
test is structural, so jit and vmap decide it the same way:

- every layer a one-row scalar raster on an axis-aligned cell (the routing's own
  one_dimensional test), with no tensor;
- phi and ky absent, or a zero that nothing traces or differentiates;
- the formulation is not first_order.

Everywhere else the word solves bit for bit as the pair it names, silently. Unlike
tetm it refuses nothing.

Engine:
- polarization accepts the words. The pair is stored as before, and the word in
  _polarization_word beside it.
- _assign_grating_type keeps its refusals and assigns _routed_grating_type, which
  refuses and warns nothing, so a report can read it before any solve.
- _polarization_word_holds is the eligibility test, and tetm_index reads a routed
  word.
- _solve_path(planned=) names the path.
- The JAX pytree carries the word in aux.
- phi, kx and ky are stored under _eager(), as the material setters are. Without
  that a stated phi=0. was staged into a tracer inside jax.jit, and the jitted run
  of an eligible problem took the conical path (measured). The eligibility
  comparison runs under _eager() for the same reason.

Adapter: the word goes to the engine as written, except under
Outputs(scattering=True). The public matrix is 2N x 2N with channel 'sp', and the
pure path's is N x N 'te'/'tm', so that run hands over the pair as before.

The path is recorded: CheckReport.resolved['solve_path'] (planned) and
Result.diagnostics['solve_path'] (per wavelength, as taken). The values are
'1d_te', '1d_tm', '1d_conical' or '2d'. Nothing recorded the path before.
Coherent-source runs report '2d', the layout their side-resolved first-order
cascade is written on (CoherentSolver._solve_path).

Measured through the public rcwa.run (NumPy, a 10-core laptop; docs/guide/rcwa/light.md
"Cost"): 2.7x, 4.1x and 5.3x faster at orders (50, 0), (200, 0) and (800, 0), with a
peak of 0.39 GB against 5.9 GB at the last; sum(R) 5e-13, 8e-12 and 4e-10 apart.

Checks:
- tests/api/test_unified_rcwa_pure_1d.py (new): the regression matrix holds the
  word against the pair to 1e-11 of each observable family; the worst measured gap
  is 1.9e-13. It also covers jax/torch backends, single precision, jax.grad,
  torch autograd and prepare -> jit / vmap / jit(vmap(grad)), with a kernel call
  counter. Fallback pins (skewed cell, nonzero phi and ky, first_order,
  scattering=True, traced or differentiated phi) are bit for bit against the
  pair.
- The fallback cases ran on origin/main 93cf7cb and on this commit, through the
  same script: 184 arrays, all bit-identical.
- tests/rcwa/test_pytree_round_trip.py lists the new aux key and checks that the
  word survives the round trip.

Not fixed here: jax.jit of a public run whose Jones pair is exactly (0, 1) aborts
the process. It does so on main too, under jaxlib 0.11.0: XLA's algebraic
simplifier reads the runtime guard's complex constant as an iota and rewrites its
abs. The new test writes that pair as (0, 2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its path

Public 1D runs are no longer always conical, so the statements that said they
were are rewritten, in English and in Korean where a translation exists.

- reference/keywords.md: the tetm row now points to the words. The closing
  paragraph no longer sends readers to the earlier interface, which was retired
  on 2026-10-15.
- guide/rcwa/light.md: new section "The words are a declaration, the pair is a
  value", with the conditions, the fallbacks, the round-off agreement, the cost,
  and resolved/diagnostics['solve_path'], including what a traced run reports.
- guide/rcwa/scene.md, fields.md (the pure path's exact zeros) and numerics.md
  (the path is not a setting).
- guide/rcwa/anisotropic-materials.md section 8: the routing account and the
  "which path ran" requirement, which the result now meets.
- learn/fundamentals light, overview and index, with their Korean pages.
- development/source-map (and its Korean page) and tests/README name the new
  test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yonghakim
yonghakim marked this pull request as ready for review November 3, 2026 17:24
@yonghakim
yonghakim enabled auto-merge (rebase) November 3, 2026 17:25
@yonghakim
yonghakim merged commit 1753c78 into main Nov 3, 2026
65 checks passed
@yonghakim
yonghakim deleted the claude/rcwa-pure-1d-path branch November 3, 2026 17:43
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