Summary
Add a GradedLayer type that represents a single physical layer whose optical properties vary continuously along the propagation direction z. It is expanded — via the piecewise-uniform approximation — into nslices ordinary homogeneous Layers evaluated at slice midpoints, then fed through the existing transfer-matrix pipeline unchanged. This is the foundational piece that later unlocks rugate filters, chirped/graded-index coatings, and (via a separate chiral-preset issue) helicoidal media.
Context
A Layer today is strictly homogeneous along z: it stores one dispersion (isotropic scalar λ -> n, or a 3-tuple (nx, ny, nz) of dispersions, optionally with fixed ZYZ Euler angles). The struct is Layer{F,T} with fields dispersion::F and thickness::T (src/layer.jl:48-56). Anisotropic layers store the dispersion as a 6-tuple (nx, ny, nz, φ, θ, ψ) built by the constructor at src/layer.jl:93-97; isanisotropic keys off dispersion isa Tuple (src/layer.jl:105); get_refractive_indices (src/layer.jl:115-123) and get_euler_angles (src/layer.jl:133-139) read those fields.
The core propagators iterate over the layers vector and call layer_matrices(layer, λ, ξ, μ) per layer (src/matrix_constructors.jl:97-126), accumulating the transfer matrix in the loop at src/general_TMM.jl:358-375 (_propagate_core) and src/general_TMM.jl:414-426 (_propagate_full). Every public entry point (transfer, sweep_angle, sweep_thickness, efield/hfield) ultimately consumes a flat Vector{<:Layer}. There is no way to express a profile that varies within a layer except by hand-building hundreds of Layers.
Why
The standard way to model a nonhomogeneous (graded) medium in the transfer-matrix method is the piecewise-uniform approximation: discretize the continuous z-profile into many thin homogeneous slices, each handled by the ordinary layer propagator (Mackay & Lakhtakia 2020, Ch. on nonhomogeneous mediums). Adding a GradedLayer that performs this expansion gives users a first-class, ergonomic way to specify graded structures while reusing the existing, validated layer machinery — the new code is the profile/slicing API, not new core physics.
Proposed approach
- Add a
GradedLayer type in src/layer.jl storing a profile, a total thickness, and an nslices::Int. The profile is a callable of the local normalized coordinate or absolute z (pick one convention and document it; normalized z ∈ [0, thickness] measured from the layer's entry face is recommended) returning one of:
z -> n (isotropic scalar index),
z -> (nx, ny, nz) (biaxial, no rotation),
z -> (nx, ny, nz, φ, θ, ψ) (biaxial with ZYZ Euler angles — the form that the chiral-preset issue will build on).
- Implement an expansion function, e.g.
expand(g::GradedLayer)::Vector{Layer}, that produces nslices homogeneous Layers, each of thickness thickness / nslices, sampling the profile at the midpoint of each slice. Map each profile return form onto the matching existing Layer constructor (Layer(n, t), Layer(nx, ny, nz, t), or Layer(nx, ny, nz, t; euler=(φ,θ,ψ)) at src/layer.jl:93-97). The per-slice dispersions must be closures of λ (frozen at the slice midpoint position), since layer_matrices evaluates dispersion at runtime per wavelength (src/matrix_constructors.jl:99-104).
- Thread the expansion through the public API before propagation. The cleanest single point is to normalize the incoming
layers collection into a flat Vector{Layer} (expanding any GradedLayer in place) at the top of transfer (src/general_TMM.jl:569), sweep_angle (src/general_TMM.jl:733), sweep_thickness (src/general_TMM.jl:764), and efield/hfield via _field (src/general_TMM.jl:789). A small helper flatten_layers(layers) that expands graded entries and passes ordinary Layers through is the least invasive approach — the propagators (_propagate_core, _propagate_full) then need no changes.
- Decide and document the interaction with
sweep_thickness's t_index: it currently rebuilds a layer by Layer(dispersion_func, ts[i]) (src/general_TMM.jl:773). If a GradedLayer occupies a swept index, either reject it with a clear error for now or define how its total thickness is varied. Rejecting with a clear ArgumentError is acceptable for this single-step issue.
- Add a docstring with at least one runnable example (graded-index profile) and export
GradedLayer from src/TransferMatrix.jl (the export block currently lists Layer but not GradedLayer).
- Add unit tests in
test/ (a new test/graded.jl wired into test/runtests.jl): a constant profile must reproduce the equivalent single homogeneous Layer to machine precision; expansion must produce nslices layers summing to the original thickness; midpoint sampling must hit the expected values.
Acceptance criteria
GradedLayer(profile, thickness; nslices) constructs and accepts all three profile return forms (scalar, 3-tuple, 6-tuple).
- A
GradedLayer passed inside the layers vector to transfer, sweep_angle, and efield runs end-to-end through the existing pipeline and returns finite results.
- A constant profile gives R/T identical (to ~
1e-12) to the equivalent single homogeneous Layer, confirming the expansion is correct.
- Convergence behavior in
nslices is documented (the dedicated convergence-control issue refines this further).
GradedLayer is exported and has a docstring with a runnable example.
Caveats / risks
- Fine slicing multiplies the layer count, so each evaluation does many more 4×4 matrix products; very large
nslices on thick stacks can be slow and, for strongly attenuating slices, may approach numerical-overflow regimes in the unmodified transfer-matrix product. Keep this issue to the expansion mechanics; performance/stability hardening is out of scope.
- The first and last layers of a stack are treated as semi-infinite (thickness ignored for propagation). A
GradedLayer only makes sense as an interior layer; document and/or validate that it is not placed first or last.
- Confirm there are no Julia 1.12 world-age issues from a newly exported function: if
GradedLayer/expand/flatten_layers is called from code defined earlier in the include order, place its definition before its callers or adjust include() order in src/TransferMatrix.jl (it currently includes layer.jl before general_TMM.jl).
References
Summary
Add a
GradedLayertype that represents a single physical layer whose optical properties vary continuously along the propagation directionz. It is expanded — via the piecewise-uniform approximation — intonslicesordinary homogeneousLayers evaluated at slice midpoints, then fed through the existing transfer-matrix pipeline unchanged. This is the foundational piece that later unlocks rugate filters, chirped/graded-index coatings, and (via a separate chiral-preset issue) helicoidal media.Context
A
Layertoday is strictly homogeneous alongz: it stores one dispersion (isotropic scalarλ -> n, or a 3-tuple(nx, ny, nz)of dispersions, optionally with fixed ZYZ Euler angles). The struct isLayer{F,T}with fieldsdispersion::Fandthickness::T(src/layer.jl:48-56). Anisotropic layers store the dispersion as a 6-tuple(nx, ny, nz, φ, θ, ψ)built by the constructor atsrc/layer.jl:93-97;isanisotropickeys offdispersion isa Tuple(src/layer.jl:105);get_refractive_indices(src/layer.jl:115-123) andget_euler_angles(src/layer.jl:133-139) read those fields.The core propagators iterate over the
layersvector and calllayer_matrices(layer, λ, ξ, μ)per layer (src/matrix_constructors.jl:97-126), accumulating the transfer matrix in the loop atsrc/general_TMM.jl:358-375(_propagate_core) andsrc/general_TMM.jl:414-426(_propagate_full). Every public entry point (transfer,sweep_angle,sweep_thickness,efield/hfield) ultimately consumes a flatVector{<:Layer}. There is no way to express a profile that varies within a layer except by hand-building hundreds ofLayers.Why
The standard way to model a nonhomogeneous (graded) medium in the transfer-matrix method is the piecewise-uniform approximation: discretize the continuous
z-profile into many thin homogeneous slices, each handled by the ordinary layer propagator (Mackay & Lakhtakia 2020, Ch. on nonhomogeneous mediums). Adding aGradedLayerthat performs this expansion gives users a first-class, ergonomic way to specify graded structures while reusing the existing, validated layer machinery — the new code is the profile/slicing API, not new core physics.Proposed approach
GradedLayertype insrc/layer.jlstoring aprofile, a totalthickness, and annslices::Int. Theprofileis a callable of the local normalized coordinate or absolutez(pick one convention and document it; normalizedz ∈ [0, thickness]measured from the layer's entry face is recommended) returning one of:z -> n(isotropic scalar index),z -> (nx, ny, nz)(biaxial, no rotation),z -> (nx, ny, nz, φ, θ, ψ)(biaxial with ZYZ Euler angles — the form that the chiral-preset issue will build on).expand(g::GradedLayer)::Vector{Layer}, that producesnsliceshomogeneousLayers, each of thicknessthickness / nslices, sampling the profile at the midpoint of each slice. Map each profile return form onto the matching existingLayerconstructor (Layer(n, t),Layer(nx, ny, nz, t), orLayer(nx, ny, nz, t; euler=(φ,θ,ψ))atsrc/layer.jl:93-97). The per-slice dispersions must be closures ofλ(frozen at the slice midpoint position), sincelayer_matricesevaluates dispersion at runtime per wavelength (src/matrix_constructors.jl:99-104).layerscollection into a flatVector{Layer}(expanding anyGradedLayerin place) at the top oftransfer(src/general_TMM.jl:569),sweep_angle(src/general_TMM.jl:733),sweep_thickness(src/general_TMM.jl:764), andefield/hfieldvia_field(src/general_TMM.jl:789). A small helperflatten_layers(layers)that expands graded entries and passes ordinaryLayers through is the least invasive approach — the propagators (_propagate_core,_propagate_full) then need no changes.sweep_thickness'st_index: it currently rebuilds a layer byLayer(dispersion_func, ts[i])(src/general_TMM.jl:773). If aGradedLayeroccupies a swept index, either reject it with a clear error for now or define how its total thickness is varied. Rejecting with a clearArgumentErroris acceptable for this single-step issue.GradedLayerfromsrc/TransferMatrix.jl(theexportblock currently listsLayerbut notGradedLayer).test/(a newtest/graded.jlwired intotest/runtests.jl): a constant profile must reproduce the equivalent single homogeneousLayerto machine precision; expansion must producensliceslayers summing to the original thickness; midpoint sampling must hit the expected values.Acceptance criteria
GradedLayer(profile, thickness; nslices)constructs and accepts all three profile return forms (scalar, 3-tuple, 6-tuple).GradedLayerpassed inside thelayersvector totransfer,sweep_angle, andefieldruns end-to-end through the existing pipeline and returns finite results.1e-12) to the equivalent single homogeneousLayer, confirming the expansion is correct.nslicesis documented (the dedicated convergence-control issue refines this further).GradedLayeris exported and has a docstring with a runnable example.Caveats / risks
nsliceson thick stacks can be slow and, for strongly attenuating slices, may approach numerical-overflow regimes in the unmodified transfer-matrix product. Keep this issue to the expansion mechanics; performance/stability hardening is out of scope.GradedLayeronly makes sense as an interior layer; document and/or validate that it is not placed first or last.GradedLayer/expand/flatten_layersis called from code defined earlier in the include order, place its definition before its callers or adjustinclude()order insrc/TransferMatrix.jl(it currently includeslayer.jlbeforegeneral_TMM.jl).References
src/layer.jl(Layer,get_refractive_indices,get_euler_angles),src/matrix_constructors.jl:97-126(layer_matrices),src/general_TMM.jl(propagators and public API).GradedLayer; Add nslices auto-refinement helper (refine_slices) #95 (refine_slicesauto-refinement) adds slice-count convergence; Validate chiral circular-Bragg stopband against literature #96 (chiral circular-Bragg validation) validates the physics.