Skip to content

SAX Models API

sax

SAX models of planar anisotropic stacks and executed payloads.

Models return SDict s over top@p, top@s, bottom@p and bottom@s, keyed (in, out), from :func:hyperbolic_optics.bcm.stack_smatrix. SAX itself is not imported (it pins JAX below 0.10). See the SAX user guide.

Functions:

  • sdict –

    SAX's SDict for a :class:~hyperbolic_optics.bcm.PlanarSMatrix: (in, out) keys.

  • planar_model –

    A SAX model of a planar stack.

  • structure_smatrix –

    The four-port S-matrix of an executed payload, on its scenario grid.

  • structure_sdict –

    SAX's SDict for an executed payload, values on its grid [A, B, F, T].

sdict

sdict(smatrix)

SAX's SDict for a :class:~hyperbolic_optics.bcm.PlanarSMatrix: (in, out) keys.

Source code in hyperbolic_optics/integrations/sax.py
def sdict(smatrix: bcm.PlanarSMatrix) -> dict[tuple[str, str], jnp.ndarray]:
    """SAX's ``SDict`` for a :class:`~hyperbolic_optics.bcm.PlanarSMatrix`: ``(in, out)`` keys."""
    s = smatrix.s
    return {
        (port_in, port_out): s[..., i_out, i_in]
        for i_in, port_in in enumerate(PORTS)
        for i_out, port_out in enumerate(PORTS)
    }

planar_model

planar_model(
    layers, eps_top, eps_bottom, doublings=DEFAULT_DOUBLINGS
)

A SAX model of a planar stack.

Parameters:

  • layers (Sequence[tuple[Material, Any]]) –

    (material, thickness_um) from the top down. A material is a number, a 3×3 tensor in the lab frame, or a callable wavelength_um → ε (for dispersion, e.g. lambda wl: dispersion.at_wavelength("hBN", wl)).

  • eps_top, eps_bottom –

    The half-spaces (real, positive, isotropic), as numbers or callables of the wavelength.

  • doublings (int, default: DEFAULT_DOUBLINGS ) –

    Doublings per layer (see :data:hyperbolic_optics.bcm.DEFAULT_DOUBLINGS).

Returns:

  • Callable[..., dict[tuple[str, str], ndarray]] –

    model(wl=1.55, kx=0.0, azimuth=0.0) -> SDict. azimuth (radians)

  • Callable[..., dict[tuple[str, str], ndarray]] –

    turns every layer about the stack normal, as the payload API's

  • Callable[..., dict[tuple[str, str], ndarray]] –

    rotationZ does; the plane of incidence stays x-z.

Source code in hyperbolic_optics/integrations/sax.py
def planar_model(
    layers: Sequence[tuple[Material, Any]],
    eps_top: Material,
    eps_bottom: Material,
    doublings: int = bcm.DEFAULT_DOUBLINGS,
) -> Callable[..., dict[tuple[str, str], jnp.ndarray]]:
    """A SAX model of a planar stack.

    Args:
        layers: ``(material, thickness_um)`` from the top down. A material is
            a number, a 3×3 tensor in the lab frame, or a callable
            ``wavelength_um → ε`` (for dispersion, e.g.
            ``lambda wl: dispersion.at_wavelength("hBN", wl)``).
        eps_top, eps_bottom: The half-spaces (real, positive, isotropic), as
            numbers or callables of the wavelength.
        doublings: Doublings per layer (see :data:`hyperbolic_optics.bcm.DEFAULT_DOUBLINGS`).

    Returns:
        ``model(wl=1.55, kx=0.0, azimuth=0.0) -> SDict``. ``azimuth`` (radians)
        turns every layer about the stack normal, as the payload API's
        ``rotationZ`` does; the plane of incidence stays x-z.
    """
    layers = list(layers)

    def model(wl=1.55, kx=0.0, azimuth=0.0):
        wl = jnp.asarray(wl, dtype=float)
        rotation = bcm.rotation_matrix(0.0, 0.0, jnp.asarray(azimuth, dtype=float))
        if layers:
            tensors = [bcm.rotate_tensor(_tensor(m, wl), rotation) for m, _ in layers]
            tensors = jnp.stack(jnp.broadcast_arrays(*tensors), axis=-3)
            thickness = jnp.stack(
                jnp.broadcast_arrays(*(jnp.asarray(d, dtype=float) for _, d in layers)), axis=-1
            )
        else:
            tensors = jnp.zeros((0, 3, 3), dtype=complex)
            thickness = jnp.zeros((0,))
        smatrix = bcm.stack_smatrix(
            tensors, thickness, wl, jnp.asarray(kx, dtype=float),
            _scalar(eps_top, wl), _scalar(eps_bottom, wl), doublings=doublings,
        )  # fmt: skip
        return sdict(smatrix)

    return model

structure_smatrix

structure_smatrix(structure)

The four-port S-matrix of an executed payload, on its scenario grid.

