Skip to content

PDK

pdk

Component-to-circuit design flow (an open process design kit).

  • :mod:.technology: layer stacks as data; :func:generic_soi is a generic 220 nm SOI stack built from open material data only.
  • :mod:.cells: parametric components (layout polygons + ports + simulation recipe).
  • :mod:.component: :class:Component, GDS output, device problems at the port reference planes, and :class:Assembly for hierarchical layouts.
  • :mod:.flow: simulate a component (dispatching through :func:lightsmith.solve), fit a compact model of its S-parameters, cache it by parameter hash, and use it in circuit netlists as pdk:<cell>.

CELLS module-attribute

CELLS: dict[str, CellSpec] = {}

Assembly

Assembly()

Place components by connecting their ports; produce a hierarchical :class:Component.

asm.add("mmi_in", mmi) places a first instance; asm.connect("arm", bend, "o1", "mmi_in,o2") moves bend so that its port o1 meets mmi_in's o2 (facing it) and records the connection; asm.expose("in", "mmi_in,o1") makes an external port. asm.build(kind, settings) returns the composite component whose circuit model is the netlist of its parts.

instances instance-attribute

instances: dict[str, Instance] = {}

connections instance-attribute

connections: list[tuple[str, str]] = []

exposed instance-attribute

exposed: dict[str, str] = {}

add

add(name: str, component: Component, origin=(0.0, 0.0), rotation_deg: float = 0.0, mirror: bool = False) -> Instance

port

port(ref: str) -> CPort

connect

connect(name: str, component: Component, port: str, to: str, mirror: bool = False) -> Instance

Add component with its port attached to the placed port to ('instance,port').

link(a: str, b: str, tol_nm: float = 1.0) -> None

Record a connection between two already placed ports (they must coincide and face each other).

expose

expose(name: str, ref: str) -> None

build

build(kind: str, settings: dict[str, Any]) -> Component

Component dataclass

Component(kind: str, settings: dict[str, Any], polygons: dict[str, list[ndarray]] = dict(), ports: list[CPort] = list(), instances: list[Instance] = list(), netlist: dict[str, Any] | None = None, model: str = 'simulate', sim: dict[str, Any] = dict())

Layout + ports + parameters of one cell.

kind is the registry name of the parametric cell (mmi1x2, ...) and settings its validated parameters; together they identify the circuit model pdk:<kind>. Composite components (from :class:Assembly) also carry instances and a netlist of their parts.

kind instance-attribute

kind: str

settings instance-attribute

settings: dict[str, Any]

polygons class-attribute instance-attribute

polygons: dict[str, list[ndarray]] = field(default_factory=dict)

ports class-attribute instance-attribute

ports: list[CPort] = field(default_factory=list)

instances class-attribute instance-attribute

instances: list[Instance] = field(default_factory=list)

netlist class-attribute instance-attribute

netlist: dict[str, Any] | None = None

model class-attribute instance-attribute

model: str = 'simulate'

sim class-attribute instance-attribute

sim: dict[str, Any] = field(default_factory=dict)

cell_name property

cell_name: str

port_names property

port_names: list[str]

port

port(name: str) -> CPort

add_polygon

add_polygon(layer: str, pts) -> None

flat_polygons

flat_polygons() -> dict[str, list[ndarray]]

All polygons including those of placed instances, in this component's coordinates.

bbox

bbox() -> tuple[ndarray, ndarray]

gds_cells

gds_cells(tech: Technology | str | None = None) -> dict[str, dict[str, Any]]

Cells for :func:lightsmith.layout.write_gds: this cell plus every distinct child cell.

write_gds

write_gds(path: str | Path, tech: Technology | str | None = None) -> Path

Write this component (hierarchically) to a GDSII file; the top cell is cell_name.

device_problem

device_problem(tech: Technology | str | None = None, wavelengths: Wavelengths | None = None, *, dimension: int = 2, options: dict[str, dict[str, Any]] | None = None, port_margin_nm: float = 1500.0, padding_nm: float = 1500.0) -> DeviceProblem

A :class:DeviceProblem of this component with ports just outside its reference planes.

dimension=2: plan view (effective-index materials, structures in xy); dimension=3: polygons extruded with the technology's z ranges. Each port gets a straight input stub reaching through the absorbing boundary, and a mode window of width + 2 * port_margin_nm (narrowed so that windows of neighbouring ports on the same plane do not overlap). Default solver options of the component are merged under options. Solver ports sit PORT_OFFSET_NM outside the component's ports; :func:lightsmith.pdk.flow.simulate moves S back onto the component's ports.

CPort dataclass

CPort(name: str, center: tuple[float, float], orientation_deg: float, width_nm: float, layer: str = 'WG')

An optical port of a component: reference plane centre, outward orientation, waveguide width.

name instance-attribute

name: str

center instance-attribute

center: tuple[float, float]

orientation_deg instance-attribute

orientation_deg: float

width_nm instance-attribute

width_nm: float

layer class-attribute instance-attribute

layer: str = 'WG'

normal_axis property

normal_axis: int

outward property

outward: ndarray

transformed

transformed(t: 'Transform', name: str | None = None) -> 'CPort'

Transform dataclass

Transform(origin: tuple[float, float] = (0.0, 0.0), rotation_deg: float = 0.0, mirror: bool = False)

Mirror about the x axis (optional), then rotate about the origin, then translate (GDS order).

origin class-attribute instance-attribute

origin: tuple[float, float] = (0.0, 0.0)

rotation_deg class-attribute instance-attribute

rotation_deg: float = 0.0

mirror class-attribute instance-attribute

mirror: bool = False

apply

apply(pts: ndarray) -> ndarray

LayerDef

Bases: _Model

One GDS layer of a technology.

gds class-attribute instance-attribute

gds: tuple[int, int] = Field(description='(layer, datatype).')

kind class-attribute instance-attribute

kind: Literal['material', 'etch', 'drawing'] = Field('material', description='material: polygons are filled with `material` over z_min..z_max; etch: polygons remove the core over z_min..z_max (filled with the cladding); drawing: no optical effect (floorplan, labels).')

material class-attribute instance-attribute

material: Material | None = None

z_min_nm class-attribute instance-attribute

z_min_nm: float | None = None

z_max_nm class-attribute instance-attribute

z_max_nm: float | None = None

sidewall_angle_deg class-attribute instance-attribute

sidewall_angle_deg: float = Field(0.0, description='0 = vertical; positive = narrower at the top.')

description class-attribute instance-attribute

description: str = ''

Technology

Bases: _Model

A planar photonic technology: patterned layers on a buried oxide, under a cladding.

z = 0 is the top of the buried oxide (bottom of the device layer). Layers are listed in override order: later layers win where polygons overlap (so an etch layer listed after the core removes part of it).

name instance-attribute

name: str

description class-attribute instance-attribute

description: str = ''

layers class-attribute instance-attribute

layers: dict[str, LayerDef] = Field(description='Layer name -> definition, in override order.')

core_layer class-attribute instance-attribute

core_layer: str = Field('WG', description='Layer of the waveguide core (sets ports and mode solves).')

cladding class-attribute instance-attribute

cladding: Material = Field(description='Material above and around the patterned layers.')

box class-attribute instance-attribute

box: Material = Field(description='Buried oxide below z = 0.')

box_thickness_nm class-attribute instance-attribute

box_thickness_nm: float = Field(2000.0, gt=0)

substrate class-attribute instance-attribute

substrate: Material | None = Field(None, description='Handle wafer below the buried oxide (None = omit).')

wg_width_nm class-attribute instance-attribute

wg_width_nm: float = Field(500.0, gt=0, description='Default single-mode waveguide width.')

min_feature_nm class-attribute instance-attribute

min_feature_nm: float = Field(100.0, gt=0, description='Smallest drawn width or gap the components use.')

min_bend_radius_nm class-attribute instance-attribute

min_bend_radius_nm: float = Field(2000.0, gt=0)

polarization class-attribute instance-attribute

polarization: Literal['te', 'tm'] = Field('te', description='Mode family: te = E mostly in the wafer plane.')

sources class-attribute instance-attribute

sources: list[str] = Field(default_factory=list, description='Where the numbers come from (open data only).')

core property

core: LayerDef

layer

layer(name: str) -> LayerDef

gds_layer

gds_layer(name: str) -> tuple[int, int]

layer_map

layer_map() -> dict[tuple[int, int], dict[str, Any]]

GDS layer -> {material, z_min_nm, z_max_nm, ...} for :mod:lightsmith.layout (3D extrusion).

Etch layers are filled with the cladding, which overrides the core where they overlap.

core_thickness_nm

core_thickness_nm(layer: str | None = None) -> float

Thickness of core material left where layer is drawn (an etch layer thins the core).

slab_neff

slab_neff(thickness_nm: float, wavelengths_nm, layer: str | None = None, order: int = 0) -> ndarray

Effective index of the box / core(thickness) / cladding slab, for this technology's polarization.

eim_material

eim_material(layer: str, wavelengths_nm, step_nm: float = 2.0) -> Material

2D material for regions drawn on layer: the slab effective index, tabulated over the wavelengths.

eim_cladding

eim_cladding(wavelengths_nm) -> Material

2D background material: the cladding (no core drawn).

waveguide_neff

waveguide_neff(wavelengths_nm, width_nm: float | None = None, dimension: int = 2, mode: int = 0, layer: str | None = None) -> ndarray

Effective index of a straight waveguide of width_nm on layer (default: the core, default width).

dimension=2: symmetric slab of the EIM core index in the cladding, E in plane (matches 2D EME/FDFD with polarization 'te'); dimension=3: full-vector FDE of the extruded cross-section.

group_index

group_index(wavelengths_nm, width_nm: float | None = None, dimension: int = 2, layer: str | None = None, dl_nm: float = 1.0) -> ndarray

ng = neff - lambda dneff/dlambda (central difference).

fingerprint

fingerprint() -> str

Stable JSON of the whole definition (used in model cache keys).

cell

cell(name: str, params: type[CellParams], ports: tuple[str, ...])

Register a parametric cell fn(params) -> Component under name.

get_component

get_component(name: str, **settings) -> Component

Build a registered component, e.g. get_component("mmi1x2", length_mmi_nm=6000).

list_components

list_components() -> list[dict[str, Any]]

generic_soi

generic_soi() -> Technology

Generic 220 nm SOI: Si device layer on 2 um buried oxide with oxide cladding (open data only).

get_technology

get_technology(tech: str | dict | Technology | None = None) -> Technology

A technology by name, JSON dict, file path (.json) or instance; None = generic_soi.

list_technologies

list_technologies() -> list[str]

load_technology

load_technology(path: str | Path) -> Technology

Read a technology from a JSON file (the output of tech.model_dump_json()).

register_technology

register_technology(tech: Technology, overwrite: bool = False) -> Technology

Make tech available by name (e.g. in circuit netlists: settings.technology).

flow

Component -> simulation -> fitted compact model -> circuit netlists.

simulate(component) builds the component's :class:DeviceProblem and solves it with :func:lightsmith.solve (method="auto" walks AUTO_ORDER, so EME, FDFD or any later device backend is chosen by what the device needs), then moves FDFD's grid-snapped port planes back onto the component's reference planes. Straight waveguides use a mode solve instead, and composite components (rings, the MZI) evaluate the circuit of their parts.

component_model(...) fits the sampled S-parameters (:mod:.fitting) and caches the result on disk under a hash of everything that determines it (cell, parameters, technology, wavelengths, solver choice and options).

In a circuit netlist, {"component": "pdk:mmi1x2", "settings": {"length_mmi_nm": 4300}} instantiates that model; the settings are the cell's parameters plus :class:FlowSettings (technology, simulated band, solver, fit). Missing models are simulated on first use.

SCHEMA module-attribute

SCHEMA = 'lightsmith-pdk-model-1'

FLOW_FIELDS module-attribute

FLOW_FIELDS = tuple(FlowSettings.model_fields)

FlowSettings

Bases: ModelSettings

How a pdk:<cell> circuit model is obtained (the rest of its settings are the cell parameters).

technology class-attribute instance-attribute

technology: str | dict[str, Any] = Field('generic_soi', description='Registered technology name or its JSON.')

wl_min_nm class-attribute instance-attribute

wl_min_nm: float = Field(1500.0, gt=0, description='Simulated band (the circuit must stay inside it).')

