Skip to content

gdsfactory Integration API

gdsfactory

gdsfactory bridge, imported as hog: PDK stacks, periodic layouts and metasurfaces.

Needs the gdsfactory extra and an active PDK; see the gdsfactory user guide.

Modules:

  • geometry –

    Line gratings: a grid-snapped PCell, and extraction that refuses anything else.

  • lattice –

    2D-periodic layouts: grid-snapped PCells, exact extraction, PDK stacks and rcwa2d layers.

  • materials –

    Materials for gdsfactory stacks: specifications, crystal orientation, PDK names.

  • metasurface –

    Metasurfaces whose cells differ site by site: layout, read-back and SAX models.

  • simulate –

    Labelled results from a layout (simulate_*) or a hand-written stack (solve_*).

  • sparameters –

    Cached four-port S-parameters for PDK stacks and metasurface cells.

  • stack –

    Vertical stacks, written by hand or read from a PDK LayerStack.

Classes:

  • LineGrating –

    A grating of identical ridges along y, repeating along x.

  • UnitCell –

    The repeating unit of a 2D-periodic pattern.

  • Oriented –

    A material and its crystal orientation.

  • CrossedGratingResponse –

    A 2D-periodic pattern's response over [wavelength, angle, order].

  • GratingResponse –

    A line grating's response over [wavelength, angle, order].

  • PlanarResponse –

    A planar stack's response over [wavelength, angle, azimuth].

  • StackLayer –

    One layer of the vertical stack, from the incident side down.

  • VerticalStack –

    Half-spaces and layers, incident side first.

Functions:

  • extract_line_grating –

    Read a line grating back from the polygons of component on layer.

  • line_grating –

    PCell: n_periods ridges of width fill · period_um and length length_um.

  • crossed_grating_layers –

    :mod:~hyperbolic_optics.rcwa2d layers for a unit cell etched into a stack.

  • crossed_grating_stack –

    A 2D-periodic pattern and the patterned stack its etch makes in the PDK's LayerStack.

  • extract_unit_cell –

    Read a 2D-periodic pattern back from the polygons of component on layer.

  • periodic_array –

    PCell: the polygons of one cell, repeated n_cells times on a lattice.

  • material_tensor –

    Lab-frame permittivity tensor [..., 3, 3] for a material specification.

  • x_cut –

    Optic axis in the wafer plane at axis_azimuth_deg from the layout x axis.

  • z_cut –

    Optic axis along the wafer normal (a z-cut wafer of a uniaxial crystal).

  • cell_from_pdk –

    Read one cell's centred lattice footprint through a PDK LayerStack.

  • cell_layers –

    A cell's :mod:~hyperbolic_optics.rcwa2d layers at a wavelength (traceable).

  • cell_sax_model –

    A SAX model of one cell in an infinite array of itself: its zeroth-order S-matrix.

  • metasurface_component –

    A metasurface: the cell cells[k] at each site sites_um[k].

  • read_metasurface –

    A metasurface layout read back into per-site cells.

  • turned –

    Turn a cell's polygons, optionally turning its crystals too (traceable).

  • simulate_crossed_grating –

    A 2D-periodic pattern in a layout, etched into the PDK's stack: cell, stack, response.

  • simulate_grating –

    A line grating in a layout, etched into the PDK's stack: geometry, stack, response.

  • simulate_stack –

    The PDK stack at one point of a layout, and its plane-wave response.

  • solve_crossed_grating –

    Diffraction by a 2D-periodic pattern etched into a stack, over wavelength and angle.

  • solve_grating –

    Diffraction by a line grating etched into a stack, over wavelength and angle.

  • solve_stack –

    Reflection and transmission of an unpatterned stack.

  • stack_sax_model –

    A SAX model of an unpatterned stack, e.g. from :func:vertical_stack.

  • model_from_npz –

    A SAX model from saved S-parameters, interpolated over wavelength, modes kept.

  • write_sparameters_cell –

    Cache a cell's four-port S-parameters; angles and solver settings follow cell_sax_model.

  • write_sparameters_stack –

    The plane-wave S-matrix of the PDK stack at a point of a layout, over wavelength.

  • grating_layers –

    :mod:~hyperbolic_optics.rcwa layers for a line grating etched into a stack.

  • grating_stack –

    A line grating and the patterned stack its etch makes in the PDK's LayerStack.

  • vertical_stack –

    The unpatterned stack at point_um: ambient above, lowest level as the exit.

LineGrating dataclass

LineGrating(
    period_um,
    fill,
    offset=0.0,
    n_periods=1,
    length_um=0.0,
    origin_um=(0.0, 0.0),
)

A grating of identical ridges along y, repeating along x.

Attributes:

  • period_um (float) –

    Grating period in µm.

  • fill (float) –

    Ridge width as a fraction of the period.

  • offset (float) –

    Position of the first ridge's left edge as a fraction of the period (mod 1). It shifts only the phases of the diffracted orders.

  • n_periods (int) –

    Number of ridges drawn.

  • length_um (float) –

    Ridge length along y in µm.

  • origin_um (tuple[float, float]) –

    Layout position (x, y) of the first ridge's lower-left corner, so the stack beneath it can be sampled.

UnitCell dataclass

UnitCell(
    period_um,
    polygons,
    n_cells=(1, 1),
    origin_um=(0.0, 0.0),
    lattice_um=None,
)

The repeating unit of a 2D-periodic pattern.

Attributes:

  • period_um (tuple[Any, Any] | None) –

    (Λx, Λy) in µm for a rectangular lattice; None when lattice_um gives another.

  • polygons (tuple[Any, ...]) –

    The shapes of one cell, each [V, 2] in µm relative to the cell origin. They may extend past the cell; the solver periodises them. May be JAX values, for optimisation.

  • n_cells (tuple[int, int]) –

    (n₁, n₂) cells drawn in the layout, along a₁ and a₂.

  • origin_um (tuple[float, float]) –

    Layout position of the first cell's origin, so the stack beneath it can be sampled.

  • lattice_um (tuple[tuple[Any, Any], tuple[Any, Any]] | None) –

    Primitive vectors ((a1x, a1y), (a2x, a2y)) in µm, for any lattice (hexagonal, oblique); overrides period_um.

lattice property

lattice

The cell's lattice, for :func:hyperbolic_optics.rcwa2d.solve_crossed_grating.

Oriented dataclass

Oriented(material, rotation_deg=(0.0, 0.0, 0.0))

A material and its crystal orientation.

Attributes:

  • material (MaterialSpec) –

    Any material specification.

  • rotation_deg (Sequence) –

    (rotationX, rotationY, rotationZ) in degrees, the payload API's convention: a uniaxial crystal's optic axis (crystal z) points at polar angle rotationY from the stack normal and azimuth rotationZ from the layout x axis.

CrossedGratingResponse dataclass

CrossedGratingResponse(
    wavelength_um,
    angle_deg,
    azimuth_deg,
    orders,
    r,
    t,
    R,
    T,
)

A 2D-periodic pattern's response over [wavelength, angle, order].

Attributes:

  • wavelength_um, (angle_deg) –

    The sweep axes; azimuth_deg the fixed plane of incidence, from the lattice's x axis.

  • orders (ndarray) –

    [N, 2], the diffraction orders (m, n) kept.

  • r, ((t, R, T)) –

    [F, A, N, out, in], 0 = p and 1 = s, in each order's own p/s basis; R and T are diffraction efficiencies.

Methods:

  • mueller –

    Mueller matrices [F, A, 4, 4] of one diffraction order.

mueller

mueller(order=(0, 0), transmission=False)

Mueller matrices [F, A, 4, 4] of one diffraction order.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def mueller(self, order: tuple[int, int] = (0, 0), transmission: bool = False) -> np.ndarray:
    """Mueller matrices ``[F, A, 4, 4]`` of one diffraction order."""
    amplitudes = self.t if transmission else self.r
    return _mueller(amplitudes[..., self.order(*order), :, :])

GratingResponse dataclass

GratingResponse(
    wavelength_um,
    angle_deg,
    azimuth_deg,
    orders,
    r,
    t,
    R,
    T,
)

A line grating's response over [wavelength, angle, order].

Attributes:

  • wavelength_um, (angle_deg) –

    The sweep axes; azimuth_deg the fixed plane of incidence relative to the grating vector.

  • orders (ndarray) –

    The diffraction orders kept.

  • r, ((t, R, T)) –

    [F, A, order, out, in], 0 = p, 1 = s; R and T are diffraction efficiencies.

Methods:

  • mueller –

    Mueller matrices [F, A, 4, 4] of one diffraction order.

mueller

mueller(order=0, transmission=False)

Mueller matrices [F, A, 4, 4] of one diffraction order.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def mueller(self, order: int = 0, transmission: bool = False) -> np.ndarray:
    """Mueller matrices ``[F, A, 4, 4]`` of one diffraction order."""
    amplitudes = self.t if transmission else self.r
    return _mueller(amplitudes[..., self.order(order), :, :])

PlanarResponse dataclass

PlanarResponse(
    wavelength_um, angle_deg, azimuth_deg, r, t, R, T
)

A planar stack's response over [wavelength, angle, azimuth].

