Skip to content

Conversation

@gronniger
Copy link

@gronniger gronniger commented Feb 9, 2026

PR makes the following changes:

  • keep origin of component (not shifting to bounding box center)
  • remove is_3d parameter in get_meep_geometry_from_component (geometry for 2D simulation just sliced out of 3D geometry)
  • add parameters normal_2d and point_2d to specify 2D simulation plane
  • adjust simulation volume, source and monitor sizes to fit 2D dimensions
  • check sources & monitors to be in simulation volume
  • in write_sparameters_meep.py do not iterate over component ports which are not in simulation domain

Using the example script

import meep as mp
import gdsfactory as gf
from gdsfactory.technology.layer_stack import LayerStack, LayerLevel, LogicalLayer
from gdsfactory.technology.layer_map import LayerMap, LayerViews
from gplugins import gmeep


class LAYER_MAP(LayerMap):
    WG1 = (85, 0)
    WG2 = (84, 0)

    PADDING = (100, 0)


LAYER_STACK = LayerStack(
    layers=dict(
        wg1=LayerLevel(
            layer=LogicalLayer(layer='WG1') - LogicalLayer(layer='WG2'),
            zmin=0,
            thickness=0.1,
            material='si',
            derived_layer=LogicalLayer(layer='WG1')
        ),
        wg2=LayerLevel(
            layer='WG2',
            zmin=0,
            thickness=0.2,
            material='si',
        ),
    )
)

PDK = gf.pdk.Pdk(
    name='test_pdk',
    layers=LAYER_MAP,
    layer_stack=LAYER_STACK,
    layer_views=LayerViews('./SiN_PDK/layer_views.yaml'),
)
PDK.activate()


c = gf.Component()
wg1 = c << gf.c.straight(cross_section=gf.cross_section.strip(layer='WG1', width=1.0), length=10)
wg2 = (c << gf.c.straight(cross_section=gf.cross_section.strip(layer='WG2', width=1.0), length=10)).rotate(90).move((5, -5))
c.add_port('o1', port=wg1.ports['o1'])
c.add_port('o2', port=wg1.ports['o2'])
c.add_port('o3', port=wg2.ports['o1'])
c.add_port('o4', port=wg2.ports['o2'])

common_kwargs = dict(
    component=c,
    xmargin=1., ymargin=1, zmargin=1,
    is_3d=False,
    point_2d=(5, 0, 0),
    overwrite=True,
    decay_by=1e-2,
    animate=True,
    animate_size=(10, 10, 0),
    animate_center=(5, 0, 0),
    wavelength_points=3,
    port_monitor_offset=0,
    port_source_offset=0.3,
    distance_source_to_monitors=0.3,
)
result_z = gmeep.write_sparameters_meep(normal_2d='Z', **common_kwargs)
result_y = gmeep.write_sparameters_meep(normal_2d='Y', port_source_names=['o1', 'o2'], plot_args={'fields': mp.Ey}, **common_kwargs)
result_x = gmeep.write_sparameters_meep(normal_2d='X', port_source_names=['o3', 'o4'], plot_args={'fields': mp.Ex}, **common_kwargs)

all simulations are working as expected (where actual S-parameters may not be meaningful/comparable as this is just an example),

Summary by Sourcery

Support 2D Meep simulations in arbitrary X-, Y-, or Z-normal planes while keeping component coordinates fixed and ensuring only in-domain ports participate in S-parameter calculations.

New Features:

  • Add support for specifying 2D simulation planes via normal_2d and point_2d parameters in Meep simulations.
  • Allow S-parameter extraction to run 2D simulations in X-, Y-, or Z-normal planes using the same 3D geometry description.

Bug Fixes:

  • Avoid including sources and monitors that lie outside the specified 2D simulation domain, preventing invalid configurations during S-parameter calculations.

