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
153 changes: 78 additions & 75 deletions nbs/meep_mode_solver_rib.ipynb

Large diffs are not rendered by default.

39 changes: 21 additions & 18 deletions nbs/meep_mode_solver_rib.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
# name: python3
# ---

# %% [markdown] papermill={"duration": 0.002045, "end_time": "2026-07-24T11:58:07.011380", "exception": false, "start_time": "2026-07-24T11:58:07.009335", "status": "completed"}
# %% [markdown] papermill={"duration": 0.004561, "end_time": "2026-07-24T12:40:26.368993", "exception": false, "start_time": "2026-07-24T12:40:26.364432", "status": "completed"}
# # Mode Solver — Rib Waveguide (meep)
#
# Fundamental TE mode of a silicon rib waveguide: ``n_eff`` + 2D (Y, Z) field profile.
#
# **Cross-section:** SiO2 box / Si slab 70 nm / Si rib 150 nm (total 220 nm).
# Lateral confinement via effective-index contrast.

# %% [markdown] papermill={"duration": 0.000842, "end_time": "2026-07-24T11:58:07.013311", "exception": false, "start_time": "2026-07-24T11:58:07.012469", "status": "completed"}
# %% [markdown] papermill={"duration": 0.002244, "end_time": "2026-07-24T12:40:26.374021", "exception": false, "start_time": "2026-07-24T12:40:26.371777", "status": "completed"}
# ### Imports

# %% papermill={"duration": 1.906519, "end_time": "2026-07-24T11:58:08.920596", "exception": false, "start_time": "2026-07-24T11:58:07.014077", "status": "completed"}
# %% papermill={"duration": 1.707128, "end_time": "2026-07-24T12:40:28.083026", "exception": false, "start_time": "2026-07-24T12:40:26.375898", "status": "completed"}
import gdsfactory as gf
import matplotlib.pyplot as plt
import numpy as np
Expand All @@ -36,10 +36,10 @@

gf.gpdk.PDK.activate()

# %% [markdown] papermill={"duration": 0.000755, "end_time": "2026-07-24T11:58:08.922376", "exception": false, "start_time": "2026-07-24T11:58:08.921621", "status": "completed"}
# %% [markdown] papermill={"duration": 0.000709, "end_time": "2026-07-24T12:40:28.084875", "exception": false, "start_time": "2026-07-24T12:40:28.084166", "status": "completed"}
# ### Build the GDS component

# %% papermill={"duration": 0.006301, "end_time": "2026-07-24T11:58:08.929378", "exception": false, "start_time": "2026-07-24T11:58:08.923077", "status": "completed"}
# %% papermill={"duration": 0.006569, "end_time": "2026-07-24T12:40:28.092129", "exception": false, "start_time": "2026-07-24T12:40:28.085560", "status": "completed"}
SLAB_WIDTH = 3.0 # um
RIB_WIDTH = 0.5 # um
LENGTH = 10.0 # um
Expand Down Expand Up @@ -69,10 +69,10 @@
print(f" Ports: {[p.name for p in c.ports]}")
print(f" Layers: {list(c.layers)}")

# %% [markdown] papermill={"duration": 0.00074, "end_time": "2026-07-24T11:58:08.931019", "exception": false, "start_time": "2026-07-24T11:58:08.930279", "status": "completed"}
# %% [markdown] papermill={"duration": 0.000703, "end_time": "2026-07-24T12:40:28.093695", "exception": false, "start_time": "2026-07-24T12:40:28.092992", "status": "completed"}
# ### Layer stack

