photonfdtd 0.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. photonfdtd-0.12.0/.gitignore +15 -0
  2. photonfdtd-0.12.0/LICENSE +21 -0
  3. photonfdtd-0.12.0/PKG-INFO +254 -0
  4. photonfdtd-0.12.0/README.md +221 -0
  5. photonfdtd-0.12.0/benchmarks/README.md +135 -0
  6. photonfdtd-0.12.0/pyproject.toml +64 -0
  7. photonfdtd-0.12.0/rust/Cargo.lock +348 -0
  8. photonfdtd-0.12.0/rust/Cargo.toml +26 -0
  9. photonfdtd-0.12.0/rust/src/adi.rs +375 -0
  10. photonfdtd-0.12.0/rust/src/blocked.rs +350 -0
  11. photonfdtd-0.12.0/rust/src/cuda.rs +1116 -0
  12. photonfdtd-0.12.0/rust/src/kernel.rs +410 -0
  13. photonfdtd-0.12.0/rust/src/lib.rs +152 -0
  14. photonfdtd-0.12.0/src/photonfdtd/__init__.py +84 -0
  15. photonfdtd-0.12.0/src/photonfdtd/adapters.py +190 -0
  16. photonfdtd-0.12.0/src/photonfdtd/adi.py +337 -0
  17. photonfdtd-0.12.0/src/photonfdtd/boundaries.py +97 -0
  18. photonfdtd-0.12.0/src/photonfdtd/constants.py +8 -0
  19. photonfdtd-0.12.0/src/photonfdtd/design.py +175 -0
  20. photonfdtd-0.12.0/src/photonfdtd/eim.py +319 -0
  21. photonfdtd-0.12.0/src/photonfdtd/eme.py +267 -0
  22. photonfdtd-0.12.0/src/photonfdtd/geometry.py +154 -0
  23. photonfdtd-0.12.0/src/photonfdtd/grid.py +111 -0
  24. photonfdtd-0.12.0/src/photonfdtd/jaxbackend.py +773 -0
  25. photonfdtd-0.12.0/src/photonfdtd/materials.py +268 -0
  26. photonfdtd-0.12.0/src/photonfdtd/memory.py +165 -0
  27. photonfdtd-0.12.0/src/photonfdtd/mode.py +387 -0
  28. photonfdtd-0.12.0/src/photonfdtd/monitors.py +191 -0
  29. photonfdtd-0.12.0/src/photonfdtd/outofcore.py +424 -0
  30. photonfdtd-0.12.0/src/photonfdtd/reversible.py +248 -0
  31. photonfdtd-0.12.0/src/photonfdtd/reversible_pml.py +287 -0
  32. photonfdtd-0.12.0/src/photonfdtd/rustbackend.py +461 -0
  33. photonfdtd-0.12.0/src/photonfdtd/simulation.py +1459 -0
  34. photonfdtd-0.12.0/src/photonfdtd/smatrix.py +118 -0
  35. photonfdtd-0.12.0/src/photonfdtd/smoothing.py +172 -0
  36. photonfdtd-0.12.0/src/photonfdtd/sources.py +544 -0
  37. photonfdtd-0.12.0/src/photonfdtd/storage.py +219 -0
  38. photonfdtd-0.12.0/tests/test_backend_dispatch.py +75 -0
  39. photonfdtd-0.12.0/tests/test_backends.py +196 -0
  40. photonfdtd-0.12.0/tests/test_design.py +84 -0
  41. photonfdtd-0.12.0/tests/test_dispersion.py +131 -0
  42. photonfdtd-0.12.0/tests/test_eim_eme_adi.py +218 -0
  43. photonfdtd-0.12.0/tests/test_jax.py +174 -0
  44. photonfdtd-0.12.0/tests/test_jax_accuracy.py +139 -0
  45. photonfdtd-0.12.0/tests/test_memory.py +306 -0
  46. photonfdtd-0.12.0/tests/test_memory_planner.py +49 -0
  47. photonfdtd-0.12.0/tests/test_mode_vectorial.py +144 -0
  48. photonfdtd-0.12.0/tests/test_physics.py +216 -0
  49. photonfdtd-0.12.0/tests/test_reversible.py +121 -0
  50. photonfdtd-0.12.0/tests/test_rust.py +238 -0
  51. photonfdtd-0.12.0/tests/test_smatrix.py +149 -0
  52. photonfdtd-0.12.0/tests/test_smoke.py +84 -0
  53. photonfdtd-0.12.0/tests/test_smoothing.py +137 -0
  54. photonfdtd-0.12.0/tests/test_version.py +47 -0
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ .eggs/
6
+ dist/
7
+ build/
8
+ .venv/
9
+ .pytest_cache/
10
+ docs/_build/
11
+ .coverage
12
+ htmlcov/
13
+ *.so
14
+ .DS_Store
15
+ rust/target/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ngpaladi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.4
2
+ Name: photonfdtd
3
+ Version: 0.12.0
4
+ Summary: Local-only FDTD + waveguide mode solver for integrated photonics (NumPy core; optional JAX and compiled Rust/CUDA backends).
5
+ Project-URL: Homepage, https://github.com/ngpaladi/photonfdtd
6
+ Project-URL: Issues, https://github.com/ngpaladi/photonfdtd/issues
7
+ Author: ngpaladi
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: electromagnetics,fdtd,mode-solver,photonics,simulation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: matplotlib>=3.7
19
+ Requires-Dist: numpy>=1.24
20
+ Requires-Dist: scipy>=1.10
21
+ Provides-Extra: compress
22
+ Requires-Dist: zstandard>=0.21; extra == 'compress'
23
+ Provides-Extra: docs
24
+ Requires-Dist: sphinx-rtd-theme>=3; extra == 'docs'
25
+ Requires-Dist: sphinx>=7; extra == 'docs'
26
+ Provides-Extra: jax
27
+ Requires-Dist: jax>=0.4; extra == 'jax'
28
+ Provides-Extra: test
29
+ Requires-Dist: numba>=0.59; extra == 'test'
30
+ Requires-Dist: pytest>=7.4; extra == 'test'
31
+ Requires-Dist: zstandard>=0.21; extra == 'test'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # photonfdtd
35
+
36
+ A small, fully-local open-source FDTD + waveguide mode solver written in
37
+ Python/NumPy. It implements a Yee-grid FDTD time-stepper with CPML
38
+ absorbing boundaries, dispersive materials, anisotropic subpixel smoothing,
39
+ and a full-vectorial waveguide mode solver, with an API intentionally similar
40
+ in spirit to Tidy3D.
41
+
42
+ This is alpha-stage software (v0.1). The pieces that exist are tested and
43
+ correct; the pieces that don't, don't. See *Status* below.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install photonfdtd
49
+ ```
50
+
51
+ or from source, either directly from GitHub:
52
+
53
+ ```bash
54
+ pip install "git+https://github.com/ngpaladi/photonfdtd"
55
+ ```
56
+
57
+ or from a checkout:
58
+
59
+ ```bash
60
+ git clone https://github.com/ngpaladi/photonfdtd
61
+ cd photonfdtd
62
+ pip install -e .
63
+ ```
64
+
65
+ The PyPI wheel is pure Python (NumPy core; `photonfdtd[jax]` for the
66
+ differentiable XLA backend). The optional compiled Rust / CUDA backends
67
+ (`backend="rust"`, `backend="rust-cuda"`) are built separately from the Rust
68
+ sources shipped in the sdist — see [`benchmarks/README.md`](benchmarks/README.md).
69
+
70
+ ### Releasing (maintainers)
71
+
72
+ Releases publish to PyPI automatically via
73
+ [`.github/workflows/publish.yml`](.github/workflows/publish.yml) using PyPI
74
+ **Trusted Publishing** (OIDC — no API token stored). To cut a release:
75
+
76
+ 1. Bump the version in `pyproject.toml` **and** `src/photonfdtd/__init__.py`
77
+ (kept in sync by `tests/test_version.py`).
78
+ 2. Publish a GitHub Release tagged `vX.Y.Z` matching that version. The workflow
79
+ runs the tests, guards tag-vs-version, builds the sdist + wheel, and
80
+ publishes.
81
+
82
+ One-time setup before the first release: on PyPI, add a *trusted publisher* for
83
+ the project (Publishing → add a GitHub Actions publisher) with owner `ngpaladi`,
84
+ repo `photonfdtd`, workflow `publish.yml`, environment `pypi`. Until the project
85
+ exists, use PyPI's *pending publisher* form with the same values.
86
+
87
+ ## Quick start
88
+
89
+ ### A point dipole radiating in 2D vacuum
90
+
91
+ ```python
92
+ import photonfdtd as pf
93
+
94
+ lam0 = 1.0e-6
95
+ freq0 = pf.C_0 / lam0
96
+ dx = lam0 / 20
97
+
98
+ grid = pf.Grid(size=(4e-6, 4e-6), cell_size=dx, pml_layers=(12, 12, 0))
99
+ src = pf.PointDipole(
100
+ position=(0.0, 0.0),
101
+ component="Ez",
102
+ waveform=pf.GaussianPulse(freq0=freq0, fwhm=10e-15),
103
+ )
104
+ mon = pf.FieldMonitor(name="snap", components=("Ez",), interval=20)
105
+
106
+ sim = pf.Simulation(grid, sources=[src], monitors=[mon], run_time=200e-15)
107
+ result = sim.run()
108
+ ```
109
+
110
+ `result.fields["snap"]["Ez"]` is a `(n_frames, ny, nz)` array of field snapshots.
111
+
112
+ ### Solving a slab waveguide mode
113
+
114
+ ```python
115
+ import photonfdtd as pf
116
+
117
+ clad = pf.Medium.from_index(1.0)
118
+ core = pf.Medium.from_index(2.0)
119
+ slab = pf.Box(center=(0.0, 0.0), size=(0.5e-6, 0.3e-6), medium=core)
120
+
121
+ ms = pf.ModeSolver(
122
+ size=(4e-6, 3e-6),
123
+ cell_size=20e-9,
124
+ structures=[slab],
125
+ wavelength=1.55e-6,
126
+ num_modes=2,
127
+ )
128
+ result = ms.solve()
129
+ print(result.n_eff) # array of effective indices
130
+ ```
131
+
132
+ ## What v0.2 actually does
133
+
134
+ - **Yee-grid FDTD** in 1D / 2D / 3D, with the Courant-stable time step
135
+ selected automatically.
136
+ - **CPML** absorbing boundaries (Roden & Gedney 2000, kappa = 1) on any
137
+ number of axes.
138
+ - **Isotropic dielectric media, non-dispersive or dispersive.** Non-dispersive
139
+ media stamp a single `eps_r` per cell; dispersive media (`DispersiveMedium`,
140
+ with `.lorentz` / `.drude` / `.sellmeier` constructors) carry Lorentz, Drude
141
+ and Sellmeier poles advanced by the auxiliary-differential-equation (ADE)
142
+ method during stepping. A cited material library (`pf.silica()`,
143
+ `pf.silicon()`, `pf.silicon_nitride()`, `pf.lithium_niobate()`, `pf.gold()`,
144
+ `pf.silver()`) reproduces each source's published index — see
145
+ [`docs/material_data.md`](docs/material_data.md).
146
+ - **Anisotropic subpixel smoothing** (`Simulation(subpixel=True)`): partially
147
+ filled interface cells get an effective diagonal permittivity tensor
148
+ (harmonic mean normal to the interface, arithmetic mean tangential), removing
149
+ the staircase error and restoring second-order boundary accuracy
150
+ (Farjadpour et al. 2006). See [`docs/accuracy.rst`](docs/accuracy.rst).
151
+ - **Geometry primitives**: axis-aligned `Box` and arbitrary-polygon
152
+ `PolySlab` (a polygon in xy extruded between two z bounds).
153
+ - **Sources**: soft point-dipole (`PointDipole`), distributed line/area
154
+ mode injection (`ModeSource`), an energy-normalised `SinglePhotonSource`
155
+ whose amplitude is set so the launched wavepacket carries exactly
156
+ $h\,\nu$ of total electromagnetic energy, and a moving-charge
157
+ `ChargedParticle` current source that emits **Cherenkov radiation** when
158
+ it outruns the local phase velocity (see `examples/04_cherenkov.py`).
159
+ - **Monitors**: time-domain field snapshots (`FieldMonitor`), Poynting flux
160
+ (`FluxMonitor`), and a frequency-domain `DFTMonitor` that accumulates a
161
+ running Fourier transform at chosen frequencies so storage scales with the
162
+ number of frequencies rather than the number of timesteps (routinely
163
+ 50-1000x smaller than an equivalent time-domain monitor, exact at those
164
+ frequencies).
165
+ - **Memory efficiency for large volumes**: per-array `float32`/`float64`
166
+ precision, the permittivity grid released during stepping, a shared curl
167
+ scratch buffer that avoids per-step full-domain temporaries, and
168
+ `FieldMonitor(compression=...)`, which streams snapshots to disk as
169
+ per-frame-scaled, quantised, compressed blocks - keeping RAM flat regardless
170
+ of recording length and shrinking stored data ~10-30x (8-bit) or ~5-7x
171
+ (16-bit) versus an uncompressed float64 monitor.
172
+ - **Out-of-core stepping** (`sim.run(out_of_core=True, tile_cells=...)`): for a
173
+ volume whose fields do not fit in RAM, the six field arrays, `ce_field` and
174
+ the CPML state are memory-mapped to disk and each timestep sweeps the domain
175
+ in slabs along one axis, so **peak RAM is bounded by the tile size, not the
176
+ grid**. Reproduces the in-core result to machine precision. NumPy backend,
177
+ point sources and `FieldMonitor` (incl. `compression=`) supported; see
178
+ `photonfdtd.outofcore`.
179
+ - **Differentiable PIC inverse design.** A substrate/etched-core/cladding stack
180
+ driven by a 2-D etch density `ρ(x,y)` (`EtchedCore`) maps — conic filter →
181
+ `tanh` projection → anisotropic subpixel sidewalls → 3-D ε — entirely in JAX,
182
+ so `value_and_grad_density` returns `d(loss)/d(ρ)` through the full time
183
+ evolution (topology optimization; gradient matches finite differences to
184
+ ~1e-8). Ports are launched one-way with `UniModeSource` (equivalence-principle
185
+ current sheets). See [`docs/accuracy.rst`](docs/accuracy.rst).
186
+ - **Mode-decomposition S-parameters** (`s_parameters`, `mode_amplitudes`):
187
+ project the frequency-domain fields on a memory-light port plane (any axis via
188
+ `DFTMonitor(plane_axis=..., plane_position=...)`) onto a solved mode to get
189
+ complex forward/backward modal amplitudes and hence S-parameters. A mode down
190
+ a straight lossless guide gives `|S21|≈1`; the overlap is backend-agnostic
191
+ (NumPy/CuPy/JAX) so `|S_ij|²` is differentiable through the adjoint — the PIC
192
+ inverse-design objective. See [`docs/accuracy.rst`](docs/accuracy.rst).
193
+ - **Full-vectorial waveguide mode solver** (`ModeSolver`): a finite-difference
194
+ eigenproblem for the transverse fields on the Yee grid that distinguishes TE
195
+ from TM and captures high-index-contrast boundary effects. Validated against
196
+ the exact symmetric-slab dispersion to better than 0.01% on both TE0 and TM0
197
+ (Zhu & Brown 2002; Fallahkhair et al. 2008).
198
+ - **gdsfactory adapter** (`from_gdsfactory`) that reads a layout
199
+ `Component`, maps its layers onto user-supplied materials, and returns a
200
+ pre-built `Simulation`.
201
+ - **Optional acceleration backends** for time-stepping: a Numba JIT path and
202
+ a CuPy GPU path (`use_gpu=True`). The GPU path uses only generic CuPy array
203
+ ops, so it runs on either an **NVIDIA** GPU (CuPy's CUDA build,
204
+ `cupy-cuda12x`) or an **AMD** GPU (CuPy's ROCm build, `cupy-rocm-5-0`).
205
+ Both are optional; without them the plain NumPy core runs.
206
+ - **Differentiable JAX backend** (`use_jax=True`): a pure-functional
207
+ reimplementation of the Yee + CPML step under `jax.lax.scan`, JIT-compiled
208
+ through XLA (CPU/GPU/TPU from one path) and matching the in-core solver to
209
+ machine precision. Because the stepper is a pure function of the
210
+ permittivity, `pf.jax_value_and_grad_eps(sim, loss)` returns the gradient of
211
+ any monitor-based scalar w.r.t. `eps_r` — a **time-domain adjoint for
212
+ gradient-based inverse design / topology optimization**. See
213
+ `photonfdtd.jaxbackend`. (`pip install "photonfdtd[jax]"`.)
214
+
215
+ ### Running on GPU
216
+
217
+ **JAX is the GPU backend.** With `use_jax=True` the forward run and the
218
+ differentiable / reversible adjoints run on the GPU automatically once a CUDA
219
+ `jaxlib` is present — no code change: `pip install "jax[cuda12]"`. Validated on
220
+ an NVIDIA RTX 4080 (CUDA 12): forward matches NumPy to ~1e-16, and both adjoints
221
+ run on-device. If JAX falls back to CPU with a "cuSPARSE not found"-style error,
222
+ the nvidia CUDA libraries just aren't on the loader path — add them, e.g.
223
+ `export LD_LIBRARY_PATH=$(python -c "import nvidia,glob,os;print(':'.join(glob.glob(os.path.dirname(nvidia.__file__)+'/*/lib')))")`.
224
+ Large differentiable runs fit in GPU memory thanks to gradient checkpointing and
225
+ the O(1)-memory reversible adjoint (`photonfdtd.reversible`).
226
+
227
+ The **CuPy backend** (`use_gpu=True`) is a deprecated legacy path — superseded by
228
+ JAX for GPU work, but retained (optional; `pip install cupy-cuda12x`) for
229
+ AMD/ROCm GPUs and for **GPU/host/disk out-of-core** (`use_gpu=True` +
230
+ `run(out_of_core=True)`), where fields live on disk and each tile is processed on
231
+ the GPU so peak device memory is one tile — a volume larger than GPU memory still
232
+ runs (JAX cannot stream to disk).
233
+
234
+ ## What v0.2 does *not* do (yet)
235
+
236
+ Items below are intentional out-of-scope and tracked in the issue
237
+ tracker:
238
+
239
+ - Dispersive media (Lorentz / Drude / Debye) - planned next, via the
240
+ auxiliary-differential-equation method.
241
+ - Full-vectorial 2D mode solving with E and H couplings.
242
+ - Anisotropic media.
243
+ - Sub-cell averaging for polygon edges.
244
+ - Total-field / scattered-field plane wave injection.
245
+
246
+ ## Roadmap
247
+
248
+ - v0.3: dispersive media (Lorentz pole-residue), full-vectorial mode
249
+ solver, anisotropic materials.
250
+ - v0.4: Numba/JAX backend for ~10-100x speed-up.
251
+
252
+ ## License
253
+
254
+ MIT.
@@ -0,0 +1,221 @@
1
+ # photonfdtd
2
+
3
+ A small, fully-local open-source FDTD + waveguide mode solver written in
4
+ Python/NumPy. It implements a Yee-grid FDTD time-stepper with CPML
5
+ absorbing boundaries, dispersive materials, anisotropic subpixel smoothing,
6
+ and a full-vectorial waveguide mode solver, with an API intentionally similar
7
+ in spirit to Tidy3D.
8
+
9
+ This is alpha-stage software (v0.1). The pieces that exist are tested and
10
+ correct; the pieces that don't, don't. See *Status* below.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install photonfdtd
16
+ ```
17
+
18
+ or from source, either directly from GitHub:
19
+
20
+ ```bash
21
+ pip install "git+https://github.com/ngpaladi/photonfdtd"
22
+ ```
23
+
24
+ or from a checkout:
25
+
26
+ ```bash
27
+ git clone https://github.com/ngpaladi/photonfdtd
28
+ cd photonfdtd
29
+ pip install -e .
30
+ ```
31
+
32
+ The PyPI wheel is pure Python (NumPy core; `photonfdtd[jax]` for the
33
+ differentiable XLA backend). The optional compiled Rust / CUDA backends
34
+ (`backend="rust"`, `backend="rust-cuda"`) are built separately from the Rust
35
+ sources shipped in the sdist — see [`benchmarks/README.md`](benchmarks/README.md).
36
+
37
+ ### Releasing (maintainers)
38
+
39
+ Releases publish to PyPI automatically via
40
+ [`.github/workflows/publish.yml`](.github/workflows/publish.yml) using PyPI
41
+ **Trusted Publishing** (OIDC — no API token stored). To cut a release:
42
+
43
+ 1. Bump the version in `pyproject.toml` **and** `src/photonfdtd/__init__.py`
44
+ (kept in sync by `tests/test_version.py`).
45
+ 2. Publish a GitHub Release tagged `vX.Y.Z` matching that version. The workflow
46
+ runs the tests, guards tag-vs-version, builds the sdist + wheel, and
47
+ publishes.
48
+
49
+ One-time setup before the first release: on PyPI, add a *trusted publisher* for
50
+ the project (Publishing → add a GitHub Actions publisher) with owner `ngpaladi`,
51
+ repo `photonfdtd`, workflow `publish.yml`, environment `pypi`. Until the project
52
+ exists, use PyPI's *pending publisher* form with the same values.
53
+
54
+ ## Quick start
55
+
56
+ ### A point dipole radiating in 2D vacuum
57
+
58
+ ```python
59
+ import photonfdtd as pf
60
+
61
+ lam0 = 1.0e-6
62
+ freq0 = pf.C_0 / lam0
63
+ dx = lam0 / 20
64
+
65
+ grid = pf.Grid(size=(4e-6, 4e-6), cell_size=dx, pml_layers=(12, 12, 0))
66
+ src = pf.PointDipole(
67
+ position=(0.0, 0.0),
68
+ component="Ez",
69
+ waveform=pf.GaussianPulse(freq0=freq0, fwhm=10e-15),
70
+ )
71
+ mon = pf.FieldMonitor(name="snap", components=("Ez",), interval=20)
72
+
73
+ sim = pf.Simulation(grid, sources=[src], monitors=[mon], run_time=200e-15)
74
+ result = sim.run()
75
+ ```
76
+
77
+ `result.fields["snap"]["Ez"]` is a `(n_frames, ny, nz)` array of field snapshots.
78
+
79
+ ### Solving a slab waveguide mode
80
+
81
+ ```python
82
+ import photonfdtd as pf
83
+
84
+ clad = pf.Medium.from_index(1.0)
85
+ core = pf.Medium.from_index(2.0)
86
+ slab = pf.Box(center=(0.0, 0.0), size=(0.5e-6, 0.3e-6), medium=core)
87
+
88
+ ms = pf.ModeSolver(
89
+ size=(4e-6, 3e-6),
90
+ cell_size=20e-9,
91
+ structures=[slab],
92
+ wavelength=1.55e-6,
93
+ num_modes=2,
94
+ )
95
+ result = ms.solve()
96
+ print(result.n_eff) # array of effective indices
97
+ ```
98
+
99
+ ## What v0.2 actually does
100
+
101
+ - **Yee-grid FDTD** in 1D / 2D / 3D, with the Courant-stable time step
102
+ selected automatically.
103
+ - **CPML** absorbing boundaries (Roden & Gedney 2000, kappa = 1) on any
104
+ number of axes.
105
+ - **Isotropic dielectric media, non-dispersive or dispersive.** Non-dispersive
106
+ media stamp a single `eps_r` per cell; dispersive media (`DispersiveMedium`,
107
+ with `.lorentz` / `.drude` / `.sellmeier` constructors) carry Lorentz, Drude
108
+ and Sellmeier poles advanced by the auxiliary-differential-equation (ADE)
109
+ method during stepping. A cited material library (`pf.silica()`,
110
+ `pf.silicon()`, `pf.silicon_nitride()`, `pf.lithium_niobate()`, `pf.gold()`,
111
+ `pf.silver()`) reproduces each source's published index — see
112
+ [`docs/material_data.md`](docs/material_data.md).
113
+ - **Anisotropic subpixel smoothing** (`Simulation(subpixel=True)`): partially
114
+ filled interface cells get an effective diagonal permittivity tensor
115
+ (harmonic mean normal to the interface, arithmetic mean tangential), removing
116
+ the staircase error and restoring second-order boundary accuracy
117
+ (Farjadpour et al. 2006). See [`docs/accuracy.rst`](docs/accuracy.rst).
118
+ - **Geometry primitives**: axis-aligned `Box` and arbitrary-polygon
119
+ `PolySlab` (a polygon in xy extruded between two z bounds).
120
+ - **Sources**: soft point-dipole (`PointDipole`), distributed line/area
121
+ mode injection (`ModeSource`), an energy-normalised `SinglePhotonSource`
122
+ whose amplitude is set so the launched wavepacket carries exactly
123
+ $h\,\nu$ of total electromagnetic energy, and a moving-charge
124
+ `ChargedParticle` current source that emits **Cherenkov radiation** when
125
+ it outruns the local phase velocity (see `examples/04_cherenkov.py`).
126
+ - **Monitors**: time-domain field snapshots (`FieldMonitor`), Poynting flux
127
+ (`FluxMonitor`), and a frequency-domain `DFTMonitor` that accumulates a
128
+ running Fourier transform at chosen frequencies so storage scales with the
129
+ number of frequencies rather than the number of timesteps (routinely
130
+ 50-1000x smaller than an equivalent time-domain monitor, exact at those
131
+ frequencies).
132
+ - **Memory efficiency for large volumes**: per-array `float32`/`float64`
133
+ precision, the permittivity grid released during stepping, a shared curl
134
+ scratch buffer that avoids per-step full-domain temporaries, and
135
+ `FieldMonitor(compression=...)`, which streams snapshots to disk as
136
+ per-frame-scaled, quantised, compressed blocks - keeping RAM flat regardless
137
+ of recording length and shrinking stored data ~10-30x (8-bit) or ~5-7x
138
+ (16-bit) versus an uncompressed float64 monitor.
139
+ - **Out-of-core stepping** (`sim.run(out_of_core=True, tile_cells=...)`): for a
140
+ volume whose fields do not fit in RAM, the six field arrays, `ce_field` and
141
+ the CPML state are memory-mapped to disk and each timestep sweeps the domain
142
+ in slabs along one axis, so **peak RAM is bounded by the tile size, not the
143
+ grid**. Reproduces the in-core result to machine precision. NumPy backend,
144
+ point sources and `FieldMonitor` (incl. `compression=`) supported; see
145
+ `photonfdtd.outofcore`.
146
+ - **Differentiable PIC inverse design.** A substrate/etched-core/cladding stack
147
+ driven by a 2-D etch density `ρ(x,y)` (`EtchedCore`) maps — conic filter →
148
+ `tanh` projection → anisotropic subpixel sidewalls → 3-D ε — entirely in JAX,
149
+ so `value_and_grad_density` returns `d(loss)/d(ρ)` through the full time
150
+ evolution (topology optimization; gradient matches finite differences to
151
+ ~1e-8). Ports are launched one-way with `UniModeSource` (equivalence-principle
152
+ current sheets). See [`docs/accuracy.rst`](docs/accuracy.rst).
153
+ - **Mode-decomposition S-parameters** (`s_parameters`, `mode_amplitudes`):
154
+ project the frequency-domain fields on a memory-light port plane (any axis via
155
+ `DFTMonitor(plane_axis=..., plane_position=...)`) onto a solved mode to get
156
+ complex forward/backward modal amplitudes and hence S-parameters. A mode down
157
+ a straight lossless guide gives `|S21|≈1`; the overlap is backend-agnostic
158
+ (NumPy/CuPy/JAX) so `|S_ij|²` is differentiable through the adjoint — the PIC
159
+ inverse-design objective. See [`docs/accuracy.rst`](docs/accuracy.rst).
160
+ - **Full-vectorial waveguide mode solver** (`ModeSolver`): a finite-difference
161
+ eigenproblem for the transverse fields on the Yee grid that distinguishes TE
162
+ from TM and captures high-index-contrast boundary effects. Validated against
163
+ the exact symmetric-slab dispersion to better than 0.01% on both TE0 and TM0
164
+ (Zhu & Brown 2002; Fallahkhair et al. 2008).
165
+ - **gdsfactory adapter** (`from_gdsfactory`) that reads a layout
166
+ `Component`, maps its layers onto user-supplied materials, and returns a
167
+ pre-built `Simulation`.
168
+ - **Optional acceleration backends** for time-stepping: a Numba JIT path and
169
+ a CuPy GPU path (`use_gpu=True`). The GPU path uses only generic CuPy array
170
+ ops, so it runs on either an **NVIDIA** GPU (CuPy's CUDA build,
171
+ `cupy-cuda12x`) or an **AMD** GPU (CuPy's ROCm build, `cupy-rocm-5-0`).
172
+ Both are optional; without them the plain NumPy core runs.
173
+ - **Differentiable JAX backend** (`use_jax=True`): a pure-functional
174
+ reimplementation of the Yee + CPML step under `jax.lax.scan`, JIT-compiled
175
+ through XLA (CPU/GPU/TPU from one path) and matching the in-core solver to
176
+ machine precision. Because the stepper is a pure function of the
177
+ permittivity, `pf.jax_value_and_grad_eps(sim, loss)` returns the gradient of
178
+ any monitor-based scalar w.r.t. `eps_r` — a **time-domain adjoint for
179
+ gradient-based inverse design / topology optimization**. See
180
+ `photonfdtd.jaxbackend`. (`pip install "photonfdtd[jax]"`.)
181
+
182
+ ### Running on GPU
183
+
184
+ **JAX is the GPU backend.** With `use_jax=True` the forward run and the
185
+ differentiable / reversible adjoints run on the GPU automatically once a CUDA
186
+ `jaxlib` is present — no code change: `pip install "jax[cuda12]"`. Validated on
187
+ an NVIDIA RTX 4080 (CUDA 12): forward matches NumPy to ~1e-16, and both adjoints
188
+ run on-device. If JAX falls back to CPU with a "cuSPARSE not found"-style error,
189
+ the nvidia CUDA libraries just aren't on the loader path — add them, e.g.
190
+ `export LD_LIBRARY_PATH=$(python -c "import nvidia,glob,os;print(':'.join(glob.glob(os.path.dirname(nvidia.__file__)+'/*/lib')))")`.
191
+ Large differentiable runs fit in GPU memory thanks to gradient checkpointing and
192
+ the O(1)-memory reversible adjoint (`photonfdtd.reversible`).
193
+
194
+ The **CuPy backend** (`use_gpu=True`) is a deprecated legacy path — superseded by
195
+ JAX for GPU work, but retained (optional; `pip install cupy-cuda12x`) for
196
+ AMD/ROCm GPUs and for **GPU/host/disk out-of-core** (`use_gpu=True` +
197
+ `run(out_of_core=True)`), where fields live on disk and each tile is processed on
198
+ the GPU so peak device memory is one tile — a volume larger than GPU memory still
199
+ runs (JAX cannot stream to disk).
200
+
201
+ ## What v0.2 does *not* do (yet)
202
+
203
+ Items below are intentional out-of-scope and tracked in the issue
204
+ tracker:
205
+
206
+ - Dispersive media (Lorentz / Drude / Debye) - planned next, via the
207
+ auxiliary-differential-equation method.
208
+ - Full-vectorial 2D mode solving with E and H couplings.
209
+ - Anisotropic media.
210
+ - Sub-cell averaging for polygon edges.
211
+ - Total-field / scattered-field plane wave injection.
212
+
213
+ ## Roadmap
214
+
215
+ - v0.3: dispersive media (Lorentz pole-residue), full-vectorial mode
216
+ solver, anisotropic materials.
217
+ - v0.4: Numba/JAX backend for ~10-100x speed-up.
218
+
219
+ ## License
220
+
221
+ MIT.
@@ -0,0 +1,135 @@
1
+ # Backend benchmarks
2
+
3
+ ## Building the Rust backends
4
+
5
+ The experimental `backend="rust"` (CPU, rayon) and `backend="rust-cuda"`
6
+ (GPU, cudarc/NVRTC) stepping cores live in `rust/src/`. Build and install
7
+ into the source tree with:
8
+
9
+ ```bash
10
+ cd rust
11
+ cargo build --release --features cuda # drop --features cuda for CPU-only
12
+ cp target/release/lib_photonfdtd_rs.so ../src/photonfdtd/_photonfdtd_rs.so
13
+ ```
14
+
15
+ (`maturin develop --release` works too, if you have it.) The `cuda` feature
16
+ pins `cudarc/cuda-12020` for a CUDA 12.2 driver - adjust to yours. The
17
+ parity suite is `tests/test_rust.py`; CUDA tests skip cleanly without a GPU.
18
+
19
+ ## Elliptic (focusing) grating coupler, 2D plan view
20
+
21
+ `bench_grating_coupler.py` runs a focusing grating coupler by the
22
+ effective-index method: a single-mode access waveguide opening into a slab
23
+ wedge crossed by the confocal-ellipse grating-line family
24
+ `q*lam = n_slab*r - n_c*sin(theta)*x`. 0.56M cells (880x640 at 25 nm),
25
+ 6295 steps, float64, CPML on all sides.
26
+
27
+ ```bash
28
+ python benchmarks/bench_grating_coupler.py rust|jax|numpy [outdir]
29
+ python benchmarks/make_gifs.py <outdir> rust jax # field movies
30
+ ```
31
+
32
+ Measured 2026-07-23 on a 24-core machine with an RTX 4080 (JAX 0.10.2),
33
+ one backend per process. JAX walls include XLA compile (~1.2 s f64 / ~0.5 s
34
+ f32; there is no persistent compilation cache configured), shown separately
35
+ as "steady":
36
+
37
+ | backend | prec | wall (s) | steady (s) | Mcell-steps/s | peak RSS (MB) |
38
+ |----------------|------|---------:|-----------:|--------------:|--------------:|
39
+ | rust-cuda | f64 | 0.81 | 0.81 | 4380 | 385 |
40
+ | rust-cuda | f32 | 0.57 | 0.57 | 6185 | 380 |
41
+ | jax (GPU) | f64 | 3.8 | ~2.6 | 928 | 1248 |
42
+ | jax (GPU) | f32 | 1.9 | ~1.4 | 1894 | 1068 |
43
+ | rust (CPU) | f64 | 8.9 | 8.9 | 397 | 258 |
44
+ | rust (CPU) | f32 | 6.7 | 6.7 | 529 | 239 |
45
+ | jax (CPU) | f64 | 15.6 | 15.6 | 227 | 858 |
46
+ | numpy | f64 | 147.9 | 147.9 | 24 | 281 |
47
+
48
+ All f64 backends agree on the final field frame to ~2e-14 relative
49
+ (double-precision round-off over 6295 steps); f32 runs agree to ~5e-6, i.e.
50
+ single-precision round-off. The Rust CUDA margin over XLA comes from fused
51
+ H/E kernels (each array is touched once per pass) and in-place updates;
52
+ both saturate the same VRAM bandwidth ceiling in principle. (This small
53
+ 0.56M-cell domain is largely cache-resident on the CPU, so CPU numbers here
54
+ are compute-bound; the RAM-bound regime below is what large runs see.)
55
+
56
+ ## RAM-bound CPU throughput (20M-cell strip, whole-chip regime)
57
+
58
+ Same strip as the memory benchmark. The fused H/E passes and ghost-zone
59
+ temporal blocking (x-slab tiles advanced T=8 steps per visit inside a
60
+ ~3.5 MB thread-local buffer; enabled automatically on large domains, exact
61
+ to the bit) both target this regime:
62
+
63
+ | CPU path | f64 Mcell-steps/s | f32 Mcell-steps/s |
64
+ |-------------------------------|------------------:|------------------:|
65
+ | unfused (previous) | 248 | - |
66
+ | fused, plain | 310 | 626 |
67
+ | fused + temporal blocking | 382 | 913 |
68
+
69
+ For scale, the measured STREAM triad on this machine is 34.3 GB/s; the
70
+ fused plain path already runs at that roof, and blocking is what buys
71
+ throughput past it.
72
+
73
+ ## Beyond-VRAM GPU streaming
74
+
75
+ When the stepping state does not fit the device (or with
76
+ `PHOTONFDTD_CUDA_STREAM=1`), `rust-cuda` keeps the domain in host RAM and
77
+ streams temporally-blocked x-slab tiles through the GPU over the DMA copy
78
+ engines - per T steps the domain crosses PCIe ~twice instead of 2T times.
79
+ Bit-identical to the resident stepper (covered by the parity tests). On the
80
+ 20M-cell f64 strip, forced streaming with a ~1/5-domain tile (T=64):
81
+
82
+ | GPU path | f64 Mcell-steps/s | f32 Mcell-steps/s | peak VRAM (MB) |
83
+ |---------------------------|------------------:|------------------:|---------------:|
84
+ | VRAM-resident | 4067 | 7835 | 1331 |
85
+ | streamed, tile-bounded | 2483 | 5243 | 499 |
86
+
87
+ Streaming holds ~60% of resident throughput while VRAM usage is set by the
88
+ tile, not the domain - so domain size is limited by host RAM (~800M cells
89
+ f32 on a 30 GB host, stepped ~13x faster than the best CPU path). Naive
90
+ per-step streaming would be PCIe-bound at roughly 1% of this.
91
+
92
+ ## Peak memory at whole-chip scale
93
+
94
+ `bench_memory.py` steps a 0.5 mm x 25 um strip at 25 nm (20M cells, 2D,
95
+ float64) a few dozen steps and reports peak memory (VRAM polled with
96
+ nvidia-smi):
97
+
98
+ | backend | peak RSS (GB) | peak VRAM (GB) |
99
+ |-----------|--------------:|---------------:|
100
+ | rust-cuda | 0.51 | 1.33 |
101
+ | rust | 1.27 | - |
102
+ | numpy | 1.72 | - |
103
+ | jax (GPU) | 1.65 | 3.33 |
104
+
105
+ Both Rust cores store their CPML psi state compacted to the PML slabs, so
106
+ the resident set is essentially the floor for an in-core run: six field
107
+ arrays + the update coefficient (7 x 8 bytes/cell ~ 1.19 GB here; XLA's
108
+ dense psi carries ~19 arrays/cell). Extrapolating, a 16 GB GPU holds a
109
+ ~230M-cell f64 domain (~460M cells at f32, e.g. ~11 x 1 mm of 2D chip at
110
+ 25 nm) on `rust-cuda`, vs ~90M cells for JAX-GPU; a 30 GB host holds
111
+ ~500M cells f64 on the CPU core. Beyond that, `run(out_of_core=True)`
112
+ remains the escape hatch.
113
+
114
+ ## Notes: GPU environment
115
+
116
+ The JAX CUDA plugin needs the `nvidia-*-cu12` wheels' lib dirs on
117
+ `LD_LIBRARY_PATH` on this machine (installed via
118
+ `pip install --user "jax[cuda12]"`). `rust-cuda` needs only the CUDA driver
119
+ plus NVRTC (from the system toolkit or the `nvidia-cuda-nvrtc-cu12` wheel).
120
+
121
+ ## Tuning knobs (env vars)
122
+
123
+ All optional; the defaults come from the scans above.
124
+
125
+ - `PHOTONFDTD_RUST_TB` - CPU temporal-block length T (0 disables; default 8
126
+ on large domains).
127
+ - `PHOTONFDTD_RUST_TILE` - CPU tile core rows (default ~3.5 MB buffers).
128
+ - `PHOTONFDTD_CUDA_STREAM` - 1 forces GPU streaming, 0 forces resident
129
+ (default: stream only when the run does not fit VRAM).
130
+ - `PHOTONFDTD_CUDA_TB` / `PHOTONFDTD_CUDA_TILE` - streaming block length
131
+ (default 32) and tile rows (default ~1/4 of free VRAM).
132
+
133
+ Streaming transfers are currently pageable; pinned-host-memory double
134
+ buffering would overlap DMA with compute and should recover most of the
135
+ remaining gap to resident throughput.