Differentiable Solver API¶
bcm
¶
Differentiable planar solver: an eigendecomposition-free cascade in JAX.
The NumPy engine (:mod:hyperbolic_optics.structure) diagonalises each layer's
4×4 Berreman matrix, whose eigenvectors are awkward to differentiate (arbitrary
phase, divergent derivative at degeneracies, which every isotropic layer has).
Here each layer's scattering matrix is built from matrix products and linear
solves only, so jax.grad gives exact derivatives with respect to
thicknesses, tensors, orientations, angle and wavelength.
For a thin slice, exp(iΔτ) − I is a short Taylor series; 2ᴺ slices are
assembled by N doublings, each a Redheffer star product of the interval
with itself (the precise integration method of Zhong & Williams 1994, as in
Yang, Song & Cai, arXiv:2608.06185). Intervals are carried as wave amplitudes
in a reference medium of permittivity 2kx² + 1, whose waves always
propagate and are scaled to unit power, so no doubling meets a singular inverse.
Coefficients are Jones matrices [..., out, in] with 0 = p and 1 = s, on the
NumPy engine's sign convention. The half-spaces are isotropic and lossless,
except in :func:solve_stack_on_crystal. Inputs broadcast over leading batch
dimensions. Reference accuracy needs jax.config.update("jax_enable_x64", True);
without it a warning says so.
Classes:
-
PlanarResult–Complex Jones matrices and powers, each
[..., out, in]with 0 = p, 1 = s. -
PlanarSMatrix–The full bidirectional scattering matrix of a planar stack.
-
StackFields–Fields and power flow through a planar stack, for p and s light from the top.
-
CrystalResult–Response of a stack on a semi-infinite anisotropic (or lossy) substrate.
Functions:
-
rotation_matrix–Euler rotation
Rz(z) · Ry(y) · Rx(x)in radians,[..., 3, 3]. -
rotate_tensor–Similarity transform
R · tensor · Rᵀ, broadcasting over batch dimensions. -
system_matrix–Berreman matrix
ΔwithdF/dz̃ = iΔF,F = [Ex, Ey, Hx, Hy]. -
solve_stack–Reflection and transmission of a planar stack of anisotropic layers.
-
stack_smatrix–The bidirectional, power-normalized scattering matrix of a planar stack.
-
cascade–Connect
upper's bottom port tolower's top port. -
stack_fields–Fields, interface fluxes and layer-resolved absorption of a planar stack.
-
solve_stack_on_crystal–A planar stack on a semi-infinite anisotropic, lossy or hyperbolic substrate.
PlanarResult
¶
Bases: NamedTuple
Complex Jones matrices and powers, each [..., out, in] with 0 = p, 1 = s.
PlanarSMatrix
¶
Bases: NamedTuple
The full bidirectional scattering matrix of a planar stack.
Attributes:
-
s(Array) –[..., 4, 4]indexed[out, in]over :data:PORTS.topis the incident-side half-space,bottomthe exit side; p and s are defined in each half-space's own plane of incidence (x-z). Reference planes are the stack's two outer interfaces. -
propagating(Array) –[..., 4], whether each port's wave propagates (it is evanescent in a half-space whose index is belowkx).
Amplitudes are scaled by √kz of their half-space, so between propagating
ports |s|² is the fraction of power transferred, and a lossless stack
has a unitary s. Reciprocal media (symmetric ε and μ) give
s(kx) = D · s(−kx)ᵀ · D with D = diag(−1, 1, −1, 1): reversing a wave
reverses its in-plane wavevector, and the p waves' sign convention (they
share the sign of Hy) flips on reversal. For an evanescent port the same scaling still applies
(with complex kz), so matrices of stacks that share a half-space
compose correctly through it -- frustrated total internal reflection
included -- but |s|² is not a power there.
StackFields
¶
Bases: NamedTuple
Fields and power flow through a planar stack, for p and s light from the top.
The last axis of every array is the incident polarization (0 = p, 1 = s), each of unit incident power.
Attributes:
-
R,(T) –[..., 2]total reflectance and transmittance (summed over the outgoing polarizations). -
absorption(Array) –[..., L, 2]fraction of the incident power absorbed in each layer. -
interface_flux(Array) –[..., L + 1, 2]normal energy fluxS_zat each interface, from the top of the first layer down, over the incident flux:1 − Rat the top andTat the bottom. -
z_um(Array) –[..., L·S]depths of the field samples below the top of the first layer (empty unlesssamples_per_layerwas given). -
fields(Array) –[..., L·S, 6, 2]complex(Ex, Ey, Ez, Hx, Hy, Hz)at those depths, H scaled by the free-space impedance.
CrystalResult
¶
Bases: NamedTuple
Response of a stack on a semi-infinite anisotropic (or lossy) substrate.
Attributes:
-
r(Array) –[..., 2, 2]reflection Jones matrix[out, in](0 = p, 1 = s), on the same convention as :func:solve_stack. -
R(Array) –|r|². -
T(Array) –[..., 2]total power carried into the substrate, per incident polarization, from the normal Poynting flux just inside it (1 − R − Tis absorbed in the layers).
rotation_matrix
¶
Euler rotation Rz(z) · Ry(y) · Rx(x) in radians, [..., 3, 3].
The payload API's rotationX/rotationY/rotationZ convention, so a
tensor rotated with :func:rotate_tensor matches the one a Crystal Layer
would use for the same angles.
Source code in hyperbolic_optics/bcm.py
rotate_tensor
¶
Similarity transform R · tensor · Rᵀ, broadcasting over batch dimensions.
system_matrix
¶
Berreman matrix Δ with dF/dz̃ = iΔF, F = [Ex, Ey, Hx, Hy].
Derived from Maxwell's equations with ∂x → i·kx and ∂y → 0 by
eliminating the longitudinal components: Ez = a·F from
(εE)_z = −kx·Hy and Hz = b·F from (μH)_z = kx·Ey.
Parameters:
-
eps(Array) –Permittivity tensor
[..., 3, 3]. -
mu(Array) –Permeability tensor
[..., 3, 3]. -
kx(Array) –Normalised in-plane wavevector
[...].
Returns:
-
Array–[..., 4, 4].
Source code in hyperbolic_optics/bcm.py
solve_stack
¶
solve_stack(
eps,
thickness_um,
wavelength_um,
incidence_rad,
eps_incident,
eps_exit,
mu=None,
doublings=DEFAULT_DOUBLINGS,
)
Reflection and transmission of a planar stack of anisotropic layers.
Parameters:
-
eps(Array) –Layer permittivity tensors in the lab frame,
[..., L, 3, 3], ordered from the incident side.Lmay be zero. -
thickness_um(Array) –Layer thicknesses in µm,
[..., L]. -
wavelength_um(Array) –Vacuum wavelength in µm,
[...]. (From a frequency in cm⁻¹:1e4 / frequency.) -
incidence_rad(Array) –Angle of incidence in the incident medium, radians.
-
eps_incident(Array) –Real, positive permittivity of the incident half-space.
-
eps_exit(Array) –Real, positive permittivity of the exit half-space.
-
mu(Array | None, default:None) –Layer permeability tensors
[..., L, 3, 3]; identity if omitted. -
doublings(int, default:DEFAULT_DOUBLINGS) –Doublings per layer (static). See :data:
DEFAULT_DOUBLINGS.
Returns:
-
PlanarResult–class:
PlanarResultwithr,t,R,T, each -
PlanarResult–[..., 2, 2]indexed[out, in], 0 = p and 1 = s. Wrap in -
PlanarResult–jax.jit(..., static_argnames="doublings")for repeated calls.
Source code in hyperbolic_optics/bcm.py
stack_smatrix
¶
stack_smatrix(
eps,
thickness_um,
wavelength_um,
kx,
eps_top,
eps_bottom,
mu=None,
doublings=DEFAULT_DOUBLINGS,
)
The bidirectional, power-normalized scattering matrix of a planar stack.
Where :func:solve_stack gives the response to light from the incident
side, this gives all four ports: light from either side, in either
polarization, as needed to connect stacks to one another (for instance in a
SAX circuit, :mod:hyperbolic_optics.integrations.sax).
Parameters:
-
eps(Array) –Layer permittivity tensors
[..., L, 3, 3], from the top down. -
thickness_um(Array) –Layer thicknesses in µm,
[..., L]. -
wavelength_um(Array) –Vacuum wavelength in µm.
-
kx(Array) –In-plane wavevector over k₀ (
n·sin θ, conserved through the stack), rather than an angle, so stacks sharing a half-space share it too. -
eps_top, eps_bottom–Real, positive permittivities of the half-spaces.
-
mu(Array | None, default:None) –Layer permeability tensors; identity if omitted.
-
doublings(int, default:DEFAULT_DOUBLINGS) –Doublings per layer (static).
Returns:
-
PlanarSMatrix–class:
PlanarSMatrixover :data:PORTS.
Source code in hyperbolic_optics/bcm.py
cascade
¶
Connect upper's bottom port to lower's top port.
The two must share that half-space (same permittivity, same kx): the
result is the stack of both, with the shared medium of zero thickness --
what a SAX circuit computes, without SAX. Evanescent channels in the shared
medium are carried, so a gap below total internal reflection tunnels.
Source code in hyperbolic_optics/bcm.py
stack_fields
¶
stack_fields(
eps,
thickness_um,
wavelength_um,
incidence_rad,
eps_incident,
eps_exit,
mu=None,
samples_per_layer=0,
doublings=DEFAULT_DOUBLINGS,
)
Fields, interface fluxes and layer-resolved absorption of a planar stack.
The fields at each plane come from the scattering matrices of the stack above and below it, never from transfer matrices, so thick, evanescent and strongly absorbing layers stay stable. Everything is differentiable, including with respect to thicknesses: the samples inside a layer sit at fixed fractions of its thickness.
Parameters:
-
samples_per_layer(int, default:0) –Field samples per layer (static), evenly spaced from its top to its bottom, both included.
0skips the depth profile.
The other arguments are as in :func:solve_stack.
Returns:
-
StackFields–class:
StackFields.
Source code in hyperbolic_optics/bcm.py
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 | |
solve_stack_on_crystal
¶
solve_stack_on_crystal(
eps,
thickness_um,
wavelength_um,
incidence_rad,
eps_incident,
eps_substrate,
mu=None,
mu_substrate=None,
doublings=DEFAULT_DOUBLINGS,
)
A planar stack on a semi-infinite anisotropic, lossy or hyperbolic substrate.
The NumPy engine's Semi Infinite Anisotropic Layer, in the
differentiable solver: bulk crystals (calcite, sapphire, hBN, SiC,
α-MoO₃, a LiNbO₃ wafer) as substrates, in any orientation. The waves the
substrate carries away are found without eigendecomposition, by a matrix
sign function (see :func:_substrate_modes), so reflection is exact and
differentiable even where the substrate's modes are degenerate.
The other arguments are as in :func:solve_stack (L may be zero: a
bare crystal).
Parameters:
-
eps_substrate(Array) –The substrate's tensor
[..., 3, 3]in the lab frame (rotate it with :func:rotate_tensor); lossy is fine. -
mu_substrate(Array | None, default:None) –Its permeability; identity if omitted.
Returns:
-
CrystalResult–class:
CrystalResult:r,Rand the power transmitted into -
CrystalResult–the substrate
T.