# %% papermill={"duration": 0.003585, "end_time": "2026-07-24T11:58:08.935300", "exception": false, "start_time": "2026-07-24T11:58:08.931715", "status": "completed"}
# %% papermill={"duration": 0.003555, "end_time": "2026-07-24T12:40:28.097911", "exception": false, "start_time": "2026-07-24T12:40:28.094356", "status": "completed"}
layers = {
"ox": Layer(
name="box",
Expand Down Expand Up @@ -110,10 +110,10 @@
f" {name:6s} z=[{l.zmin:+.3f}, {l.zmax:+.3f}] t={l.thickness:.3f} material={l.material}"
)

# %% [markdown] papermill={"duration": 0.000739, "end_time": "2026-07-24T11:58:08.937474", "exception": false, "start_time": "2026-07-24T11:58:08.936735", "status": "completed"}
# %% [markdown] papermill={"duration": 0.000703, "end_time": "2026-07-24T12:40:28.099909", "exception": false, "start_time": "2026-07-24T12:40:28.099206", "status": "completed"}
# ### Solve

# %% papermill={"duration": 104.636625, "end_time": "2026-07-24T11:59:53.574787", "exception": false, "start_time": "2026-07-24T11:58:08.938162", "status": "completed"}
# %% papermill={"duration": 174.673569, "end_time": "2026-07-24T12:43:22.774141", "exception": false, "start_time": "2026-07-24T12:40:28.100572", "status": "completed"}
WAVELENGTH = 1.55 # um
RESOLUTION = 64
PML_THICKNESS = 1 * WAVELENGTH
Expand Down Expand Up @@ -153,20 +153,20 @@
for comp, arr in mode.fields.items():
print(f" {comp}: shape={arr.shape} |max|={np.abs(arr).max():.6f}")

# %% [markdown] papermill={"duration": 0.001324, "end_time": "2026-07-24T11:59:53.577025", "exception": false, "start_time": "2026-07-24T11:59:53.575701", "status": "completed"}
# %% [markdown] papermill={"duration": 0.000845, "end_time": "2026-07-24T12:43:22.776363", "exception": false, "start_time": "2026-07-24T12:43:22.775518", "status": "completed"}
# ### Index profile

# %% papermill={"duration": 0.093477, "end_time": "2026-07-24T11:59:53.671339", "exception": false, "start_time": "2026-07-24T11:59:53.577862", "status": "completed"}
# %% papermill={"duration": 0.101406, "end_time": "2026-07-24T12:43:22.878556", "exception": false, "start_time": "2026-07-24T12:43:22.777150", "status": "completed"}
mode.plot_index(show=True)


# %% [markdown] papermill={"duration": 0.000954, "end_time": "2026-07-24T11:59:53.673436", "exception": false, "start_time": "2026-07-24T11:59:53.672482", "status": "completed"}
# %% [markdown] papermill={"duration": 0.000938, "end_time": "2026-07-24T12:43:22.881329", "exception": false, "start_time": "2026-07-24T12:43:22.880391", "status": "completed"}
# ### Mode profile (interactive)
#
# ``|field|`` maps for every component with zoom / pan / hover. Axes use equal
# aspect so the cross-section is physically proportioned.

# %% papermill={"duration": 0.203272, "end_time": "2026-07-24T11:59:53.877703", "exception": false, "start_time": "2026-07-24T11:59:53.674431", "status": "completed"}
# %% papermill={"duration": 0.232122, "end_time": "2026-07-24T12:43:23.114292", "exception": false, "start_time": "2026-07-24T12:43:22.882170", "status": "completed"}
import plotly.graph_objects as go
from plotly.subplots import make_subplots

Expand Down Expand Up @@ -197,7 +197,7 @@
cols=3,
subplot_titles=[f"|{c}|" for c in comps],
horizontal_spacing=0.08,
vertical_spacing=0.12,
vertical_spacing=0.2,
)

for i, comp in enumerate(comps):
Expand Down Expand Up @@ -231,11 +231,14 @@
)

