Skip to content

Anisotropic EME bends - #462

Merged
FilipeFcp merged 7 commits into
developfrom
filipe/AnisotropicEME
Jun 29, 2026
Merged

Anisotropic EME bends#462
FilipeFcp merged 7 commits into
developfrom
filipe/AnisotropicEME

Conversation

@FilipeFcp

@FilipeFcp FilipeFcp commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Hi guys,

These are the results and a sketch of an example notebook. The final formatting and text were mainly done with Claude Code, with some guidance. I think it looks reasonable.

I think we should wait for the next API release before publishing this right? So I will remove the solver version part latter.


Note

Low Risk
Documentation and example-only changes with no production solver or API code in this PR.

Overview
Adds a new EME example for anisotropic bent waveguides, alongside the existing EME solver and isotropic bends notebooks.

The EME docs toctree in eme.rst now links AnisotropicBendsEME, so this workflow shows up in the published EME feature section. The notebook is meant to ship after the next API release; solver-version notes in the draft may be trimmed before publish.

Reviewed by Cursor Bugbot for commit 1ebeedb. Bugbot is set up for automated code reviews on this repo. Configure here.

@caseyflex

Copy link
Copy Markdown
Contributor

This is great! I love the convergence plot. Thanks for finding the bug and helping address it.

  • We should note at the top that the entire notebook is a bit expensive to run
  • I updated the solver and added a flag EMEModeSpec.bend_medium_frame which controls whether the rotation is applied to the material tensor. The default value will capture the behavior from this notebook which is a change in the EME solver behavior and matches FDTD. However the user has the option to change it to co_rotating if they are imagining bending a fiber for example, and they want the tensor to already be defined in the co-rotating frame. Maybe you could take a look at this flag and the docstrings, and echo some of that in this notebook, making that flag explicit too?

@caseyflex

Copy link
Copy Markdown
Contributor

I wonder if there is a better way to use mode sorting / filtering to find the well-guided quasi-TE mode, which could be cleaner than just taking the max transmission

@caseyflex

Copy link
Copy Markdown
Contributor

Actually it would be excellent to explicitly show the permittivity tensor somewhere along the bend for both cases of bend_medium_frame. So we can have a very explicit discussion of the two use cases

@momchil-flex

Copy link
Copy Markdown
Collaborator

Really nice verification thanks @FilipeFcp ! I don't have much to say beyond Casey's comments. Maybe no need to run 20 cells as the convergence seems pretty clear already beyond like 4.

Actually it would be excellent to explicitly show the permittivity tensor somewhere along the bend for both cases of bend_medium_frame. So we can have a very explicit discussion of the two use cases

Is PermittivityMonitor supported in some way in EME? But generally yeah I think this is a good idea, maybe take a 3-cell simulation and plot the diagonal components of the permittivity at the 3 locations. Or even plot eps_xx, eps_xy, eps_xz (first row of the tensor) to show how it becomes tensorial in the "lab" frame and just stays the same in the "co-rotating" frame.

@caseyflex

Copy link
Copy Markdown
Contributor

Permittivitymonitor is fully supported in eme . Or you can probably use sim.epsilon, and you even get subpixel if tidy3d-extras is installed!

@github-actions

github-actions Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Spell Check Report

AnisotropicBendsEME.ipynb:

Cell 3, Line 1: 'Bent-Waveguide'
  > ## Physics Background: Why Anisotropy Complicates Bent-Waveguide EME
Cell 3, Line 22: 'back-reflections'
  > EME divides the waveguide into $N$ cells and matches eigenmodes at each cell boundary. At every interface the two adjacent mode bases are slightly misaligned due to the incremental tensor rotation — generating partial **back-reflections** and **mode conversion**.
Cell 3, Line 26: 'inter-cell'
  > | **1** | Entire 180° bend is one uniform section. No inter-cell interfaces → **no reflections from tensor rotation** → transmission is overestimated. |
Cell 3, Line 27: 'back-reflection'
  > | **few** | Tensor rotation is approximated as a staircase; dominant back-reflection is captured. |
Cell 3, Line 30: 'polarization-state'
  > The loss mechanism is not geometric bending loss (negligible at $r = 5$ µm for this waveguide) but **polarization-state evolution** driven by the rotating anisotropy.
Cell 4, Line 17: 'orientation-sensitive'
  > # orientation-sensitive case. Use (2) to place the optic axis along z instead.
Cell 5, Line 12: 'ModeSortSpec', 'core-confined', 'tensor-rotation'
  > The source and monitor sort their modes with `ModeSortSpec(sort_key="fill_fraction_box", bounding_box=core_box)`: in a bent, anisotropic waveguide the default ordering (by effective index) does not reliably put the guided fundamental first, so we instead rank modes by the fraction of their intensity that falls inside a box wrapping the core. The most core-confined (guided) mode then lands at `mode_index = 0` regardless of polarization or tensor-rotation mixing. (`fill_fraction_box` ranks by **confinement, not polarization** — the polarization is verified separately.)