Enhancements:

  • Keep component origins unchanged instead of shifting to the bounding box center when building Meep simulations.
  • Derive 2D simulation geometry directly from full 3D geometry without a separate is_3d flag in get_meep_geometry_from_component.
  • Adjust simulation cell size, source and monitor extents, and plotting centers to respect the chosen 2D plane and component origin.
  • Improve robustness of port-based monitors and eigenmode coefficient parsing by using the component's own ports and the active monitor set.

- remove is_3d parameter in get_meep_geometry_from_component (geometry for 2D simulation just sliced out of 3D geometry)
- add parameters normal_2d and point_2d to specify 2D simulation plane
- adjust simulation volume, source and monitor sizes to fit 2D dimensions
- check sources & monitors to be in simulation volume
- in write_sparameters_meep.py do not iterate over component ports which are not in simulation domain
@gronniger gronniger requested a review from joamatab as a code owner February 9, 2026 13:43
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 9, 2026

Reviewer's Guide

Adds support for running Meep 2D simulations on arbitrary X/Y/Z-normal planes without re-centering components, by configuring cell size and geometry slicing from full 3D geometry, enforcing that sources/monitors lie in the chosen 2D plane, and updating S-parameter calculation and plotting to use the new behavior.

Sequence diagram for write_sparameters_meep and 2D-plane-aware simulation and S-parameter flow

sequenceDiagram
    participant UserScript
    participant write_sparameters_meep
    participant get_simulation
    participant is_point_in_plane
    participant MeepSimulation as Meep_Simulation
    participant sparameter_calculation
    participant parse_port_eigenmode_coeff

    UserScript->>write_sparameters_meep: call(component, is_3d, normal_2d, point_2d, ...)

    write_sparameters_meep->>get_simulation: get_simulation(component, is_3d, normal_2d, point_2d, ...)

    get_simulation->>get_simulation: compute_normal_vec_from_normal_2d
    get_simulation->>get_simulation: compute_cell_size_with_zero_dim_for_2D

    get_simulation->>is_point_in_plane: check_source_center(test_point=center, plane_support=point_2d, plane_normal=normal_vec)
    is_point_in_plane-->>get_simulation: bool
    get_simulation->>get_simulation: raise_error_if_source_not_in_plane

    loop for_each_port_for_monitor
        get_simulation->>is_point_in_plane: check_monitor_center
        is_point_in_plane-->>get_simulation: bool
        alt in_plane
            get_simulation->>MeepSimulation: sim.add_mode_monitor
        else not_in_plane
            get_simulation->>get_simulation: warn_monitor_ignored
        end
    end

    get_simulation-->>write_sparameters_meep: sim_dict(sim, cell_size, monitors, freqs)

    write_sparameters_meep->>sparameter_calculation: sparameter_calculation(..., sim_dict, ...)

    sparameter_calculation->>MeepSimulation: run_simulation_until_energy_decayed

    loop for_each_port_in_sim_dict_monitors
        loop for_each_port_mode
            sparameter_calculation->>parse_port_eigenmode_coeff: parse_port_eigenmode_coeff(port_name, ports, sim_dict, port_mode)
            parse_port_eigenmode_coeff-->>sparameter_calculation: (entering, exiting)
            sparameter_calculation->>sparameter_calculation: update_smatrix_entries
        end
    end

    sparameter_calculation-->>write_sparameters_meep: sparameters
    write_sparameters_meep-->>UserScript: sparameters_results
Loading

Class diagram for updated Meep simulation helpers with 2D plane support

