Bridged Engine for EME and FDTD — for photonic MMI/coupler devices.
BEEF is a hybrid-methodology orchestration layer on top of meep (FDTD),
meow/MPB (eigenmode expansion), and optionally
tidy3d's mode solver, it is not a competing raw solver; meep/meow/tidy3d do the actual field/mode math.
BEEF's real contribution is HybridSolver: a generic N-section FDTD/EME cascade (any solver type, any
order, any count ≥ 2) with reciprocity-based excitation (minimum simulation count) and a shared
section_prisms/section_gds/width_at/center_at interface that lets any geometry slot into that cascade.
A fresh pass over the OSS photonics landscape (meep, tidy3d, gdsfactory, SAX/Simphony, FDTDX, and
others) didn't turn up another open-source tool that does this specific thing: a
generic, reciprocity-based FDTD/EME cascade over an arbitrary number of sections.
Requires Python ≥3.11 (meow pulls in jax via sax, and jax no longer ships
for 3.10). Both simulation backends need to come from somewhere other than a
plain PyPI install — do steps 1 and 2 first, then install BEEF.
1. meow (EME backend) — from git main, not the latest release.
pip install "meow-sim @ git+https://github.com/gdsfactory/meow.git@b6ba461"BEEF's default mode_solver="meep" calls meow.compute_modes_meep, which was
merged upstream on 2026-09-16 and is not in any release: the newest PyPI
meow-sim is 0.15.0 (2026-03-15), so pip install meow-sim gives a package
where BEEF's default path fails on import. Install from git.
The commit above is pinned on purpose, so this environment stays reproducible;
@main also works if you want the tip. (flaport/meow is the same project —
it now redirects to gdsfactory/meow.)
2. meep (FDTD backend) — from conda-forge, not pip.
conda install -c conda-forge pymeep
# or: micromamba install -c conda-forge pymeepPyPI's meep package is an unrelated project (a lightweight task runner).
pip install meep installs that silently, with no error — it just leaves
import meep broken. The MIT electromagnetics package ships only as
conda-forge pymeep. Skip this step if you only need EMESolver.
3. BEEF itself.
pip install -e . # core (numpy, scipy, matplotlib)
pip install -e ".[gds]" # + gdstk, for GdsGeometry / load_gds_polygons
pip install -e ".[dev]" # + pytest
pip install -e ".[dev,gds]" # typical development installVerify:
python -c "import beef, meow; print(beef.__name__, meow.__version__)"
python -c "import meep; print(meep.__version__)" # only if you did step 2The [eme] and [all] extras now carry the same pinned git reference as step 1,
so pip install -e ".[all]" reproduces steps 1 and 3 in one command. Step 2
stays manual: there is no [fdtd] extra, because meep cannot be expressed as
a pip dependency at all.
from beef import (
# geometry
MMI, MMIPort, BendGeometry, DirectionalCoupler, Taper, SBend,
CompoundGeometry, WaveguideRun, GdsGeometry, load_gds_polygons,
arc_polygon, RingResonatorCouplerSection, RingResonatorBendSection,
# materials
MaterialStack, SOI_160, SOI_220, SIN_300,
# solvers
EMESolver, FDTDSolver, PortSpec, HybridSolver,
# network
SMatrixBlock, connect, close_loop, close_loop_pair, Circuit,
from_transfer_matrix, from_smatrix, to_smatrix,
# mode identity
field_overlap_matrix, best_correspondence, mesh_cell_centers, stack_meow_mode_fields,
overlap_diagnostics, self_overlap_diagnostics,
# device-agnostic checks
cut_position_invariance, interface_modes_convergence,
# spectral resampling
interp_complex, unwrap_is_safe,
# results
SMatrix,
# sweep
ParallelSweep,
# plot
plot_eme_fields, plot_eme_geometry, plot_fdtd_geometry, plot_hybrid_geometry,
plot_section_fdtd_geometry, plot_performance, plot_splitting_ratio,
plot_beta_calibration, plot_beta_calibration_fits,
plot_beta_calibration_geometry, plot_beta_calibration_merged_geometry,
plot_beta_calibration_summary,
# utilities
extrude_polygons, atomic_replace_write,
)This block mirrors
beef/__init__.py's__all__; if you add a public name there, add it here too. (UniformCrossSectionis exported frombeef.geometryonly, not from the top level.)
- Geometry (
beef/geometry/) —MMI(N×M splitter/combiner, per-portMMIPorttaper specs,input_idx/output_idxto isolate a single port on multi-port devices like a 2×2),DirectionalCoupler(2-waveguide coupler w/ S-bend I/O), standaloneTaper/SBendbuilding blocks,CompoundGeometry/WaveguideRun(concatenate several geometries into one cascade), legacyBendGeometry(90° bend for non-collinear FDTD I/O),arc_polygon(quarter-annulus wedge, for constant-radius bends),RingResonatorCouplerSection/RingResonatorBendSection(ring resonator geometry primitives — not yet wired intoHybridSolveror an example). - Materials (
beef/materials/) —MaterialStack(Si/SiO2 layer stack), plus pre-builtSOI_160/SOI_220stacks. One analytic Sellmeier model (Salzberg Si, Malitson SiO2) backs both engines, so an EME mode solve and an FDTD run agree on n(λ) to machine precision. The FDTD still freezes one index per broadband run. - Solvers (
beef/solvers/) —EMESolver(meow-based EME),FDTDSolver(meep-based FDTD, 2D or 3D),HybridSolver(chains FDTD and EME sections together — the core BEEF method; optionally skips re-solving a section declared as the x-mirror of an already-solved one viamirror_pairs, and can sweep one uniform section's length analytically viasolve_length_sweep()at no extra simulation cost). Every solve-family method (FDTDSolver.solve()/solve_section(),EMESolver.solve()/solve_section()/solve_section_length_sweep(),HybridSolver.solve()/solve_length_sweep()) accepts an optionalsave_dir— see Simulation cache below.FDTDSolver.solve_section()also acceptsy0/y1andexcite_port/measure_port(PortSpec) to place a source/monitor on a y-normal (not just x-normal) plane — additive, every existing call is unaffected — for sections with a non-x-normal port (e.g. a ring resonator's coupling port).FDTDSolver.section_mode_fields()is the field-profile counterpart ofsection_mode_neff()— same cheapget_eigenmode()-only solve, but returns sampledEx..Hzper mode instead of just n_eff, forbeef.mode_identitybelow;EMESolver.section_modes()is its EME-side counterpart, surfacing the rawmeow.Modelistsolve_section()already computes internally but never returns.HybridSolver( check_mode_correspondence=True)wiresbeef.mode_identityintosolve()/solve_length_sweep()itself — an opt-in runtime check that each interior interface's adjoining bases actually correspond, rather than trusting the index-i-to-index-i contraction blindly. Both sides' modes are compared on one shared mesh whose resolution defaults to the coarser of the two adjoining solvers' ownres(override withmode_correspondence_res; raise it if a reported mismatch looks like under-resolution rather than a real disagreement). - Results (
beef/results/) —SMatrix, a unified S-parameter result type with transmission/insertion-loss helpers and acheck_passive()sanity check. - Network (
beef/network/) — general S-matrix composition via Redheffer star products:connect()joins two blocks' ports,close_loop()eliminates a block's own port against itself (node elimination — verified against the ring resonator's proven closed-form all-pass formula),Circuitbuilds a multi-block network. Depends only onSMatrix/numpy; not yet wired intoHybridSolveror any example. - Mode identity (
beef/mode_identity.py) — device- and resolution-agnostic check for whetherHybridSolver's index-i-to-index-i contraction between an FDTD interface basis and an EME body basis is actually valid.field_overlap_matrix()computes the normalized transverse E-field overlap between every pair of modes from the two engines (matching field shape, not the scalar n_eff, so it does not fool the way nearest-neighbor n_eff matching does when several modes crowd close together — found directly on the modal multiplexer's 16-mode basis);best_correspondence()then picks the overlap-maximizing one-to-one assignment (Hungarian algorithm), not nearest-neighbor. Deliberately E-only, not the full E-x-H reciprocity overlap: meep and meow were found to report E and H in mutually inconsistent absolute units (~375x apart on a real solved mode, each engine internally self-consistent) — comparing E against E only sidesteps that entirely.mesh_cell_centers()/stack_meow_mode_fields()bridge anEMESolvermesh/modes to the grid and array shapeFDTDSolver.section_mode_fields()expects (remapping meow's own local field-axis convention into beef's device frame along the way), so the two engines' output is directly comparable regardless of device symmetry or which resolution each side used. - Sweep (
beef/sweep/) —ParallelSweep, a localmultiprocessing-based parameter grid sweep. - Plot (
beef/plot/) — geometry, field, and performance (transmission/insertion-loss/splitting-ratio) plots.
import numpy as np
from beef import MMI, EMESolver, SOI_160, plot_performance
mmi = MMI(
n_inputs=1, n_outputs=2,
mm_length=4.5, mm_width=2.0,
waveguide_width=0.35, taper_length=1.0, taper_width=0.8,
waveguide_offset=1.0, waveguide_height=0.16,
)
solver = EMESolver(
num_modes=[1, 5, 12, 5, 1],
num_cells=[1, 8, 1, 8, 1],
lengths=[0.5, 1.0, 4.5, 1.0, 0.5],
oxide_width=5.0, oxide_height=1.5, res=40.0,
material_stack=SOI_160, mode_solver="tidy3d",
)
wavelengths = np.linspace(1.26, 1.36, 11)
smat = solver.solve(mmi, wavelengths, ports=["out_top"], output_idx=1)
smat.check_passive()
plot_performance(smat, output_dir="out", title="1x2 MMI Splitter (EME)")Every solve-family method accepts an optional save_dir: str (default None,
i.e. disabled — no behavior change if you don't pass it). When set, a call is
fingerprinted from the solver's own parameters, the call's arguments, the exact
geometry it draws, and the refractive indices it resolved; a matching prior run
is loaded from disk instead of re-simulated, and a new run is written there for
next time:
smat = solver.solve(mmi, wavelengths, port="top", input_mode=1, save_dir="sim_cache")Each cache entry gets its own subfolder (named by a hash of the fingerprint)
holding config.json (human-readable), result.pkl, and a geometry.png
preview. examples/ wires every applicable example up to a shared
example_sim_cache/ directory this way — committed at the repo root, so a
fresh clone can replay the shipped examples without re-simulating — re-running
an example, or another example that happens to solve an identical section, reuses cached
results automatically; every example's .py script and its paired .ipynb
notebook fingerprint identically too, so running either one hits a cache
entry the other left behind. One known gap: EMESolver.solve_sweep() does
not support the cache yet.
See examples/ — one folder per device, each with a python/ subfolder (.py scripts) and,
where a notebook version exists, a notebooks/ subfolder (matching .ipynb):
examples/mmi_1x2/— 1×2 MMI splitter (EME, FDTD, hybrid)examples/mmi_1x4/— 1×4 MMI splitter on 300 nm SiN (EME, FDTD, hybrid) — exercises the generalizedcore_kind/clad_kindmaterial stacksexamples/mmi_2x2/— 2×2 MMI splitter (EME, FDTD, hybrid) — a genuine multi-input device, exercisingMMI'sinput_idx/input_indicessupportexamples/mmi_2x2_tapered/— waisted 2×2 MMI splitter (EME, FDTD, hybrid), the per-portMMIPorttaper specs applied to a non-uniform bodyexamples/mmi_4x4_90hybrid/— 4×4 MMI 90° optical hybrid (EME, hybrid) — the 167.3 µm device that full-device FDTD cannot reach in practiceexamples/directional_coupler/— directional coupler (FDTD, FDTD length sweep, hybrid, even/odd supermode coupling-length analysis)examples/high_contrast_grating/— high-contrast-grating waveguide mirror (FDTD only, no EME orHybridSolver)examples/high_contrast_grating_fabry_perot/— HCG Fabry–Perot cavity tapped mid-cavity by a directional coupler (hybrid)examples/ring_resonator/— racetrack ring resonator (hybrid: raw-meep coupler-section + 90° bend FDTD, mode-solve neff interpolation, closed-form all-pass feedback assembly — noDeviceGeometry/FDTDSolversupport yet for this device)
Every folder ships .py scripts; all but mmi_1x4 also ship matching notebooks.
Run any example directly:
python examples/mmi_1x2/python/mmi_1x2_eme.pyOutput (geometry/field/performance plots, smatrix.pkl) goes to example_output/<device>/<example>/.
examples/Makefile runs examples via mpirun/srun (auto-detects SLURM/Phoenix), with per-device
and all aggregate targets:
cd examples
make mmi_1x2 # all mmi_1x2 variants
make directional_coupler_fdtd_sweep SWEEP_INDEX=3
make cleanFast unit tests (tests/test_geometry.py, test_smatrix.py, test_hybrid.py — no meep/meow
required) run in CI on every push/PR (.github/workflows/ci.yml):
pytest tests/tests/test_golden_examples.py additionally runs a real (reduced-resolution) meep/meow
simulation per target — mmi_1x2 (EME, FDTD, hybrid) and directional_coupler (hybrid) — and checks
transmission against a recorded reference in tests/golden/. These are genuinely slow (each
target is a real simulation, not a mock, at res=20 vs the real examples' res=40) and are
skipped unless explicitly requested:
pytest tests/ --runslowReal per-target wall-clock times on a single CPU core (macOS laptop, no MPI parallelism):
mmi_1x2_eme ~7 min, mmi_1x2_fdtd ~12 min, mmi_1x2_hybrid ~58 min, directional_coupler_hybrid
~96 min — even at reduced resolution, a real FDTD/EME solve is real solve time, not a mock; the
hybrid targets are the slowest since HybridSolver's short interior solve_section() spans need
dpml raised (0.5→1.0 µm) to keep enough PML pixels at res=20, which grows the domain.
Budget roughly 3 hours to run the full --runslow suite locally. Not covered yet: mmi_2x2_*
and modal_multiplexer_hybrid have no golden test (modal_multiplexer_hybrid deliberately — its
numerical accuracy isn't yet trusted enough to lock in as a reference).
To regenerate the recorded references after a deliberate change to solver behavior:
python scripts/generate_golden_values.py # all targets
python scripts/generate_golden_values.py mmi_1x2_eme # one targetIf you're on macOS behind a VPN, meep's MPI backend can crash on interpreter exit with an
OFI/libfabric error (OFI poll failed) — tests/conftest.py sets FI_PROVIDER=tcp by default to
avoid this; results are unaffected either way, it's purely a finalize-time crash.
.github/workflows/slow-tests.yml runs the 4 golden targets as a 4-way parallel matrix (one job
per target, ~96 min wall-clock for the slowest instead of ~3hr serial) — manual trigger only
(workflow_dispatch), not on every push: there's no value in paying the wall-clock cost on
every PR for tests this coarse. Trigger it from
the repo's Actions tab, or:
gh workflow run slow-tests.ymlPackaging note: this workflow installs pymeep from conda-forge
(mamba-org/setup-micromamba) rather than through a pip extra, for the reason given under
Install — PyPI's meep is an unrelated project, which is why pyproject.toml declares no
[fdtd] extra at all.
If you use BEEF in published work, please cite the paper:
J. M. Hiesener and S. E. Ralph, "BEEF: A Hybrid Integrated Photonic Simulation Framework," arXiv:2611.00393 [physics.optics], 2026. arxiv.org/abs/2611.00393
@misc{hiesener2026beef,
title = {{BEEF}: A Hybrid Integrated Photonic Simulation Framework},
author = {Hiesener, Jacob M. and Ralph, Stephen E.},
year = {2026},
eprint = {2611.00393},
archivePrefix = {arXiv},
primaryClass = {physics.optics},
doi = {10.48550/arXiv.2611.00393},
url = {https://arxiv.org/abs/2611.00393}
}CITATION.cff in this repository carries the machine-readable version, which
GitHub renders as a "Cite this repository" button.
BEEF is released under the Apache License, Version 2.0 — see LICENSE.
BEEF is an orchestration layer: it calls Meep, MEOW, and the rest at run time
but does not bundle or redistribute any of them, and each is installed
separately by the user under its own license.
NOTICE lists them, and explains how BEEF's Apache-2.0 license
sits alongside Meep's GPL-2.0-or-later.