Skip to content

Add GradedLayer type with piecewise-uniform expansion into homogeneous Layers #87

Description

@garrekstemo

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

  1. 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).
  2. 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).
  3. 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.
  4. 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.
  5. 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).
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions