Skip to content

Commit 5bdabe7

Browse files
authored
Merge pull request #8 from QuentinWach/windows
[Windows Release]: Add platform support and documentation
2 parents b872b38 + 31ae430 commit 5bdabe7

9 files changed

Lines changed: 180 additions & 79 deletions

File tree

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fixtures/mode_solver/**/*.json text eol=lf
2+
fixtures/mode_solver/**/*.hdf5 binary

.github/workflows/publish.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
strategy:
4848
fail-fast: false
4949
matrix:
50-
os: [ubuntu-latest, macos-15-intel, macos-15]
50+
os: [ubuntu-latest, macos-15-intel, macos-15, windows-latest]
5151
python-version: ["3.10", "3.11", "3.12", "3.13"]
5252

5353
steps:
@@ -60,16 +60,25 @@ jobs:
6060
run: uv python install ${{ matrix.python-version }}
6161

6262
- name: Build wheel
63+
shell: bash
6364
run: |
65+
extra_args=()
66+
if [[ "${{ runner.os }}" == "Linux" ]]; then
67+
extra_args+=(--auditwheel repair)
68+
fi
69+
6470
uv tool run --from "maturin>=1.7,<2" maturin build \
6571
--release \
6672
--out dist \
6773
--interpreter "$(uv python find ${{ matrix.python-version }})" \
6874
--compatibility pypi \
69-
--auditwheel repair
75+
"${extra_args[@]}"
7076
7177
- name: Smoke-test wheel
72-
run: PYTHON="$(uv python find ${{ matrix.python-version }})" ./scripts/smoke_dist.sh
78+
shell: bash
79+
run: |
80+
python_bin="$(uv python find ${{ matrix.python-version }})"
81+
"$python_bin" scripts/smoke_dist.py --python "$python_bin"
7382
7483
- name: Upload wheel
7584
uses: actions/upload-artifact@v4
@@ -106,7 +115,7 @@ jobs:
106115
python scripts/check_release_metadata.py
107116
python scripts/check_dist_artifacts.py dist \
108117
--require-cpython 3.10 3.11 3.12 3.13 \
109-
--require-platform macosx manylinux
118+
--require-platform macosx manylinux win
110119
twine check dist/*
111120
112121
- name: Upload checked distributions

.github/workflows/tests.yml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747

4848
- name: Smoke-test wheel
4949
if: matrix.python-version == '3.13'
50-
run: PYTHON="$(uv python find ${{ matrix.python-version }})" ./scripts/smoke_dist.sh
50+
run: uv run python scripts/smoke_dist.py --python "$(uv python find ${{ matrix.python-version }})"
5151

5252
- name: Upload coverage to Codecov
5353
uses: codecov/codecov-action@v4
@@ -84,3 +84,36 @@ jobs:
8484

8585
- name: Run Rust tests
8686
run: cargo test --no-default-features
87+
88+
test-windows:
89+
name: Windows portable tests
90+
runs-on: windows-latest
91+
92+
steps:
93+
- uses: actions/checkout@v4
94+
95+
- name: Install uv
96+
uses: astral-sh/setup-uv@v5
97+
with:
98+
version: "latest"
99+
100+
- name: Set up Python 3.13
101+
run: uv python install 3.13
102+
103+
- name: Install dependencies
104+
run: uv sync --all-extras
105+
106+
- name: Build extension
107+
run: uv run maturin develop
108+
109+
- name: Run Python tests
110+
run: uv run pytest -m "not slow"
111+
112+
- name: Run Rust tests
113+
run: cargo test --no-default-features
114+
115+
- name: Build distributions
116+
run: uv build
117+
118+
- name: Smoke-test wheel
119+
run: uv run python scripts/smoke_dist.py --python "$(uv python find 3.13)"

README.md

Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# micromode
22

3-
A minimal FDFD electromagnetic mode solver for rasterized waveguide cross sections with a Rust core made to be a standard plugin for FDTD engines.
3+
A high-performance electromagnetic mode solver.
4+
It uses the FDFD method on a regular Yee-grid and is written in native Rust.
45

56
[![License](https://img.shields.io/github/license/QuentinWach/micromode)](LICENSE)
67
[![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)
@@ -10,73 +11,21 @@ A minimal FDFD electromagnetic mode solver for rasterized waveguide cross sectio
1011
pip install micromode
1112
```
1213

13-
## Supported Platforms
14-
15-
The Python package uses the portable Rust sparse backend. It does not require
16-
external native sparse-solver libraries at install time.
17-
18-
Published wheels are built for:
19-
20-
- Linux, CPython 3.10-3.13
21-
- macOS Intel and Apple Silicon, CPython 3.10-3.13
22-
23-
Source builds should work on platforms with a supported Rust toolchain and
24-
Python 3.10-3.13. Windows source builds use the same portable backend, but
25-
Windows wheels are not release-tested yet.
26-
27-
## Source Builds
28-
29-
For the portable backend, install Rust and build normally:
30-
31-
```bash
32-
pip install .
33-
```
34-
35-
or, for local development:
36-
37-
```bash
38-
uv sync --all-extras
39-
uv run maturin develop
40-
```
41-
42-
## Performance
43-
44-
MicroMode is designed to make high-performance mode solving available without
45-
requiring users to install external solver stacks. The production backend is a
46-
portable Rust sparse shift-invert eigensolver, so source installs and wheels do
47-
not depend on ARPACK, UMFPACK, SuiteSparse, BLAS/LAPACK, or a Fortran compiler.
48-
That matters for simulation workflows that need to run in CI, notebooks,
49-
container images, FDTD plugins, and cross-platform design tools.
50-
51-
The native solver is not a dense fallback. It uses sparse finite-difference
52-
operators throughout, applies AMD fill-reducing ordering before sparse LU
53-
factorization, stores LU factors in a packed format for repeated triangular
54-
solves, and runs an Arnoldi iteration targeted around the requested effective
55-
index. The Arnoldi stage uses shift-invert, adaptive Ritz-pair checkpointing,
56-
early stopping once requested modes are stable, and selective Ritz vector
57-
reconstruction so work is spent on the modes that will actually be returned.
58-
59-
On the repository benchmark problem, the pure Rust backend solves larger grids
60-
in the same performance class as the previous optional UMFPACK-backed path while
61-
remaining much easier to install and distribute. For example, a release build on
62-
an Apple Silicon development machine solves an `80x50` diagonal benchmark grid
63-
in roughly `90 ms` for two modes with residuals around `1e-12`. Exact timings
64-
depend on hardware and problem shape, but the important point is architectural:
65-
MicroMode keeps the deployability of a pure Rust package without giving up the
66-
sparse-solver performance expected for practical waveguide grids.
6714

6815
## Why Use It?
6916

70-
- Grid-first API: pass arrays directly, with no required geometry model.
71-
- Fast, portable Rust sparse backend: one production solve path.
72-
- Practical outputs: fields, `n_eff`, `k_eff`, mode area, polarization fractions,
17+
- **Grid-first API**: pass arrays directly, with no required geometry model.
18+
- **Fast**, portable Rust sparse backend: one production solve path.
19+
- **Practical** outputs: fields, `n_eff`, `k_eff`, mode area, polarization fractions,
7320
Lorentz overlaps, plotting, dataframe export, and HDF5 save/load.
74-
- Tensor-aware: supports scalar, diagonal anisotropic, and full tensor material
21+
- **Tensor-aware**: supports scalar, diagonal anisotropic, and full tensor material
7522
grids.
76-
- Works for both 2D cross sections and 1D slices.
23+
- Works for both **2D cross sections and 1D slices**.
7724

7825
You give it a material grid. It returns guided modes: effective indices, six-component fields, polarization metrics, mode area, overlaps, diagnostics, plots, and HDF5 output. MicroMode is intentionally not a CAD or geometry package. It is the solver piece you use after geometry has already been rasterized onto a mode-plane grid.
7926

27+
_Micromode is the **default mode solver** in the [BEAMZ FDTD engine](https://github.com/beamzorg/beamz)._
28+
8029

8130
## Quick Start
8231

@@ -106,3 +55,44 @@ print(data.n_eff.values)
10655
data.plot_field("Ex", mode_index=0)
10756
data.to_hdf5("modes.h5")
10857
```
58+
59+
60+
## High Performance
61+
62+
MicroMode is designed to make high-performance mode solving available without
63+
requiring users to install external solver stacks. The production backend is a
64+
**portable Rust [sparse](https://en.wikipedia.org/wiki/Sparse_matrix)
65+
[shift-invert](https://en.wikipedia.org/wiki/Preconditioner#Spectral_transformation)
66+
[eigensolver](https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors)**, so
67+
source installs and wheels do **not** depend on
68+
[ARPACK](https://en.wikipedia.org/wiki/ARPACK),
69+
[UMFPACK](https://en.wikipedia.org/wiki/UMFPACK),
70+
[SuiteSparse](https://en.wikipedia.org/wiki/SuiteSparse),
71+
[BLAS](https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms)/
72+
[LAPACK](https://en.wikipedia.org/wiki/LAPACK), or a Fortran compiler.
73+
That matters for simulation workflows that need to run in CI, notebooks,
74+
container images, FDTD plugins, and cross-platform design tools.
75+
76+
The native solver is **not a dense fallback**. It uses
77+
[sparse](https://en.wikipedia.org/wiki/Sparse_matrix)
78+
[finite-difference](https://en.wikipedia.org/wiki/Finite_difference_method)
79+
operators throughout, applies
80+
[AMD fill-reducing ordering](https://en.wikipedia.org/wiki/Minimum_degree_algorithm)
81+
before sparse [LU factorization](https://en.wikipedia.org/wiki/LU_decomposition),
82+
stores LU factors in a packed format for repeated triangular solves, and runs an
83+
[Arnoldi iteration](https://en.wikipedia.org/wiki/Arnoldi_iteration) targeted
84+
around the requested effective index. The Arnoldi stage uses
85+
**shift-invert**, adaptive
86+
[Ritz-pair](https://en.wikipedia.org/wiki/Ritz_method) checkpointing, early
87+
stopping once requested modes are stable, and selective Ritz vector
88+
reconstruction so work is spent on the modes that will actually be returned.
89+
90+
On the repository benchmark problem, the **pure Rust backend** solves larger grids
91+
in the same performance class as the previous optional UMFPACK-backed path while
92+
remaining much easier to install and distribute. For example, a release build on
93+
an Apple Silicon development machine solves an `80x50` diagonal benchmark grid
94+
in roughly **`90 ms` for two modes** with residuals around **`1e-12`**. Exact
95+
timings depend on hardware and problem shape, but the important point is
96+
architectural: MicroMode keeps the **deployability of a pure Rust package**
97+
without giving up the sparse-solver performance expected for practical
98+
waveguide grids.

docs/release.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,5 @@ checks metadata, and publishes to PyPI using Trusted Publishing.
7171

7272
## Current Wheel Scope
7373

74-
The release workflow builds macOS and Linux wheels for Python 3.10 through
75-
3.13. Windows wheels are intentionally not enabled yet; add Windows once the
76-
portable backend has release coverage on that target.
74+
The release workflow builds Linux, macOS, and Windows wheels for Python 3.10
75+
through 3.13.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ classifiers = [
2525
"Programming Language :: Python :: 3.12",
2626
"Programming Language :: Python :: 3.13",
2727
"Programming Language :: Rust",
28+
"Operating System :: MacOS",
29+
"Operating System :: Microsoft :: Windows",
30+
"Operating System :: POSIX :: Linux",
2831
"Topic :: Scientific/Engineering",
2932
"Topic :: Scientific/Engineering :: Physics",
3033
]

scripts/check_release_metadata.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ def main() -> None:
4343
)
4444
require("--compatibility pypi" in publish_workflow, "publish workflow must request PyPI-compatible wheels")
4545
require("--auditwheel repair" in publish_workflow, "Linux release wheels must be auditwheel-repaired")
46-
require("--require-platform macosx manylinux" in publish_workflow, "release artifact check must require macOS and manylinux wheels")
46+
require("windows-latest" in publish_workflow, "publish workflow is missing Windows wheel builds")
47+
require(
48+
"--require-platform macosx manylinux win" in publish_workflow,
49+
"release artifact check must require macOS, manylinux, and Windows wheels",
50+
)
4751

4852
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
4953
require(project["version"] in changelog, "Python version is not mentioned in CHANGELOG.md")

scripts/smoke_dist.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Smoke-test a built wheel in a fresh virtual environment."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import os
7+
import subprocess
8+
import sys
9+
import tempfile
10+
from pathlib import Path
11+
12+
13+
ROOT = Path(__file__).resolve().parents[1]
14+
15+
16+
def main() -> None:
17+
parser = argparse.ArgumentParser()
18+
parser.add_argument(
19+
"wheel",
20+
nargs="?",
21+
help="Wheel to install; defaults to the newest matching dist/micromode-*.whl",
22+
)
23+
parser.add_argument(
24+
"--python",
25+
default=os.environ.get("PYTHON", sys.executable),
26+
help="Python executable used to create the temporary virtual environment",
27+
)
28+
args = parser.parse_args()
29+
30+
wheel = Path(args.wheel).resolve() if args.wheel else latest_wheel(python_tag(args.python))
31+
with tempfile.TemporaryDirectory() as tmp:
32+
venv_dir = Path(tmp) / "venv"
33+
run([args.python, "-m", "venv", str(venv_dir)])
34+
venv_python = venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
35+
run([str(venv_python), "-m", "pip", "install", "--upgrade", "pip"])
36+
run([str(venv_python), "-m", "pip", "install", str(wheel)])
37+
run([str(venv_python), str(ROOT / "scripts/smoke_wheel.py")])
38+
39+
40+
def latest_wheel(required_tag: str) -> Path:
41+
wheels = sorted(
42+
(ROOT / "dist").glob(f"micromode-*-{required_tag}-*.whl"),
43+
key=lambda path: path.stat().st_mtime,
44+
reverse=True,
45+
)
46+
if not wheels:
47+
raise SystemExit(f"no {required_tag} wheel found in dist/")
48+
return wheels[0].resolve()
49+
50+
51+
def python_tag(python: str) -> str:
52+
command = [
53+
python,
54+
"-c",
55+
"import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')",
56+
]
57+
result = subprocess.run(command, check=True, capture_output=True, text=True)
58+
return result.stdout.strip()
59+
60+
61+
def run(command: list[str]) -> None:
62+
subprocess.run(command, check=True)
63+
64+
65+
if __name__ == "__main__":
66+
main()

scripts/smoke_dist.sh

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@
22
set -euo pipefail
33

44
wheel="${1:-}"
5-
if [[ -z "$wheel" ]]; then
6-
wheel="$(ls -t dist/micromode-*.whl | head -n 1)"
7-
fi
85
python_bin="${PYTHON:-python}"
96

10-
tmpdir="$(mktemp -d)"
11-
trap 'rm -rf "$tmpdir"' EXIT
12-
13-
"$python_bin" -m venv "$tmpdir/venv"
14-
"$tmpdir/venv/bin/python" -m pip install --upgrade pip
15-
"$tmpdir/venv/bin/python" -m pip install "$wheel"
16-
"$tmpdir/venv/bin/python" scripts/smoke_wheel.py
7+
if [[ -n "$wheel" ]]; then
8+
"$python_bin" scripts/smoke_dist.py --python "$python_bin" "$wheel"
9+
else
10+
"$python_bin" scripts/smoke_dist.py --python "$python_bin"
11+
fi

0 commit comments

Comments
 (0)