Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,587 changes: 4,587 additions & 0 deletions nbs/palace_2d_twmzm.ipynb

Large diffs are not rendered by default.

679 changes: 679 additions & 0 deletions nbs/palace_2d_twmzm.py

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions output/palace/palace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"GitTag": "v0.16.1-51-g4f2e2d97",
"Problem": {
"MPISize": 4,
"OpenMPThreads": 1
}
}
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,19 @@ dependencies = [
# GitHub Releases. Currently only Linux x86_64 wheels are available, so
# the extra is platform-marked accordingly.
palace-toolkit-cpu = [
"palacetoolkit-palace-cpu @ https://github.com/EpsilonForge/PalaceToolkit/releases/download/palace-cpu-v0.1.2/palacetoolkit_palace_cpu-0.1.0-py3-none-linux_x86_64.whl ; sys_platform == 'linux' and platform_machine == 'x86_64'",
"palacetoolkit-palace-cpu @ https://github.com/EpsilonForge/PalaceToolkit/releases/download/palace-cpu-v0.17.0/palacetoolkit_palace_cpu-0.17.0-py3-none-linux_x86_64.whl ; sys_platform == 'linux' and platform_machine == 'x86_64'",
]
# Interactive 3-D visualization (PyVista + trame server rendering) with the
# extra packages needed for reliable headless and notebook use.
visualize = [
"jupyter-server-proxy>=4.1.0",
"nest-asyncio",
"pyvirtualdisplay>=3.0.0",
"trame>=3.6.0,<4",
"trame-client>=3.6.0,<4",
"trame-server>=3.6.0,<4",
"trame-vtk>=2.8.0,<3",
"trame-vuetify>=2.7.0,<3",
]

[dependency-groups]
Expand Down
256 changes: 253 additions & 3 deletions src/gsim/common/cross_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
from __future__ import annotations

import logging
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal, overload
from typing import TYPE_CHECKING, Any, Literal, overload

logger = logging.getLogger(__name__)
from gsim.common.stack import Layer, LayerStack, get_stack, make_doped_materials

if TYPE_CHECKING:
import gdsfactory as gf

from gsim.common.stack import LayerStack
logger = logging.getLogger(__name__)


@dataclass(frozen=True)
Expand Down Expand Up @@ -213,6 +214,253 @@ def extract_plane_section(
)


def build_doped_cross_section(
component: gf.Component,
*,
axis: Literal["x", "y", "z"],
value: float,
substrate_thickness: float,
include_substrate: bool = False,
doping: Mapping[str, Mapping[str, Any]] | None = None,
metal1: tuple[float, float] | None = None,
rib_layers: Iterable[tuple[str, tuple[int, int], float]] = (),
rib_height: float = 0.22,
permittivity: float = 11.9,
fmax: float = 200e9,
verbose: bool = True,
) -> tuple[LayerStack, list[Rect2D] | list[RectYZ2D] | list[PolygonXY2D]]:
"""Assemble a doped ``LayerStack`` and extract a 2D plane section from it.

Builds the base PDK stack, optionally overrides the ``metal1`` electrodes,
registers the gradient-doping layers/materials produced by
:func:`gsim.common.stack.doping.make_doping_profile` plus any additional rib
doping layers (e.g. a PN junction), and slices the component at the requested
plane.

Args:
component: gdsfactory component the cross-section is extracted from.
axis: Cross-section normal axis.
value: Plane coordinate in um.
substrate_thickness: Thickness below z=0 in um.
include_substrate: Include the lossy silicon substrate.
doping: Output of ``make_doping_profile`` (keys ``layer_specs`` and
``materials``), or ``None`` to skip gradient doping.
metal1: ``(zmin, thickness)`` override for the ``metal1`` layer, or
``None`` to keep the PDK value.
rib_layers: Sequence of ``(name, gds_layer, sigma)`` for extra rib doping
regions (e.g. PN junction) added on top of the gradient profile.
rib_height: Z-extent (z=0..rib_height) of the rib doping regions (um).
permittivity: Relative permittivity of the doping materials.
fmax: Upper frequency of the doping dispersion-model validity (Hz).
verbose: Print the assembled stack and the extracted section.

Returns:
``(stack, section)`` with the populated ``LayerStack`` and the list of
``Rect2D`` / ``RectYZ2D`` / ``PolygonXY2D`` regions at the plane.
"""
stack = get_stack(
substrate_thickness=substrate_thickness,
include_substrate=include_substrate,
)

if metal1 is not None:
metal1_zmin, metal1_thickness = metal1
m1 = stack.layers.get("metal1")
if m1:
m1.zmin = metal1_zmin
m1.zmax = metal1_zmin + metal1_thickness
m1.thickness = metal1_thickness
if verbose:
logger.info(
"M1 updated: zmin=%s, zmax=%s, thickness=%s",
m1.zmin,
m1.zmax,
m1.thickness,
)

layer_specs: dict[str, Any] = {}
materials: dict[str, Any] = {}
if doping:
layer_specs.update(doping.get("layer_specs", {}))
materials.update(doping.get("materials", {}))

rib_materials = make_doped_materials(
[(name, sigma) for name, _gds, sigma in rib_layers],
permittivity=permittivity,
fmax=fmax,
)
materials.update(rib_materials)
for name, gds_layer, _sigma in rib_layers:
layer_specs[name] = Layer(
name=name,
gds_layer=gds_layer,
zmin=0.0,
zmax=rib_height,
thickness=rib_height,
material=name,
layer_type="dielectric",
mesh_resolution="fine",
)

for name, layer in layer_specs.items():
stack.layers[name] = layer

section = extract_plane_section(
component.copy(),
stack,
axis=axis,
value=value,
)

if verbose:
logger.info("Stack: %s", stack.pdk_name)
logger.info("Layers: %s", sorted(stack.layers.keys()))
logger.info("Cross materials: %s", sorted(materials.keys()))
logger.info("Dielectrics: %s", stack.dielectrics)
logger.info("")
logger.info(
"Cross-section %s=%s intersects %s layer regions:",
axis,
value,
len(section),
)
for r in section:
lo = getattr(r, "y0", getattr(r, "x0", None))
hi = getattr(r, "y1", getattr(r, "x1", None))
zlo = getattr(r, "zmin", None)
zhi = getattr(r, "zmax", None)
if zlo is None:
logger.info(" %-12s material=%-10s", r.layer_name, r.material)
else:
logger.info(
" %-12s material=%-10s y=[%8.3f, %8.3f] z=[%6.3f, %6.3f]",
r.layer_name,
r.material,
lo,
hi,
zlo,
zhi,
)

return stack, section


def build_optical_cross_section(
component: gf.Component,
*,
axis: Literal["x", "y", "z"],
value: float,
device_layers: Mapping[str, tuple[tuple[int, int], float, float]],
substrate_thickness: float = 2.0,
cladding_top: float = 2.0,
device_material: str = "si",
cladding_material: str = "sio2",
mesh_resolution: str | float = "fine",
verbose: bool = True,
) -> tuple[LayerStack, list[Rect2D] | list[RectYZ2D] | list[PolygonXY2D]]:
"""Assemble a minimal all-dielectric ``LayerStack`` and extract a plane section.

Builds a photonic ``LayerStack`` for a simplified device — e.g. a rib +
slab + PN junction made of a single semiconductor — sitting in a uniform
cladding background. Unlike :func:`build_doped_cross_section`, every device
region is mapped to the same plain dielectric material (e.g. ``"si"``) and
no electrodes, vias, or graded doping are registered. A single ``oxide``
dielectric slab (the cladding material) spans the full stack z-range, so the
simulation domain is a uniform cladding with only the drawn device embedded
in it.

Args:
component: gdsfactory component the cross-section is extracted from.
axis: Cross-section normal axis.
value: Plane coordinate in um.
device_layers: Mapping of ``name -> (gds_layer, zmin, zmax)`` for every
patterned device region (e.g. ``{"core": ((1, 0), 0.0, 0.22)}``).
All regions share ``device_material``.
substrate_thickness: Cladding thickness below z=0 in um.
cladding_top: Cladding thickness above z=0 in um.
device_material: Material name for all device regions (default ``"si"``).
cladding_material: Material name of the uniform background (default
``"sio2"``).
mesh_resolution: Mesh resolution assigned to the device ``Layer`` specs.
verbose: Print the assembled stack and the extracted section.

Returns:
``(stack, section)`` with the populated ``LayerStack`` (device layers +
uniform cladding dielectric + materials) and the list of ``Rect2D`` /
``RectYZ2D`` / ``PolygonXY2D`` regions at the plane.
"""
from gsim.common.stack.extractor import Layer, LayerStack
from gsim.common.stack.materials import get_material_properties

stack = LayerStack(pdk_name="optical")

for name, (gds_layer, zmin, zmax) in device_layers.items():
stack.layers[name] = Layer(
name=name,
gds_layer=gds_layer,
zmin=zmin,
zmax=zmax,
thickness=zmax - zmin,
material=device_material,
layer_type="dielectric",
mesh_resolution=mesh_resolution,
)

stack.dielectrics.append(
{
"name": "oxide",
"zmin": -substrate_thickness,
"zmax": cladding_top,
"material": cladding_material,
}
)

for material in (device_material, cladding_material):
props = get_material_properties(material)
if props is not None:
stack.materials[material] = props.to_dict()

section = extract_plane_section(
component.copy(),
stack,
axis=axis,
value=value,
)

if verbose:
logger.info("Stack: %s", stack.pdk_name)
logger.info("Layers: %s", sorted(stack.layers.keys()))
logger.info("Device material: %s", device_material)
logger.info("Cladding material: %s", cladding_material)
logger.info("Dielectrics: %s", stack.dielectrics)
logger.info("")
logger.info(
"Cross-section %s=%s intersects %s layer regions:",
axis,
value,
len(section),
)
for r in section:
lo = getattr(r, "y0", getattr(r, "x0", None))
hi = getattr(r, "y1", getattr(r, "x1", None))
zlo = getattr(r, "zmin", None)
zhi = getattr(r, "zmax", None)
if zlo is None:
logger.info(" %-12s material=%-10s", r.layer_name, r.material)
else:
logger.info(
" %-12s material=%-10s y=[%8.3f, %8.3f] z=[%6.3f, %6.3f]",
r.layer_name,
r.material,
lo,
hi,
zlo,
zhi,
)

return stack, section


@overload
def _extract_line_slice_rectangles(
component,
Expand Down Expand Up @@ -436,6 +684,8 @@ def _line_intervals(
"PolygonXY2D",
"Rect2D",
"RectYZ2D",
"build_doped_cross_section",
"build_optical_cross_section",
"extract_plane_section",
"extract_xy_polygons",
"extract_xz_rectangles",
Expand Down
6 changes: 6 additions & 0 deletions src/gsim/common/stack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import gdsfactory as gf
import yaml

from gsim.common.stack.doping import make_doping_profile
from gsim.common.stack.extractor import (
Layer,
LayerStack,
Expand All @@ -39,6 +40,8 @@
SellmeierTerm,
ValidityRange,
get_material_properties,
make_doped_material,
make_doped_materials,
resolve_material_at_wavelength,
should_enable_dispersion,
)
Expand Down Expand Up @@ -180,6 +183,9 @@ def load_stack_yaml(yaml_path: str | Path) -> LayerStack:
"get_stack",
"load_overlay",
"load_stack_yaml",
"make_doped_material",
"make_doped_materials",
"make_doping_profile",
"merge_overlay",
"parse_layer_stack",
"plot_stack",
Expand Down
Loading
Loading