Skip to content

feat: Simulation.run_local() for MEEP and sim fixes#144

Open
benvial wants to merge 25 commits into
gdsfactory:mainfrom
benvial:feat/meep-run-local-and-sim-fixes
Open

feat: Simulation.run_local() for MEEP and sim fixes#144
benvial wants to merge 25 commits into
gdsfactory:mainfrom
benvial:feat/meep-run-local-and-sim-fixes

Conversation

@benvial
Copy link
Copy Markdown
Member

@benvial benvial commented May 21, 2026

Summary

  • Add Simulation.run_local() for MEEP simulations
  • Fix run_local() to auto-call write_config() matching run() behavior
  • Fix: always regenerate config.json before simulation
  • Update dispersion tests for MEEP-sourced SiO2/Ge coefficients
  • Use scipy.constants.c instead of hardcoded 299792458
  • Suppress matplotlib headless warning and test validity-range warning
  • Use run_local() in meep straight example notebook

Dependencies

Depends on PR #143 (frequency-dependent materials) — uses the refactored material API.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a major refactor of the material system to support frequency-dependent dispersion and anisotropic tensors across both MEEP and Palace solvers, including a new PDK overlay system and support for shaped dielectric volumes. Key improvements involve the implementation of Sellmeier and Lorentzian dispersion models and a three-tier material resolution strategy. Critical feedback highlights that the MEEP material builder ignores new fields, identifies a unit mismatch in conductivity conversion for MEEP, and points out logic errors in tensor rotation. Furthermore, several debug artifacts were found in notebooks and scripts, and a redundant function call was identified in the Palace material resolution logic.

Comment thread src/gsim/meep/script.py
Comment on lines 61 to 70
for name, props in config["materials"].items():
n = props["refractive_index"]
k = props["extinction_coeff"]
if k > 0:
materials[name] = mp.Medium(index=n, D_conductivity=2 * cmath.pi * k / n)
eps_diag = props.get("epsilon_diag")
if eps_diag is not None:
if len(set(eps_diag)) == 1:
materials[name] = mp.Medium(epsilon=eps_diag[0])
else:
materials[name] = mp.Medium(epsilon_diag=eps_diag)
else:
materials[name] = mp.Medium(index=n)
materials[name] = mp.Medium(epsilon=1.0)
return materials
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The build_materials function has not been updated to handle the new fields in MaterialData. It currently ignores epsilon_offdiag, mu_diag, D_conductivity, D_conductivity_diag, and epsilon_susceptibilities. This means that dispersion models, loss, and anisotropy will be ignored when running MEEP simulations locally via run_local(). The function should be refactored to pass all available properties to the mp.Medium constructor.

Returns:
Conductivity in S/m
"""
return 2.0 * math.pi * freq_hz * _EPS0_SI * permittivity * loss_tangent
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The loss_tangent_to_conductivity function returns conductivity in SI units (S/m). However, MEEP's conductivity and D_conductivity parameters expect values in MEEP units (dimensionless, typically $\omega_{MEEP} \tan \delta$). Using SI values directly will result in incorrect loss levels in the simulation. For a unit length $a=1\mu m$, the conversion should be $\sigma_{MEEP} = \sigma_{SI} \cdot \frac{a}{c \epsilon_0 \epsilon_r}$. Alternatively, simply return $\omega_{MEEP} \tan \delta = \frac{2\pi}{\lambda_{\mu m}} \tan \delta$.

Comment on lines +208 to +215
"""
rotated = [[0.0] * 3 for _ in range(3)]
for i in range(3):
for j in range(3):
for k in range(3):
rotated[i][j] += material_axes[i][k] * diag[k] * material_axes[j][k]

return [rotated[0][1], rotated[0][2], rotated[1][2]]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The tensor rotation logic in _rotate_diagonal_tensor has two issues:

  1. The indices in the matrix multiplication are swapped. If material_axes rows are the local axis vectors ($R$), the global tensor $T$ is $R^T D R$, which corresponds to $T_{ij} = \sum_k R_{ki} D_{kk} R_{kj}$. The current code computes $R D R^T$.
  2. Rotation generally affects both diagonal and off-diagonal components. This function should return the full rotated tensor so that _resolved_to_material_data can update data.epsilon_diag as well. Currently, data.epsilon_diag remains set to the local diagonal values while epsilon_offdiag is set to global values, which is inconsistent.
References
  1. Ensure appropriate handling of anisotropic tensors and coordinate transformations.

Comment thread nbs/meep_crossing.ipynb
"print(sim.validate_config())"
"print(sim.validate_config())\n",
"\n",
"xsxs"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Debug artifact xsxs found in notebook cell. This will cause a NameError when the notebook is executed.

Comment thread nbs/meep_crossing.py

print(sim.validate_config())

xsxs
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Debug artifact xsxs found. Please remove it before merging.

Comment thread nbs/palace_microstrip.ipynb Outdated
"# Static PNG\n",
"sim.plot_mesh(show_groups=[\"metal\", \"P\"])"
"sim.plot_mesh(show_groups=[\"metal\", \"P\"])\n",
"xsx"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Debug artifact xsx found in notebook cell.

