You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Expose a Layer constructor that accepts a full 3×3 complex dielectric tensor ε (including off-diagonal and non-symmetric entries, e.g. gyrotropic media with ε₁₂ = -ε₂₁), and verify it flows correctly through the existing transfer-matrix math, which already accepts a full ε internally. Today there is no user-facing way to set off-diagonal ε except indirectly by rotating a diagonal tensor with Euler angles, so genuinely gyrotropic (non-symmetric) materials cannot be modeled.
Context
The core builds the 6×6 material matrix M from ε and μ in layer_matrices (src/matrix_constructors.jl:97-126). There are already three construct_M methods (src/matrix_constructors.jl:128-164):
a specialized full 3×3 ε path, construct_M(ε::SMatrix{3,3,ComplexF64}, μ::Diagonal{…}) (:153-164), which copies all nine ε entries ε[1,1]…ε[3,3] into the upper-left block.
The downstream Δ-matrix construction (construct_a:176-197, construct_Δ:215-243) reads individual M[i,j] entries and already handles a fully populated ε block. The γ eigenvector code (calculate_γ:331-400) likewise indexes ε[1,2], ε[2,1], ε[1,3], ε[3,1], etc. — it does not assume symmetry. So the math layer already supports a full, non-symmetric ε.
The gap is purely the public API. Layer (src/layer.jl:48-97) stores either a single dispersion function (isotropic) or a Tuple of (nx, ny, nz, φ, θ, ψ) (anisotropic, diagonal-in-crystal-frame + Euler rotation). layer_matrices reconstructs ε at each λ via get_refractive_indices → dielectric_tensor(εx, εy, εz) (a Diagonal, src/layer.jl:16) and only ever produces off-diagonal entries through rotate_dielectric_tensor (src/matrix_constructors.jl:108-113). Rotation of a diagonal tensor always yields a symmetric ε, so non-symmetric/gyrotropic tensors are unreachable.
Why
Gyrotropic and magneto-optic media (Faraday rotation, magnetoplasmonics, chiral/optically-active crystals) require an antisymmetric off-diagonal part of ε (e.g. ε = [[εxx, ig, 0],[-ig, εyy, 0],[0,0,εzz]]). These cannot be produced by Euler-rotating a diagonal tensor. The internal machinery already accepts such a tensor; only a constructor and a dispersion-evaluation path are missing.
Proposed approach
Add a Layer constructor that accepts a tensor-valued dispersion: Layer(εfunc, thickness) where εfunc(λ) -> SMatrix{3,3,ComplexF64} (the full ε tensor at wavelength λ). Decide and document whether the input is ε or n; ε is the natural choice for gyrotropic media. Store it so it is distinguishable from the existing isotropic-function and anisotropic-tuple cases (e.g. a small wrapper type or a tagged tuple), and update isanisotropic (src/layer.jl:105) and the predicates get_euler_angles/isrotated (src/layer.jl:133-149) to handle the new variant.
In layer_matrices (src/matrix_constructors.jl:97-126), branch so that a full-tensor layer obtains ε directly from εfunc(λ) (bypassing get_refractive_indices/dielectric_tensor/Euler rotation) and passes it to the existing construct_M(ε::SMatrix{3,3,ComplexF64}, μ) path (:153-164). The rest of the pipeline (construct_a, construct_Δ, calculate_q, calculate_γ, dynamical_matrix) is unchanged.
Confirm cross-polarization terms (Rps, Rsp, Tps, Tsp in TransferResult, src/general_TMM.jl:99-108) are populated for a gyrotropic layer — a Faraday rotator must produce non-zero Rsp/Tsp.
Add tests: (a) a diagonal ε supplied through the new tensor API reproduces the existing diagonal-Layer result bit-for-bit (regression guard); (b) a symmetric off-diagonal ε supplied directly matches the same tensor produced via Euler rotation; (c) a known gyrotropic case (e.g. Faraday rotation through a magneto-optic slab at normal incidence) produces the expected polarization rotation / non-zero cross terms.
Document the new constructor in the Layer docstring (src/layer.jl:1-47) and export-facing docs, and add it to the API table in CLAUDE.md.
Acceptance criteria
A Layer can be constructed from a λ -> SMatrix{3,3,ComplexF64} full dielectric tensor, including non-symmetric entries.
transfer, sweep_angle, sweep_thickness, and efield accept such layers without code changes to those functions.
Diagonal ε through the new API exactly matches the existing diagonal-Layer path (regression test passes).
A gyrotropic slab produces non-zero cross-polarization (Rsp/Tsp) consistent with an analytic Faraday-rotation check.
Existing test suite remains green; new tests cover the three cases above.
Energy conservation for strongly anisotropic/gyrotropic lossless media may surface the known Poynting-normalization limitation in R+T ≈ 0.998 for strongly rotated lossless crystals #70 (R+T ≈ 0.998 for strongly rotated lossless crystals); verify but do not be alarmed if a gyrotropic lossless case shows the same small deficit — that is a pre-existing normalization issue, not introduced here.
The p/s sorting in evaluate_birefringence (src/general_TMM.jl:271) sorts modes via Poynting-vector components and was validated against symmetric / Euler-rotated anisotropic cases; confirm it still classifies modes sensibly for an antisymmetric off-diagonal ε, and add a regression test that would catch a misclassification.
Summary
Expose a
Layerconstructor that accepts a full 3×3 complex dielectric tensorε(including off-diagonal and non-symmetric entries, e.g. gyrotropic media withε₁₂ = -ε₂₁), and verify it flows correctly through the existing transfer-matrix math, which already accepts a full ε internally. Today there is no user-facing way to set off-diagonal ε except indirectly by rotating a diagonal tensor with Euler angles, so genuinely gyrotropic (non-symmetric) materials cannot be modeled.Context
The core builds the 6×6 material matrix
Mfrom ε and μ inlayer_matrices(src/matrix_constructors.jl:97-126). There are already threeconstruct_Mmethods (src/matrix_constructors.jl:128-164):construct_M(ε, μ=…, ρ1=…, ρ2=…)— generic fallback (:133-135):139-150)construct_M(ε::SMatrix{3,3,ComplexF64}, μ::Diagonal{…})(:153-164), which copies all nine ε entriesε[1,1]…ε[3,3]into the upper-left block.The downstream Δ-matrix construction (
construct_a:176-197,construct_Δ:215-243) reads individualM[i,j]entries and already handles a fully populated ε block. The γ eigenvector code (calculate_γ:331-400) likewise indexesε[1,2],ε[2,1],ε[1,3],ε[3,1], etc. — it does not assume symmetry. So the math layer already supports a full, non-symmetric ε.The gap is purely the public API.
Layer(src/layer.jl:48-97) stores either a single dispersion function (isotropic) or aTupleof(nx, ny, nz, φ, θ, ψ)(anisotropic, diagonal-in-crystal-frame + Euler rotation).layer_matricesreconstructs ε at each λ viaget_refractive_indices→dielectric_tensor(εx, εy, εz)(aDiagonal,src/layer.jl:16) and only ever produces off-diagonal entries throughrotate_dielectric_tensor(src/matrix_constructors.jl:108-113). Rotation of a diagonal tensor always yields a symmetric ε, so non-symmetric/gyrotropic tensors are unreachable.Why
Gyrotropic and magneto-optic media (Faraday rotation, magnetoplasmonics, chiral/optically-active crystals) require an antisymmetric off-diagonal part of ε (e.g.
ε = [[εxx, ig, 0],[-ig, εyy, 0],[0,0,εzz]]). These cannot be produced by Euler-rotating a diagonal tensor. The internal machinery already accepts such a tensor; only a constructor and a dispersion-evaluation path are missing.Proposed approach
Layerconstructor that accepts a tensor-valued dispersion:Layer(εfunc, thickness)whereεfunc(λ) -> SMatrix{3,3,ComplexF64}(the full ε tensor at wavelength λ). Decide and document whether the input is ε or n; ε is the natural choice for gyrotropic media. Store it so it is distinguishable from the existing isotropic-function and anisotropic-tuple cases (e.g. a small wrapper type or a tagged tuple), and updateisanisotropic(src/layer.jl:105) and the predicatesget_euler_angles/isrotated(src/layer.jl:133-149) to handle the new variant.layer_matrices(src/matrix_constructors.jl:97-126), branch so that a full-tensor layer obtains ε directly fromεfunc(λ)(bypassingget_refractive_indices/dielectric_tensor/Euler rotation) and passes it to the existingconstruct_M(ε::SMatrix{3,3,ComplexF64}, μ)path (:153-164). The rest of the pipeline (construct_a,construct_Δ,calculate_q,calculate_γ,dynamical_matrix) is unchanged.Rps,Rsp,Tps,TspinTransferResult,src/general_TMM.jl:99-108) are populated for a gyrotropic layer — a Faraday rotator must produce non-zeroRsp/Tsp.Layerdocstring (src/layer.jl:1-47) and export-facing docs, and add it to the API table inCLAUDE.md.Acceptance criteria
Layercan be constructed from aλ -> SMatrix{3,3,ComplexF64}full dielectric tensor, including non-symmetric entries.transfer,sweep_angle,sweep_thickness, andefieldaccept such layers without code changes to those functions.Rsp/Tsp) consistent with an analytic Faraday-rotation check.Caveats / risks
M) is out of scope here and is tracked in Activate magnetoelectric coupling (ρ1/ρ2) for bianisotropic media #93 ("Activate magnetoelectric coupling (ρ1/ρ2) for bianisotropic media").evaluate_birefringence(src/general_TMM.jl:271) sorts modes via Poynting-vector components and was validated against symmetric / Euler-rotated anisotropic cases; confirm it still classifies modes sensibly for an antisymmetric off-diagonal ε, and add a regression test that would catch a misclassification.References
calculate_γ)