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
6 changes: 6 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@v5
Expand Down Expand Up @@ -52,6 +54,8 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@v5
Expand Down Expand Up @@ -92,6 +96,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Download artifacts
uses: actions/download-artifact@v4
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,47 @@ permissions:
contents: read

jobs:
lint:
name: Lint and type check
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Install Rust tooling
run: rustup component add rustfmt clippy

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
Comment on lines +18 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unpinned action refs and checkout credential persistence in workflow files.
rg -n 'uses:\s*[^ ]+@v[0-9]+' .github/workflows/tests.yml
rg -n -A3 -B1 'uses:\s*actions/checkout@' .github/workflows/tests.yml

Repository: QuentinWach/micromode

Length of output: 1232


Pin action SHAs and disable checkout credential persistence.

The actions at these lines are tag-pinned (@v4, @v5) rather than commit-pinned, and actions/checkout lacks persist-credentials: false. This weakens CI supply-chain hardening.

Suggested hardening
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<pinned-commit-sha>
+        with:
+          persist-credentials: false

-      - name: Install uv
-        uses: astral-sh/setup-uv@v5
+      - name: Install uv
+        uses: astral-sh/setup-uv@<pinned-commit-sha>
         with:
           version: "latest"

Note: This pattern appears throughout the workflow file; consider applying the same hardening to all action usages.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 18-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/tests.yml around lines 18 - 26, The workflow uses
tag-pinned actions and leaves checkout credentials persisted; update the steps
using actions/checkout and astral-sh/setup-uv (and any other action usages) to
use commit SHAs instead of tags and add persist-credentials: false to the
actions/checkout step; specifically locate the checkout step named "uses:
actions/checkout@v4" and set persist-credentials: false, and replace "uses:
astral-sh/setup-uv@v5" (and any other tag-pinned actions such as those for rust
tooling if applicable) with their corresponding full commit SHA refs to harden
supply-chain integrity.


- name: Set up Python 3.13
run: uv python install 3.13

- name: Install dependencies
run: uv sync --all-extras

- name: Check Rust formatting
run: cargo fmt --all -- --check

- name: Run Rust clippy
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Check Python formatting
run: uv run ruff format --check .

- name: Run Python lint
run: uv run ruff check .

- name: Run Python type check
run: uv run pyright

test-portable:
name: Portable tests (Python ${{ matrix.python-version }})
needs: lint
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -20,6 +59,8 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@v5
Expand Down Expand Up @@ -83,6 +124,8 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Download local coverage badge
uses: actions/download-artifact@v4
Expand All @@ -105,6 +148,7 @@ jobs:

test-full:
name: Full portable tests
needs: lint
runs-on: ubuntu-latest

steps:
Expand Down Expand Up @@ -132,6 +176,7 @@ jobs:

test-windows:
name: Windows portable tests
needs: lint
runs-on: windows-latest

steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ examples/example_outputs_contact_sheet.png
examples/material_grid_outputs/
examples/ridge_waveguide_outputs/
examples/soi_hybridization_outputs/
examples/tidy3d_modal_outputs/
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Changelog

## 0.1.0a3 - Unreleased
## 0.1.0a4 - Unreleased

- Fixed y-normal field mapping so returned global fields use a right-handed
basis and physical +y power overlap is positive.
- Added a regression test for y-normal power-overlap sign.

## 0.1.0a3

Initial alpha release candidate.

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "micromode-core"
version = "0.1.0-alpha.3"
version = "0.1.0-alpha.4"
edition = "2021"
description = "Rust core for the MicroMode photonics mode solver."
license = "Apache-2.0"
Expand Down Expand Up @@ -32,3 +32,7 @@ rlu = "0.7"
default = []
python = ["pyo3"]
extension-module = ["python", "pyo3/extension-module"]

[lints.clippy]
needless_range_loop = "allow"
too_many_arguments = "allow"
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,33 @@ data.plot_field("Ex", mode_index=0)
data.to_hdf5("modes.h5")
```

## Examples


### Tidy3D Waveguide
![Tidy3D modal monitor example](docs/assets/tidy3d_modal_modes.png)

The Tidy3D modal monitor example recreates the strip-waveguide setup from
Flexcompute's modal sources and monitors notebook. It solves the first three
x-propagating modes of a silicon waveguide on a silica substrate and plots
`|Ey|` and `|Ez|` on the same y-z mode plane. (See [Tidy3D, "Defining Mode Sources and Monitors"](https://www.flexcompute.com/tidy3d/examples/notebooks/ModalSourcesMonitors/).)

```bash
uv run --extra dev python examples/tidy3d_modal_sources_monitors.py
```

### Hybridization Sweep
![Hybridization sweep example](docs/assets/hybridization_sweep.png)

The SOI hybridization example sweeps the width of a 220 nm silicon ridge and
solves several modes at each step. It shows how nearby modes exchange character
as the geometry changes by plotting effective index and TE fraction across the
sweep, then rendering representative field profiles.

```bash
uv run --extra dev python examples/soi_hybridization_sweep.py
```


## Physics

Expand Down Expand Up @@ -96,4 +123,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.
reconstruction so work is spent on the modes that will actually be returned.
15 changes: 7 additions & 8 deletions benchmarks/compare_mode_solver_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,7 @@ def main() -> None:
report["cases"].append({"case_id": entry["case_id"], **status})

print(f"Inspected {len(entries)} fixture(s) from {fixture_root}")
print(
"Local validation: "
+ ", ".join(f"{key}={value}" for key, value in report["summary"].items() if value)
)
print("Local validation: " + ", ".join(f"{key}={value}" for key, value in report["summary"].items() if value))
if args.report_json is not None:
args.report_json.parent.mkdir(parents=True, exist_ok=True)
args.report_json.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
Expand Down Expand Up @@ -325,11 +322,13 @@ def _compare_local_case(root: Path, entry: dict) -> dict:
_AXIS_INDEX = {"x": 0, "y": 1, "z": 2}


def _solver_edges_from_field_coords(edges: tuple[np.ndarray, np.ndarray], recipe: dict) -> tuple[np.ndarray, np.ndarray]:
def _solver_edges_from_field_coords(
edges: tuple[np.ndarray, np.ndarray], recipe: dict
) -> tuple[np.ndarray, np.ndarray]:
dmin_pmc = tuple(bool(value) for value in recipe.get("dmin_pmc", (False, False)))
trim_edges = tuple(recipe.get("trim_edges", ((0, 0), (0, 0))))
out = []
for axis_edges, has_min_symmetry, (trim_start, trim_end) in zip(edges, dmin_pmc, trim_edges):
for axis_edges, has_min_symmetry, (trim_start, trim_end) in zip(edges, dmin_pmc, trim_edges, strict=True):
if not has_min_symmetry:
trimmed = axis_edges
if trim_start or trim_end:
Expand Down Expand Up @@ -488,7 +487,7 @@ def _eps_from_recipe(
mask = np.ones(eps.shape, dtype=bool)
center = box.get("center", (0.0, 0.0, 0.0))
size = box["size"]
for grid, dim in zip(grids, tangent_dims):
for grid, dim in zip(grids, tangent_dims, strict=True):
axis = _AXIS_INDEX[dim]
mask &= np.abs(grid - center[axis]) <= abs(size[axis]) / 2
eps_value = complex(box["eps"])
Expand All @@ -500,7 +499,7 @@ def _eps_from_recipe(
center = circle.get("center", (0.0, 0.0, 0.0))
radius = float(circle["radius"])
distance_sq = np.zeros(eps.shape, dtype=float)
for grid, dim in zip(grids, tangent_dims):
for grid, dim in zip(grids, tangent_dims, strict=True):
distance_sq += (grid - center[_AXIS_INDEX[dim]]) ** 2
eps[distance_sq <= radius * radius] = complex(circle["eps"])
return eps
Expand Down
8 changes: 2 additions & 6 deletions benchmarks/mode_solver/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,15 @@ def _read_xarray_group(group: Any) -> xr.DataArray:
if _XARRAY_VALUE_NAME not in group:
raise KeyError(f"HDF5 group {group.name!r} is missing {_XARRAY_VALUE_NAME!r}")
values = group[_XARRAY_VALUE_NAME][()]
coords = {
name: group[name][()]
for name in group.keys()
if name != _XARRAY_VALUE_NAME and hasattr(group[name], "shape")
}
coords = {name: group[name][()] for name in group if name != _XARRAY_VALUE_NAME and hasattr(group[name], "shape")}
dims = _infer_dims(values.shape, coords)
xarray_coords = {dim: coords[dim] for dim in dims if dim in coords}
return xr.DataArray(values, dims=dims, coords=xarray_coords)


def _infer_dims(shape: tuple[int, ...], coords: dict[str, np.ndarray]) -> tuple[str, ...]:
dims = tuple(dim for dim in _PREFERRED_DIMS if dim in coords)
if len(dims) == len(shape) and all(len(coords[dim]) == size for dim, size in zip(dims, shape)):
if len(dims) == len(shape) and all(len(coords[dim]) == size for dim, size in zip(dims, shape, strict=True)):
return dims

exact = [dim for dim in _PREFERRED_DIMS if dim in coords and len(coords[dim]) in set(shape)]
Expand Down
Binary file added docs/assets/hybridization_sweep.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/ridge_modes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/tidy3d_modal_modes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 13 additions & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,22 @@ That example sweeps a 220 nm fully etched SOI ridge width and writes the
effective-index and TE-fraction plots to
`examples/soi_hybridization_outputs/`.

Run the README ridge-waveguide example:
Recreate the Tidy3D modal sources/monitors mode plot:

```bash
uv run --extra dev python examples/tidy3d_modal_sources_monitors.py
```

That example uses the strip-waveguide geometry from the Tidy3D modal
sources/monitors notebook and writes `|Ey|` and `|Ez|` field plots for the first
three modes to `examples/tidy3d_modal_outputs/`.

Run the README angled-slab waveguide example:

```bash
uv run --extra dev python examples/ridge_waveguide_readme.py
```

That example rasterizes a 220 nm SOI rib waveguide with a 90 nm slab, 500 nm
top ridge width, inverted angled sidewalls, and subpixel material averaging.
That example rasterizes a 220 nm silicon slab with 500 nm top width and 80
degree angled sidewalls on an oxide substrate with air around it.
It writes publication-style plots to `examples/ridge_waveguide_outputs/`.
20 changes: 11 additions & 9 deletions examples/material_grid_demos.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from __future__ import annotations

import argparse
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Sequence

import matplotlib
import numpy as np
Expand All @@ -15,7 +15,6 @@
import matplotlib.pyplot as plt
from matplotlib.axes import Axes


FREQ_1550 = sm.C_0 / 1.55
SI_EPS = 3.48**2
SIO2_EPS = 1.44**2
Expand Down Expand Up @@ -167,17 +166,17 @@ def make_slot_grid() -> tuple[sm.Materials, np.ndarray]:
x_edges, y_edges, xx, yy = demo_grid(nx=42, ny=30)
eps = np.full(xx.shape, SIO2_EPS, dtype=np.complex128)
rail = np.abs(yy) <= 0.11
left = (-0.30 <= xx) & (xx <= -0.06)
right = (0.06 <= xx) & (xx <= 0.30)
left = (xx >= -0.30) & (xx <= -0.06)
right = (xx >= 0.06) & (xx <= 0.30)
eps[rail & (left | right)] = SI_EPS
return sm.Materials.from_diagonal(eps_xx=eps, x_edges=x_edges, y_edges=y_edges), eps


def make_rib_grid() -> tuple[sm.Materials, np.ndarray]:
x_edges, y_edges, xx, yy = demo_grid(nx=46, ny=30)
eps = np.full(xx.shape, SIO2_EPS, dtype=np.complex128)
slab = (np.abs(xx) <= 0.72) & (-0.16 <= yy) & (yy <= -0.05)
ridge = (np.abs(xx) <= 0.28) & (-0.05 <= yy) & (yy <= 0.18)
slab = (np.abs(xx) <= 0.72) & (yy >= -0.16) & (yy <= -0.05)
ridge = (np.abs(xx) <= 0.28) & (yy >= -0.05) & (yy <= 0.18)
eps[slab | ridge] = SI_EPS
return sm.Materials.from_diagonal(eps_xx=eps, x_edges=x_edges, y_edges=y_edges), eps

Expand Down Expand Up @@ -267,7 +266,7 @@ def plot_demo(
else:
image = draw_image(ax, dims, coords, images[column], cmap="RdBu_r", symmetric=True)
plot_eps_contours(ax, coords, eps_background)
ax.set_title(f"mode {mode_index}, {column}\n" f"n_eff={n_eff.real:.5f}{n_eff.imag:+.1e}j")
ax.set_title(f"mode {mode_index}, {column}\nn_eff={n_eff.real:.5f}{n_eff.imag:+.1e}j")
fig.colorbar(image, ax=ax, fraction=0.046, pad=0.04)

fig.suptitle(f"{demo.title}: {demo.description}", fontsize=14)
Expand Down Expand Up @@ -296,7 +295,7 @@ def electric_magnitude_image(
magnitude_squared = np.abs(ex) ** 2
for component in ("Ey", "Ez"):
other_dims, other_coords, values = component_image(data, component, mode_index)
coords_match = all(len(a) == len(b) and np.allclose(a, b) for a, b in zip(coords, other_coords))
coords_match = all(len(a) == len(b) and np.allclose(a, b) for a, b in zip(coords, other_coords, strict=True))
if other_dims != dims or not coords_match:
raise ValueError("field components are not colocated on a common plotting grid")
magnitude_squared += np.abs(values) ** 2
Expand Down Expand Up @@ -327,7 +326,10 @@ def draw_image(
limit = max(max_abs, np.finfo(float).eps)
kwargs = {"vmin": -limit, "vmax": limit}
else:
kwargs = {"vmin": float(np.nanmin(plot_values)), "vmax": max(float(np.nanmax(plot_values)), np.finfo(float).eps)}
kwargs = {
"vmin": float(np.nanmin(plot_values)),
"vmax": max(float(np.nanmax(plot_values)), np.finfo(float).eps),
}
image = ax.imshow(
plot_values.T,
extent=extent,
Expand Down
Loading
Loading