Cell 12, Line 1: 'most-confined'
  > # Side-by-side view of the two most-confined modes (mode_index = 0 and 1).
Cell 16, Line 11: 'EMEModeSpec'
  > > **On `bend_medium_frame`.** This example relies on `bend_medium_frame="global"` — the default in `EMEModeSpec` since Tidy3D **2.11** — which keeps the LiNbO₃ crystal axes fixed in physical space, the same convention as FDTD. Before this option existed (and as in many other EME solvers) the effective behavior was `"co_rotating"`, where the medium bends together with the local cross-section; in that frame the tensor is identical in every cell, so you would obtain the single-cell ($N=1$) result regardless of the number of cells, and the convergence effect studied here would not appear.
Cell 16, Line 15: 'back-scattering'
  > - **$N = 1$**: No interfaces → no back-scattering from tensor rotation → **overestimated transmission**.
Cell 17, Line 39: 'well-confined'
  > # Sort modes by fill_fraction_box so the well-confined guided mode sits at
Cell 19, Line 1: 'fundamental-to-fundamental'
  > # S00 is the fundamental-to-fundamental transmission. fill_fraction_box sorts the
Cell 19, Line 2: 'core-confined'
  > # most core-confined mode to index 0, but not by polarization — so here we also
Cell 19, Line 3: 'index-'
  > # confirm the index-0 port mode is the quasi-TE fundamental: effective index
Cell 19, Line 9: 'index-'
  > # Effective index and TE fraction of the index-0 port mode (n_eff averaged
Cell 21, Line 10: 'back-reflection'
  > | **Transition** | $N = 2$ | T drops sharply as the dominant back-reflection is captured. |
Cell 21, Line 15: 'straight-waveguide'
  > With a single EME cell the solver computes one set of curved eigenmodes for the entire 180° arc, assuming a fixed tensor orientation. At the ports, these modes are matched to straight-waveguide modes. With no internal interfaces, the system behaves close to a lossless propagator.
Cell 21, Line 17: 'reprojected'
  > In reality, the eigenmodes evolve continuously as the tensor rotates. Splitting the arc into $N$ cells approximates this as discrete steps. At each step, modes are reprojected onto a new basis, introducing reflections and mode mismatch. Increasing $N$ improves this approximation.
Cell 22, Line 3: 'cell-count'
  > The cell-count curve is only meaningful if, at each cell count, the result is already converged in the **number of expansion modes**, and if the mode tracked at index 0 is really the intended quasi-TE fundamental. For the converged `N = 5` case we therefore:
Cell 23, Line 1: '-cell', 'mode-count'
  > # Reuse the converged 5-cell EME, but for this mode-count study bump the solved
Cell 23, Line 2: 'EMEModeSpec'
  > # basis to 20 modes (via the EMEModeSpec), and add a per-cell mode-solver monitor
Cell 23, Line 3: 'interface-residual'
  > # and interface-residual diagnostics.
Cell 24, Line 21: 'Mode-count'
  > ax1.set_title("Mode-count convergence")
Cell 24, Line 26: 'fundamental-mode', 'tangential-E'
  > ax2.set_ylabel("fundamental-mode aperture tangential-E residual")
Cell 24, Line 27: 'excl'
  > ax2.set_title("Interface residual (excl. PML), incident mode 0")
Cell 25, Line 1: 'index-'
  > # Mode identity: effective index and TE fraction of the index-0 mode at every cell.

Checked 1 notebook(s). Found spelling errors in 1 file(s).
Generated by GitHub Action run: https://github.com/flexcompute/tidy3d-notebooks/actions/runs/28203780339

@FilipeFcp

Copy link
Copy Markdown
Contributor Author

@caseyflex @momchil-flex

This is the results running with the new version.
I also sorted the modes for the smallest k_eff, and it seems to work well to pick the correct one.

@caseyflex caseyflex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks pretty good, a few more suggestions

  • it says "store_coeffs=True, # needed to access the full S-matrix". I don't think that is true, the full s-matrix should always be stored.
  • sorting by keff is not something I'm super confident in. You already do a standalone mode solve first to show the guided mode, which is good. Maybe we can show some metrics for all the modes, like the fill_fraction_box, te_fraction, keff, neff, etc. So that we are confident in the filtering approach

@FilipeFcp

Copy link
Copy Markdown
Contributor Author

Thanks Casey, these are good points.

Is the fill factor from this new release? I didn’t find it in the documentation when I first created this notebook. It’s very handy.

@caseyflex

Copy link
Copy Markdown
Contributor

Thanks Casey, these are good points.

Is the fill factor from this new release? I didn’t find it in the documentation when I first created this notebook. It’s very handy.

yes it's very handy! It's been around for a while. the latest version of the docs is actually broken / pointing at an older version -- @daquinteroflex is aware of this and working on it I think

https://docs.flexcompute.com/projects/tidy3d/en/v2.11.0/api/_autosummary/tidy3d.ModeSortSpec.html#tidy3d.ModeSortSpec

