Skip to content

feat(meep): standalone eigenmode solver for 1D slab and 2D cross-sections#174

Open
benvial wants to merge 35 commits into
gdsfactory:mainfrom
benvial:feat/mode-solver
Open

feat(meep): standalone eigenmode solver for 1D slab and 2D cross-sections#174
benvial wants to merge 35 commits into
gdsfactory:mainfrom
benvial:feat/mode-solver

Conversation

@benvial

@benvial benvial commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

Exposes MEEP eigenmode solving as a standalone callable in src/gsim/meep/, no longer requiring the full cloud FDTD job.

Closes #158.

Changes

Core — src/gsim/meep/mode_solver.py (775 lines)

Function Description
solve_slab_mode(stack, wavelength, ...) 1D slab mode from a layer stack
solve_cross_section_mode(component, stack, port=..., ...) 2D waveguide cross-section at a port or position
mode_z_grid(stack, n_points) Z-axis coordinates for field profile arrays
refractive_index_profile(stack, z_grid, wavelength) n(z) refractive index profile

All solvers return ModeResult (n_eff, fields, kdom, n_group, band_num, parity).

Integration — Simulation.solve_mode()

High-level wrapper on Simulation that resolves stack/materials via the existing three-tier pipeline, then delegates to slab or cross-section solver.

ModeResult model (src/gsim/meep/models/results.py)

Pydantic model with n_eff, fields (complex field arrays by component), kdom, n_group, metadata.

Tests — tests/meep/test_mode_solver.py (571 lines)

  • Unit tests for ModeResult validation, import guard, mode_z_grid, refractive_index_profile (no MEEP required)
  • Integration tests for slab and cross-section modes (marked meep_local, deselected by default)
  • Simulation.solve_mode() delegation test

Side fixes

  • Silence gmsh stderr noise in unit tests (palace/mesh/generator.py)
  • Suppress meep/mpb C++ verbosity via Python logging levels
  • Fix kdom units and analytical solver

Example notebook — nbs/_meep_mode_solver.py

Demonstrates slab mode solving, cross-section mode solving at a port, field profile and refractive index overlay plots.

benvial added 14 commits June 23, 2026 10:47
Expose ModeResult model and solve_slab_mode / solve_cross_section_mode
functions with lazy MEEP import. Add Simulation.solve_mode() wrapper and
_resolve_stack_and_materials() for lighter material resolution separate
from FDTD build_config.

Closes gdsfactory#158
Demonstrates solve_slab_mode(), solve_cross_section_mode(), and
Simulation.solve_mode() with slab, port, position, and wavelength sweep.
- Monkeypatch _import_meep in no-meep error-path tests so they pass
  when meep is actually installed
- test_delegates_to_mode_solver now tests real delegation path,
  asserting ModeResult return with valid n_eff
- Extract _poly_to_shapely helper; add component.dup() fallback
  for polygon retrieval
- Replace mode.get_field() with point-scan amplitude extraction
  for 1D Z field profiles
…index

- mode_z_grid(stack, n_points) — z-axis coords on stack midpoint
- refractive_index_profile(stack, z_grid, wavelength) — n(z) via resolve_materials
- compute_group_index kwarg on solve_slab_mode/solve_cross_section_mode
  for two-pass Bloch-periodic group velocity from MEEP
- Export new utils from gsim.meep
- Add unit tests (TestModeZGrid, TestRefractiveIndexProfile) + integration test
- Rewrite notebook in 5 parts with analytical SOI slab benchmark
- Fix preexisting ruff PT012/cp1252 issues
- _validate_mode() warns on leaky/radiation modes (n_eff < min_n_clad,
  slab kz != 0, n_group < 0)
- ModeResult.kdom docstring: 2pi/um -> MPB k-units (n_eff/lambda)
- Analytical solver: reject spurious roots from tan pole crossings
- Notebook: switch multi-band examples to SOI slab, add V-parameter guide
Map `meep.native` logger level to `mp.verbosity()` (WARNING+\u21920, INFO\u21921,
DEBUG\u21922), wire `run(verbose="full")` override, and snap mode-solver cell
dimensions to integer pixel counts to eliminate grid-volume warnings.

