Quickstart: 2x2 MMI with S-Bends
Simulate a 2x2 multimode interference coupler using gdsfactory for layout and Hyperwave for cloud-accelerated 3D FDTD.
What you will learn
- 1.Convert a gdsfactory component to a simulation-ready structure
- 2.Set up mode sources, monitors, and absorbing boundaries
- 3.Run a cloud GPU simulation and analyze transmission
Create Your Account
To get started with Hyperwave, you need a free account on the SPINS Photonics platform.
Fill in your name, email, and a password.
Click the verification link to activate your account.
You will land on your personal dashboard with simulation history, credits, and settings.
Interested in trying Hyperwave? and we'll find a time to walk you through it.
Get Your API Key
Hyperwave uses API keys to authenticate simulation requests from your scripts and notebooks.
After logging in, click Overview in the left sidebar. Your API Key card is on the main dashboard.
Your key is created instantly and auto-copied to your clipboard.
Store it in a .env file or password manager. You can click the eye icon to reveal it, or the copy icon to copy it again.
Your API key is shown only once when generated. For security, you will not be able to view the full key again after leaving the page. Copy it immediately and store it somewhere safe.
Set Up Your Environment
We recommend using a Python virtual environment to keep dependencies isolated.
python -m venv hyperwave-env && source hyperwave-env/bin/activatePrefer zero setup? Open in Google Colab. Want to run locally without copying each cell? Download the notebook.
Prerequisites
Before you begin, make sure you have the following ready.
Verify with python --version. Hyperwave supports Python 3.9 through 3.12.
Sign up at spinsphotonics.com/signup if you do not have one yet.
Your machine only handles problem setup. The FDTD computation runs on cloud GPUs.
No special hardware is required on your local machine. The actual FDTD computation happens on our cloud infrastructure.
Installation
Install the two main packages for this tutorial. hyperwave-community is the Hyperwave FDTD simulation SDK. gdsfactory is a widely used open-source library for designing photonic components using the GDSII layout format.
JAX is installed automatically as a dependency. You do not need to configure it separately.
In Google Colab, prefix the command with % to install packages directly in a notebook cell.
pip install hyperwave-community gdsfactoryImports and Configuration
Import the core libraries. hyperwave_community (imported as hwc) is the main SDK. gdsfactory (imported as gf) provides the photonic component library. numpy and jax.numpy handle numerical operations.
hwc.set_device("auto") auto-detects GPU/CPU and installs the correct JAX variant. hwc.set_verbose(True) enables detailed logging so you can see what happens at each stage.
The PDK (Process Design Kit) is gdsfactory's library of pre-built standard photonic components. Activating the generic PDK gives you access to silicon photonics building blocks with sensible defaults.
You can also set resolution by cells per wavelength: CELLS_PER_WL = 20 then RESOLUTION_UM = WL_UM / (N_CORE * CELLS_PER_WL). More cells means higher accuracy but slower simulation.
import hyperwave_community as hwc
import gdsfactory as gf
import numpy as np
import jax.numpy as jnp
hwc.set_device("auto")
hwc.set_verbose(True)
PDK = gf.gpdk.get_generic_pdk()
PDK.activate()
WL_UM = 1.55 # Wavelength
N_CORE = 3.48 # Silicon refractive index at 1550 nm
N_CLAD = 1.45 # SiO2 cladding
RESOLUTION_UM = 0.02 # 20 nm grid spacing
WG_HEIGHT_UM = 0.22 # Waveguide core height
TOTAL_HEIGHT_UM = 4.0 # Total stack height
PADDING = (100, 100, 0, 0) # (left, right, top, bottom) in theta pixelsStep 1: Load Component
A 2x2 MMI coupler is one of the most fundamental building blocks in integrated photonics. It uses the self-imaging property of multimode waveguides to split light into two output ports.
The mmi2x2_with_sbend variant from gdsfactory connects waveguides using S-shaped bends for smoother transitions that reduce back-reflections.
We extend the ports by 2 um using gf.c.extend_ports() to ensure mode sources and monitors are in straight waveguide sections. hwc.component_to_theta() converts the GDS geometry into a 2D density array called theta.
For GDS files: theta, device_info = hwc.gds_to_theta(gds_filepath="your_device.gds", resolution=RESOLUTION_UM)
EXTENSION_LENGTH = 2.0 # Extend ports by 2 um
gf_device = gf.components.mmi2x2_with_sbend()
gf_extended = gf.c.extend_ports(gf_device, length=EXTENSION_LENGTH)
theta, device_info = hwc.component_to_theta(
component=gf_extended,
resolution=RESOLUTION_UM,
)
hwc.plot_theta(theta)Step 2: Build 3D Structure
Convert the 2D layout into a full 3D electromagnetic structure. FDTD simulates Maxwell's equations on a 3D grid of permittivity values, where epsilon = n^2 for each material.
The layer stack follows standard silicon-on-insulator (SOI): bottom SiO2 cladding, thin silicon core (220 nm), and top SiO2 cladding. hwc.density() applies density filtering to smooth sharp edges, improving simulation stability.
hwc.create_structure() stacks the layers vertically. The vertical_radius parameter smooths transitions between layers. z_wg_center marks the waveguide core center for plotting.
The density radius controls how sharp waveguide edges are. Larger radius = smoother edges, which can improve simulation stability.
eps_core = N_CORE ** 2
eps_clad = N_CLAD ** 2
density_core = hwc.density(theta=theta, pad_width=PADDING, radius=3)
density_clad = hwc.density(theta=jnp.zeros_like(theta), pad_width=PADDING, radius=5)
wg_cells = max(1, int(np.round(WG_HEIGHT_UM / RESOLUTION_UM)))
clad_cells = int(np.round((TOTAL_HEIGHT_UM - WG_HEIGHT_UM) / 2 / RESOLUTION_UM))
structure = hwc.create_structure(
layers=[
hwc.Layer(density_pattern=density_clad, permittivity_values=eps_clad, layer_thickness=clad_cells),
hwc.Layer(density_pattern=density_core, permittivity_values=(eps_clad, eps_core), layer_thickness=wg_cells),
hwc.Layer(density_pattern=density_clad, permittivity_values=eps_clad, layer_thickness=clad_cells),
],
vertical_radius=2,
)
z_wg_center = clad_cells + wg_cells // 2
hwc.plot_structure(structure, axis="z", position=z_wg_center)
hwc.plot_structure(structure, view_mode="3d")Step 3: Absorbing Boundaries
FDTD simulations need absorbing boundaries to prevent outgoing waves from reflecting back into the simulation. Hyperwave uses adiabatic absorbers where conductivity gradually increases toward the grid edges.
hwc.get_optimized_absorber_params() calculates optimal absorber width and conductivity for your grid. hwc.create_absorption_mask() generates the 3D conductivity array.
The absorber width is automatically calculated to be thick enough for less than -40 dB reflection.
_, Lx, Ly, Lz = structure.permittivity.shape
abs_params = hwc.get_optimized_absorber_params(
resolution_nm=RESOLUTION_UM * 1000,
structure_dimensions=(Lx, Ly, Lz),
)
abs_widths = abs_params["absorption_widths"]
abs_coeff = abs_params["absorber_coeff"]
absorber = hwc.create_absorption_mask(
grid_shape=(Lx, Ly, Lz),
absorption_widths=abs_widths,
absorption_coeff=abs_coeff,
)
structure.conductivity = jnp.zeros_like(structure.conductivity) + absorber
hwc.plot_absorption_mask(absorber)Step 4: Mode Source
To simulate how a device processes light, we inject the exact guided mode of the input waveguide. This is called a mode source, the standard approach in photonic simulation.
A guided mode is a self-consistent field pattern that propagates without changing shape. We select mode_num=0 (fundamental TE mode), the most commonly used in silicon photonics.
hwc.create_mode_source() identifies the waveguide cross-section, solves for guided modes, and returns the source field pattern, offset, and mode info including the propagation constant beta.
The propagation constant beta gives the effective index: n_eff = beta * wavelength / (2 * pi). Expect n_eff around 2.4 for standard SOI.
wl_cells = WL_UM / RESOLUTION_UM
freq_band = (2 * jnp.pi / wl_cells, 2 * jnp.pi / wl_cells, 1)
source_field, source_offset, mode_info = hwc.create_mode_source(
structure=structure,
freq_band=freq_band,
mode_num=0,
propagation_axis="x",
source_position=abs_widths[0],
)
hwc.plot_mode(
mode_field=mode_info["field"],
beta=mode_info["beta"],
mode_num=0,
propagation_axis="x",
)Step 5: Monitors
Monitors are virtual measurement probes placed inside the simulation to record electromagnetic fields at specific locations. They capture the field passing through a cross-section so you can extract power and transmission.
Place one monitor at the input port and additional monitors at each output port. The ratio of output to input power gives you the transmission coefficient for each port.
create_port_monitors() automates placement by reading port metadata from the gdsfactory component. Input-facing ports get an "Input_" prefix and output-facing ports get an "Output_" prefix.
Monitor names starting with "Input_" and "Output_" are recognized by analyze_transmission(). Follow this convention for custom monitors.
monitors = hwc.create_port_monitors(
component=gf_device,
structure=structure,
device_info=device_info,
padding=PADDING,
absorption_widths=abs_widths,
)
hwc.plot_monitor_layout(
structure.permittivity, monitors,
axis="z", position=z_wg_center, source_position=abs_widths[0],
)Step 6: Run Simulation
Configure your API key first. The code checks for a key in Google Colab secrets, then falls back to the HYPERWAVE_API_KEY environment variable.
When you call simulate(), your structure and source data are sent to cloud GPUs (NVIDIA B200). A simulation with ~125 million cells that might take hours locally completes in under two minutes on the cloud.
simulation_steps=20000 runs enough time steps for light to propagate through the device and reach steady state. hwc.save_results() caches results locally so you can reload them without re-running.
Simulation time depends on grid size. This MMI (~125 million cells) typically completes in under 2 minutes on cloud GPU.
try:
from google.colab import userdata
hwc.configure_api(api_key=userdata.get("HYPERWAVE_API_KEY"))
except ImportError:
import os
hwc.configure_api(api_key=os.environ.get("HYPERWAVE_API_KEY"))
results = hwc.simulate(
structure_recipe=structure.extract_recipe(),
source_field=source_field,
source_offset=source_offset,
freq_band=freq_band,
monitors_recipe=monitors.recipe,
mode_info=mode_info,
simulation_steps=20000,
absorption_widths=abs_widths,
absorption_coeff=abs_coeff,
)
hwc.save_results(results, "quickstart_results.npz")Step 7: Analyze Results
analyze_transmission() computes how much optical power reaches each output port by calculating the Poynting vector at each monitor, integrating power flux, and normalizing by the input.
For the default 2x2 MMI coupler, expect roughly equal power splitting between outputs (T around 0.48, or -3 dB each). Total transmission should be close to 1.0.
plot_monitors() visualizes the Hz field at each monitor. The xy_mid plane shows the characteristic MMI self-imaging pattern inside the device.
Transmission values slightly above 1.0 can occur in simulations without perfect absorbing boundaries, due to small reflections from domain edges.
transmission = hwc.analyze_transmission(
results, input_monitor="Input_o1", direction="x",
)
hwc.plot_monitors(results, component="Hz")Next steps
- →Explore the full API documentation
- →Sign up and get your API key
- →Try inverse design optimization (coming soon)