Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [main, master]

permissions:
contents: read

jobs:
test-portable:
name: Portable tests (Python ${{ matrix.python-version }})
Expand Down Expand Up @@ -50,14 +53,54 @@ jobs:
run: uv run python scripts/smoke_dist.py --python "$(uv python find ${{ matrix.python-version }})"

- name: Upload coverage to Codecov
id: codecov_upload
uses: codecov/codecov-action@v4
if: matrix.python-version == '3.13'
with:
files: ./coverage.xml
fail_ci_if_error: false
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

- name: Generate local coverage badge
if: matrix.python-version == '3.13' && steps.codecov_upload.outcome == 'success'
run: uv run python scripts/generate_coverage_badge.py coverage.xml docs/assets/coverage.svg

- name: Upload local coverage badge
if: matrix.python-version == '3.13' && steps.codecov_upload.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: coverage-badge
path: docs/assets/coverage.svg

update-coverage-badge:
name: Update coverage badge
needs: test-portable
runs-on: ubuntu-latest
if: github.event_name == 'push'
permissions:
contents: write

steps:
- uses: actions/checkout@v4

- name: Download local coverage badge
uses: actions/download-artifact@v4
with:
name: coverage-badge
path: tmp/coverage-badge

- name: Commit local coverage badge
run: |
cp tmp/coverage-badge/coverage.svg docs/assets/coverage.svg
if ! git diff --quiet -- docs/assets/coverage.svg; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add docs/assets/coverage.svg
git commit -m "Update coverage badge [skip ci]"
git push
fi

test-full:
name: Full portable tests
runs-on: ubuntu-latest
Expand Down
54 changes: 37 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# micromode

An electromagnetic mode solver using the FDFD method on a regular Yee-grid, written in native Rust.

