Layout Frontends
================
A **frontend** is the layer that turns a *layout*, however you drew it, into
the engine-agnostic :class:`~gds_fdtd.geometry.Component` that every solver
consumes. It reads the polygons on each technology layer, extrudes them to the
z-heights and materials the :doc:`technology` defines, and **auto-detects the
ports** from the layout's pin/DevRec conventions.
.. code-block:: text
layout source frontend Component
───────────── ──────── ─────────
gdsfactory component ─────▶ layout.gdsfactory.from_gdsfactory ─┐
SiEPIC / KLayout cell ─────▶ lyprocessor.load_cell ─┤─▶ Component ─▶ get_solver(...)
raw .gds (any tool) ─────▶ + load_component_from_tech ─┤ (polygons +
litho-predicted .gds ─────▶ lyprocessor.load_device (PreFab) ─┘ ports + bounds)
The Component is the boundary: **your frontend choice and your engine choice are
independent.** The same y-branch reaches beamz, tidy3d, and Lumerical whether it
came from gdsfactory or a foundry PDK; conversely one frontend feeds every
engine. The worked example is :doc:`_notebooks/08_frontends`.
.. figure:: images/frontends_geometry.png
:width: 100%
:align: center
One ``Component``, three frontends. Each device carries device polygons,
auto-detected ports (arrows), the DevRec bounds, and, given a
``SimulationSpec``, the FDTD region. Downstream everything is identical.
Built-in frontends
------------------
gdsfactory
^^^^^^^^^^
`gdsfactory `_ (>= 9) builds
components parametrically in Python. :func:`~gds_fdtd.layout.gdsfactory.from_gdsfactory`
reads the polygons on each technology layer and lifts the gdsfactory ports into
gds_fdtd ports:
.. code-block:: python
import gdsfactory as gf
from gds_fdtd.layout.gdsfactory import from_gdsfactory
from gds_fdtd.technology import Technology
tech = Technology.from_yaml("tech.yaml")
gf.gpdk.PDK.activate()
component = from_gdsfactory(gf.components.mmi1x2(), tech)
SiEPIC / KLayout
^^^^^^^^^^^^^^^^
A cell from a KLayout/SiEPIC foundry PDK, the path most silicon-photonics
tapeouts use. :func:`~gds_fdtd.lyprocessor.load_cell` opens the layout;
:func:`~gds_fdtd.simprocessor.load_component_from_tech` extrudes it against the
technology. Ports come from the SiEPIC pin + DevRec layers:
.. code-block:: python
import os
import siepic_ebeam_pdk as pdk
from gds_fdtd.lyprocessor import load_cell
from gds_fdtd.simprocessor import load_component_from_tech
gds = os.path.join(os.path.dirname(pdk.__file__), "gds", "EBeam", "ebeam_y_1550.gds")
cell, layout = load_cell(gds, top_cell="ebeam_y_1550")
component = load_component_from_tech(cell=cell, tech=tech)
Raw GDS
^^^^^^^
No PDK, no framework, any ``.gds`` from any tool. As long as its polygons land
on the technology's device / pin / DevRec layers, ports are detected the same
way:
.. code-block:: python
cell, layout = load_cell("my_layout.gds", top_cell="my_cell")
component = load_component_from_tech(cell=cell, tech=tech)
PreFab (lithography prediction)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`PreFab `_ predicts how a nanofabrication
process will actually print your design (corner rounding, proximity effects).
:func:`~gds_fdtd.lyprocessor.load_device` runs the model, writes a
``| _predicted.gds``, and loads it like any other GDS, so the litho-predicted
shape flows through the identical path (needs a PreFab account):
.. code-block:: python
from gds_fdtd.lyprocessor import load_device
predicted = load_device(
"my_design.gds", tech=tech, top_cell="my_cell",
prefab_model="ANT_NanoSOI_ANF1_d10", output_dir="out/",
)
What every frontend produces
----------------------------
A frontend's only job is to return a :class:`~gds_fdtd.geometry.Component` with
three things:
``structures``
A flat list of :class:`~gds_fdtd.geometry.Structure`, each a 2-D polygon
extruded in z, tagged by **role**: ``"device"`` (your patterned layers),
``"substrate"``, and ``"superstrate"`` (the backgrounds, filled from the
technology). Each carries its GDS ``layer``, z-extent, and the resolved
``material``.
``ports``
A list of :class:`~gds_fdtd.geometry.Port`, each with a name, µm ``center``
``[x, y, z]``, ``width``, and a snapped ``direction`` (0/90/180/270°). These
are where the solver launches and collects modes.
``bounds``
A :class:`~gds_fdtd.geometry.Region`, the simulation extent (from the DevRec
layer for the GDS frontends).
Write your own frontend
-----------------------
To support a tool that is not listed above (a different layout library, a
procedural generator, an in-house format), write a function that produces the
three objects above. The two reference implementations to copy are
:func:`~gds_fdtd.simprocessor.load_component_from_tech` and
:func:`~gds_fdtd.layout.gdsfactory.from_gdsfactory`.
The example below builds a 5 µm straight waveguide entirely by hand, no GDS, no
layout library, and it runs on any engine. It uses the technology's own
z-heights and materials (via :meth:`~gds_fdtd.technology.Technology.to_solver_dict`)
so a custom frontend needs to supply only the polygons and ports:
.. code-block:: python
from gds_fdtd.geometry import Component, Port, Region, Structure
from gds_fdtd.technology import Technology
from gds_fdtd.solvers import get_solver
from gds_fdtd.spec import SimulationSpec
tech = Technology.from_yaml("tech.yaml")
layers = tech.to_solver_dict() # device / substrate / superstrate as dicts
dev, sub, sup = layers["device"][0], layers["substrate"][0], layers["superstrate"][0]
z_center = dev["z_base"] + dev["z_span"] / 2
background = [[-2.0, -3.0], [7.0, -3.0], [7.0, 3.0], [-2.0, 3.0]]
core = [[0.0, -0.25], [5.0, -0.25], [5.0, 0.25], [0.0, 0.25]] # 5 x 0.5 um strip
structures = [
Structure(name="core", polygon=core, z_base=dev["z_base"], z_span=dev["z_span"],
material=dev["material"], layer=list(dev["layer"])), # role="device" (default)
Structure(name="sub", polygon=background, z_base=sub["z_base"], z_span=sub["z_span"],
material=sub["material"], role="substrate"),
Structure(name="sup", polygon=background, z_base=sup["z_base"], z_span=sup["z_span"],
material=sup["material"], role="superstrate"),
]
ports = [
Port(name="opt1", center=[0.0, 0.0, z_center], width=0.5, direction=180),
Port(name="opt2", center=[5.0, 0.0, z_center], width=0.5, direction=0),
]
component = Component(name="hand_built", structures=structures, ports=ports,
bounds=Region(vertices=background, z_center=z_center, z_span=2.0))
solver = get_solver("beamz")(component, tech, SimulationSpec(mesh=5, z_min=-1.0, z_max=1.11))
assert solver.validate() == [] # runs like any other Component
The ``material`` passed to each ``Structure`` is the neutral material mapping the
technology resolves (constant ``nk``, a ``rii`` reference, or engine hints); the
solver picks the right optical-constant source per engine, see :doc:`technology`.
.. seealso::
- :doc:`_notebooks/08_frontends`, the same three frontends, executed, plus
the full frontend × engine matrix.
- :doc:`technology`, the layer stack and materials each frontend extrudes
against.
- :mod:`gds_fdtd.geometry`, the ``Component`` / ``Structure`` / ``Port`` API.
|