- SimConfig: add `meep_verbosity` field (default 0, quiet)
- build_config: resolve verbosity from logger + verbose param override
- runner template: apply `mp.verbosity()` from config JSON
- _import_meep: apply `mp.verbosity()` from logger level
- mode_solver: round cell_size to integer pixel counts
ty: replace `object` with `Any` for MEEP types, fix dict invariance
jupytext: sync palace_cpw notebooks, fix version in headers
_add_domain_bbox, _collect_pec_surface_lines, and _line_on_domain_boundary
each called gmsh.model.* without checking gmsh.isInitialized().  When run
under monkeypatched unit tests (no live gmsh session), every call emitted
'Error: Gmsh has not been initialized' to stderr — up to 6 per test.

Added early-return guards that short-circuit to safe defaults (zero bbox,
empty list, False) when gmsh has not been initialized.
This test calls sim.solve_mode() which requires a local MEEP installation.
It was incorrectly placed in a class claiming 'no meep required'.
get_eigenmode always provides mode.group_velocity at Gamma point.
The two-pass compute_group_index path was unnecessary — same result
as single-pass. Removed ~90 lines of dead code.
- _import_meep() now called before None guards in solve_slab_mode
  and solve_cross_section_mode so ImportError surfaces correctly
- Add gmsh:geometrical cell data to test MSH fixtures to suppress
  meshio "Appending zeros" warnings
- Convert class-scoped test fixtures to @classmethod per pytest 10
  deprecation
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 25.77108% with 1083 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.19%. Comparing base (f0e4b0f) to head (77e66bc).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/gsim/meep/mode_solver.py 8.73% 711 Missing and 10 partials ⚠️
src/gsim/meep/models/results.py 46.24% 162 Missing and 24 partials ⚠️
src/gsim/meep/viz.py 1.11% 89 Missing ⚠️
src/gsim/meep/simulation.py 37.34% 46 Missing and 6 partials ⚠️
src/gsim/common/stack/materials.py 44.23% 28 Missing and 1 partial ⚠️
src/gsim/palace/mesh/generator.py 0.00% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #174      +/-   ##
==========================================
- Coverage   63.27%   58.19%   -5.09%     
==========================================
  Files          67       69       +2     
  Lines        9757    11200    +1443     
  Branches     1970     2332     +362     
==========================================
+ Hits         6174     6518     +344     
- Misses       2900     3950    +1050     
- Partials      683      732      +49     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

benvial added 2 commits June 25, 2026 14:34
- _compute_eigenmode: sample fields on 2D (nz, nx) grid instead of 1D Z-scan
- Add mode_x_grid() utility for X-axis coordinate reconstruction
- Export mode_x_grid from gsim.meep
- Notebook: add 2D cross-section visualization section (pcolormesh, contours, multi-component grid)
- Tests: add TestModeXGrid class
- gitignore: add .kilo/
- _meep_mode_solver.py -> _meep_mode_solver_soi.py (SOI strip waveguide, full tutorial)
- New _meep_mode_solver_rib.py: rib waveguide example with 2D XZ field maps
- Rib: dual Si layers (slab 70nm + rib 150nm), manual component build, n_eff + profile
@benvial
benvial marked this pull request as draft June 25, 2026 12:45
benvial added 7 commits June 25, 2026 14:51
Remove cross-section mode sections, mode_x_grid import, 2D XZ plots.
SOI notebook focuses on slab mode solving exclusively.
2D cross-section content lives in _meep_mode_solver_rib.py.
- Add pml_thickness param to builder cells and public API (default 0.0)
- Expose eigensolver_tol on solve_slab_mode/solve_cross_section_mode
  (default 1e-6, up from hardcoded 1e-12)
- Add z_margin param for cell padding above/below stack
- Remove broken n_eff_guess (kpoint is Bloch boundary, not MPB guess)
- Add _layer_has_any_polygon helper for background layer fallback
- Fix rib notebook: add slab layer polygon, port width, layer stack
Solve once per band (4 calls), then plot all 6 field components from
stored results. Avoids ~20 redundant init_sim/get_eigenmode/reset_meep
cycles.
- Add solve_slab_modes: builds cell once, solves N bands
  -> 2.4x faster than N separate solve_slab_mode calls
