Skip to content

Allow raw refractive index input (no manual conjugation required)#101

Open
jhpark94 wants to merge 5 commits intokc-ml2:mainfrom
jhpark94:fix/exp-minus-iwt
Open

Allow raw refractive index input (no manual conjugation required)#101
jhpark94 wants to merge 5 commits intokc-ml2:mainfrom
jhpark94:fix/exp-minus-iwt

Conversation

@jhpark94
Copy link
Copy Markdown

Summary

Users can now pass standard refractive indices directly — no manual conj(n) needed. Meent handles the exp(+iωt) convention internally.

Previously, users had to:

ucell = np.array([[[np.conj(n_au)]]])  # manual conj required
mee = meent.call_mee(n_bot=n_au, ...)   # raw n for n_bot

Now:

ucell = np.array([[[n_au]]])             # raw n, just like n_bot
mee = meent.call_mee(n_bot=n_au, ...)

Changes

  1. convolution_matrix.py: eps = np.conj(layer)**2 — applies conjugation internally instead of expecting pre-conjugated input
  2. transfer_method.py: Substrate boundary uses eigenvalue-consistent q = sqrt(kx² - eps) instead of 1j * conj(kz) / n², fixing incorrect results for lossy substrates

Verification

Test case Before (original) After Analytical
Air → Au, TM R = 1.056 ❌ R = 0.981936 Fresnel = 0.981936
Au(20nm)/Glass, TM R = 0.911680 R = 0.911680 TMM = 0.911680
Au(20nm)/Glass, TM (T) T = 0.050702 T = 0.050702 TMM = 0.050702
Si(200nm)/Glass, TM R = 0.602732 R = 0.602732 no regression
Au grating on Au, TM T = -0.143 ❌ T = +0.054 ✅ T must be ≥ 0

Breaking change

Users who previously passed conj(n) in ucell will now get double conjugation. They should remove the manual conj() call.

Scope

  • Only numpy backend modified in this PR
  • Torch and JAX backends need the same changes

Closes #100

🤖 Generated with Claude Code

Three changes for correct lossy material support:

1. convolution_matrix.py: eps = conj(n)^2 internally, so users pass
   raw n (standard exp(-iwt) convention, Im(n)>0 = loss).

2. transfer_method.py (transfer_1d_1): Substrate boundary uses
   eigenvalue-consistent q = sqrt(kx^2 - eps) instead of 1j*conj(kz)/n^2.
   Fixes incorrect Fresnel for lossy substrates.

3. transfer_method.py (transfer_1d_3): Detect same-material interfaces
   (V_i @ G ≈ -I) and skip boundary matching, applying propagation only.
   Fixes uniform lossy layer on same lossy substrate (e.g., Au on Au).

Verified against analytical results:
- Air -> Au, TM: R=0.981936 (Fresnel exact)
- Au(20nm)/Glass, TM: R=0.911680, T=0.050702 (thin-film exact)
- Au(20nm)/Glass, TE: R=0.911680, T=0.050702 (exact)
- Si(200nm)/Glass, TM: R=0.602732, T=0.397268 (no regression)
- Au on Au, all thicknesses: R=0.981936 (Fresnel exact)
- Au grating on Au: R+T=0.945 (physical, T>0)

Breaking change: users should pass raw n in ucell (remove conj()).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jhpark94 jhpark94 force-pushed the fix/exp-minus-iwt branch from d98feee to 9a2a994 Compare March 28, 2026 04:17
Juho Park and others added 4 commits March 27, 2026 23:24
Same three changes as numpy backend:
1. convolution_matrix: eps = conj(n)^2 internally
2. transfer_*_1: eigenvalue-consistent substrate boundary
3. transfer_*_3: same-material interface detection

JAX uses jax.lax.cond for JIT compatibility.

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

Replace the heuristic V_i@G ≈ -I check (atol=0.1) with explicit eps
comparison: compare the layer's diagonal eps (from convolution matrix)
with the eps below (substrate or previous layer). This is more robust,
interpretable, and doesn't require tuning a threshold.

Applied to all three backends (numpy, torch, JAX).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- transfer_1d_conical_3, transfer_2d_3: add same_material parameter
- _base.py solve_1d_conical, solve_2d: add eps comparison logic
- Fix torch transfer_1d_1 type_complex casting (eps_bot)
- Applied to all three backends (numpy, torch, JAX)

Verified (numpy backend):
- Oblique incidence (0-60°, TE/TM): all match Fresnel
- 1D conical: Si/Glass R+T=1, Au/Glass R+T<1
- 2D grating: Si/Glass R+T=1, Au patch on Au T>0
- Au uniform on Au (2D): R=0.981936 = Fresnel

Note: torch backend has a pre-existing issue with 1D dielectric
(R=1.659 for Si/Glass) that exists in the original meent code
and is not caused by this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs fixed:

1. sqrt branch cut: torch loses -0j imaginary sign from kx.conj(),
   causing sqrt(-real+0j) to select wrong branch (+1.5j instead of
   -1.5j). This flips the G matrix sign for propagating orders,
   producing R=1.659 instead of 0.603 for Si/Glass. Fixed by adding
   -1e-20j perturbation to force correct branch selection.

2. same_material T calculation: eigenvalue ordering from torch.linalg.eig
   differs from numpy, so T = T @ X applied propagation phases to wrong
   diffraction orders. Fixed by transforming X to physical basis:
   T = T @ W @ X @ W^{-1}.

3. TM de_ti in torch transfer_1d_4: used kz_bot instead of kz_bot.conj(),
   giving wrong/negative transmittance for lossy substrates.

All three fixes applied consistently across numpy, torch, and jax backends
for 1D, conical, and 2D solvers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jhpark94
Copy link
Copy Markdown
Author

Update: Three additional torch backend bugs fixed

Bug 1: sqrt branch cut (critical)

torch.linalg loses the -0j imaginary sign from kx.conj(), causing sqrt(-real+0j) to select the wrong branch (+1.5j instead of -1.5j). This flips the G matrix sign for propagating diffraction orders.

Impact: Si/Glass TM gave R=1.659 instead of correct R=0.603.
Fix: Add -1e-20j perturbation to q_bot computation to force correct branch selection.

Bug 2: same_material eigenvalue ordering

torch.linalg.eig returns eigenvalues in different order than numpy.linalg.eig. The same_material propagation T = T @ X applied phases to wrong diffraction orders.

Impact: Si/Si gave T=0.000 instead of correct T=0.694.
Fix: Transform X to physical basis: T = T @ W @ X @ W⁻¹.

Bug 3: TM de_ti missing .conj()

Torch transfer_1d_4 used kz_bot instead of kz_bot.conj() for TM transmittance.

Impact: Air/Au TM gave T=-0.398 instead of correct T=0.133.
Fix: kz_botkz_bot.conj().

All fixes applied to numpy/torch/jax × 1D/conical/2D. All test cases now pass with numpy-torch agreement to machine precision.

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.

Inconsistent sign convention between ucell layers and substrate/superstrate for lossy materials

1 participant