fig.update_layout(
height=550,
height=650,
autosize=True,
title_text=(
f"Rib waveguide fundamental TE mode "
f"(lambda={WAVELENGTH:.2f} µm, n_eff={mode.n_eff:.4f})"
margin=dict(t=70, b=60),
title=dict(
text=f"lambda={WAVELENGTH:.2f} µm, n_eff={mode.n_eff:.4f}",
x=0.5,
xanchor="center",
font=dict(size=13),
),
)
fig.show(config={"responsive": True})
639 changes: 561 additions & 78 deletions nbs/meep_mode_solver_tfln.ipynb

Large diffs are not rendered by default.

192 changes: 173 additions & 19 deletions nbs/meep_mode_solver_tfln.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# name: python3
# ---

# %% [markdown] papermill={"duration": 0.003127, "end_time": "2026-07-24T12:00:00.754624", "exception": false, "start_time": "2026-07-24T12:00:00.751497", "status": "completed"}
# %% [markdown] papermill={"duration": 0.006539, "end_time": "2026-07-24T16:33:50.087547", "exception": false, "start_time": "2026-07-24T16:33:50.081008", "status": "completed"}
# # Mode Solver — TFLN Ridge Waveguide (meep)
#
# Fundamental TE mode of thin-film lithium niobate ridge waveguide at lambda=1.55 µm.
Expand All @@ -25,10 +25,10 @@
# **Expected:** n_eff ~ 1.85, n_group ~ 2.20.
# ``background_material="sio2"`` fills unpatterned space with SiO2.

# %% [markdown] papermill={"duration": 0.001785, "end_time": "2026-07-24T12:00:00.758676", "exception": false, "start_time": "2026-07-24T12:00:00.756891", "status": "completed"}
# %% [markdown] papermill={"duration": 0.002776, "end_time": "2026-07-24T16:33:50.095297", "exception": false, "start_time": "2026-07-24T16:33:50.092521", "status": "completed"}
# ### Imports

# %% papermill={"duration": 1.835389, "end_time": "2026-07-24T12:00:02.595708", "exception": false, "start_time": "2026-07-24T12:00:00.760319", "status": "completed"}
# %% papermill={"duration": 7.319947, "end_time": "2026-07-24T16:33:57.417774", "exception": false, "start_time": "2026-07-24T16:33:50.097827", "status": "completed"}
import gdsfactory as gf
import matplotlib.pyplot as plt
import numpy as np
Expand All @@ -40,7 +40,7 @@

gf.gpdk.PDK.activate()

# %% [markdown] papermill={"duration": 0.001066, "end_time": "2026-07-24T12:00:02.598170", "exception": false, "start_time": "2026-07-24T12:00:02.597104", "status": "completed"}
# %% [markdown] papermill={"duration": 0.004634, "end_time": "2026-07-24T16:33:57.427896", "exception": false, "start_time": "2026-07-24T16:33:57.423262", "status": "completed"}
# ### LiNbO3 material (Zelmon 1997)
#
# LiNbO3 is birefringent and is now registered as a uniaxial material
Expand All @@ -60,10 +60,10 @@
# extraordinary axis (zz).


# %% [markdown] papermill={"duration": 0.001125, "end_time": "2026-07-24T12:00:02.600276", "exception": false, "start_time": "2026-07-24T12:00:02.599151", "status": "completed"}
# %% [markdown] papermill={"duration": 0.0038, "end_time": "2026-07-24T16:33:57.436203", "exception": false, "start_time": "2026-07-24T16:33:57.432403", "status": "completed"}
# ### Build the GDS component

# %% papermill={"duration": 0.00652, "end_time": "2026-07-24T12:00:02.608029", "exception": false, "start_time": "2026-07-24T12:00:02.601509", "status": "completed"}
# %% papermill={"duration": 0.174397, "end_time": "2026-07-24T16:33:57.625614", "exception": false, "start_time": "2026-07-24T16:33:57.451217", "status": "completed"}
SLAB_WIDTH = 5.0 # um --- wide slab
CORE_WIDTH = 1.1 # um --- w0 from reference design
LENGTH = 10.0 # um --- waveguide length (arbitrary for mode solving)
Expand Down Expand Up @@ -101,12 +101,12 @@
print(f" Ports: {[p.name for p in c.ports]}")
print(f" Layers: {list(c.layers)}")

# %% [markdown] papermill={"duration": 0.001177, "end_time": "2026-07-24T12:00:02.611178", "exception": false, "start_time": "2026-07-24T12:00:02.610001", "status": "completed"}
# %% [markdown] papermill={"duration": 0.003233, "end_time": "2026-07-24T16:33:57.635212", "exception": false, "start_time": "2026-07-24T16:33:57.631979", "status": "completed"}
# ### Layer stack
#
# SiO2 fills background via ``background_material="sio2"``.

# %% papermill={"duration": 0.005052, "end_time": "2026-07-24T12:00:02.617336", "exception": false, "start_time": "2026-07-24T12:00:02.612284", "status": "completed"}
# %% papermill={"duration": 0.016785, "end_time": "2026-07-24T16:33:57.660454", "exception": false, "start_time": "2026-07-24T16:33:57.643669", "status": "completed"}
SLAB_THICKNESS = 0.22 # um (h3)
CORE_THICKNESS = 0.40 # um --- total LiNbO3 thickness
RIDGE_THICKNESS = CORE_THICKNESS - SLAB_THICKNESS # 0.18 um
Expand Down Expand Up @@ -150,10 +150,10 @@
f"t={l.thickness:.3f} material={l.material} gds={l.gds_layer}"
)