- Notebook: res=64->32, use solve_slab_modes for multi-band sections
- MaterialData cache (64-entry LRU) avoids redundant Sellmeier eval
- solve_slab_wavelength_sweep() reuses cell geometry across lambda
- Reduce slab x_span from 4.0um to 2/resolution (2px) — MPB minimum
- mode_z_grid / mode_x_grid / mode_y_grid gain pml_thickness param
- Notebook: loops replaced with batch solver calls
- Runtime: 16.4s -> 5.8s (3x), results unchanged
… into feat/mode-solver

# Conflicts:
#	nbs/palace_cpw_lumped.ipynb
Cell builders accept (bottom, top) tuple z_margin with shifted
z_center. mode_z_grid returns absolute coordinates instead of
MEEP-frame. refractive_index_profile accepts absolute z_grid.
Solver functions pass asymmetric margins directly instead of
max() coercion. field_z_grid converted absolute->MEEP internally.

Closes z-margin-asymmetric-fix plan.
benvial added 12 commits July 2, 2026 13:01
Remove port_width+2.0 auto-default in solve_cross_section_mode and
Simulation.solve_mode. Span must be passed explicitly - no more
silent defaults that hide solver configuration from the caller.
Replace Simulation.solve_mode() with declarative ModeSolver Pydantic model
and solve_modes() execution method. ModeSolver provides validated fields,
fluent chaining, and shared-cell optimization for slab wavelength sweeps.
ModeSweepResult wraps results with .at(), .band(), .to_dict() accessors.

- Add ModeSolver model to gsim.meep.models.api with 13 fields + validators
- Add mode_solver field to Simulation with default_factory
- Replace solve_mode() with solve_modes() dispatching to low-level solvers
- Add ModeSweepResult wrapper in gsim.meep.results
- Export ModeSolver, ModeSweepResult from gsim.meep
- 27 model unit tests + 12 integration tests (meep_local)
- Update notebook to use new API
ModeResult gains 7 optional grid/context fields populated by solvers.
plot_mode() renders 1D/2D field profiles with index overlay, auto
component detection, norm transforms, shared colorbar, ax injection.
plot_index() renders n(z) or n(y,z) from stored context.
ModeSweepResult delegates via plot_mode(wavelength, band) and
plot_index(wavelength).
Unintended 0.5 µm defaults caused mode-field z-grid to extend
past the layer-stack boundaries without explicit overrides.
- 2D cross-section builders staircase-approximate sidewall_angle via
  stacked mp.Block sub-layers (ceil(h*res) layers)
- LiNbO3 registered as uniaxial in MATERIALS_DB (Zelmon 1997) with
  per-axis DispersionModel.axis; TFLN ridge gets sidewall_angle=17
- TFLN/rib notebooks refactored to declarative Simulation+ModeSolver
- plot_mode: rm plt.close, auto-colormap by norm, index overlay under
  field data with alpha=0.25
- fix solve_modes ignoring mode_solver.background_material
Replace stacked mp.Block sub-layer approximation with single mp.Prism
per layer interval using native sidewall_angle parameter.
Positive angle = top-narrower, matches previous staircase convention.
plot_mode(geometry=True) used build_geometry_model which reads the
gdsfactory PDK layer stack. Components with custom GDS layers (e.g.
TFLN ridge (2,0), slab (1,0)) had no matching PDK named layers,
producing an empty geometry overlay.

Replace with build_cross_section_rectangles that extracts polygon
intersections directly from the component GDS data + gsim LayerStack.
Wire domain_config into ModeResult so the guard check is satisfied.
Geometry has no z_crop field; _apply_z_crop was called with 0 args
(wrong signature). Real z-crop runs in build_config(). Fixes ty hook.
Replace low-level solve_slab_mode/solve_slab_modes calls with
declarative meep.Simulation() + mode_solver fluent API throughout.
Deduplicate _soi_slab/_make_soi_stack. ~50% line reduction.
- Replace hardcoded n_si/n_sio2 with Sellmeier dispersion from gsim
- Add analytic group index via implicit differentiation of char. eq.
- Add wavelength sweep and thickness sweep Meep-vs-analytical plots
- Move comparison cells after analytical fn definitions
@benvial
benvial marked this pull request as ready for review July 16, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Expose meep mode solver

1 participant