resolved[name] = dict(props)
continue

evaluated = resolve_material_at_wavelength(name, wavelength_um)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Redundant call to resolve_material_at_wavelength. The evaluated variable is already assigned and checked at line 51.

benvial added 18 commits May 21, 2026 16:28
Add dispersion model types (ValidityRange, SellmeierTerm,
LorentzianTerm, DispersionModel) to the materials database.
Each material entry can store multiple dispersion models covering
different frequency regimes (e.g. SiO2: Sellmeier for optical,
constant-ε for RF), each annotated with validity range and citation.

Key changes:
- MaterialProperties gains dispersion_models, conductivity_diagonal
- evaluate_at_wavelength/frequency resolves the correct model
- resolve_material_at_wavelength() with validity checking and warnings
- should_enable_dispersion() auto-heuristic (Δn/n > 0.5%)
- ResolvedMaterial carries evaluation metadata
- MEEP materials.py uses wavelength-aware resolution
- MaterialData config supports anisotropic + dispersive fields
- LorentzianPoleConfig for susceptibility poles
- FDTD.dispersion flag (auto/true/false)
- Palace config_generator handles conductivity_diagonal
- PDK overlay system (load_overlay, merge_overlay)
- MATERIALS_DB updated with Sellmeier fits and validity ranges
- Comprehensive tests for dispersion models and overlays
Conductors have no optical data; silently skip them instead of
emitting spurious warnings. Add germanium (Ge) to MATERIALS_DB
with Sellmeier fit from Barnes & Piltch 1979 (2.5-12 µm).
Replace Unicode symbols with ASCII equivalents for cp1252
compatibility. Add missing docstrings for interrogate coverage.
Fix line length violations. Fix ruff lint warnings in tests.
Add overlay parameter to resolve_material_at_wavelength() and
should_enable_dispersion(). Add _resolve_with_overlay() helper
implementing priority: user override > PDK overlay > built-in DB.
Thread overlay through MEEP resolve_materials(). Add
Simulation.load_pdk_overlay() and pass overlay in build_config().
Map ResolvedMaterial anisotropic fields to MaterialData: epsilon_diag,
mu_diag, D_conductivity, D_conductivity_diag, epsilon_offdiag.
Add loss_tangent_to_conductivity() converter (sigma = 2*pi*f*eps0*er*tand).
Add _rotate_diagonal_tensor() for material_axes rotation.
Add _resolved_to_material_data() as unified converter.
Implement sellmeier_to_lorentzian_poles() converter mapping Sellmeier
terms to MEEP LorentzianSusceptibility poles (w0=1/sqrt(C),
sigma=B*w0^2, gamma=0). Add lorentzian_to_meep_poles() for direct
Lorentzian mapping. Reimplement resolve_materials_with_dispersion()
with full dispersion flag logic: auto evaluates dn/n per material,
true forces all dispersive, false forces constant-epsilon.
Wire through Simulation.build_config() when solver.dispersion != 'false'.
Add resolve_palace_materials_at_frequency() to evaluate dispersion
models at a target frequency, producing a materials dict for Palace
config generation. Add frequency_hz parameter to generate_palace_config()
and write_config(). When provided, material permittivity is evaluated
from Sellmeier/Lorentzian models at that frequency instead of using
static values. Implements the RFC's external frequency loop strategy
for Palace (which lacks native epsilon(f) support).
Merge permittivity_diagonal → permittivity (float | list[float]),
conductivity_diagonal → conductivity, loss_tangent_diagonal →
loss_tangent. Add _as_list() and _is_tensor() helpers. Add scalar
convenience properties. Remove pydantic ge/le constraints from
union-typed fields. Update overlays, MEEP, Palace, and all tests.
541 tests pass, all pre-commit hooks pass.
Replace refractive_index + extinction_coeff with permittivity +
loss_tangent as the sole solver-facing representation. Removed from
ResolvedMaterial, MaterialProperties, DispersionModel, MaterialData,
and all downstream consumers (MEEP, Palace, overlays, ports, script).

- evaluate_at_wavelength() always sets permittivity directly
- index_variation() computes deps/eps instead of dn/n
- MaterialData uses epsilon_diag as primary field (no refractive_index)
- build_materials() in script.py uses mp.Medium(epsilon=) directly
- Removed MaterialProperties.optical() classmethod
- Removed DispersionModel.refractive_index field
- 540 tests pass, all 23 pre-commit hooks pass
…loat shorthand

Replace refractive-index-based Material(n, k) with permittivity-based
Material(permittivity, loss_tangent). Float shorthand like {"si": 12.0}
is interpreted as permittivity. Remove float-to-n**2 bridge in
_material_overrides. Update dispersion docstrings to deps/eps. Fix
pre-existing indentation bug in _cloud_fixtures.py.
…ter freq