# %% [markdown] papermill={"duration": 0.00124, "end_time": "2026-07-24T12:00:02.619810", "exception": false, "start_time": "2026-07-24T12:00:02.618570", "status": "completed"}
# %% [markdown] papermill={"duration": 0.015262, "end_time": "2026-07-24T16:33:57.679862", "exception": false, "start_time": "2026-07-24T16:33:57.664600", "status": "completed"}
# ### Solve

# %% papermill={"duration": 80.363473, "end_time": "2026-07-24T12:01:22.984371", "exception": false, "start_time": "2026-07-24T12:00:02.620898", "status": "completed"}
# %% papermill={"duration": 1925.213727, "end_time": "2026-07-24T17:06:02.898833", "exception": false, "start_time": "2026-07-24T16:33:57.685106", "status": "completed"}
WAVELENGTH = 1.55 # um
RESOLUTION = 64 # grid points per um
PML_THICKNESS = WAVELENGTH # um
Expand Down Expand Up @@ -183,19 +183,19 @@
for comp, arr in mode.fields.items():
print(f" {comp}: shape={arr.shape} |max|={np.abs(arr).max():.6f}")

# %% [markdown] papermill={"duration": 0.001038, "end_time": "2026-07-24T12:01:22.986822", "exception": false, "start_time": "2026-07-24T12:01:22.985784", "status": "completed"}
# %% [markdown] papermill={"duration": 0.016131, "end_time": "2026-07-24T17:06:02.925467", "exception": false, "start_time": "2026-07-24T17:06:02.909336", "status": "completed"}
# ### Index profile

# %% papermill={"duration": 0.256239, "end_time": "2026-07-24T12:01:23.243947", "exception": false, "start_time": "2026-07-24T12:01:22.987708", "status": "completed"}
# %% papermill={"duration": 1.695063, "end_time": "2026-07-24T17:06:04.627531", "exception": false, "start_time": "2026-07-24T17:06:02.932468", "status": "completed"}
mode.plot_index(show=True)

# %% [markdown] papermill={"duration": 0.001004, "end_time": "2026-07-24T12:01:23.246232", "exception": false, "start_time": "2026-07-24T12:01:23.245228", "status": "completed"}
# %% [markdown] papermill={"duration": 0.004876, "end_time": "2026-07-24T17:06:04.637601", "exception": false, "start_time": "2026-07-24T17:06:04.632725", "status": "completed"}
# ### Mode profile (interactive)
#
# ``|field|`` maps for every component with zoom / pan / hover. The view is
# auto-cropped to the mode region and axes use equal aspect.

