Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ with minimal boilerplate.

| Module | Solver | Method | Use Case |
| ------------- | ------------------------------------------- | ------ | ------------------------------------------------------ |
| `gsim.fdtd` | GDSFactory FDTD | FDTD | PDK-native mesh and runtime configuration generation |
| `gsim.palace` | [Palace](https://awslabs.github.io/palace/) | FEM | RF/microwave, impedance extraction, driven simulations |
| `gsim.meep` | [Meep](https://meep.readthedocs.io/) | FDTD | Photonic components, S-parameters, mode propagation |

Expand Down
75 changes: 75 additions & 0 deletions docs/api/fdtd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# GDSFactory FDTD API

`gsim.fdtd` generates the coarse tetrahedral mesh and validated `config.json`
consumed by GDSFactory FDTD. The backend voxelizes this mesh onto its own Yee
grid, so the Gmsh mesh does not need to resolve the electromagnetic fields.

## PDK-native workflow

Pass the PDK module when it exposes project-level `MATERIAL_CARDS`; otherwise,
pass a PDK object or use the active PDK. Material names are resolved exactly,
using the project's cards first and gsim's built-in cards as fallbacks.

```python
import gpdk

from gsim import fdtd

simulation = fdtd.Simulation(pdk=gpdk)
simulation.geometry("mmi1x2")
artifacts = simulation.write("fdtd_output")

print(artifacts.mesh_path) # fdtd_output/mesh.msh
print(artifacts.config_path) # fdtd_output/config.json
```

The generated mesh is ASCII Gmsh MSH 2.2 with linear tetrahedra for material
regions and linear triangles for `port_<name>` groups. Geometry and wavelength
values in the artifacts are in nanometers. PML extrusion is left to GDSFactory
FDTD.

## Initial geometry limits

The backend supports disconnected polygons, polygon holes, axis-aligned guided
ports, and vertical or constant-angle sidewalls. Tapered layers use a small
number of midpoint-sampled prisms selected from the Yee-cell size, keeping the
lateral approximation error below one quarter cell while leaving field
resolution to the backend voxelizer.

Vertical `vertical_te` and `vertical_tm` ports are free-space apertures rather
than material-owned eigenmode ports. By default a vertical port becomes a
plane/fiber monitor while the first guided port is excited. Select the vertical
port explicitly to generate a Gaussian-beam source:

```python
simulation = fdtd.Simulation(pdk=gpdk, default_port="o2")
simulation.geometry("grating_coupler_elliptical")
simulation.write("fdtd_output/grating")
```

The aperture defaults to a square using the port width, top-facing `+z`, with a
beam waist equal to half the aperture width. Override these policies with
`vertical_port_axis`, `vertical_port_aperture_width_um`, and
`vertical_port_waist_radius_um`.

The initial implementation rejects unsupported `bias`/`z_to_bias` profiles and
lossy material snapshots because config schema version 1 accepts only real
scalar refractive indices.

## Reference

::: gsim.fdtd.Simulation
options:
show_source: false

::: gsim.fdtd.SimulationArtifacts
options:
show_source: false

::: gsim.fdtd.MeshManifest
options:
show_source: false

::: gsim.fdtd.FDTDConfig
options:
show_source: false
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ with minimal boilerplate.

| Module | Solver | Method | Use Case |
| ------------- | ------------------------------------------- | ------ | ------------------------------------------------------ |
| `gsim.fdtd` | GDSFactory FDTD | FDTD | PDK-native mesh and runtime configuration generation |
| `gsim.palace` | [Palace](https://awslabs.github.io/palace/) | FEM | RF/microwave, impedance extraction, driven simulations |
| `gsim.meep` | [Meep](https://meep.readthedocs.io/) | FDTD | Photonic components, S-parameters, mode propagation |

Expand Down Expand Up @@ -62,5 +63,5 @@ result = sim.run()

## API Reference

See the API docs for full details: [Palace](api/palace.md), [Meep](api/meep.md), [Common](api/common.md),
[Cloud](api/cloud.md).
See the API docs for full details: [GDSFactory FDTD](api/fdtd.md), [Palace](api/palace.md), [Meep](api/meep.md),
[Common](api/common.md), [Cloud](api/cloud.md).
1 change: 1 addition & 0 deletions docs/zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ nav = [
] },
{ "Changelog" = "CHANGELOG.md" },
{ "API Reference" = [
{ "GDSFactory FDTD" = "api/fdtd.md" },
{ "Palace" = "api/palace.md" },
{ "Meep" = "api/meep.md" },
{ "Common" = "api/common.md" },
Expand Down
3 changes: 3 additions & 0 deletions src/gsim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
Currently includes:
- palace: Palace EM simulation API
- meep: MEEP photonic FDTD simulation API
- fdtd: PDK-native GDSFactory FDTD artifact generation
"""

from __future__ import annotations

from gsim import fdtd as fdtd
from gsim.gcloud import get_status, wait_for_results

__version__ = "0.3.0"

__all__ = [
"__version__",
"fdtd",
"get_status",
"wait_for_results",
]
2 changes: 2 additions & 0 deletions src/gsim/common/materials/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
)
from gsim.common.materials.si_li_293k import SI_LI_293K
from gsim.common.materials.si_salzberg import SI_SALZBERG
from gsim.common.materials.sin_luke import SIN_LUKE
from gsim.common.materials.sio2_malitson import SIO2_MALITSON
from gsim.common.materials.snapshots import (
MaterialModelError,
Expand All @@ -20,6 +21,7 @@

__all__ = [
"GSIM_MATERIAL_CARDS",
"SIN_LUKE",
"SIO2_MALITSON",
"SI_LI_293K",
"SI_SALZBERG",
Expand Down
2 changes: 1 addition & 1 deletion src/gsim/common/materials/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def wavelength_validity(minimum_um: float, maximum_um: float) -> Validity:
def material_card(
name: str,
permittivity: Index | Sellmeier,
temperature_ref: float,
temperature_ref: float | None,
) -> MaterialCard:
"""Build a compact optical material card."""
provenance = Provenance(
Expand Down
3 changes: 3 additions & 0 deletions src/gsim/common/materials/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from gsim.common.materials.si_li_293k import SI_LI_293K
from gsim.common.materials.si_salzberg import SI_SALZBERG
from gsim.common.materials.sin_luke import SIN_LUKE
from gsim.common.materials.sio2_malitson import SIO2_MALITSON

MaterialSource = Literal["project", "gsim"]
Expand All @@ -16,6 +17,8 @@
"Si": SI_SALZBERG.model_copy(update={"name": "Si"}),
"Si-Salzberg": SI_SALZBERG,
"Si-Li-293K": SI_LI_293K,
"SiN": SIN_LUKE.model_copy(update={"name": "SiN"}),
"SiN-Luke": SIN_LUKE,
"SiO2": SIO2_MALITSON.model_copy(update={"name": "SiO2"}),
"SiO2-Malitson": SIO2_MALITSON,
}
Expand Down
22 changes: 22 additions & 0 deletions src/gsim/common/materials/sin_luke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Luke et al. silicon-nitride model."""

from pdk_schema import Sellmeier, SellmeierTerm

from gsim.common.materials._helpers import material_card, wavelength_validity

SIN_LUKE = material_card(
name="SiN-Luke",
temperature_ref=None,
permittivity=Sellmeier(
validity=wavelength_validity(0.310, 5.504),
variation=None,
conductivity=None,
terms=(
SellmeierTerm(b=3.0249, c_um=0.1353406),
SellmeierTerm(b=40314.0, c_um=1239.842),
),
offset=0.0,
),
)

__all__ = ["SIN_LUKE"]
10 changes: 8 additions & 2 deletions src/gsim/common/pdk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,14 @@ class ResolvedPort:
width: float
orientation: float
normal: tuple[int, int, int]
layer_key: str
material: str
port_type: str
layer_key: str | None
material: str | None

@property
def is_vertical(self) -> bool:
"""Return whether this is a free-space vertical optical port."""
return self.port_type.startswith("vertical_")


@dataclass(frozen=True)
Expand Down
28 changes: 21 additions & 7 deletions src/gsim/common/pdk/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ def _axis_aligned_orientation(
return aligned, (round(cos(angle)), round(sin(angle)), 0)


def _port_orientation_and_normal(
port: Any,
) -> tuple[float, tuple[int, int, int]]:
"""Resolve guided-port normals while preserving vertical-port semantics."""
port_type = str(getattr(port, "port_type", ""))
if port_type.startswith("vertical_"):
orientation = 0.0 if port.orientation is None else float(port.orientation)
return orientation % 360.0, (0, 0, 1)
return _axis_aligned_orientation(port.name, port.orientation)


def _port_layer(port: Any) -> tuple[int, int]:
"""Return a concrete GDS tuple for a component port."""
if isinstance(port.layer, int):
Expand Down Expand Up @@ -227,15 +238,17 @@ def _resolved_ports(
"""Map every component port to one resolved physical layer."""
resolved: dict[str, ResolvedPort] = {}
for port in component.ports:
orientation, normal = _axis_aligned_orientation(port.name, port.orientation)
orientation, normal = _port_orientation_and_normal(port)
port_type = str(getattr(port, "port_type", ""))
candidates = _port_candidates(port, layers)
if not candidates:
is_vertical = port_type.startswith("vertical_")
if not candidates and not is_vertical:
raise UnsupportedPortError(
f"Port {port.name!r} on layer {_port_layer(port)} does not map to "
"resolved LayerStack geometry."
)
layer = candidates[0]
z_lower, z_upper = layer.z_bounds
layer = candidates[0] if candidates else None
z_lower, z_upper = layer.z_bounds if layer is not None else (0.0, 0.0)
resolved[port.name] = ResolvedPort(
name=port.name,
center=(
Expand All @@ -246,8 +259,9 @@ def _resolved_ports(
width=float(port.width),
orientation=orientation,
normal=normal,
layer_key=layer.key,
material=layer.material,
port_type=port_type,
layer_key=layer.key if layer is not None else None,
material=layer.material if layer is not None else None,
)
return resolved

Expand Down Expand Up @@ -297,7 +311,7 @@ def resolve_passive_pcell(
raise LayerResolutionError(
f"Could not evaluate derived layers: {error}"
) from error
layers = _resolved_layers(derived_component, layer_stack)
layers = _resolved_layers(resolved_component, layer_stack)
project_cards = _resolve_project_cards(pdk_object, pdk_or_module)
materials = {}
for material_name in dict.fromkeys(layer.material for layer in layers.values()):
Expand Down
20 changes: 1 addition & 19 deletions src/gsim/common/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,7 @@ def fuse_polygons(
Merged Shapely Polygon or MultiPolygon
"""
source_layer = getattr(layer, "layer", layer)
derived_layer = getattr(layer, "derived_layer", None)

# Simulation export materializes derived layers onto their GDS targets.
# Prefer those polygons when present: re-evaluating the source Boolean
# expression loses LayerLevel.background semantics (notably full-height
# grating teeth). Fall back to evaluating the source expression for the
# ordinary, unmaterialized visualization path.
layer_region = None
if derived_layer is not None:
target = tuple(derived_layer.layer)
layer_index = component.kcl.layer(*target)
target_region = component.kdb_cell.begin_shapes_rec(layer_index)
if not target_region.at_end():
from kfactory import kdb

layer_region = kdb.Region(target_region)

if layer_region is None:
layer_region = source_layer.get_shapes(component)
layer_region = source_layer.get_shapes(component)

shapely_polygons = []
for klayout_polygon in layer_region.each_merged():
Expand Down
21 changes: 21 additions & 0 deletions src/gsim/fdtd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Passive artifact generation for GDSFactory FDTD."""

from gsim.fdtd.config import FDTDConfig
from gsim.fdtd.models import (
FDTDArtifactError,
FDTDConfigError,
FDTDGeometryError,
MeshManifest,
SimulationArtifacts,
)
from gsim.fdtd.simulation import Simulation

__all__ = [
"FDTDArtifactError",
"FDTDConfig",
"FDTDConfigError",
"FDTDGeometryError",
"MeshManifest",
"Simulation",
"SimulationArtifacts",
]
Loading
Loading