classDiagram
    class get_simulation {
        +dict get_simulation(component, resolution, extend_ports_length, port_margin, port_source_name, port_source_mode, layer_stack, material_name_to_meep, dispersive, zmargin_top, zmargin_bot, xmargin, ymargin, tpml, clad_material, is_3d, normal_2d, point_2d, wavelength_start, wavelength_stop, wavelength_points, dfcen, port_symmetries, port_names, port_modes, distance_source_to_monitors, port_monitor_offset, port_source_offset, port_symmetries, **settings)
    }

    class is_point_in_plane {
        +bool is_point_in_plane(test_point, plane_support, plane_normal, tolerance)
    }

    class get_meep_geometry_from_component {
        +list get_meep_geometry_from_component(component, layer_stack, material_name_to_meep, wavelength, dispersive, exclude_layers, **kwargs)
    }

    class write_sparameters_meep {
        +dict write_sparameters_meep(component, layer_stack, material_name_to_meep, resolution, is_3d, normal_2d, point_2d, wavelength_start, wavelength_stop, wavelength_points, port_source_name, port_source_mode, port_symmetries, port_source_names, port_monitor_names, port_names, port_modes, xmargin, ymargin, zmargin, tpml, clad_material, extend_ports_length, distance_source_to_monitors, port_monitor_offset, port_source_offset, animate, animate_size, animate_center, decay_by, plot_args, exclude_layers, dispersive, **settings)
    }

    class sparameter_calculation {
        +dict sparameter_calculation(port_source_names, component, port_symmetries, port_source_mode, wavelength_start, wavelength_stop, wavelength_points, normal_2d, point_2d, is_3d, animate, decay_by, **settings)
    }

    class parse_port_eigenmode_coeff {
        +tuple parse_port_eigenmode_coeff(port_name, ports, sim_dict, port_mode)
    }

    class MeepSimulation {
        +mp.Simulation sim
        +mp.Vector3 cell_size
        +dict monitors
        +list freqs
    }

    get_simulation --> get_meep_geometry_from_component : uses
    get_simulation --> is_point_in_plane : validates_sources_and_monitors
    get_simulation --> MeepSimulation : builds

    write_sparameters_meep --> get_simulation : obtains_initial_sim
    write_sparameters_meep --> sparameter_calculation : computes_sparameters

    sparameter_calculation --> parse_port_eigenmode_coeff : reads_coefficients
    sparameter_calculation --> MeepSimulation : uses

    parse_port_eigenmode_coeff --> MeepSimulation : accesses_monitors
Loading

File-Level Changes

Change Details Files
Extend simulation setup to support 2D simulations in X-, Y-, or Z-normal planes using a shared 3D geometry description.
  • Add normal_2d and point_2d parameters to get_simulation to define the 2D simulation plane and its support point.
  • Derive a plane normal vector from normal_2d and use it with a new is_point_in_plane helper to validate that objects lie in the 2D slice.
  • Compute cell_size with one dimension collapsed to zero for non-3D runs depending on the selected normal_2d.
  • Set geometry_center based on point_2d for the collapsed dimension so that the 2D slice is positioned correctly within the 3D geometry.
gplugins/gmeep/get_simulation.py
Adjust source and monitor placement/sizing logic so that they are compatible with arbitrary 2D planes and are validated against the simulation domain.
  • Stop wrapping the component in a dummy reference and operate directly on component ports for source/monitor creation.
  • Always extend ports using gf.c.extend_ports without re-centering to preserve component origin.
  • Make source and monitor region sizes zero along the collapsed dimension in 2D simulations while keeping full 3D sizes otherwise.
  • Round source/monitor centers for numerical stability and enforce that 2D sources lie in the specified plane, raising on invalid sources and skipping out-of-plane monitors with a warning.
  • Simplify eigenmode source parity to mp.NO_PARITY for both 2D and 3D cases.
gplugins/gmeep/get_simulation.py
Simplify Meep geometry construction to always use true layer z-positions and sidewall angles, independent of 2D vs 3D mode, and remove the is_3d flag.
  • Remove the is_3d parameter from get_meep_geometry_from_component and its call sites.
  • Always place polygon vertices at the layer level.zmin instead of flattening to z=0 for 2D.
  • Always compute sidewall_angle from the layer sidewall_angle setting, with a note about libctl issues for slanted prisms.