# %% papermill={"duration": 0.209603, "end_time": "2026-07-24T12:01:23.456715", "exception": false, "start_time": "2026-07-24T12:01:23.247112", "status": "completed"}
# %% papermill={"duration": 1.508412, "end_time": "2026-07-24T17:06:06.152802", "exception": false, "start_time": "2026-07-24T17:06:04.644390", "status": "completed"}
import plotly.graph_objects as go
from plotly.subplots import make_subplots

Expand Down Expand Up @@ -226,7 +226,7 @@
cols=3,
subplot_titles=[f"|{c}|" for c in comps],
horizontal_spacing=0.08,
vertical_spacing=0.12,
vertical_spacing=0.2,
)

for i, comp in enumerate(comps):
Expand Down Expand Up @@ -260,11 +260,165 @@
)

fig.update_layout(
height=550,
height=650,
autosize=True,
title_text=(
f"TFLN ridge waveguide fundamental TE mode "
f"(lambda={WAVELENGTH:.2f} µm, w0={CORE_WIDTH:.1f} µm, n_eff={mode.n_eff:.4f})"
margin=dict(t=70, b=60),
title=dict(
text=(
f"lambda={WAVELENGTH:.2f} µm, w0={CORE_WIDTH:.1f} µm, "
f"n_eff={mode.n_eff:.4f}"
),
x=0.5,
xanchor="center",
font=dict(size=13),
),
)
fig.show(config={"responsive": True})

# %% [markdown] papermill={"duration": 0.007377, "end_time": "2026-07-24T17:06:06.169543", "exception": false, "start_time": "2026-07-24T17:06:06.162166", "status": "completed"}
# ### Width sweep — band diagram
#
# Sweep the ridge width ``w0`` and track ``n_eff`` of the first few bands.
# Narrow ridges guide fewer modes, so higher-band curves appear only once the
# width is wide enough to support them. Modes below the SiO2 cladding index are
# radiative and are dropped (dashed line = cladding cutoff).
#
# Each width is an independent cloud mode-solve. Jobs are submitted in batches
# (up to ``MAX_CONCURRENT`` in flight) and waited on concurrently, so the whole
# sweep finishes in a few minutes rather than running one width at a time.

# %% papermill={"duration": 2210.842074, "end_time": "2026-07-24T17:42:57.024025", "exception": false, "start_time": "2026-07-24T17:06:06.181951", "status": "completed"}
import shutil
import tempfile
from pathlib import Path

from gsim import gcloud
from gsim.common.stack.materials import resolve_material_at_wavelength

# Snap widths to the 0.002 µm grid so gdsfactory ports stay grid-aligned.
W0_VALUES = np.round(np.linspace(0.3, 3.0, 25) / 0.002) * 0.002 # um
N_SWEEP_BANDS = 8
MAX_CONCURRENT = 10 # cloud jobs in flight at once

# SiO2 cladding index at this wavelength — guided modes must lie above it.
_clad = resolve_material_at_wavelength("sio2", WAVELENGTH)
_eps = _clad.permittivity
n_clad = float(np.sqrt(_eps if np.isscalar(_eps) else np.mean(_eps)))


def tfln_component(core_width: float) -> gf.Component:
"""Build the TFLN ridge component for a given ridge width."""
comp = gf.Component()
comp.add_polygon(
[
(-LENGTH / 2, -core_width / 2),
(LENGTH / 2, -core_width / 2),
(LENGTH / 2, core_width / 2),
(-LENGTH / 2, core_width / 2),
],
layer=(2, 0),
)
comp.add_port(
name="o1",
center=(-LENGTH / 2, 0),
width=core_width,
orientation=180,
layer=(1, 0),
)
comp.add_port(
name="o2",
center=(LENGTH / 2, 0),
width=core_width,
orientation=0,
layer=(1, 0),
)
return comp


def submit_mode_solve(core_width: float) -> str:
"""Upload + start one cloud mode-solve job (non-blocking). Returns job_id."""
sim = gm.Simulation(
geometry=gm.Geometry(component=tfln_component(core_width), stack=stack),
domain=gm.Domain(pml=PML_THICKNESS, margin_z=(0.0, 0.5)),
)
sim.mode_solver.wavelengths = [WAVELENGTH]
sim.mode_solver.first(N_SWEEP_BANDS).at_port("o1")
sim.mode_solver.y_span = SLAB_WIDTH
sim.mode_solver.background_material = "sio2" # n_field=0 -> n_eff only

tmp = Path(tempfile.mkdtemp(prefix="meep_mode_sweep_"))
try:
sim.write_mode_solver_config(tmp)
job_id = gcloud.upload(tmp, "meep", verbose=False)
gcloud.start(job_id, verbose=False)
return job_id
finally:
shutil.rmtree(tmp, ignore_errors=True)


# Submit jobs in batches so at most MAX_CONCURRENT run on the cloud at once,
# then wait for the whole batch concurrently. n_eff only (no field download).
sweep_results = [] # ModeSweepResult per width, in W0_VALUES order
for i in range(0, len(W0_VALUES), MAX_CONCURRENT):
batch = W0_VALUES[i : i + MAX_CONCURRENT]
job_ids = [submit_mode_solve(w0) for w0 in batch]
print(f"batch {i // MAX_CONCURRENT + 1}: started {len(job_ids)} jobs")
batch_results = gcloud.wait_for_results(job_ids, verbose="quiet")
if not isinstance(batch_results, list): # single-job batch returns one result
batch_results = [batch_results]
sweep_results.extend(batch_results)

# n_eff per band across the width sweep (NaN where the band is not guided).
neff_by_band = {b: [] for b in range(1, N_SWEEP_BANDS + 1)}
for w0, res in zip(W0_VALUES, sweep_results, strict=True):
found = {r.band_num: r.n_eff for r in res.at(WAVELENGTH).results}
for b in range(1, N_SWEEP_BANDS + 1):
neff = found.get(b, np.nan)
# drop radiative modes below the cladding index
neff_by_band[b].append(neff if neff > n_clad else np.nan)
guided = [b for b in range(1, N_SWEEP_BANDS + 1) if found.get(b, 0) > n_clad]
summary = ", ".join(f"b{b}={found[b]:.4f}" for b in guided)
print(f"w0={w0:.2f} µm -> {len(guided)} guided mode(s): {summary}")

# %% [markdown] papermill={"duration": 0.00381, "end_time": "2026-07-24T17:42:57.033543", "exception": false, "start_time": "2026-07-24T17:42:57.029733", "status": "completed"}
# ``n_eff`` vs ``w0`` for each band (interactive).

# %% papermill={"duration": 0.034951, "end_time": "2026-07-24T17:42:57.071777", "exception": false, "start_time": "2026-07-24T17:42:57.036826", "status": "completed"}
band_fig = go.Figure()
for b in range(1, N_SWEEP_BANDS + 1):
band_fig.add_trace(
go.Scatter(
x=W0_VALUES,
y=neff_by_band[b],
mode="lines+markers",
name=f"band {b}",
connectgaps=False,
hovertemplate="w0=%{x:.2f} µm<br>n_eff=%{y:.4f}<extra>band "
+ str(b)
+ "</extra>",
)
)
band_fig.add_hline(
y=n_clad,
line_dash="dash",
line_color="gray",
annotation_text=f"SiO2 cladding (n={n_clad:.3f})",
annotation_position="bottom right",
)
band_fig.update_layout(
height=550,
width=550,
autosize=False,
margin=dict(t=60, b=60),
xaxis_title="ridge width w0 (µm)",
yaxis_title="n_eff",
title=dict(
text=f"TFLN band diagram (lambda={WAVELENGTH:.2f} µm)",
x=0.5,
xanchor="center",
font=dict(size=13),
),
legend=dict(title=""),
)
# Fixed square aspect (width == height); no responsive stretch.
band_fig.show()
Loading
Loading