Attributes:

  • wavelength_um, ((angle_deg, azimuth_deg)) –

    The sweep axes. The azimuth turns the sample about the stack normal (every layer's tensor), as the payload API's rotationZ does; the plane of incidence stays x-z.

  • r, (t) –

    Complex Jones matrices [F, A, B, out, in], 0 = p, 1 = s.

  • R, (T) –

    The corresponding powers.

Methods:

  • mueller –

    Mueller matrices [F, A, B, 4, 4] of the reflected (or transmitted) light.

  • ellipsometry –

    Generalized ellipsometry: Psi, Delta and Psi_ps, Delta_ps, Psi_sp, Delta_sp.

mueller

mueller(transmission=False)

Mueller matrices [F, A, B, 4, 4] of the reflected (or transmitted) light.

The transmitted one describes amplitudes, not flux: its M₀₀ equals the transmittance only when the incident and exit media match.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def mueller(self, transmission: bool = False) -> np.ndarray:
    """Mueller matrices ``[F, A, B, 4, 4]`` of the reflected (or transmitted) light.

    The transmitted one describes amplitudes, not flux: its M₀₀ equals the
    transmittance only when the incident and exit media match.
    """
    return _mueller(self.jones(transmission))

ellipsometry

ellipsometry()

Generalized ellipsometry: Psi, Delta and Psi_ps, Delta_ps, Psi_sp, Delta_sp.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def ellipsometry(self) -> dict[str, np.ndarray]:
    """Generalized ellipsometry: ``Psi, Delta`` and ``Psi_ps, Delta_ps, Psi_sp, Delta_sp``."""
    return _ellipsometry(self.r)

StackLayer dataclass

StackLayer(
    thickness_um,
    material,
    background=None,
    rotation_deg=(0.0, 0.0, 0.0),
    name="",
    polygons=None,
)

One layer of the vertical stack, from the incident side down.

Attributes:

  • thickness_um (Any) –

    Thickness in µm (may be a JAX value for optimisation).

  • material (MaterialSpec | Oriented) –

    The layer's material -- the ridges or shapes, if background is set.

  • background (MaterialSpec | Oriented | None) –

    Material around them. None makes the layer unpatterned (a uniform film of material).

  • rotation_deg (Sequence) –

    Orientation of the layer's crystals, material and background alike (payload-style (x, y, z), degrees), applied after an :class:Oriented material's own cut: a twist.

  • name (str) –

    Label, e.g. the PDK level it came from.

  • polygons (tuple | None) –

    The layer's own shapes (vertices [V, 2] in µm, cell coordinates) for a 2D-periodic cell whose layers are patterned differently; None to use the unit cell's.

Methods:

  • tensors –

    Lab-frame (material, background) tensors; background is None if uniform.

tensors

tensors(wavelength_um)

Lab-frame (material, background) tensors; background is None if uniform.

Source code in hyperbolic_optics/integrations/gdsfactory/stack.py
def tensors(self, wavelength_um: Any) -> tuple[jax.Array, jax.Array | None]:
    """Lab-frame ``(material, background)`` tensors; ``background`` is None if uniform."""
    eps = material_tensor(self.material, wavelength_um, self.rotation_deg)
    if self.background is None:
        return eps, None
    return eps, material_tensor(self.background, wavelength_um, self.rotation_deg)

VerticalStack dataclass

VerticalStack(layers, incident=1.0, exit=1.0)

Half-spaces and layers, incident side first.

Attributes:

  • layers (tuple[StackLayer, ...]) –

    The finite layers, from the incident side down.

  • incident (MaterialSpec) –

    Material of the incident half-space (isotropic, lossless).

  • exit (MaterialSpec) –

    Material of the exit half-space: isotropic and lossless, or for 2D-periodic solves a crystal or absorbing wafer (:meth:exit_medium).

Methods:

  • half_space_permittivity –

    Real scalar ε; invalid half-spaces raise eagerly and return NaN under JIT.

  • exit_medium –

    The exit half-space for 2D-periodic solves (:mod:~hyperbolic_optics.rcwa2d).

half_space_permittivity

half_space_permittivity(which, wavelength_um)

Real scalar ε; invalid half-spaces raise eagerly and return NaN under JIT.

Source code in hyperbolic_optics/integrations/gdsfactory/stack.py
def half_space_permittivity(self, which: str, wavelength_um: Any) -> jax.Array:
    """Real scalar ε; invalid half-spaces raise eagerly and return NaN under JIT."""
    spec = self.incident if which == "incident" else self.exit
    tensor = material_tensor(spec, jnp.asarray(wavelength_um, dtype=float))
    eps = tensor[..., 0, 0]
    if isinstance(tensor, jax.core.Tracer):
        return jnp.where(_real_scalar(tensor, jnp), eps.real, jnp.nan)
    if not np.all(_real_scalar(tensor, np)):
        raise ValueError(
            f"The {which} half-space ({spec!r}) must be isotropic and lossless; the solvers "
            "need real, positive ε there. Put an absorbing or anisotropic material in a "
            "finite layer instead."
        )
    return eps.real

exit_medium

exit_medium(wavelength_um)

The exit half-space for 2D-periodic solves (:mod:~hyperbolic_optics.rcwa2d).

A real scalar ε where it is isotropic and lossless; otherwise its 3×3 tensor -- a crystal, hyperbolic or absorbing substrate, whose two transmitted modes then replace p and s. Decided at wavelength_um, or under jit at a representative wavelength of the material.

Source code in hyperbolic_optics/integrations/gdsfactory/stack.py
def exit_medium(self, wavelength_um: Any) -> jax.Array:
    """The exit half-space for 2D-periodic solves (:mod:`~hyperbolic_optics.rcwa2d`).

    A real scalar ε where it is isotropic and lossless; otherwise its 3×3
    tensor -- a crystal, hyperbolic or absorbing substrate, whose two
    transmitted modes then replace p and s. Decided at ``wavelength_um``, or
    under ``jit`` at a representative wavelength of the material.
    """
    wavelength = jnp.asarray(wavelength_um, dtype=float)
    traced = isinstance(wavelength, jax.core.Tracer)
    spec = self.exit
    while isinstance(spec, Oriented):
        spec = spec.material
    if not traced:
        probe = wavelength
    elif isinstance(spec, str):
        probe = 2e4 / sum(dispersion.frequency_range(spec))  # mid-window, cm⁻¹ → µm
    else:
        probe = 1.55
    with jax.ensure_compile_time_eval():  # a static decision, even under jit
        tensor = np.asarray(material_tensor(self.exit, probe))
    if np.all(_real_scalar(tensor, np)):
        return (
            self.half_space_permittivity("exit", wavelength)
            if traced
            else tensor[..., 0, 0].real
        )
    return material_tensor(self.exit, wavelength)

extract_line_grating

extract_line_grating(component, layer, tolerance_um=None)

Read a line grating back from the polygons of component on layer.

Every polygon must be an axis-aligned rectangle; all must span the same y range, have the same width, and start at equally spaced x. Anything else raises ValueError naming what failed, because a solver fed a guessed geometry returns a confident, wrong answer.

Parameters:

  • component (Component) –

    A gdsfactory component, e.g. from gf.import_gds.

  • layer (LayerSpec) –

    The layer the ridges are drawn on.

  • tolerance_um (float | None, default: None ) –

    Allowed deviation; defaults to one database unit.

Returns:

  • The ( LineGrating ) –

    class:LineGrating, with the period from a least-squares fit to the

  • LineGrating –

    ridge positions.

Source code in hyperbolic_optics/integrations/gdsfactory/geometry.py
def extract_line_grating(
    component: gf.Component,
    layer: gf.typings.LayerSpec,
    tolerance_um: float | None = None,
) -> LineGrating:
    """Read a line grating back from the polygons of ``component`` on ``layer``.

    Every polygon must be an axis-aligned rectangle; all must span the same y
    range, have the same width, and start at equally spaced x. Anything else
    raises ``ValueError`` naming what failed, because a solver fed a guessed
    geometry returns a confident, wrong answer.

    Args:
        component: A gdsfactory component, e.g. from ``gf.import_gds``.
        layer: The layer the ridges are drawn on.
        tolerance_um: Allowed deviation; defaults to one database unit.

    Returns:
        The :class:`LineGrating`, with the period from a least-squares fit to the
        ridge positions.
    """
    tolerance = component.kcl.dbu * 1.01 if tolerance_um is None else tolerance_um
    layer_index = gf.get_layer(layer)
    polygons = component.get_polygons_points(layers=[layer])
    points = polygons.get(layer_index, [])
    if len(points) < 2:
        raise ValueError(f"need at least two ridges on layer {layer}, found {len(points)}")

    rectangles = []
    for polygon in points:
        polygon = np.asarray(polygon, dtype=float)
        xs, ys = np.unique(np.round(polygon[:, 0], 9)), np.unique(np.round(polygon[:, 1], 9))
        if len(polygon) != 4 or len(xs) != 2 or len(ys) != 2:
            raise ValueError(
                f"polygon on layer {layer} is not an axis-aligned rectangle: {polygon}"
            )
        rectangles.append((xs[0], xs[1], ys[0], ys[1]))
    rectangles.sort()
    x0, x1, y0, y1 = (np.array(column) for column in zip(*rectangles, strict=True))

    if np.ptp(y0) > tolerance or np.ptp(y1) > tolerance:
        raise ValueError("ridges do not all span the same y range; not a line grating")
    widths = x1 - x0
    if np.ptp(widths) > tolerance:
        raise ValueError(f"ridge widths differ by {np.ptp(widths):.4g} µm; not a line grating")
    index = np.arange(len(x0))
    period, start = np.polyfit(index, x0, 1)
    if np.max(np.abs(x0 - (start + period * index))) > tolerance:
        raise ValueError("ridges are not equally spaced; not a periodic line grating")
    if np.mean(widths) >= period - tolerance:  # the fitted period carries rounding
        raise ValueError("ridges touch or overlap; no gap to form a grating")
    return LineGrating(
        period_um=float(period),
        fill=float(np.mean(widths) / period),
        offset=float((start / period) % 1.0),
        n_periods=len(x0),
        length_um=float(np.mean(y1 - y0)),
        origin_um=(float(x0[0]), float(np.mean(y0))),
    )

line_grating

line_grating(
    period_um=2.0,
    fill=0.5,
    n_periods=20,
    length_um=20.0,
    layer=(1, 0),
)

PCell: n_periods ridges of width fill · period_um and length length_um.

The period and the ridge width are snapped to the layout grid (the active PDK's database unit), so every period drawn is identical; the snapped values are recorded in component.info and are what :func:extract_line_grating recovers.

Source code in hyperbolic_optics/integrations/gdsfactory/geometry.py
@gf.cell
def line_grating(
    period_um: float = 2.0,
    fill: float = 0.5,
    n_periods: int = 20,
    length_um: float = 20.0,
    layer: gf.typings.LayerSpec = (1, 0),
) -> gf.Component:
    """PCell: ``n_periods`` ridges of width ``fill · period_um`` and length ``length_um``.

    The period and the ridge width are snapped to the layout grid (the active
    PDK's database unit), so every period drawn is identical; the snapped values
    are recorded in ``component.info`` and are what :func:`extract_line_grating`
    recovers.
    """
    if not 0.0 < fill < 1.0:
        raise ValueError(f"fill must lie strictly between 0 and 1, got {fill}")
    component = gf.Component()
    dbu = component.kcl.dbu
    period_dbu = round(period_um / dbu)
    width_dbu = round(fill * period_um / dbu)
    length_dbu = round(length_um / dbu)
    if not 0 < width_dbu < period_dbu:
        raise ValueError("ridge width rounds to zero or to the full period on this grid")
    for index in range(n_periods):
        x0, x1 = index * period_dbu * dbu, (index * period_dbu + width_dbu) * dbu
        top = length_dbu * dbu
        component.add_polygon([(x0, 0.0), (x1, 0.0), (x1, top), (x0, top)], layer=layer)
    component.info["period_um"] = period_dbu * dbu
    component.info["fill"] = width_dbu / period_dbu
    component.info["n_periods"] = n_periods
    component.info["length_um"] = length_dbu * dbu
    return component

crossed_grating_layers

crossed_grating_layers(cell, stack, wavelength_um)

:mod:~hyperbolic_optics.rcwa2d layers for a unit cell etched into a stack.

A patterned layer uses its own polygons if it has them, else the cell's shapes (a single etch); cell may be None when every patterned layer has its own. Polygons, thicknesses, orientations and the wavelength may be JAX values.

Source code in hyperbolic_optics/integrations/gdsfactory/lattice.py
def crossed_grating_layers(
    cell: UnitCell | None,
    stack: VerticalStack | Sequence[StackLayer],
    wavelength_um: Any,
) -> list[rcwa2d.PatternedLayer]:
    """:mod:`~hyperbolic_optics.rcwa2d` layers for a unit cell etched into a stack.

    A patterned layer uses its own ``polygons`` if it has them, else the
    cell's shapes (a single etch); ``cell`` may be ``None`` when every
    patterned layer has its own. Polygons, thicknesses, orientations and the
    wavelength may be JAX values.
    """
    layers = stack.layers if isinstance(stack, VerticalStack) else stack
    out = []
    for spec in layers:
        eps, background = spec.tensors(wavelength_um)
        if background is None:
            out.append(rcwa2d.uniform(eps, spec.thickness_um))
        else:
            polygons = spec.polygons if spec.polygons is not None else cell.polygons
            shapes = tuple(rcwa2d.Shape(jnp.asarray(p), eps) for p in polygons)
            out.append(rcwa2d.PatternedLayer(spec.thickness_um, background, shapes))
    return out

crossed_grating_stack

crossed_grating_stack(
    component,
    pattern_layer,
    layer_stack,
    materials=None,
    ambient=1.0,
    period_um=None,
    samples=3,
    lattice_um=None,
)

A 2D-periodic pattern and the patterned stack its etch makes in the PDK's LayerStack.

The stack is sampled at points inside every shape and around them, in the first cell and in one in the middle of the array; every inside sample must agree, and every outside one, or another layer is patterned differently and a ValueError says so. Where the two columns differ, the layer is patterned (one etch shared by every patterned layer).

Source code in hyperbolic_optics/integrations/gdsfactory/lattice.py
def crossed_grating_stack(
    component: gf.Component,
    pattern_layer: gf.typings.LayerSpec,
    layer_stack: LayerStack,
    materials: Mapping[str, MaterialSpec] | None = None,
    ambient: MaterialSpec = 1.0,
    period_um: tuple[float, float] | None = None,
    samples: int = 3,
    lattice_um: tuple[tuple[float, float], tuple[float, float]] | None = None,
) -> tuple[UnitCell, VerticalStack]:
    """A 2D-periodic pattern and the patterned stack its etch makes in the PDK's LayerStack.

    The stack is sampled at points inside every shape and around them, in the
    first cell and in one in the middle of the array; every inside sample must
    agree, and every outside one, or another layer is patterned differently and
    a ``ValueError`` says so. Where the two columns differ, the layer is
    patterned (one etch shared by every patterned layer).
    """
    cell = extract_unit_cell(component, pattern_layer, period_um, lattice_um)
    a1, a2 = cell._vectors()
    origin = np.asarray(cell.origin_um)
    sites = sorted({(0, 0), (cell.n_cells[0] // 2, cell.n_cells[1] // 2)})
    inside, outside = _sample_points(cell, samples)

    def column(points, where):
        profiles = {
            tuple(vertical_profile(component, layer_stack, tuple(origin + i * a1 + j * a2 + p)))
            for i, j in sites
            for p in points
        }
        if len(profiles) != 1:
            raise ValueError(
                f"The stack varies {where} the shapes on {pattern_layer}: another layer is "
                "patterned differently there, so this is not a single 2D grating in this "
                "layer stack."
            )
        return list(profiles.pop())

    stack = patterned_stack(column(inside, "inside"), column(outside, "around"), materials, ambient)
    return cell, stack

extract_unit_cell

extract_unit_cell(
    component, layer, period_um=None, lattice_um=None
)

Read a 2D-periodic pattern back from the polygons of component on layer.

The polygons are grouped into translated copies of one another. Each group must fill the same complete n₁ × n₂ lattice, so every cell holds the same motif. Shapes must not overlap, even across cell edges, and must not have holes. Anything else raises ValueError, because a solver fed a guessed geometry returns a confident, wrong answer.

Parameters:

  • component (Component) –

    A gdsfactory component, e.g. from gf.import_gds.

  • layer (LayerSpec) –

    The layer the pattern is drawn on.

  • period_um (tuple[float, float] | None, default: None ) –

    A rectangular lattice's period.

  • lattice_um (tuple[tuple[float, float], tuple[float, float]] | None, default: None ) –

    Any lattice's primitive vectors ((a1x, a1y), (a2x, a2y)). Without either, the lattice is inferred -- rectangular, hexagonal or oblique, motifs that repeat a shape (a dimer) included: the shortest basis under which the drawing is one motif on a complete parallelogram of cells. Pass it to impose a lattice instead.

Source code in hyperbolic_optics/integrations/gdsfactory/lattice.py
def extract_unit_cell(
    component: gf.Component,
    layer: gf.typings.LayerSpec,
    period_um: tuple[float, float] | None = None,
    lattice_um: tuple[tuple[float, float], tuple[float, float]] | None = None,
) -> UnitCell:
    """Read a 2D-periodic pattern back from the polygons of ``component`` on ``layer``.

    The polygons are grouped into translated copies of one another. Each group
    must fill the same complete ``n₁ × n₂`` lattice, so every cell holds the
    same motif. Shapes must not overlap, even across cell edges, and must not
    have holes. Anything else raises ``ValueError``, because a solver fed a
    guessed geometry returns a confident, wrong answer.

    Args:
        component: A gdsfactory component, e.g. from ``gf.import_gds``.
        layer: The layer the pattern is drawn on.
        period_um: A rectangular lattice's period.
        lattice_um: Any lattice's primitive vectors ``((a1x, a1y), (a2x, a2y))``.
            Without either, the lattice is inferred -- rectangular, hexagonal or
            oblique, motifs that repeat a shape (a dimer) included: the
            shortest basis under which the drawing is one motif on a complete
            parallelogram of cells. Pass it to impose a lattice instead.
    """
    dbu = component.kcl.dbu
    polygons = _hulls(kf.kdb.Region(component.begin_shapes_rec(gf.get_layer(layer))))
    if len(polygons) < 2:
        raise ValueError(
            f"need a repeated pattern on layer {layer}, found {len(polygons)} polygons"
        )

    groups: dict[tuple, list[np.ndarray]] = {}
    for points in polygons:
        groups.setdefault(_shape_key(points), []).append(points)
    corners = {key: np.array([p.min(axis=0) for p in members]) for key, members in groups.items()}

    if lattice_um is None and period_um is not None:
        lattice_um = ((period_um[0], 0.0), (0.0, period_um[1]))
    if lattice_um is not None:
        bases = [
            tuple(np.round(np.asarray(v, dtype=float) / dbu).astype(np.int64) for v in lattice_um)
        ]
    else:
        reference = max(corners.values(), key=len)
        bases = _candidate_bases(reference)
        if not bases:
            raise ValueError(
                "cannot infer the lattice: the pattern does not repeat in two directions. "
                "Pass period_um or lattice_um, or draw at least two cells each way."
            )

    found, overlapping = None, None
    for a1, a2 in bases:
        split = _complete_lattices(groups, corners, a1, a2)
        if split is None:
            continue
        counts, motif = split
        origin = np.min([first for _, first in motif], axis=0)
        drawn = {(_shape_key(points), tuple(points.min(axis=0))) for points in polygons}
        rebuilt = {
            (_shape_key(points), tuple(first + i * a1 + j * a2))
            for points, first in motif
            for i in range(counts[0])
            for j in range(counts[1])
        }
        if drawn != rebuilt or len(polygons) != len(motif) * counts[0] * counts[1]:
            continue
        if _overlaps(tuple(points - origin for points, _ in motif), a1, a2):
            overlapping = overlapping or (a1, a2)
            continue
        found = (a1, a2, counts, motif, origin)
        break
    if found is None and overlapping is not None:
        a1, a2 = overlapping
        raise ValueError(
            f"shapes on layer {layer} overlap (within a cell or across its edges) on the lattice "
            f"{tuple(a1 * dbu)}, {tuple(a2 * dbu)} µm; the solver needs non-overlapping shapes"
        )
    if found is None:
        described = "the given lattice" if lattice_um is not None else "any lattice inferred"
        raise ValueError(
            f"the pattern on layer {layer} does not fill a complete lattice at {described}: it "
            "is not one motif repeated on a periodic array. If a cell repeats a shape, pass "
            "period_um or lattice_um"
        )
    a1, a2, counts, motif, origin = found
    cell_polygons = tuple((points - origin) for points, _ in motif)

    rectangular = a1[1] == 0 and a2[0] == 0
    return UnitCell(
        period_um=(a1[0] * dbu, a2[1] * dbu) if rectangular else None,
        polygons=tuple(p.astype(float) * dbu for p in cell_polygons),
        n_cells=counts,
        origin_um=(float(origin[0] * dbu), float(origin[1] * dbu)),
        lattice_um=None if rectangular else (tuple(a1 * dbu), tuple(a2 * dbu)),
    )

periodic_array

periodic_array(
    period_um=(1.0, 1.0),
    polygons=(
        (
            (0.25, 0.25),
            (0.75, 0.25),
            (0.75, 0.75),
            (0.25, 0.75),
        ),
    ),
    n_cells=(10, 10),
    layer=(1, 0),
    lattice_um=None,
)

PCell: the polygons of one cell, repeated n_cells times on a lattice.

The lattice is rectangular at period_um, or given by its primitive vectors lattice_um = ((a1x, a1y), (a2x, a2y)) -- e.g. a hexagonal one, ((a, 0), (a/2, a·√3/2)). The lattice vectors and every vertex are snapped to the layout grid (the database unit), so every cell drawn is an exact translate of the first; the snapped values are in component.info and are what :func:extract_unit_cell recovers. Pass polygon vertices as nested tuples (a PCell's arguments must be hashable), e.g. tuple(map(tuple, rcwa2d.ellipse(...).tolist())).

Source code in hyperbolic_optics/integrations/gdsfactory/lattice.py
@gf.cell
def periodic_array(
    period_um: tuple[float, float] = (1.0, 1.0),
    polygons: tuple[tuple[tuple[float, float], ...], ...] = (
        ((0.25, 0.25), (0.75, 0.25), (0.75, 0.75), (0.25, 0.75)),
    ),
    n_cells: tuple[int, int] = (10, 10),
    layer: gf.typings.LayerSpec = (1, 0),
    lattice_um: tuple[tuple[float, float], tuple[float, float]] | None = None,
) -> gf.Component:
    """PCell: the ``polygons`` of one cell, repeated ``n_cells`` times on a lattice.

    The lattice is rectangular at ``period_um``, or given by its primitive
    vectors ``lattice_um = ((a1x, a1y), (a2x, a2y))`` -- e.g. a hexagonal one,
    ``((a, 0), (a/2, a·√3/2))``. The lattice vectors and every vertex are
    snapped to the layout grid (the database unit), so every cell drawn is an
    exact translate of the first; the snapped values are in ``component.info``
    and are what :func:`extract_unit_cell` recovers. Pass polygon vertices as
    nested tuples (a PCell's arguments must be hashable), e.g.
    ``tuple(map(tuple, rcwa2d.ellipse(...).tolist()))``.
    """
    component = gf.Component()
    dbu = component.kcl.dbu
    if lattice_um is None:
        lattice_um = ((period_um[0], 0.0), (0.0, period_um[1]))
    a1, a2 = (np.round(np.asarray(v, dtype=float) / dbu).astype(int) for v in lattice_um)
    if a1[0] * a2[1] - a1[1] * a2[0] == 0:
        raise ValueError(f"lattice {lattice_um} is degenerate on this grid")
    snapped = [np.round(np.asarray(polygon, dtype=float) / dbu).astype(int) for polygon in polygons]
    for i in range(n_cells[0]):
        for j in range(n_cells[1]):
            for vertices in snapped:
                shifted = (vertices + i * a1 + j * a2) * dbu
                component.add_polygon([tuple(v) for v in shifted], layer=layer)
    rectangular = a1[1] == 0 and a2[0] == 0
    component.info["period_um"] = (a1[0] * dbu, a2[1] * dbu) if rectangular else None
    component.info["lattice_um"] = (tuple(a1 * dbu), tuple(a2 * dbu))
    component.info["n_cells"] = tuple(n_cells)
    component.info["polygons"] = tuple(_as_vertices(v * dbu) for v in snapped)
    return component

material_tensor

material_tensor(
    spec, wavelength_um, rotation_deg=(0.0, 0.0, 0.0)
)

Lab-frame permittivity tensor [..., 3, 3] for a material specification.

Parameters:

  • spec (MaterialSpec | Oriented) –

    A material specification or :class:Oriented. An oriented crystal is cut first, then turned by rotation_deg: a layer's twist turns the crystal as cut.

  • wavelength_um (Array) –

    Vacuum wavelength in µm; differentiable.

  • rotation_deg (Sequence, default: (0.0, 0.0, 0.0) ) –

    Payload-style (rotationX, rotationY, rotationZ), degrees.

Source code in hyperbolic_optics/integrations/gdsfactory/materials.py
def material_tensor(
    spec: MaterialSpec | Oriented,
    wavelength_um: jax.Array,
    rotation_deg: Sequence = (0.0, 0.0, 0.0),
) -> jax.Array:
    """Lab-frame permittivity tensor ``[..., 3, 3]`` for a material specification.

    Args:
        spec: A material specification or :class:`Oriented`. An oriented
            crystal is cut first, then turned by ``rotation_deg``: a layer's
            twist turns the crystal as cut.
        wavelength_um: Vacuum wavelength in µm; differentiable.
        rotation_deg: Payload-style ``(rotationX, rotationY, rotationZ)``, degrees.
    """
    if isinstance(spec, Oriented):
        base = material_tensor(spec.material, wavelength_um)
        rotated = _rotate(base, spec.rotation_deg)
        return _rotate(rotated, rotation_deg)
    if isinstance(spec, str):
        _check_range(spec, wavelength_um)
        tensor = dispersion.at_wavelength(spec, wavelength_um)
    elif callable(spec):
        tensor = jnp.asarray(spec(wavelength_um))
    else:
        tensor = jnp.asarray(spec, dtype=complex)
    if tensor.ndim < 2 or tensor.shape[-2:] != (3, 3):  # an isotropic ε
        tensor = tensor[..., None, None] * jnp.eye(3)
    tensor = jnp.broadcast_to(tensor, jnp.shape(wavelength_um) + (3, 3)).astype(complex)
    return _rotate(tensor, rotation_deg)

x_cut

x_cut(axis_azimuth_deg=0.0)

Optic axis in the wafer plane at axis_azimuth_deg from the layout x axis.

An x- or y-cut wafer of a uniaxial crystal such as LiNbO₃: which of the two only matters for biaxial crystals. For a waveguide along layout y, TE light (E along x) sees the extraordinary index when axis_azimuth_deg = 0.

Source code in hyperbolic_optics/integrations/gdsfactory/materials.py
def x_cut(axis_azimuth_deg: float = 0.0) -> tuple[float, float, float]:
    """Optic axis in the wafer plane at ``axis_azimuth_deg`` from the layout x axis.

    An x- or y-cut wafer of a uniaxial crystal such as LiNbO₃: which of the two
    only matters for biaxial crystals. For a waveguide along layout y, TE light
    (E along x) sees the extraordinary index when ``axis_azimuth_deg = 0``.
    """
    return (0.0, 90.0, float(axis_azimuth_deg))

z_cut

z_cut()

Optic axis along the wafer normal (a z-cut wafer of a uniaxial crystal).

Source code in hyperbolic_optics/integrations/gdsfactory/materials.py
def z_cut() -> tuple[float, float, float]:
    """Optic axis along the wafer normal (a z-cut wafer of a uniaxial crystal)."""
    return (0.0, 0.0, 0.0)

cell_from_pdk

cell_from_pdk(
    component,
    layer_stack,
    lattice_um,
    materials=None,
    ambient=1.0,
    *,
    sidewall_slices=0,
)

Read one cell's centred lattice footprint through a PDK LayerStack.

Each z interval uses its own mask polygons, including derived layers. Lower mesh_order wins; WAFER covers the footprint. Uncovered regions are ambient. The lowest interval must be uniform and becomes the substrate. A StackLayer can represent two materials per interval; more are refused. Crystal cuts come from materials, as in :func:vertical_stack. sidewall_slices=N approximates each nonvertical level by N midpoint slices, respecting width_to_z and scalar bias (µm per edge). Positive angles narrow towards increasing z. Polygon offsets snap to dbu; refine N and the layout grid separately. The default warns and uses vertical walls. Generic z_to_bias and directional biases are refused when slicing.

Source code in hyperbolic_optics/integrations/gdsfactory/metasurface.py
def cell_from_pdk(
    component: gf.Component,
    layer_stack: LayerStack,
    lattice_um,
    materials: Mapping[str, MaterialSpec] | None = None,
    ambient: MaterialSpec = 1.0,
    *,
    sidewall_slices: int = 0,
) -> VerticalStack:
    """Read one cell's centred lattice footprint through a PDK LayerStack.

    Each z interval uses its own mask polygons, including derived layers.
    Lower mesh_order wins; WAFER covers the footprint. Uncovered regions are
    ambient. The lowest interval must be uniform and becomes the substrate.
    A StackLayer can represent two materials per interval; more are refused.
    Crystal cuts come from ``materials``, as in :func:`vertical_stack`.
    ``sidewall_slices=N`` approximates each nonvertical level by N midpoint
    slices, respecting ``width_to_z`` and scalar ``bias`` (µm per edge).
    Positive angles narrow towards increasing z. Polygon offsets snap to dbu;
    refine N and the layout grid separately. The default warns and uses vertical
    walls. Generic ``z_to_bias`` and directional biases are refused when slicing.
    """
    dbu = component.kcl.dbu
    if not isinstance(sidewall_slices, int) or sidewall_slices < 0:
        raise ValueError("sidewall_slices must be a nonnegative integer.")
    a1, a2 = np.asarray(lattice_um, float)
    # one rounded corner plus whole lattice steps, so the footprint tiles the plane exactly
    step1, step2 = _steps(a1, a2, dbu)
    start = np.floor(_corners(a1, a2)[0] / dbu + 0.5).astype(int)
    footprint = kf.kdb.Region(
        kf.kdb.Polygon(
            [kf.kdb.Point(*map(int, start + s)) for s in (0 * step1, step1, step1 + step2, step2)]
        )
    )
    if footprint.area() == 0:
        raise ValueError("The lattice footprint must have nonzero area on the layout grid.")
    present = []
    for name, level in layer_stack.layers.items():
        if not level.thickness:
            continue
        wafer = isinstance(level.layer, LogicalLayer) and gf.get_layer(
            level.layer.layer
        ) == gf.get_layer("WAFER")
        # shapes crossing the footprint's edge wrap around, as RCWA periodises them
        periodic = None if wafer else _tiled(level.layer.get_shapes(component), step1, step2)
        region = footprint if wafer else periodic & footprint
        if not region.is_empty():
            low, high = sorted((level.zmin, level.zmin + level.thickness))
            if wafer:
                present.append((low, high, name, level, region))
                continue
            if not sidewall_slices:
                _warn_process_geometry(name, level)
                present.append((low, high, name, level, region))
                continue
            if level.z_to_bias is not None or isinstance(level.bias, (tuple, list)):
                raise ValueError(
                    f"PDK level {name!r}: slicing supports angle and scalar bias only."
                )
            if not np.isfinite(level.sidewall_angle) or abs(level.sidewall_angle) >= 90:
                raise ValueError(f"Invalid sidewall angle for level {name!r}.")
            edges = (
                np.linspace(low, high, sidewall_slices + 1) if level.sidewall_angle else [low, high]
            )
            reference_z = level.zmin + level.width_to_z * level.thickness
            altitude = _altitude(a1, a2)
            for lo, hi in zip(edges[:-1], edges[1:], strict=True):
                offset = (level.bias or 0.0) + (reference_z - (lo + hi) / 2) * np.tan(
                    np.radians(level.sidewall_angle)
                )
                if abs(offset) >= altitude / 2:
                    raise ValueError(f"PDK level {name!r}: offset exceeds half the cell altitude.")
                sliced = periodic.sized(round(offset / dbu)) & footprint
                # Keep even an empty slice: its z interval still contains background.
                present.append((float(lo), float(hi), name, level, sliced))
    if not present:
        raise ValueError("No PDK material is present in the cell footprint.")
    edges = sorted({z for low, high, *_ in present for z in (low, high)})

    def spec(name):
        return ambient if name is None else resolve_material(name, materials)

    layers, substrate = [], None
    for low, high in zip(edges[:-1], edges[1:], strict=True):
        active = sorted(
            (p for p in present if p[0] <= low and p[1] >= high), key=lambda p: _mesh_order(p[3])
        )
        occupied, regions, ties = kf.kdb.Region(), {}, {}
        for _, _, name, level, region in active:
            priority = _mesh_order(level)
            for other_material, other in ties.get(priority, []):
                if other_material != level.material and not (region & other).is_empty():
                    raise ValueError(f"PDK materials tie on mesh_order near level {name!r}.")
            ties.setdefault(priority, []).append((level.material, region))
            visible = region - occupied
            if not visible.is_empty():
                regions[level.material] = regions.get(level.material, kf.kdb.Region()) | visible
            occupied = occupied | region
        uncovered = footprint - occupied
        if not uncovered.is_empty():
            regions[None] = uncovered
        if substrate is None:
            if len(regions) != 1:
                raise ValueError("The lowest PDK interval must be a uniform substrate.")
            substrate = spec(next(iter(regions)))
            continue
        if len(regions) > 2:
            raise ValueError(f"More than two materials in the cell at z={low:g}–{high:g} µm.")
        label = f"z={low:g}–{high:g}"
        if len(regions) == 1:
            layers.append(StackLayer(high - low, spec(next(iter(regions))), name=label))
            continue
        background = None if None in regions else max(regions, key=lambda k: regions[k].area())
        material = next(k for k in regions if k != background)
        region = regions[material].merged()
        # Trapezoids represent holes without discarding their interiors. All
        # pieces have the same material, so their common edges cancel in RCWA.
        if any(p.holes() for p in region.each()):
            region = region.decompose_trapezoids_to_region()
        polygons = tuple(_vertices_um(region, dbu))
        layers.append(
            StackLayer(
                high - low,
                spec(material),
                background=spec(background),
                polygons=polygons,
                name=label,
            )
        )
    return VerticalStack(tuple(reversed(layers)), incident=ambient, exit=substrate)

cell_layers

cell_layers(cell, wavelength_um)

A cell's :mod:~hyperbolic_optics.rcwa2d layers at a wavelength (traceable).

Source code in hyperbolic_optics/integrations/gdsfactory/metasurface.py
def cell_layers(cell: VerticalStack, wavelength_um: Any) -> list[rcwa2d.PatternedLayer]:
    """A cell's :mod:`~hyperbolic_optics.rcwa2d` layers at a wavelength (traceable)."""
    return crossed_grating_layers(None, cell, wavelength_um)

cell_sax_model

cell_sax_model(
    cell, lattice_um, max_order=(4, 4), **kwargs
)

A SAX model of one cell in an infinite array of itself: its zeroth-order S-matrix.

Returns model(wl, kx=0, azimuth=0) -> SDict over top@p, top@s, bottom@p, bottom@s, from :func:hyperbolic_optics.rcwa2d.crossed_grating_smatrix, with every material evaluated at SAX's wl; kx is n·sin θ in the incident medium. Only the zeroth order is a port, so the model describes a subwavelength array (higher orders evanescent). azimuth rotates the sample, as in stack_sax_model; the ports retain a common p/s basis. On a crystal or absorbing substrate (:meth:~.stack.VerticalStack.exit_medium) the bottom ports are its two power-normalised modes.

Source code in hyperbolic_optics/integrations/gdsfactory/metasurface.py
def cell_sax_model(
    cell: VerticalStack,
    lattice_um,
    max_order: tuple[int, int] = (4, 4),
    **kwargs,
):
    """A SAX model of one cell in an infinite array of itself: its zeroth-order S-matrix.

    Returns ``model(wl, kx=0, azimuth=0) -> SDict`` over ``top@p``, ``top@s``,
    ``bottom@p``, ``bottom@s``, from
    :func:`hyperbolic_optics.rcwa2d.crossed_grating_smatrix`, with every
    material evaluated at SAX's ``wl``; ``kx`` is ``n·sin θ`` in the incident
    medium. Only the zeroth order is a port, so the model describes a
    subwavelength array (higher orders evanescent). ``azimuth`` rotates the
    sample, as in ``stack_sax_model``; the ports retain a common p/s basis.
    On a crystal or absorbing substrate (:meth:`~.stack.VerticalStack.exit_medium`)
    the bottom ports are its two power-normalised modes.
    """
    import jax.numpy as jnp

    from hyperbolic_optics.integrations import sax as hsax

    lattice = rcwa2d.Lattice(*(tuple(a) for a in lattice_um))

    def model(wl=1.55, kx=0.0, azimuth=0.0):
        eps_top = cell.half_space_permittivity("incident", wl)
        eps_bottom = cell.exit_medium(wl)
        smatrix = rcwa2d.crossed_grating_smatrix(
            cell_layers(cell, jnp.asarray(wl)), lattice, jnp.asarray(wl), 0.0,
            eps_top, eps_bottom, max_order, azimuth_rad=-jnp.asarray(azimuth), kx=kx, **kwargs,
        )  # fmt: skip
        return hsax.sdict(smatrix)

    return model

metasurface_component

metasurface_component(
    sites_um,
    cells,
    layers,
    lattice_um,
    rotations_rad=None,
    *,
    rotate_materials=True,
)

A metasurface: the cell cells[k] at each site sites_um[k].

Parameters:

  • sites_um –

    Cell centres [K, 2].

  • cells (Sequence[VerticalStack]) –

    One per site (the same object may repeat): a :class:~.stack.VerticalStack, levels from the top down and patterned ones with polygons about the cell's centre; or a gf.Component drawn on PDK layers about its centre, read back with read_metasurface(..., layer_stack=...).

  • layers (Sequence[LayerSpec]) –

    The GDS layer of each level: level k of every stack cell is drawn on layers[k].

  • lattice_um (tuple[tuple[float, float], tuple[float, float]]) –

    (a1, a2): the lattice the cells were designed on; an unpatterned level is drawn as the cell's footprint.

  • rotations_rad –

    A turn of each site's cell about its centre [K].

  • rotate_materials (bool, default: True ) –

    Turn the crystals with each reference (the default, e.g. placed flakes). False turns only the mask polygons, preserving the wafer axes and each layer's explicit crystal orientation. Stored in each cell's metadata for GDS read-back.

Returns:

  • Component –

    The component, a reference per site to one cell per distinct design.

  • Component –

    The same design gives the same component (cached like any @gf.cell).

Source code in hyperbolic_optics/integrations/gdsfactory/metasurface.py
def metasurface_component(
    sites_um,
    cells: Sequence[VerticalStack],
    layers: Sequence[gf.typings.LayerSpec],
    lattice_um: tuple[tuple[float, float], tuple[float, float]],
    rotations_rad=None,
    *,
    rotate_materials: bool = True,
) -> gf.Component:
    """A metasurface: the cell ``cells[k]`` at each site ``sites_um[k]``.

    Args:
        sites_um: Cell centres ``[K, 2]``.
        cells: One per site (the same object may repeat): a
            :class:`~.stack.VerticalStack`, levels from the top down and
            patterned ones with ``polygons`` about the cell's centre; or a
            ``gf.Component`` drawn on PDK layers about its centre, read back
            with ``read_metasurface(..., layer_stack=...)``.
        layers: The GDS layer of each level: level ``k`` of every stack cell is
            drawn on ``layers[k]``.
        lattice_um: ``(a1, a2)``: the lattice the cells were designed on; an
            unpatterned level is drawn as the cell's footprint.
        rotations_rad: A turn of each site's cell about its centre ``[K]``.
        rotate_materials: Turn the crystals with each reference (the default,
            e.g. placed flakes). False turns only the mask polygons, preserving
            the wafer axes and each layer's explicit crystal orientation.
            Stored in each cell's metadata for GDS read-back.

    Returns:
        The component, a reference per site to one cell per distinct design.
        The same design gives the same component (cached like any ``@gf.cell``).
    """
    sites = np.asarray(sites_um, dtype=float).reshape(-1, 2)
    if len(cells) != len(sites):
        raise ValueError(f"{len(sites)} sites but {len(cells)} cells")
    angles = np.zeros(len(sites)) if rotations_rad is None else np.degrees(rotations_rad)
    angles = np.round(np.broadcast_to(angles, len(sites)), 6)
    payloads, indices, seen = [], [], {}
    for cell in cells:
        if id(cell) not in seen:
            drawn = isinstance(cell, gf.Component)
            payload = json.dumps(
                _drawn_key(cell) if drawn else _payload(cell, layers, lattice_um, rotate_materials),
                sort_keys=True,
            )
            if payload not in payloads:
                payloads.append(payload)
                if not drawn:
                    _warn_off_grid(cell, gf.kcl.dbu)
            seen[id(cell)] = payloads.index(payload)
        indices.append(seen[id(cell)])
    for index, angle in sorted(set(zip(indices, angles.tolist(), strict=True))):
        cell = _drawn(payloads[index])
        if "levels" in cell.info:  # PDK-drawn cells merge their overlaps on read-back
            _check_overlaps(cell, angle, lattice_um)
    design = json.dumps({
        "cells": payloads, "indices": indices,
        "lattice_um": [[float(v) for v in a] for a in lattice_um],
        "sites": np.round(sites, 6).tolist(), "angles_deg": angles.tolist(),
    })  # fmt: skip
    return _placed(design)

read_metasurface

read_metasurface(
    component,
    materials=None,
    *,
    layer_stack=None,
    lattice_um=None,
    ambient=1.0,
    sidewall_slices=0,
)

A metasurface layout read back into per-site cells.

Returns (sites_um [K, 2], cells, lattice_um). Geometry always comes from the polygons, so what is re-simulated is what was drawn.

Without layer_stack, materials, thicknesses and orientations come from each cell's info (named materials looked up in materials) and the reference's turn is applied with :func:turned, turning crystals unless the cell was drawn with rotate_materials=False.

With layer_stack, each reference's cell, turned as placed, is read through the PDK by :func:cell_from_pdk (materials, ambient and sidewall_slices as there): crystals keep the wafer's axes. Cells need no info; lattice_um is required unless the layout records it.

Pass the metasurface's top cell. Nested layouts, arrays, mirrors and magnification are refused rather than reconstructed incompletely.

Source code in hyperbolic_optics/integrations/gdsfactory/metasurface.py
def read_metasurface(
    component: gf.Component,
    materials: Mapping[str, MaterialSpec] | None = None,
    *,
    layer_stack: LayerStack | None = None,
    lattice_um=None,
    ambient: MaterialSpec = 1.0,
    sidewall_slices: int = 0,
) -> tuple[np.ndarray, list[VerticalStack], tuple[tuple[float, float], tuple[float, float]]]:
    """A metasurface layout read back into per-site cells.

    Returns ``(sites_um [K, 2], cells, lattice_um)``. Geometry always comes from
    the polygons, so what is re-simulated is what was drawn.

    Without ``layer_stack``, materials, thicknesses and orientations come from
    each cell's ``info`` (named materials looked up in ``materials``) and the
    reference's turn is applied with :func:`turned`, turning crystals unless
    the cell was drawn with ``rotate_materials=False``.

    With ``layer_stack``, each reference's cell, turned as placed, is read
    through the PDK by :func:`cell_from_pdk` (``materials``, ``ambient`` and
    ``sidewall_slices`` as there): crystals keep the wafer's axes. Cells need
    no ``info``; ``lattice_um`` is required unless the layout records it.

    Pass the metasurface's top cell. Nested layouts, arrays, mirrors and
    magnification are refused rather than reconstructed incompletely.
    """
    lattice = lattice_um if lattice_um is not None else component.info.get("lattice_um")
    lattice = None if lattice is None else tuple(tuple(float(v) for v in a) for a in lattice)
    sites, cells, solved = [], [], {}
    # a snapshot: drawing while iterating defers KLayout's hierarchy update
    for inst in list(component.insts):
        trans = inst.dcplx_trans
        if trans.is_mirror() or not np.isclose(trans.mag, 1.0, rtol=0, atol=1e-12):
            raise ValueError("Mirrored or magnified metasurface references are unsupported.")
        if inst.na > 1 or inst.nb > 1:
            raise ValueError("Expand array references into individual metasurface sites.")
        info = inst.cell.info
        if layer_stack is None and "levels" not in info:
            raise ValueError(
                "Expected direct cell references with metasurface metadata; "
                "pass layer_stack= for cells drawn on PDK layers."
            )
        if "lattice_um" in info:
            cell_lattice = tuple(tuple(float(v) for v in a) for a in info["lattice_um"])
            if lattice is not None and lattice != cell_lattice:
                raise ValueError("All metasurface cells must share the same design lattice.")
            lattice = cell_lattice
        if lattice is None:
            raise ValueError("No design lattice recorded in the layout; pass lattice_um.")
        key = (inst.cell.name, trans.angle)
        if key not in solved:
            if layer_stack is None:
                solved[key] = _cell_from_info(inst.cell, trans.angle, materials)
            else:
                placed = gf.Component()
                placed.create_inst(inst.cell, kf.kdb.DCplxTrans(1, trans.angle, False, 0, 0))
                try:
                    solved[key] = cell_from_pdk(
                        placed, layer_stack, lattice, materials, ambient,
                        sidewall_slices=sidewall_slices,
                    )  # fmt: skip
                finally:
                    placed.delete()
        cells.append(solved[key])
        sites.append((trans.disp.x, trans.disp.y))
    if not cells:
        raise ValueError("No metasurface cell references found; pass an unflattened layout.")
    return np.asarray(sites), cells, lattice

turned

turned(cell, angle_rad, *, rotate_materials=True)

Turn a cell's polygons, optionally turning its crystals too (traceable).

The default rotates the whole cell, suitable for a placed crystal flake. rotate_materials=False models a mask turned on a fixed wafer: material and background tensors retain their cuts and per-layer orientations. Half-spaces and the periodic lattice are unchanged in either case.

Source code in hyperbolic_optics/integrations/gdsfactory/metasurface.py
def turned(cell: VerticalStack, angle_rad, *, rotate_materials: bool = True) -> VerticalStack:
    """Turn a cell's polygons, optionally turning its crystals too (traceable).

    The default rotates the whole cell, suitable for a placed crystal flake.
    ``rotate_materials=False`` models a mask turned on a fixed wafer: material
    and background tensors retain their cuts and per-layer orientations.
    Half-spaces and the periodic lattice are unchanged in either case.
    """
    import jax.numpy as jnp

    turn = bcm.rotation_matrix(0.0, 0.0, angle_rad)[:2, :2]
    return replace(cell, layers=tuple(
        replace(level,
                polygons=None if level.polygons is None
                else tuple(jnp.asarray(p) @ turn.T for p in level.polygons),
                rotation_deg=(level.rotation_deg[0], level.rotation_deg[1],
                              level.rotation_deg[2] + jnp.degrees(angle_rad))
                if rotate_materials else level.rotation_deg)
        for level in cell.layers
    ))  # fmt: skip

simulate_crossed_grating

simulate_crossed_grating(
    component,
    pattern_layer,
    layer_stack,
    wavelengths_um,
    angles_deg,
    azimuth_deg=0.0,
    max_order=(5, 5),
    materials=None,
    ambient=1.0,
    period_um=None,
    factorization="normal",
    truncation="parallelogram",
    lattice_um=None,
)

A 2D-periodic pattern in a layout, etched into the PDK's stack: cell, stack, response.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def simulate_crossed_grating(
    component: gf.Component,
    pattern_layer: gf.typings.LayerSpec,
    layer_stack: LayerStack,
    wavelengths_um: ArrayLike,
    angles_deg: ArrayLike,
    azimuth_deg: float = 0.0,
    max_order: tuple[int, int] = (5, 5),
    materials: Mapping[str, MaterialSpec] | None = None,
    ambient: MaterialSpec = 1.0,
    period_um: tuple[float, float] | None = None,
    factorization: str = "normal",
    truncation: str = "parallelogram",
    lattice_um: tuple[tuple[float, float], tuple[float, float]] | None = None,
) -> tuple[UnitCell, VerticalStack, CrossedGratingResponse]:
    """A 2D-periodic pattern in a layout, etched into the PDK's stack: cell, stack, response."""
    cell, stack = crossed_grating_stack(
        component, pattern_layer, layer_stack, materials, ambient, period_um,
        lattice_um=lattice_um,
    )  # fmt: skip
    response = solve_crossed_grating(
        cell, stack, wavelengths_um, angles_deg, azimuth_deg, max_order, factorization, truncation
    )
    return cell, stack, response

simulate_grating

simulate_grating(
    component,
    grating_layer,
    layer_stack,
    wavelengths_um,
    angles_deg,
    azimuth_deg=0.0,
    max_order=20,
    materials=None,
    ambient=1.0,
    edge_width=0.0,
)

A line grating in a layout, etched into the PDK's stack: geometry, stack, response.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def simulate_grating(
    component: gf.Component,
    grating_layer: gf.typings.LayerSpec,
    layer_stack: LayerStack,
    wavelengths_um: ArrayLike,
    angles_deg: ArrayLike,
    azimuth_deg: float = 0.0,
    max_order: int = 20,
    materials: Mapping[str, MaterialSpec] | None = None,
    ambient: MaterialSpec = 1.0,
    edge_width: float = 0.0,
) -> tuple[LineGrating, VerticalStack, GratingResponse]:
    """A line grating in a layout, etched into the PDK's stack: geometry, stack, response."""
    grating, stack = grating_stack(component, grating_layer, layer_stack, materials, ambient)
    response = solve_grating(
        grating, stack, wavelengths_um, angles_deg, azimuth_deg, max_order, edge_width
    )
    return grating, stack, response

simulate_stack

simulate_stack(
    component,
    layer_stack,
    point_um,
    wavelengths_um,
    angles_deg,
    azimuths_deg=(0.0,),
    materials=None,
    ambient=1.0,
    solver="jax",
)

The PDK stack at one point of a layout, and its plane-wave response.

Returns the extracted :class:VerticalStack alongside the response, so what was simulated can be inspected.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def simulate_stack(
    component: gf.Component,
    layer_stack: LayerStack,
    point_um: tuple[float, float],
    wavelengths_um: ArrayLike,
    angles_deg: ArrayLike,
    azimuths_deg: ArrayLike = (0.0,),
    materials: Mapping[str, MaterialSpec] | None = None,
    ambient: MaterialSpec = 1.0,
    solver: str = "jax",
) -> tuple[VerticalStack, PlanarResponse]:
    """The PDK stack at one point of a layout, and its plane-wave response.

    Returns the extracted :class:`VerticalStack` alongside the response, so what
    was simulated can be inspected.
    """
    stack = vertical_stack(component, layer_stack, point_um, materials, ambient)
    return stack, solve_stack(stack, wavelengths_um, angles_deg, azimuths_deg, solver)

solve_crossed_grating

solve_crossed_grating(
    cell,
    stack,
    wavelengths_um,
    angles_deg,
    azimuth_deg=0.0,
    max_order=(5, 5),
    factorization="normal",
    truncation="parallelogram",
)

Diffraction by a 2D-periodic pattern etched into a stack, over wavelength and angle.

The lattice is the cell's (:attr:UnitCell.lattice): rectangular, hexagonal or oblique. truncation="circular" suits non-rectangular ones (see :func:hyperbolic_optics.rcwa2d.solve_crossed_grating).

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def solve_crossed_grating(
    cell: UnitCell,
    stack: VerticalStack,
    wavelengths_um: ArrayLike,
    angles_deg: ArrayLike,
    azimuth_deg: float = 0.0,
    max_order: tuple[int, int] = (5, 5),
    factorization: str = "normal",
    truncation: str = "parallelogram",
) -> CrossedGratingResponse:
    """Diffraction by a 2D-periodic pattern etched into a stack, over wavelength and angle.

    The lattice is the cell's (:attr:`UnitCell.lattice`): rectangular,
    hexagonal or oblique. ``truncation="circular"`` suits non-rectangular ones
    (see :func:`hyperbolic_optics.rcwa2d.solve_crossed_grating`).
    """
    wavelengths = np.atleast_1d(np.asarray(wavelengths_um, dtype=float))
    angles = np.atleast_1d(np.asarray(angles_deg, dtype=float))
    eps_in = stack.half_space_permittivity("incident", wavelengths)[:, None]  # [F, 1]
    eps_out = stack.exit_medium(wavelengths)[:, None]  # [F, 1] or a crystal's [F, 1, 3, 3]
    layers = crossed_grating_layers(cell, stack, jnp.asarray(wavelengths)[:, None])
    result = rcwa2d.solve_crossed_grating(
        layers,
        cell.lattice,
        jnp.asarray(wavelengths)[:, None],
        jnp.radians(jnp.asarray(angles))[None, :],
        jnp.asarray(eps_in),
        jnp.asarray(eps_out),
        max_order,
        azimuth_rad=np.radians(azimuth_deg),
        factorization=factorization,
        truncation=truncation,
    )
    size = result.orders.shape[0]
    shape = (len(wavelengths), len(angles), size, 2, 2)
    arrays = [np.broadcast_to(np.asarray(getattr(result, k)), shape) for k in ("r", "t", "R", "T")]
    return CrossedGratingResponse(
        wavelengths, angles, azimuth_deg, np.asarray(result.orders), *arrays
    )

solve_grating

solve_grating(
    grating,
    stack,
    wavelengths_um,
    angles_deg,
    azimuth_deg=0.0,
    max_order=20,
    edge_width=0.0,
)

Diffraction by a line grating etched into a stack, over wavelength and angle.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def solve_grating(
    grating: LineGrating,
    stack: VerticalStack,
    wavelengths_um: ArrayLike,
    angles_deg: ArrayLike,
    azimuth_deg: float = 0.0,
    max_order: int = 20,
    edge_width: float = 0.0,
) -> GratingResponse:
    """Diffraction by a line grating etched into a stack, over wavelength and angle."""
    wavelengths = np.atleast_1d(np.asarray(wavelengths_um, dtype=float))
    angles = np.atleast_1d(np.asarray(angles_deg, dtype=float))
    eps_in = stack.half_space_permittivity("incident", wavelengths)[:, None]  # [F, 1]
    eps_out = stack.half_space_permittivity("exit", wavelengths)[:, None]
    layers = grating_layers(grating, stack, jnp.asarray(wavelengths)[:, None], edge_width)
    result = rcwa.solve_grating(
        layers,
        grating.period_um,
        jnp.asarray(wavelengths)[:, None],
        jnp.radians(jnp.asarray(angles))[None, :],
        jnp.asarray(eps_in),
        jnp.asarray(eps_out),
        max_order,
        azimuth_rad=np.radians(azimuth_deg),
    )
    shape = (len(wavelengths), len(angles), 2 * max_order + 1, 2, 2)
    arrays = [np.broadcast_to(np.asarray(getattr(result, k)), shape) for k in ("r", "t", "R", "T")]
    return GratingResponse(wavelengths, angles, azimuth_deg, np.asarray(result.orders), *arrays)

solve_stack

solve_stack(
    stack,
    wavelengths_um,
    angles_deg,
    azimuths_deg=(0.0,),
    solver="jax",
)

Reflection and transmission of an unpatterned stack.

Parameters:

  • stack (VerticalStack) –

    The stack, incident side first.

  • wavelengths_um (ArrayLike) –

    Vacuum wavelengths (µm).

  • angles_deg (ArrayLike) –

    Angles of incidence in the incident medium (degrees).

  • azimuths_deg (ArrayLike, default: (0.0,) ) –

    In-plane rotations of the sample (degrees).

  • solver (str, default: 'jax' ) –

    "jax" (differentiable) or "numpy" (the eigendecomposition engine, the package's reference).

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def solve_stack(
    stack: VerticalStack,
    wavelengths_um: ArrayLike,
    angles_deg: ArrayLike,
    azimuths_deg: ArrayLike = (0.0,),
    solver: str = "jax",
) -> PlanarResponse:
    """Reflection and transmission of an unpatterned stack.

    Args:
        stack: The stack, incident side first.
        wavelengths_um: Vacuum wavelengths (µm).
        angles_deg: Angles of incidence in the incident medium (degrees).
        azimuths_deg: In-plane rotations of the sample (degrees).
        solver: ``"jax"`` (differentiable) or ``"numpy"`` (the eigendecomposition
            engine, the package's reference).
    """
    wavelengths = np.atleast_1d(np.asarray(wavelengths_um, dtype=float))
    angles = np.atleast_1d(np.asarray(angles_deg, dtype=float))
    azimuths = np.atleast_1d(np.asarray(azimuths_deg, dtype=float))
    eps_in = stack.half_space_permittivity("incident", wavelengths)[:, None, None]  # [F, 1, 1]
    eps_out = stack.half_space_permittivity("exit", wavelengths)[:, None, None]
    tensors = _layer_tensors(stack, wavelengths, azimuths)  # [F, B, L, 3, 3]
    thickness = np.array([float(layer.thickness_um) for layer in stack.layers])
    theta = np.radians(angles)[None, :, None]

    if solver == "jax":
        result = bcm.solve_stack(
            tensors[:, None],  # [F, 1, B, L, 3, 3]
            jnp.asarray(thickness),
            jnp.asarray(wavelengths)[:, None, None],
            jnp.asarray(theta),
            jnp.asarray(eps_in),
            jnp.asarray(eps_out),
        )
        shape = (len(wavelengths), len(angles), len(azimuths), 2, 2)
        r = np.broadcast_to(np.asarray(result.r), shape)
        t = np.broadcast_to(np.asarray(result.t), shape)
    elif solver == "numpy":
        r, t = _numpy_engine(np.asarray(tensors), thickness, wavelengths, angles, eps_in, eps_out)
    else:
        raise ValueError(f"Unknown solver {solver!r}; use 'jax' or 'numpy'.")
    R, T = _power(r, t, eps_in, eps_out, np.sin(theta))
    return PlanarResponse(wavelengths, angles, azimuths, r, t, R, T)

stack_sax_model

stack_sax_model(stack, doublings=DEFAULT_DOUBLINGS)

A SAX model of an unpatterned stack, e.g. from :func:vertical_stack.

Returns model(wl, kx, azimuth) -> SDict over top@p, top@s, bottom@p and bottom@s (see :mod:hyperbolic_optics.integrations.sax), with every material evaluated with its dispersion at SAX's wl.

Source code in hyperbolic_optics/integrations/gdsfactory/simulate.py
def stack_sax_model(stack: VerticalStack, doublings: int = bcm.DEFAULT_DOUBLINGS):
    """A SAX model of an unpatterned stack, e.g. from :func:`vertical_stack`.

    Returns ``model(wl, kx, azimuth) -> SDict`` over ``top@p``, ``top@s``,
    ``bottom@p`` and ``bottom@s`` (see :mod:`hyperbolic_optics.integrations.sax`),
    with every material evaluated with its dispersion at SAX's ``wl``.
    """
    from hyperbolic_optics.integrations import sax as hsax

    if stack.patterned:
        raise ValueError("stack_sax_model takes an unpatterned stack; this one has a grating")

    def evaluate(spec, rotation=(0.0, 0.0, 0.0)):
        return lambda wl: material_tensor(spec, wl, rotation)

    layers = [
        (evaluate(layer.material, layer.rotation_deg), layer.thickness_um) for layer in stack.layers
    ]
    # half-spaces validated: planar models need isotropic, lossless ones
    top, bottom = (
        lambda wl, w=which: stack.half_space_permittivity(w, wl) for which in ("incident", "exit")
    )
    return hsax.planar_model(layers, top, bottom, doublings=doublings)

model_from_npz

model_from_npz(filepath)

A SAX model from saved S-parameters, interpolated over wavelength, modes kept.

Returns model(wl) → SDict keyed ("port@mode", "port@mode"). Real and imaginary parts are interpolated linearly (jnp.interp), as gplugins.sax.read.model_from_npz does, which instead drops the mode.

Source code in hyperbolic_optics/integrations/gdsfactory/sparameters.py
def model_from_npz(filepath: Path | str | Mapping[str, np.ndarray]):
    """A SAX model from saved S-parameters, interpolated over wavelength, modes kept.

    Returns ``model(wl) → SDict`` keyed ``("port@mode", "port@mode")``. Real and
    imaginary parts are interpolated linearly (``jnp.interp``), as
    ``gplugins.sax.read.model_from_npz`` does, which instead drops the mode.
    """
    data = _load(Path(filepath)) if not isinstance(filepath, Mapping) else dict(filepath)
    x = jnp.asarray(data["wavelengths"])
    order = jnp.argsort(x)
    x = x[order]
    entries = {
        tuple(key.split(",")): jnp.asarray(value)[order]
        for key, value in data.items()
        if key != "wavelengths"
    }

    def model(wl=1.55):
        wl = jnp.asarray(wl, dtype=float)
        return {
            ports: jnp.interp(wl, x, jnp.real(v)) + 1j * jnp.interp(wl, x, jnp.imag(v))
            for ports, v in entries.items()
        }

    return model

write_sparameters_cell

write_sparameters_cell(
    cell,
    lattice_um,
    wavelengths_um,
    max_order=(4, 4),
    kx=0.0,
    azimuth=0.0,
    dirpath=None,
    overwrite=False,
    **solver_settings,
)

Cache a cell's four-port S-parameters; angles and solver settings follow cell_sax_model.

The key includes evaluated tensors, thicknesses, polygons, lattice and truncation. Named or callable material changes therefore invalidate it. Read the returned dictionary with :func:model_from_npz.

Source code in hyperbolic_optics/integrations/gdsfactory/sparameters.py
def write_sparameters_cell(
    cell: VerticalStack,
    lattice_um,
    wavelengths_um,
    max_order: tuple[int, int] = (4, 4),
    kx=0.0,
    azimuth=0.0,
    dirpath: Path | str | None = None,
    overwrite: bool = False,
    **solver_settings,
) -> dict[str, np.ndarray]:
    """Cache a cell's four-port S-parameters; angles and solver settings follow cell_sax_model.

    The key includes evaluated tensors, thicknesses, polygons, lattice and
    truncation. Named or callable material changes therefore invalidate it.
    Read the returned dictionary with :func:`model_from_npz`.
    """
    wavelengths = np.atleast_1d(np.asarray(wavelengths_um, dtype=float))
    settings = dict(
        tool="hyperbolic_optics.cell",
        wavelengths_um=wavelengths,
        stack=_stack_settings(cell, wavelengths),
        lattice_um=lattice_um,
        max_order=max_order,
        kx=kx,
        azimuth=azimuth,
        solver_settings=solver_settings,
    )
    path = _cache_dir(dirpath) / f"cell_{_settings_hash(settings)}.npz"
    return _cached_model(
        path,
        settings,
        cell_sax_model(cell, lattice_um, max_order, **solver_settings),
        wavelengths,
        kx,
        azimuth,
        overwrite,
    )

write_sparameters_stack

write_sparameters_stack(
    component,
    layer_stack,
    point_um,
    wavelengths_um,
    angle_deg=0.0,
    azimuth_deg=0.0,
    materials=None,
    ambient=1.0,
    dirpath=None,
    overwrite=False,
)

The plane-wave S-matrix of the PDK stack at a point of a layout, over wavelength.

Returns {"top@p,bottom@s": …, …, "wavelengths": …}: all 16 entries of :func:hyperbolic_optics.bcm.stack_smatrix, power-normalised, for light at angle_deg in the ambient (including its dispersion), the sample turned by azimuth_deg.

Source code in hyperbolic_optics/integrations/gdsfactory/sparameters.py
def write_sparameters_stack(
    component: gf.Component,
    layer_stack: LayerStack,
    point_um: tuple[float, float],
    wavelengths_um,
    angle_deg: float = 0.0,
    azimuth_deg: float = 0.0,
    materials: Mapping[str, MaterialSpec] | None = None,
    ambient: MaterialSpec = 1.0,
    dirpath: Path | str | None = None,
    overwrite: bool = False,
) -> dict[str, np.ndarray]:
    """The plane-wave S-matrix of the PDK stack at a point of a layout, over wavelength.

    Returns ``{"top@p,bottom@s": …, …, "wavelengths": …}``: all 16 entries of
    :func:`hyperbolic_optics.bcm.stack_smatrix`, power-normalised, for light at
    ``angle_deg`` in the ambient (including its dispersion), the sample turned by
    ``azimuth_deg``.
    """
    wavelengths = np.atleast_1d(np.asarray(wavelengths_um, dtype=float))
    stack = vertical_stack(component, layer_stack, point_um, materials, ambient)
    settings = dict(
        tool="hyperbolic_optics.stack", point_um=point_um, wavelengths_um=wavelengths,
        angle_deg=angle_deg, azimuth_deg=azimuth_deg, stack=_stack_settings(stack, wavelengths),
    )  # fmt: skip
    path = get_sparameters_path(component, dirpath, **settings)
    eps_ambient = settings["stack"]["incident"]
    kx = np.sqrt(eps_ambient) * np.sin(np.radians(angle_deg))
    return _cached_model(
        path, settings, stack_sax_model(stack), wavelengths, kx, np.radians(azimuth_deg), overwrite
    )

grating_layers

grating_layers(
    grating, stack, wavelength_um, edge_width=0.0
)

:mod:~hyperbolic_optics.rcwa layers for a line grating etched into a stack.

Every patterned layer shares the grating's geometry (a single etch). Fields of grating may be JAX values, so fill and period can be optimised.

Source code in hyperbolic_optics/integrations/gdsfactory/stack.py
def grating_layers(
    grating: LineGrating,
    stack: VerticalStack | Sequence[StackLayer],
    wavelength_um: Any,
    edge_width: float = 0.0,
) -> list[rcwa.GratingLayer]:
    """:mod:`~hyperbolic_optics.rcwa` layers for a line grating etched into a stack.

    Every patterned layer shares the grating's geometry (a single etch). Fields
    of ``grating`` may be JAX values, so fill and period can be optimised.
    """
    layers = stack.layers if isinstance(stack, VerticalStack) else stack
    out = []
    for spec in layers:
        eps, groove = spec.tensors(wavelength_um)
        if groove is None:
            out.append(rcwa.uniform_layer(eps, spec.thickness_um))
        else:
            out.append(
                rcwa.lamellar_layer(
                    eps,
                    groove,
                    grating.fill,
                    spec.thickness_um,
                    offset=grating.offset,
                    edge_width=edge_width,
                )
            )
    return out

grating_stack

grating_stack(
    component,
    grating_layer,
    layer_stack,
    materials=None,
    ambient=1.0,
    samples=3,
)

A line grating and the patterned stack its etch makes in the PDK's LayerStack.

The stack is sampled at samples points across a ridge and across a gap, in two periods; every ridge sample must agree, and every gap sample, or the layout is not a single line grating in this stack and a ValueError says so. Where the ridge and gap columns differ, the layer is patterned.

Source code in hyperbolic_optics/integrations/gdsfactory/stack.py
def grating_stack(
    component: gf.Component,
    grating_layer: gf.typings.LayerSpec,
    layer_stack: LayerStack,
    materials: Mapping[str, MaterialSpec] | None = None,
    ambient: MaterialSpec = 1.0,
    samples: int = 3,
) -> tuple[LineGrating, VerticalStack]:
    """A line grating and the patterned stack its etch makes in the PDK's LayerStack.

    The stack is sampled at ``samples`` points across a ridge and across a gap,
    in two periods; every ridge sample must agree, and every gap sample, or the
    layout is not a single line grating in this stack and a ``ValueError`` says
    so. Where the ridge and gap columns differ, the layer is patterned.
    """
    grating = extract_line_grating(component, grating_layer)
    period, width = grating.period_um, grating.fill * grating.period_um
    x0, y0 = grating.origin_um
    y = y0 + grating.length_um / 2
    periods = sorted({0, grating.n_periods // 2})

    def column(x_in_period, span):
        profiles = [
            tuple(vertical_profile(component, layer_stack, (x0 + k * period + x_in_period(i), y)))
            for k in periods
            for i in range(samples)
        ]
        if len(set(profiles)) != 1:
            raise ValueError(
                f"The stack varies within the {span} of the grating on {grating_layer}: "
                "another layer is patterned differently there, so this is not a single "
                "line grating in this layer stack."
            )
        return list(profiles[0])

    ridge = column(lambda i: width * (i + 0.5) / samples, "ridges")
    gap = column(lambda i: width + (period - width) * (i + 0.5) / samples, "gaps")

    return grating, patterned_stack(ridge, gap, materials, ambient)

vertical_stack

vertical_stack(
    component,
    layer_stack,
    point_um,
    materials=None,
    ambient=1.0,
)

The unpatterned stack at point_um: ambient above, lowest level as the exit.

Parameters:

  • component (Component) –

    The layout.

  • layer_stack (LayerStack) –

    The PDK's LayerStack (e.g. gf.get_active_pdk().layer_stack).

  • point_um (tuple[float, float]) –

    Layout position (x, y) in µm.

  • materials (Mapping[str, MaterialSpec] | None, default: None ) –

    PDK material name → specification, over :data:~hyperbolic_optics.integrations.gdsfactory.materials.DEFAULT_MATERIALS.

  • ambient (MaterialSpec, default: 1.0 ) –

    The incident half-space above the stack.

Source code in hyperbolic_optics/integrations/gdsfactory/stack.py
def vertical_stack(
    component: gf.Component,
    layer_stack: LayerStack,
    point_um: tuple[float, float],
    materials: Mapping[str, MaterialSpec] | None = None,
    ambient: MaterialSpec = 1.0,
) -> VerticalStack:
    """The unpatterned stack at ``point_um``: ambient above, lowest level as the exit.

    Args:
        component: The layout.
        layer_stack: The PDK's ``LayerStack`` (e.g. ``gf.get_active_pdk().layer_stack``).
        point_um: Layout position ``(x, y)`` in µm.
        materials: PDK material name → specification, over
            :data:`~hyperbolic_optics.integrations.gdsfactory.materials.DEFAULT_MATERIALS`.
        ambient: The incident half-space above the stack.
    """
    profile = vertical_profile(component, layer_stack, point_um)
    exit_material = resolve_material(profile[0].material, materials)
    layers = tuple(
        StackLayer(
            thickness_um=interval.zmax - interval.zmin,
            material=resolve_material(interval.material, materials),
            name=interval.level,
        )
        for interval in reversed(profile[1:])
    )
    return VerticalStack(layers=layers, incident=ambient, exit=exit_material)