gplugins/gmeep/get_meep_geometry.py
gplugins/gmeep/get_simulation.py
Update S-parameter post-processing to work with a subset of ports that actually have monitors in the selected 2D domain and to improve plotting center handling.
  • Change parse_port_eigenmode_coeff to accept ports of type Ports and clarify its arguments.
  • In S-parameter calculation, iterate over sim_dict['monitors'].keys() instead of a static port_names list so ports without monitors (e.g., out-of-plane) are ignored.
  • Remove port_names from sparameter_calculation signature and its internal calls to avoid mismatches.
  • Update 2D plotting to use the component’s (x, y) as the slice center instead of hard-coded (0, 0).
gplugins/gmeep/write_sparameters_meep.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • Now that get_meep_geometry_from_component always uses level.zmin and a nonzero sidewall_angle, the geometry for Z-normal "2D" simulations is no longer flattened to z=0 – if this isn’t intentional you may want to conditionally zero zmin/sidewall_angle when running a Z-normal 2D setup so objects remain inside a zero-thickness z cell.
  • The change from a conditional extend_ports(..., centered=True) to always calling extend_ports(component=component, length=extend_ports_length) without centered=True will shift port extensions relative to the original behavior; if callers rely on symmetric extensions or extend_ports_length being optional/False-y, consider restoring the conditional call or explicitly handling the None/0 case and the centered option.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Now that `get_meep_geometry_from_component` always uses `level.zmin` and a nonzero `sidewall_angle`, the geometry for Z-normal "2D" simulations is no longer flattened to z=0 – if this isn’t intentional you may want to conditionally zero `zmin`/`sidewall_angle` when running a Z-normal 2D setup so objects remain inside a zero-thickness z cell.
- The change from a conditional `extend_ports(..., centered=True)` to always calling `extend_ports(component=component, length=extend_ports_length)` without `centered=True` will shift port extensions relative to the original behavior; if callers rely on symmetric extensions or `extend_ports_length` being optional/False-y, consider restoring the conditional call or explicitly handling the `None/0` case and the `centered` option.

## Individual Comments

### Comment 1
<location> `gplugins/gmeep/get_simulation.py:29-38` </location>
<code_context>
 settings_meep = set(sig.parameters.keys())


+def is_point_in_plane(
+        test_point: Float3,
+        plane_support: Float3,
+        plane_normal: Float3,
+        tolerance: float = 1e-6,        # 1 pm for coordinates in µm
+):
+    a, b, c = plane_normal
+    xt, yt, zt = test_point
+    x0, y0, z0 = plane_support
+    distance = (a*(xt-x0) + b*(yt-y0) + c*(zt-z0)) / np.linalg.norm(plane_normal)
+    return bool(abs(distance) <= tolerance)
+
</code_context>

<issue_to_address>
**issue:** Guard against a zero-length plane_normal in is_point_in_plane.

`is_point_in_plane` will divide by `np.linalg.norm(plane_normal)` without handling the zero (or near-zero) case. Since this is a general helper, please add a guard (e.g., raise if the norm is below a small threshold) to avoid silent NaNs when called with invalid normals.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +29 to +38
def is_point_in_plane(
test_point: Float3,
plane_support: Float3,
plane_normal: Float3,
tolerance: float = 1e-6, # 1 pm for coordinates in µm
):
a, b, c = plane_normal
xt, yt, zt = test_point
x0, y0, z0 = plane_support
distance = (a*(xt-x0) + b*(yt-y0) + c*(zt-z0)) / np.linalg.norm(plane_normal)
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: Guard against a zero-length plane_normal in is_point_in_plane.

is_point_in_plane will divide by np.linalg.norm(plane_normal) without handling the zero (or near-zero) case. Since this is a general helper, please add a guard (e.g., raise if the norm is below a small threshold) to avoid silent NaNs when called with invalid normals.

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