Structure.execute computes the response to light from the incident side only: 8 of the 16 entries. A SAX circuit also needs light arriving from below, so the whole matrix is built here from the executed structure's own layer tensors and thicknesses, with the JAX solver (which matches the NumPy engine to ~1e-14). Its incident-side entries are the engine's r and t, scaled by √(kz_out / kz_in).

Parameters:

  • structure –

    A :class:~hyperbolic_optics.structure.Structure after execute, ending in a Semi Infinite Isotropic Layer.

Returns:

  • PlanarSMatrix –

    class:~hyperbolic_optics.bcm.PlanarSMatrix with s shaped

  • PlanarSMatrix –

    [A, B, F, T, 4, 4]: the canonical axes (incident angle, azimuth,

  • PlanarSMatrix –

    frequency, swept thickness), each of size 1 where the scenario does not

  • PlanarSMatrix –

    sweep it. The azimuth is already in the tensors, and kx comes from

  • PlanarSMatrix –

    the incident angle in the prism.

Source code in hyperbolic_optics/integrations/sax.py
def structure_smatrix(structure) -> bcm.PlanarSMatrix:
    """The four-port S-matrix of an executed payload, on its scenario grid.

    ``Structure.execute`` computes the response to light from the incident side
    only: 8 of the 16 entries. A SAX circuit also needs light arriving from
    below, so the whole matrix is built here from the executed structure's own
    layer tensors and thicknesses, with the JAX solver (which matches the NumPy
    engine to ~1e-14). Its incident-side entries are the engine's r and t,
    scaled by √(kz_out / kz_in).

    Args:
        structure: A :class:`~hyperbolic_optics.structure.Structure` after
            ``execute``, ending in a ``Semi Infinite Isotropic Layer``.

    Returns:
        :class:`~hyperbolic_optics.bcm.PlanarSMatrix` with ``s`` shaped
        ``[A, B, F, T, 4, 4]``: the canonical axes (incident angle, azimuth,
        frequency, swept thickness), each of size 1 where the scenario does not
        sweep it. The azimuth is already in the tensors, and ``kx`` comes from
        the incident angle in the prism.
    """
    from hyperbolic_optics.layers import IsotropicSemiInfiniteLayer

    if structure.k_0 is None:
        raise ValueError("execute the structure first")
    exit_layer = structure.layers[-1]
    if not isinstance(exit_layer, IsotropicSemiInfiniteLayer):
        raise NotImplementedError(
            f"the exit is a {type(exit_layer).__name__}; the S-matrix needs an isotropic, "
            "lossless exit half-space (a Semi Infinite Isotropic Layer)"
        )
    layers = structure.layers[1:-1]
    thickness_um = [jnp.asarray(layer.thickness) * 1e4 for layer in layers]  # cm -> µm
    grid = jnp.broadcast_shapes(
        jnp.shape(structure.k_x),
        jnp.shape(structure.k_0),
        *(jnp.shape(layer.eps_tensor)[:-2] for layer in layers),
        *(jnp.shape(layer.mu_tensor)[:-2] for layer in layers),
        *(jnp.shape(d) for d in thickness_um),
    )

    def stacked(tensors):
        if not tensors:
            return jnp.zeros(grid + (0, 3, 3), dtype=complex)
        return jnp.stack([jnp.broadcast_to(jnp.asarray(t), grid + (3, 3)) for t in tensors], -3)

    eps = stacked([layer.eps_tensor for layer in layers])
    mu = stacked([layer.mu_tensor for layer in layers])
    thickness = (
        jnp.stack([jnp.broadcast_to(d, grid) for d in thickness_um], -1)
        if layers
        else jnp.zeros(grid + (0,))
    )
    wavelength_um = jnp.broadcast_to(2 * jnp.pi / jnp.asarray(structure.k_0) * 1e4, grid)
    kx = jnp.broadcast_to(jnp.real(jnp.asarray(structure.k_x)), grid)
    return bcm.stack_smatrix(
        eps, thickness, wavelength_um, kx, float(structure.eps_prism),
        float(exit_layer.eps_exit), mu=mu,
    )  # fmt: skip

structure_sdict

structure_sdict(structure)

SAX's SDict for an executed payload, values on its grid [A, B, F, T].

SAX combines S-matrices elementwise, so these arrays compose with any other model evaluated on the same grid (same wavelengths, same kx). Wrap it as a SAX model with lambda: structure_sdict(structure).

Source code in hyperbolic_optics/integrations/sax.py
def structure_sdict(structure) -> dict[tuple[str, str], jnp.ndarray]:
    """SAX's ``SDict`` for an executed payload, values on its grid ``[A, B, F, T]``.

    SAX combines S-matrices elementwise, so these arrays compose with any other
    model evaluated on the same grid (same wavelengths, same ``kx``). Wrap it
    as a SAX model with ``lambda: structure_sdict(structure)``.
    """
    return sdict(structure_smatrix(structure))