Add f parameter to set_driven(): sim.set_driven(f=50e9) sets fmin=fmax
with num_points=1. Explicit fmin/fmax override f. Add
DrivenConfig.center_frequency property. Remove frequency_hz from
generate_palace_config/write_config; dispersion is now auto-evaluated at
(fmin+fmax)/2. Fix c: 3e8 -> 299_792_458 everywhere. Remove stray
'xsx' in palace_microstrip notebook.
Auto-detect shaped dielectrics from layer_type=dielectric + thin patterned
layers (<5um) with polygon geometry. Remove manual shaped_dielectrics
parameter and layer.shaped field entirely. Photonic stacks (ubcpdk) get
BOX preserved as SiO2 via has_patterned_dielectrics auto-detection.
Silicon conductivity dropped when Sellmeier covers target frequency.
…e check

Shaped dielectric detection now uses _detect_shaped_dielectric_layers():
a dielectric layer is shaped iff it has polygon geometry AND its
(material, z-range) is NOT already covered by a stack.dielectrics box.
Thickness threshold was fragile; box-coverage is structurally correct.
Silicon: Salzberg & Villa 3-term with correct B3=1.541/C3=1104^2
  (n=3.478 at 1550nm, was 3.634 with wrong 3rd term).
SiO2: Malitson 1965 full-precision coefficients, wider 0.21-6.7um range.
Si3N4: Luke 2015 NIR 2-term model (meep: Si3N4_NIR), wider 0.31-5.5um.
Sapphire: Malitson & Dodge 1972 3-term ordinary-axis (n_o=1.746 at 1550nm).
Germanium: Icenogle 1979 with epsilon_inf=9.28 (meep: Ge).
…equency-aware behavior

Material.type (conductor/dielectric/semiconductor) is frequency-independent
but material behavior is not — Si is dielectric at optical, conductive at RF.
Replace with ResolvedMaterial.behavior property that decides "conductive" vs
"dielectric" at evaluation time using conductivity threshold (>=1e4 S/m)
and dispersive model presence.

BREAKING CHANGE: MaterialProperties.type field removed. Use
resolved.behavior instead of material_is_conductor/dielectric.
…erials

Conductive materials (Al, Cu, etc.) have no permittivity field — this is
expected, not an error. Move behavior check before permittivity-is-None
check in resolve_materials_with_dispersion, and skip warning in
resolve_material_at_wavelength when behavior is conductive.
SiO2 validity range updated 3.71->6.7um, source string relaxed.
Ge dispersion test moved to wl=3.0/bw=1.0 where variation exceeds
threshold (new Icenogle model is much flatter at 4um than old model).
MEEP run_local() writes config/GDS/script, then executes run_meep.py
locally via Python (with meep installed). Supports MPI via
num_processes parameter. Returns SParameterResult parsed from CSV.

Also updates silicon n@1550nm test bounds from 3.5-4.2 to 3.4-3.6
after correcting Sellmeier coefficients (n=3.478, was 4.034).
@benvial benvial force-pushed the feat/meep-run-local-and-sim-fixes branch 2 times, most recently from 5f028d4 to 04b2237 Compare May 21, 2026 14:35
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

❌ Patch coverage is 74.05063% with 205 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.42%. Comparing base (ccd7b62) to head (634208c).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/gsim/meep/simulation.py 15.62% 52 Missing and 2 partials ⚠️
src/gsim/meep/materials.py 75.14% 33 Missing and 9 partials ⚠️
src/gsim/common/stack/extractor.py 21.62% 29 Missing ⚠️
src/gsim/common/stack/materials.py 87.44% 17 Missing and 12 partials ⚠️
src/gsim/palace/mesh/geometry.py 79.71% 9 Missing and 5 partials ⚠️
src/gsim/palace/mesh/config_generator.py 71.42% 4 Missing and 4 partials ⚠️
src/gsim/common/stack/overlays.py 90.00% 3 Missing and 4 partials ⚠️
src/gsim/palace/materials.py 87.09% 2 Missing and 2 partials ⚠️
src/gsim/palace/mesh/validation.py 60.00% 2 Missing and 2 partials ⚠️
src/gsim/palace/models/problems.py 55.55% 2 Missing and 2 partials ⚠️
... and 4 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #144      +/-   ##
==========================================
+ Coverage   55.55%   57.42%   +1.87%     
==========================================
  Files          59       61       +2     
  Lines        6966     7655     +689     
  Branches     1270     1445     +175     
==========================================
+ Hits         3870     4396     +526     
- Misses       2697     2827     +130     
- Partials      399      432      +33     

☔ View full report in Codecov by Sentry.
📢 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 4 commits May 21, 2026 16:46
Previously run_local() required manual write_config() call before running,
while run() auto-generated config.json. Now both methods auto-generate
config.json when missing.
Stale config from prior runs with different settings (e.g.
include_substrate toggled) caused Palace 'Material attribute tags
must correspond to attributes in the mesh' errors. Palace run_local()
was the only run* method that skipped config regeneration; Meep and
Palace cloud run() always overwrote it.
@benvial benvial force-pushed the feat/meep-run-local-and-sim-fixes branch from 04b2237 to 634208c Compare May 21, 2026 14:53
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.

1 participant