wl_max_nm class-attribute instance-attribute

wl_max_nm: float = Field(1600.0, gt=0)

num_points class-attribute instance-attribute

num_points: int = Field(11, ge=1, description='Simulated wavelengths across the band.')

method class-attribute instance-attribute

method: str = Field('auto', description="Device solver (auto = first applicable in AUTO_ORDER['device']).")

device class-attribute instance-attribute

device: str = Field('auto', description='Where to simulate (not part of the cache key).')

dimension class-attribute instance-attribute

dimension: Literal[2, 3] = Field(2, description='2 = plan view with effective indices; 3 = full 3D.')

waveguide_model class-attribute instance-attribute

waveguide_model: Literal['mode', 'device'] = Field('mode', description='Straights: S from a mode solve (exact phase, any length), or a device solve with `method` (same discretisation as a direct simulation of a larger layout).')

fit class-attribute instance-attribute

fit: Literal['auto', 'pole_residue', 'table'] = 'auto'

fit_tol class-attribute instance-attribute

fit_tol: float = Field(0.005, gt=0, description='Max |S_fit - S| on held-out samples for a pole-residue fit.')

sim_options class-attribute instance-attribute

sim_options: dict[str, dict[str, Any]] = Field(default_factory=dict, description='Per-method solver options.')

cache class-attribute instance-attribute

cache: bool = Field(True, description='Read/write the on-disk model cache.')

wavelengths

wavelengths() -> list[float]

FittedModel

FittedModel(kind: str, settings: dict, technology: str, key: str, ports: list[str], result: SParameterResult, fit: SFit)

A cached compact model: sampled S-parameters, the fit, and where they came from.

wavelengths_nm property

wavelengths_nm: ndarray

neff_poly property

neff_poly: ndarray | None

Polynomial neff(lambda) (degree <= 3) for mode-solved straights, else None.

evaluate

evaluate(wavelengths_nm) -> ndarray

to_json

to_json() -> str

from_json classmethod

from_json(text: str) -> 'FittedModel'

summary

summary() -> dict[str, Any]

simulate

simulate(component: Component | str, tech: Technology | str | dict | None = None, wavelengths: Wavelengths | None = None, *, method: str = 'auto', device: str = 'auto', dimension: int = 2, options: dict[str, dict[str, Any]] | None = None, direct: bool = False, **settings) -> SParameterResult

S-parameters of a component at its port reference planes.

component is a :class:Component or a cell name (with settings). By default straights use a mode solve and composites the circuit of their parts; direct=True solves the whole layout as one device. method/device/options go to :func:lightsmith.solve (options per method, e.g. {"fdfd": {"dl_nm": 25}}).

cache_dir

cache_dir() -> Path

Model cache directory: LIGHTSMITH_PDK_CACHE, else <lightsmith cache>/pdk.

model_key

model_key(kind: str, settings: dict[str, Any], tech: Technology, flow: FlowSettings) -> str

component_model

component_model(cell: str | Component, flow: FlowSettings | dict | None = None, *, refresh: bool = False, **settings) -> FittedModel

The fitted compact model of a cell (simulating it if no cached model matches).

component_model("mmi1x2", length_mmi_nm=4300) or component_model(comp, flow={"num_points": 21}).

clear_memory_cache

clear_memory_cache() -> None

split_settings

split_settings(values: dict[str, Any]) -> tuple[dict[str, Any], FlowSettings]

netlist_problem

netlist_problem(comp: Component, wavelengths: Wavelengths, flow: FlowSettings | dict | None = None, port_names: dict[str, str] | None = None) -> CircuitProblem

The circuit of a composite component's parts, every pdk: part using flow.

instance

instance(cell: str, flow: FlowSettings | dict | None = None, **settings) -> Instance

A netlist instance pdk:<cell> with validated cell parameters and flow settings.

pdk_model

pdk_model(cell: str) -> CompactModel

The circuit :class:CompactModel behind pdk:<cell> (used by circuits.models.get_model).

fsr_estimate

fsr_estimate(tech: Technology, length_nm: float, wavelength_nm: float = 1550.0, width_nm: float | None = None, dimension: int = 2) -> float

lambda^2 / (ng L) with the technology's group index.