@FilipeFcp

Copy link
Copy Markdown
Contributor Author

@caseyflex and @momchil-flex, I think this is as good as it gets.

I tested convergence for both EME and FDTD, and the results appear to be converged.

The main issue I was running into was precision. I had naively assumed that EMEModeSpec used double precision by default, like the regular ModeSpec. Thanks, @momchil-flex, for pointing that out.

@caseyflex

caseyflex commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Thanks Filipe, this is much improved. I think we should do one more pass before merging, mainly because the
current notebook can still be misleading about what is being demonstrated and how to interpret convergence.

Main required fixes:

  1. The notebook still says this is z-cut LiNbO3, but the code uses:

LiNbO3 = td.material_library["LiNbO3"]"Zelmon1997"

In the material API, optical axis index 1 means the extraordinary axis is along global y; global z would be
index 2. For this bend, the current setup is really demonstrating an in-plane/global-y optic axis. That is also
the orientation-sensitive case here. If this is changed to true global-z, the tensor is nearly invariant under
this bend rotation, so the cell-count effect largely disappears. I think the text should be updated to describe
the current case accurately, rather than calling it z-cut.

  1. The mode identification issue is only partially addressed. The source-plane table is useful, but the notebook
    still assumes that S21[0, 0] always means the same physical mode across the EME ports/cells. Since this was
    part of the original concern, I think the example should include at least a compact sanity check of the EME
    modes: print/plot n_complex, TE fraction if available, and/or fill fraction for the modes at the ports and
    representative cells. The text should avoid implying that fill_fraction_box alone guarantees the desired
    polarization.

  2. I would add a small EMEModeSweep convergence check. The cell-count plot is not enough unless each cell count
    is already converged in mode count. A compact sweep such as [4, 6, 8, 10, 12, 15] for one or two
    representative num_cells values would make the convergence claim much more solid.

  3. Please enable and show EME diagnostics, especially the aperture/interface residual excluding PML. That gives
    a better stopping criterion than expecting strict monotonicity in |S00|^2.

  4. The narrative still implies that transmission should decrease monotonically and match FDTD to numerical
    precision. I would soften this. A piecewise-constant EME approximation to a smoothly rotating anisotropic
    tensor should converge, but exact monotonicity of S00 is not guaranteed, especially with modal truncation and
    possible mode reordering/conversion.

Smaller cleanup:

  • Please set bend_medium_frame explicitly and explain the choice. For the current physics I think this should be
    the global/lab-frame material-axis interpretation.

  • store_coeffs=True does not seem to be used; remove it unless needed.

  • Some executed cloud-run cells have no visible output, while later cells depend on their results. For a
    published notebook, the FDTD reference value and the final EME/FDTD comparison should be visible and
    auditable.

  • The ModeSource documentation link points to an older version.

  • It would be good to include a short note that this is a relatively expensive example and that precision is
    intentionally set to double.

Overall, I think the revised numerical results look much smoother, and the double precision / relaxed
increasing-mode tolerance changes are in the right direction. I just do not think we should publish it until the
optic-axis description and the mode/convergence diagnostics are tightened up, because otherwise readers may
overinterpret S00 and the monotonic cell-count story.

@caseyflex

Copy link
Copy Markdown
Contributor

So this is AI, but it's based off the recommendations I gave you here and on slack, some of which presumably were important while you were making sure this was converged, and showing the correct mode. So maybe not every point is strictly needed, but some of it could certainly be helpful. can you have your AI agent take a look?

@caseyflex
caseyflex self-requested a review June 24, 2026 00:17
@caseyflex

Copy link
Copy Markdown
Contributor

I did go ahead and approve it, so you can use your judgement about which ones of those you care about.

@caseyflex

Copy link
Copy Markdown
Contributor

But thanks for your efforts on this - it is a nice notebook and will be great to have

@momchil-flex momchil-flex left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me, except I still think we can take this chance to educate users a little more about what's happening.

Currently bend_medium_frame is not discussed at all. Even though the new default setting of "global" works for this example out of the box, I think it's worth mentioning that this is only the case starting 2.11, and that before this argument was introduced (and in many other EME solvers), the default is effectively the "co_rotating" setting. In that setting, you will always get the N=1 result here, regardless of how many EME cells you put.

This comment from before is related and still not addressed, and I think it would be worth it.

Actually it would be excellent to explicitly show the permittivity tensor somewhere along the bend for both cases of bend_medium_frame. So we can have a very explicit discussion of the two use cases

@FilipeFcp

Copy link
Copy Markdown
Contributor Author

Hi all,

I think I have addressed all of your comments.

Since this will likely be read more by AI than by humans from now on, I decided to keep all the convergence tests and be a bit more verbose in explaining everything.

@caseyflex caseyflex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks Filipe this looks really good to me! A really nice feature showcase and a good teaching opportunity for how users can debug EME simulations.

@FilipeFcp
FilipeFcp merged commit c37c785 into develop Jun 29, 2026
1 of 2 checks passed
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.

3 participants