[![License](https://img.shields.io/github/license/QuentinWach/micromode)](LICENSE)
[![Tests](https://img.shields.io/github/actions/workflow/status/QuentinWach/micromode/tests.yml?branch=main&label=tests)](https://github.com/QuentinWach/micromode/actions/workflows/tests.yml)
![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen)
An **electromagnetic mode solver** using the **[FDFD method](https://en.wikipedia.org/wiki/Finite-difference_frequency-domain_method)** on a **[rectilinear Yee-grid](https://en.wikipedia.org/wiki/Finite-difference_time-domain_method)**, written in native **[Rust](https://rust-lang.org/)**.

```bash
pip install micromode
```

[![License](https://img.shields.io/github/license/QuentinWach/micromode)](LICENSE)
[![Tests](https://img.shields.io/github/actions/workflow/status/QuentinWach/micromode/tests.yml?branch=main&label=tests)](https://github.com/QuentinWach/micromode/actions/workflows/tests.yml)
![Coverage](docs/assets/coverage.svg)
[![PyPI](https://img.shields.io/pypi/v/micromode)](https://pypi.org/project/micromode/)
![Status](https://img.shields.io/badge/status-alpha-orange)


## Why Use It?

Expand Down Expand Up @@ -56,7 +58,35 @@ data.to_hdf5("modes.h5")
```


## High Performance
## Physics

MicroMode solves the source-free frequency-domain Maxwell equations on a
rasterized Yee mode plane,

$$
\nabla\times\mathbf{E}=-i\omega\mu\mathbf{H},
\qquad
\nabla\times\mathbf{H}=i\omega\epsilon\mathbf{E},
$$
with modal fields $\mathbf{E},\mathbf{H}\propto e^{i k_0 n_\mathrm{eff} z}.$

On diagonal material grids this becomes a transverse eigenproblem,

$$
A_\mathrm{diag}
\begin{bmatrix}E_x\\E_y\end{bmatrix}
=
-n_\mathrm{eff}^2
\begin{bmatrix}E_x\\E_y\end{bmatrix}
$$

while full tensor or transformed grids use a first-order tensorial form. The
detailed derivation is in [docs/physics-model.md](docs/physics-model.md), and
the public solver controls are summarized in
[docs/mode-solver-methods.md](docs/mode-solver-methods.md).


## Performance

MicroMode is designed to make high-performance mode solving available without
requiring users to install external solver stacks. The production backend is a
Expand Down Expand Up @@ -84,14 +114,4 @@ around the requested effective index. The Arnoldi stage uses
**shift-invert**, adaptive
[Ritz-pair](https://en.wikipedia.org/wiki/Ritz_method) checkpointing, early
stopping once requested modes are stable, and selective Ritz vector
reconstruction so work is spent on the modes that will actually be returned.

On the repository benchmark problem, the **pure Rust backend** solves larger grids
in the same performance class as the previous optional UMFPACK-backed path while
remaining much easier to install and distribute. For example, a release build on
an Apple Silicon development machine solves an `80x50` diagonal benchmark grid
in roughly **`90 ms` for two modes** with residuals around **`1e-12`**. Exact
timings depend on hardware and problem shape, but the important point is
architectural: MicroMode keeps the **deployability of a pure Rust package**
without giving up the sparse-solver performance expected for practical
waveguide grids.
reconstruction so work is spent on the modes that will actually be returned.
21 changes: 21 additions & 0 deletions docs/assets/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions docs/mode-solver-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Mode Solver Methods

`solve_modes(...)` is the main entry point. It validates the `Materials` grid,
resolves frequencies or wavelengths, builds the Yee derivative matrices, chooses
the diagonal or tensorial Rust backend, solves one frequency at a time, and
returns a coordinate-aware `Result`.

## Material Grids

- `Materials.from_diagonal(...)` builds scalar or diagonal-anisotropic grids.
These usually use the faster diagonal sparse formulation.
- `Materials.from_components(...)` accepts full \(3\times3\) material tensors.
Any off-diagonal component routes the solve through the tensorial sparse
formulation.
- `Materials.from_slice(...)` creates a 1D mode-plane slice with an invariant
width so integrations and overlaps still have physical weights.
- `Materials.from_subpixel_diagonal(...)` downsamples a high-resolution
diagonal raster with arithmetic, harmonic, geometric, min, or max averaging.

## Solver Controls

- `num_modes`: number of modes returned near the requested target.
- `target_neff`: center of the shift-invert search. If omitted, MicroMode uses
a heuristic based on the largest absolute permittivity component.
- `pml`: absorbing boundary thickness and stretch profile via `PmlSpec`.
- `boundary`: low-edge PEC/PMC symmetry settings via `BoundarySpec`.
- `direction`: `"+"` or `"-"` propagation; the backward solve flips the
appropriate magnetic and longitudinal electric signs.
- `components`: optional subset of returned field components.
- `krylov_dim`: dimension of the Arnoldi search space.
- `angle_theta`, `angle_phi`, `bend_radius`, `bend_axis`: transformation-optics
controls that update \(\epsilon\) and \(\mu\) before the sparse solve.

## Eigenpair Selection

Internally, eigenpairs are selected with sparse shift-invert Arnoldi [1, 2].
For a matrix \(A\) and shift \(\sigma\), Arnoldi is applied to

$$
(A-\sigma I)^{-1},
\qquad
\lambda = \sigma + 1/\theta,
$$

where \(\theta\) is a Ritz value of the inverse-shifted operator. The diagonal
backend uses \(\sigma=-\texttt{target_neff}^2\); the tensorial backend uses
\(\sigma=\texttt{target_neff}\).

Returned modes are sorted by decreasing real effective index, normalized to
unit transverse power,

$$
\int (\mathbf{E}\times\mathbf{H}^*)\cdot\hat{\mathbf{n}}\,dA,
$$

and orthogonalized with the unconjugated Lorentz product

$$
L(a,b)=\frac{1}{2}\int
\left[(\mathbf{E}_a\times\mathbf{H}_b)
+(\mathbf{E}_b\times\mathbf{H}_a)\right]\cdot\hat{\mathbf{n}}\,dA.
$$

## Result Helpers

`Result` exposes the post-processing methods users normally need: `n_eff`,
`k_eff`, `mode_area`, `pol_fraction`, `pol_fraction_waveguide`, `modes_info`,
`to_dataframe()`, `overlap()`, `overlap_matrix()`, `plot_field()`,
`plot_field_components()`, `to_hdf5()`, and `Result.from_hdf5()`.

## References

[1] W. E. Arnoldi, "The principle of minimized iterations in the solution of the
matrix eigenvalue problem," *Quarterly of Applied Mathematics*, vol. 9, no. 1,
pp. 17-29, 1951.
[AMS record](https://www.ams.org/qam/1951-09-01/S0033-569X-1951-42792-9/).

[2] R. B. Lehoucq, D. C. Sorensen, and C. Yang, *ARPACK Users' Guide: Solution
of Large-Scale Eigenvalue Problems with Implicitly Restarted Arnoldi Methods*,
SIAM, 1998.
doi:[10.1137/1.9780898719628](https://doi.org/10.1137/1.9780898719628).
Loading
Loading