Compare commits
113 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf99f35f9b | ||
|
|
39291a8314 | ||
|
|
061c3f2e90 | ||
|
|
35fc67faa3 | ||
|
|
a1568a6f16 | ||
|
|
c6c9159b13 | ||
|
|
eec3fc28a7 | ||
|
|
010da1ccf5 | ||
|
|
1a2c6ab524 | ||
|
|
8b67696d7f | ||
|
|
f8ad0250d1 | ||
|
|
9a0c693848 | ||
|
|
bb920b8e33 | ||
|
|
ff278e6fa1 | ||
|
|
dc92d4a79d | ||
|
|
40efe7a450 | ||
|
|
e50637dc1c | ||
|
|
f7aa21a42a | ||
|
|
3e4aee1197 | ||
|
|
4b8a462df7 | ||
|
|
c0b41752e1 | ||
|
|
318c43d62d | ||
|
|
6f29dd89a8 | ||
| bedb338ac9 | |||
| a82eb5858a | |||
| 5e95d66a7e | |||
| 0568e1ba50 | |||
| d4c1082ca9 | |||
| d99ef96c96 | |||
| 8cdcd08ba0 | |||
| 267d161769 | |||
| 0afe2297b0 | |||
| 0ff23542ac | |||
| 9ac24892d6 | |||
| f3d13e1486 | |||
| e6756742be | |||
| 87bb3af3f9 | |||
| 07b16ad86a | |||
| f35b334100 | |||
| 593098bf8f | |||
| 38a5c1a9aa | |||
| bc55baf4a6 | |||
| 7eea919f94 | |||
| 74bebea837 | |||
| 8d49901b58 | |||
| 9d419aa3ea | |||
| 4913211883 | |||
| f5af0fef55 | |||
| 7e8ff23356 | |||
| be647658d3 | |||
| c46bed8298 | |||
| fb3bef23bf | |||
| d4f1008c5c | |||
| b486fa325b | |||
| b7ad5dea2b | |||
| 684b891e0f | |||
| 4a80ca8b12 | |||
| e3169b9e20 | |||
| 35ecbad15e | |||
| 43e01a814d | |||
| 9eb0e28bcb | |||
| c858b20d47 | |||
| 777ecbc024 | |||
| c4f8749941 | |||
| cd5cc9eb83 | |||
| 99e8d32eb1 | |||
| 1cb0cb2e4f | |||
| 234e8d7ac3 | |||
| 83f4d87ad8 | |||
| 1987ee473a | |||
| 4afc6cf62e | |||
| 53d5812b4a | |||
| 651e255704 | |||
| 71c2bbfada | |||
| 6a56921c12 | |||
| 006833acf2 | |||
| 155f30068f | |||
| 7987dc796f | |||
| 829007c672 | |||
| 659566750f | |||
| 76701f593c | |||
| 4e3a163522 | |||
| 50f92e1cc8 | |||
| b3c2fd391b | |||
| c543868c0b | |||
| e54735d9c6 | |||
| 4f2433320d | |||
| 47415a0beb | |||
| e459b5e61f | |||
| 36431cd0e4 | |||
| 739e96df3d | |||
| 63e7cb949f | |||
| c53a3c4d84 | |||
| 5dd9994e76 | |||
| 1021768e30 | |||
| 95e923d7b7 | |||
| 3f8802cb5f | |||
| 43bb0ba379 | |||
| e19968bb9f | |||
| 43f038d761 | |||
| d5fca741d1 | |||
| ca94ad1b25 | |||
| 10f26c12b4 | |||
| ee51c7db49 | |||
| 36bea6a593 | |||
| b16b35d84a | |||
| 6f3ae5a64f | |||
| 99c22d572f | |||
| 2f00baf0c6 | |||
| 2712d96f2a | |||
| dc3e733e7f | |||
| 95e3f71b40 | |||
| 639f88bba8 |
122 changed files with 10831 additions and 3110 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -54,6 +54,10 @@ coverage.xml
|
||||||
|
|
||||||
# documentation
|
# documentation
|
||||||
doc/
|
doc/
|
||||||
|
site/
|
||||||
|
_doc_mathimg/
|
||||||
|
doc.md
|
||||||
|
doc.htex
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
|
|
|
||||||
148
README.md
148
README.md
|
|
@ -56,6 +56,21 @@ linear systems, ideally with double precision.
|
||||||
|
|
||||||
Install from PyPI with pip:
|
Install from PyPI with pip:
|
||||||
```bash
|
```bash
|
||||||
|
pip3 install meanas
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional extras:
|
||||||
|
|
||||||
|
- `meanas[test]`: pytest and coverage
|
||||||
|
- `meanas[docs]`: MkDocs-based documentation toolchain
|
||||||
|
- `meanas[examples]`: optional runtime dependencies used by the tracked examples
|
||||||
|
- `meanas[dev]`: the union of `test`, `docs`, and `examples`, plus local lint/docs-publish helpers
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```bash
|
||||||
|
pip3 install 'meanas[test]'
|
||||||
|
pip3 install 'meanas[docs]'
|
||||||
|
pip3 install 'meanas[examples]'
|
||||||
pip3 install 'meanas[dev]'
|
pip3 install 'meanas[dev]'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -80,9 +95,13 @@ source my_venv/bin/activate
|
||||||
# Install in-place (-e, editable) from ./meanas, including development dependencies ([dev])
|
# Install in-place (-e, editable) from ./meanas, including development dependencies ([dev])
|
||||||
pip3 install --user -e './meanas[dev]'
|
pip3 install --user -e './meanas[dev]'
|
||||||
|
|
||||||
# Run tests
|
# Fast local iteration: excludes slower 3D/integration/example-smoke checks
|
||||||
cd meanas
|
cd meanas
|
||||||
python3 -m pytest -rsxX | tee test_results.txt
|
python3 -m pytest -q -m "not complete"
|
||||||
|
|
||||||
|
# Complete pre-commit confidence run: includes the slower integration tests and
|
||||||
|
# tracked example smoke tests
|
||||||
|
python3 -m pytest -q | tee test_results.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
#### See also:
|
#### See also:
|
||||||
|
|
@ -94,6 +113,125 @@ python3 -m pytest -rsxX | tee test_results.txt
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
See `examples/` for some simple examples; you may need additional
|
`meanas` is a collection of finite-difference electromagnetics tools:
|
||||||
packages such as [gridlock](https://mpxd.net/code/jan/gridlock)
|
|
||||||
to run the examples.
|
- `meanas.fdfd`: frequency-domain wave equations, sparse operators, SCPML, and
|
||||||
|
iterative solves for driven problems.
|
||||||
|
- `meanas.fdfd.waveguide_2d` / `meanas.fdfd.waveguide_3d`: waveguide mode
|
||||||
|
solvers, mode-source construction, and overlap windows for port-based
|
||||||
|
excitation and analysis.
|
||||||
|
- `meanas.fdtd`: Yee-step updates, CPML boundaries, flux/energy accounting, and
|
||||||
|
on-the-fly phasor extraction for comparing time-domain runs against FDFD.
|
||||||
|
- `meanas.fdmath`: low-level finite-difference operators, vectorization helpers,
|
||||||
|
and derivations shared by the FDTD and FDFD layers.
|
||||||
|
|
||||||
|
For most users, the tracked examples under `examples/` are the right entry
|
||||||
|
point. The library API is primarily a toolbox; the module docstrings and API
|
||||||
|
pages are there to document the mathematical conventions and derivations behind
|
||||||
|
those tools.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
API and workflow docs are generated from the package docstrings with
|
||||||
|
[MkDocs](https://www.mkdocs.org/), [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
|
||||||
|
and [mkdocstrings](https://mkdocstrings.github.io/).
|
||||||
|
|
||||||
|
Install the docs toolchain with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install -e './meanas[docs]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then build the docs site with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./make_docs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces:
|
||||||
|
|
||||||
|
- a normal multi-page site under `site/`
|
||||||
|
- a combined printable single-page HTML site under `site/print_page/`
|
||||||
|
- an optional fully inlined `site/standalone.html` when `htmlark` is available
|
||||||
|
|
||||||
|
The docs build uses a local MathJax bundle vendored under `docs/assets/`, so
|
||||||
|
the rendered HTML does not rely on external services for equation rendering.
|
||||||
|
|
||||||
|
The tracked examples under `examples/` are the intended entry points for users:
|
||||||
|
|
||||||
|
- `examples/fdtd.py`: broadband FDTD pulse excitation, phasor extraction, and a
|
||||||
|
residual check against the matching FDFD operator.
|
||||||
|
- `examples/waveguide.py`: waveguide mode solving, unidirectional mode-source
|
||||||
|
construction, overlap readout, and FDTD/FDFD comparison on a guided structure.
|
||||||
|
- `examples/waveguide_real.py`: real-valued continuous-wave FDTD on a straight
|
||||||
|
guide, with late-time monitor slices, guided-core windows, and mode-weighted
|
||||||
|
errors compared directly against real fields reconstructed from the matching
|
||||||
|
FDFD solution, plus a guided-mode / orthogonal-residual split.
|
||||||
|
- `examples/eme.py`: straight-interface mode matching / EME, including port
|
||||||
|
mode solving, interface scattering, and modal field visualization.
|
||||||
|
- `examples/eme_bend.py`: straight-to-bent waveguide mode matching with
|
||||||
|
cylindrical bend modes, interface scattering, and a cascaded bend-network
|
||||||
|
example built with `scikit-rf`.
|
||||||
|
- `examples/fdfd.py`: direct frequency-domain waveguide excitation and overlap /
|
||||||
|
Poynting analysis without a time-domain run.
|
||||||
|
|
||||||
|
Several examples rely on optional packages such as
|
||||||
|
[gridlock](https://mpxd.net/code/jan/gridlock).
|
||||||
|
|
||||||
|
### Frequency-domain waveguide workflow
|
||||||
|
|
||||||
|
For a structure with a constant cross-section in one direction:
|
||||||
|
|
||||||
|
1. Build `dxes` and the diagonal `epsilon` / `mu` distributions on the Yee grid.
|
||||||
|
2. Solve the port mode with `meanas.fdfd.waveguide_3d.solve_mode(...)`.
|
||||||
|
3. Build a unidirectional source with `compute_source(...)`.
|
||||||
|
4. Build a matching overlap window with `compute_overlap_e(...)`.
|
||||||
|
5. Solve the full FDFD problem and project the result onto the overlap window or
|
||||||
|
evaluate plane flux with `meanas.fdfd.functional.poynting_e_cross_h(...)`.
|
||||||
|
|
||||||
|
### Time-domain phasor workflow
|
||||||
|
|
||||||
|
For a broadband or continuous-wave FDTD run:
|
||||||
|
|
||||||
|
1. Advance the fields with `meanas.fdtd.maxwell_e/maxwell_h` or
|
||||||
|
`updates_with_cpml(...)`.
|
||||||
|
2. Inject electric current using the same sign convention used throughout the
|
||||||
|
examples and library: `E -= dt * J / epsilon`.
|
||||||
|
3. Accumulate the desired phasor with `accumulate_phasor(...)` or the Yee-aware
|
||||||
|
wrappers `accumulate_phasor_e/h/j(...)`.
|
||||||
|
4. Build the matching FDFD operator on the stretched `dxes` if CPML/SCPML is
|
||||||
|
part of the simulation, and compare the extracted phasor to the FDFD field or
|
||||||
|
residual.
|
||||||
|
|
||||||
|
This is the primary FDTD/FDFD equivalence workflow. The phasor extraction step
|
||||||
|
filters the time-domain run down to the guided `+\omega` content that FDFD
|
||||||
|
solves for directly, so it is the cleanest apples-to-apples comparison.
|
||||||
|
|
||||||
|
### Real-field reconstruction workflow
|
||||||
|
|
||||||
|
For a continuous-wave real-valued FDTD run:
|
||||||
|
|
||||||
|
1. Build the analytic source phasor for the structure, for example with
|
||||||
|
`waveguide_3d.compute_source(...)`.
|
||||||
|
2. Run the real-valued FDTD simulation using the real part of that source.
|
||||||
|
3. Solve the matching FDFD problem from the analytic source phasor on the
|
||||||
|
stretched `dxes`.
|
||||||
|
4. Reconstruct late real `E/H/J` snapshots with
|
||||||
|
`reconstruct_real_e/h/j(...)` and compare those directly against the
|
||||||
|
real-valued FDTD fields, ideally on a monitor window or mode-weighted norm
|
||||||
|
centered on the guided field rather than on the full transverse plane. When
|
||||||
|
needed, split the monitor field into guided-mode and orthogonal residual
|
||||||
|
pieces to see whether the remaining mismatch is actually in the mode or in
|
||||||
|
weak nonguided tails.
|
||||||
|
|
||||||
|
This is a stricter diagnostic, not the primary equivalence benchmark. A raw
|
||||||
|
monitor slice contains both the guided field and the remaining orthogonal
|
||||||
|
content on that plane,
|
||||||
|
|
||||||
|
$$ E_{\text{monitor}} = E_{\text{guided}} + E_{\text{residual}} , $$
|
||||||
|
|
||||||
|
so its full-plane instantaneous error is naturally noisier than the extracted
|
||||||
|
phasor comparison even when the underlying guided `+\omega` content matches
|
||||||
|
well.
|
||||||
|
|
||||||
|
`examples/waveguide_real.py` is the reference implementation of this workflow.
|
||||||
|
|
|
||||||
3
docs/api/eigensolvers.md
Normal file
3
docs/api/eigensolvers.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# eigensolvers
|
||||||
|
|
||||||
|
::: meanas.eigensolvers
|
||||||
15
docs/api/fdfd.md
Normal file
15
docs/api/fdfd.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# fdfd
|
||||||
|
|
||||||
|
::: meanas.fdfd
|
||||||
|
|
||||||
|
## Core operator layers
|
||||||
|
|
||||||
|
::: meanas.fdfd.functional
|
||||||
|
|
||||||
|
::: meanas.fdfd.operators
|
||||||
|
|
||||||
|
::: meanas.fdfd.solvers
|
||||||
|
|
||||||
|
::: meanas.fdfd.scpml
|
||||||
|
|
||||||
|
::: meanas.fdfd.farfield
|
||||||
13
docs/api/fdmath.md
Normal file
13
docs/api/fdmath.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# fdmath
|
||||||
|
|
||||||
|
::: meanas.fdmath
|
||||||
|
|
||||||
|
## Functional and sparse operators
|
||||||
|
|
||||||
|
::: meanas.fdmath.functional
|
||||||
|
|
||||||
|
::: meanas.fdmath.operators
|
||||||
|
|
||||||
|
::: meanas.fdmath.vectorization
|
||||||
|
|
||||||
|
::: meanas.fdmath.types
|
||||||
15
docs/api/fdtd.md
Normal file
15
docs/api/fdtd.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# fdtd
|
||||||
|
|
||||||
|
::: meanas.fdtd
|
||||||
|
|
||||||
|
## Core update and analysis helpers
|
||||||
|
|
||||||
|
::: meanas.fdtd.base
|
||||||
|
|
||||||
|
::: meanas.fdtd.pml
|
||||||
|
|
||||||
|
::: meanas.fdtd.boundaries
|
||||||
|
|
||||||
|
::: meanas.fdtd.energy
|
||||||
|
|
||||||
|
::: meanas.fdtd.phasor
|
||||||
14
docs/api/index.md
Normal file
14
docs/api/index.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# API Overview
|
||||||
|
|
||||||
|
The package is documented directly from its docstrings. The most useful entry
|
||||||
|
points are:
|
||||||
|
|
||||||
|
- [meanas](meanas.md): top-level package overview
|
||||||
|
- [eigensolvers](eigensolvers.md): generic eigenvalue utilities used by the mode solvers
|
||||||
|
- [fdfd](fdfd.md): frequency-domain operators, sources, PML, solvers, and far-field transforms
|
||||||
|
- [waveguides](waveguides.md): straight, cylindrical, and 3D waveguide mode helpers
|
||||||
|
- [fdtd](fdtd.md): timestepping, CPML, energy/flux helpers, and phasor extraction
|
||||||
|
- [fdmath](fdmath.md): shared discrete operators, vectorization helpers, and derivation background
|
||||||
|
|
||||||
|
The waveguide and FDTD pages are the best places to start if you want the
|
||||||
|
mathematical derivations rather than just the callable reference.
|
||||||
3
docs/api/meanas.md
Normal file
3
docs/api/meanas.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# meanas
|
||||||
|
|
||||||
|
::: meanas
|
||||||
7
docs/api/waveguides.md
Normal file
7
docs/api/waveguides.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# waveguides
|
||||||
|
|
||||||
|
::: meanas.fdfd.waveguide_2d
|
||||||
|
|
||||||
|
::: meanas.fdfd.waveguide_3d
|
||||||
|
|
||||||
|
::: meanas.fdfd.waveguide_cyl
|
||||||
1
docs/assets/vendor/mathjax/core.js
vendored
Normal file
1
docs/assets/vendor/mathjax/core.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
docs/assets/vendor/mathjax/input/asciimath.js
vendored
Normal file
1
docs/assets/vendor/mathjax/input/asciimath.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
docs/assets/vendor/mathjax/input/mml.js
vendored
Normal file
1
docs/assets/vendor/mathjax/input/mml.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
docs/assets/vendor/mathjax/input/mml/entities.js
vendored
Normal file
1
docs/assets/vendor/mathjax/input/mml/entities.js
vendored
Normal file
File diff suppressed because one or more lines are too long
34
docs/assets/vendor/mathjax/input/tex-full.js
vendored
Normal file
34
docs/assets/vendor/mathjax/input/tex-full.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
docs/assets/vendor/mathjax/loader.js
vendored
Normal file
1
docs/assets/vendor/mathjax/loader.js
vendored
Normal file
File diff suppressed because one or more lines are too long
38
docs/assets/vendor/mathjax/manifest.json
vendored
Normal file
38
docs/assets/vendor/mathjax/manifest.json
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"etag": "7ab8013dfc2c395304109fe189c1772ef87b366a",
|
||||||
|
"files": {
|
||||||
|
"core.js": 212262,
|
||||||
|
"loader.js": 16370,
|
||||||
|
"startup.js": 22870,
|
||||||
|
"input/tex-full.js": 270711,
|
||||||
|
"input/asciimath.js": 107690,
|
||||||
|
"input/mml.js": 13601,
|
||||||
|
"input/mml/entities.js": 32232,
|
||||||
|
"output/chtml.js": 211041,
|
||||||
|
"output/chtml/fonts/tex.js": 104359,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Zero.woff": 1368,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff": 1136,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff": 1116,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff": 17604,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff": 5148,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff": 3244,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff": 5464,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff": 5792,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff": 11852,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff": 12660,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff": 14628,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff": 15944,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff": 19288,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff": 19360,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff": 19776,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff": 34160,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff": 20832,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff": 34464,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff": 21480,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff": 22340,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff": 9600,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff": 9908,
|
||||||
|
"output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff": 40808
|
||||||
|
},
|
||||||
|
"version": "3.1.4"
|
||||||
|
}
|
||||||
1
docs/assets/vendor/mathjax/output/chtml.js
vendored
Normal file
1
docs/assets/vendor/mathjax/output/chtml.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
docs/assets/vendor/mathjax/output/chtml/fonts/tex.js
vendored
Normal file
1
docs/assets/vendor/mathjax/output/chtml/fonts/tex.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Zero.woff
vendored
Normal file
BIN
docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Zero.woff
vendored
Normal file
Binary file not shown.
1
docs/assets/vendor/mathjax/startup.js
vendored
Normal file
1
docs/assets/vendor/mathjax/startup.js
vendored
Normal file
File diff suppressed because one or more lines are too long
61
docs/index.md
Normal file
61
docs/index.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# meanas
|
||||||
|
|
||||||
|
`meanas` is a Python package for finite-difference electromagnetic simulation.
|
||||||
|
It combines:
|
||||||
|
|
||||||
|
- `meanas.fdfd` for frequency-domain operators, sources, waveguide modes, and SCPML
|
||||||
|
- `meanas.fdtd` for Yee-grid timestepping, CPML, energy/flux accounting, and phasor extraction
|
||||||
|
- `meanas.fdmath` for the shared discrete operators and derivations underneath both solvers
|
||||||
|
|
||||||
|
This documentation is built directly from the package docstrings. The API pages
|
||||||
|
are the source of truth for the mathematical derivations and calling
|
||||||
|
conventions.
|
||||||
|
|
||||||
|
## Examples and API Map
|
||||||
|
|
||||||
|
For most users, the tracked examples under `examples/` are the right entry
|
||||||
|
point. They show the intended combinations of tools for solving complete
|
||||||
|
problems.
|
||||||
|
|
||||||
|
Relevant starting examples:
|
||||||
|
|
||||||
|
- `examples/fdtd.py` for broadband pulse excitation and phasor extraction
|
||||||
|
- `examples/waveguide.py` for guided phasor-domain FDTD/FDFD comparison
|
||||||
|
- `examples/waveguide_real.py` for real-valued continuous-wave FDTD compared
|
||||||
|
against real fields reconstructed from an FDFD solution, including guided-core,
|
||||||
|
mode-weighted, and guided-mode / residual comparisons
|
||||||
|
- `examples/eme.py` for straight-interface mode matching / EME and modal
|
||||||
|
scattering between two nearby waveguide cross-sections
|
||||||
|
- `examples/eme_bend.py` for straight-to-bent mode matching with cylindrical
|
||||||
|
bend modes and a cascaded bend-network example
|
||||||
|
- `examples/fdfd.py` for direct frequency-domain waveguide excitation
|
||||||
|
|
||||||
|
For solver equivalence, prefer the phasor-based examples first. They compare
|
||||||
|
the extracted `+\omega` content of the FDTD run directly against the FDFD
|
||||||
|
solution and are the main accuracy benchmarks in the test suite.
|
||||||
|
|
||||||
|
`examples/waveguide_real.py` answers a different, stricter question: how well a
|
||||||
|
late raw real snapshot matches `Re(E_\omega e^{i\omega t})` on a monitor plane.
|
||||||
|
That diagnostic is useful, but it also includes orthogonal residual structure
|
||||||
|
that the phasor comparison intentionally filters out.
|
||||||
|
|
||||||
|
The API pages are better read as a toolbox map and derivation reference:
|
||||||
|
|
||||||
|
- Use the [FDTD API](api/fdtd.md) for time-domain stepping, CPML, phasor
|
||||||
|
extraction, and real-field reconstruction from FDFD phasors.
|
||||||
|
- Use the [FDFD API](api/fdfd.md) for driven frequency-domain solves and sparse
|
||||||
|
operator algebra.
|
||||||
|
- Use the [Waveguide API](api/waveguides.md) for mode solving, port sources,
|
||||||
|
and overlap windows.
|
||||||
|
- Use the [fdmath API](api/fdmath.md) for the lower-level finite-difference
|
||||||
|
operators and the shared discrete derivations underneath both solvers.
|
||||||
|
|
||||||
|
## Build outputs
|
||||||
|
|
||||||
|
The docs build generates two HTML views from the same source:
|
||||||
|
|
||||||
|
- a normal multi-page site
|
||||||
|
- a print-oriented combined page under `site/print_page/`
|
||||||
|
|
||||||
|
If `htmlark` is installed, `./make_docs.sh` also writes a fully inlined
|
||||||
|
`site/standalone.html`.
|
||||||
19
docs/javascripts/mathjax.js
Normal file
19
docs/javascripts/mathjax.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
window.MathJax = {
|
||||||
|
loader: {
|
||||||
|
load: ["input/tex-full", "output/chtml"]
|
||||||
|
},
|
||||||
|
tex: {
|
||||||
|
processEscapes: true,
|
||||||
|
processEnvironments: true
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
ignoreHtmlClass: ".*|",
|
||||||
|
processHtmlClass: "arithmatex"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document$.subscribe(() => {
|
||||||
|
MathJax.typesetPromise?.(
|
||||||
|
document.querySelectorAll(".arithmatex")
|
||||||
|
).catch((error) => console.error(error));
|
||||||
|
});
|
||||||
64
docs/stylesheets/extra.css
Normal file
64
docs/stylesheets/extra.css
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
.md-typeset .arithmatex {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-typeset .doc-contents {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-typeset h1 code,
|
||||||
|
.md-typeset h2 code,
|
||||||
|
.md-typeset h3 code {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] {
|
||||||
|
--md-default-bg-color: #000000;
|
||||||
|
--md-default-bg-color--light: #050505;
|
||||||
|
--md-default-bg-color--lighter: #0a0a0a;
|
||||||
|
--md-default-bg-color--lightest: #111111;
|
||||||
|
--md-default-fg-color: #e8eef7;
|
||||||
|
--md-default-fg-color--light: #b3bfd1;
|
||||||
|
--md-default-fg-color--lighter: #7f8ba0;
|
||||||
|
--md-default-fg-color--lightest: #5d6880;
|
||||||
|
--md-code-bg-color: #050505;
|
||||||
|
--md-code-fg-color: #e4edf8;
|
||||||
|
--md-accent-fg-color: #7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] .md-header,
|
||||||
|
[data-md-color-scheme="slate"] .md-tabs {
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] .md-main,
|
||||||
|
[data-md-color-scheme="slate"] .md-main__inner,
|
||||||
|
[data-md-color-scheme="slate"] .md-content,
|
||||||
|
[data-md-color-scheme="slate"] .md-content__inner,
|
||||||
|
[data-md-color-scheme="slate"] .md-sidebar,
|
||||||
|
[data-md-color-scheme="slate"] .md-sidebar__scrollwrap {
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset pre > code,
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset code {
|
||||||
|
border: 1px solid rgba(125, 211, 252, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset table:not([class]) {
|
||||||
|
background: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset table:not([class]) th {
|
||||||
|
background: rgba(125, 211, 252, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset .admonition,
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset details {
|
||||||
|
background: #050505;
|
||||||
|
border-color: rgba(125, 211, 252, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset .arithmatex {
|
||||||
|
padding: 0.1rem 0;
|
||||||
|
}
|
||||||
221
examples/eme.py
Normal file
221
examples/eme.py
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
"""
|
||||||
|
Mode-matching / EME example for a straight rib-waveguide interface.
|
||||||
|
|
||||||
|
This example shows the intended user-facing workflow for `meanas.fdfd.eme` on a
|
||||||
|
simple straight interface:
|
||||||
|
|
||||||
|
1. build two nearby waveguide cross-sections on a Yee grid,
|
||||||
|
2. solve a small set of guided modes on each side,
|
||||||
|
3. normalize those modes into E/H port fields,
|
||||||
|
4. assemble the interface scattering matrix with `meanas.fdfd.eme.get_s(...)`,
|
||||||
|
5. inspect the dominant modal coupling numerically and visually.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
import gridlock
|
||||||
|
from gridlock import Extent
|
||||||
|
|
||||||
|
from meanas.fdfd import eme, waveguide_2d
|
||||||
|
from meanas.fdmath import unvec
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
WL = 1310.0
|
||||||
|
DX = 40.0
|
||||||
|
WIDTH = 400.0
|
||||||
|
THF = 161.0
|
||||||
|
THP = 77.0
|
||||||
|
EPS_SI = 3.51 ** 2
|
||||||
|
EPS_OX = 1.453 ** 2
|
||||||
|
MODE_NUMBERS = numpy.array([0])
|
||||||
|
|
||||||
|
|
||||||
|
def require_optional(name: str, package_name: str | None = None) -> ModuleType:
|
||||||
|
package_name = package_name or name
|
||||||
|
try:
|
||||||
|
return importlib.import_module(name)
|
||||||
|
except ImportError as exc: # pragma: no cover - user environment guard
|
||||||
|
raise SystemExit(
|
||||||
|
f"This example requires the optional package '{package_name}'. "
|
||||||
|
"Install example dependencies with `pip install -e './meanas[examples]'`.",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def build_geometry(
|
||||||
|
*,
|
||||||
|
dx: float = DX,
|
||||||
|
width: float = WIDTH,
|
||||||
|
thf: float = THF,
|
||||||
|
thp: float = THP,
|
||||||
|
eps_si: float = EPS_SI,
|
||||||
|
eps_ox: float = EPS_OX,
|
||||||
|
) -> tuple[gridlock.Grid, numpy.ndarray, list[list[numpy.ndarray]], float]:
|
||||||
|
x0 = (width / 2) % dx
|
||||||
|
omega = 2 * pi / WL
|
||||||
|
|
||||||
|
grid = gridlock.Grid(
|
||||||
|
[
|
||||||
|
numpy.arange(-800, 800 + dx, dx),
|
||||||
|
numpy.arange(-400, 400 + dx, dx),
|
||||||
|
numpy.arange(-2 * dx, 2 * dx + dx, dx),
|
||||||
|
],
|
||||||
|
periodic=True,
|
||||||
|
)
|
||||||
|
epsilon = grid.allocate(eps_ox)
|
||||||
|
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
foreground=eps_si,
|
||||||
|
x=Extent(center=x0, span=width + 1200),
|
||||||
|
y=Extent(min=0, max=thf),
|
||||||
|
z=Extent(min=-1e6, max=0),
|
||||||
|
)
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
foreground=eps_ox,
|
||||||
|
x=Extent(max=-width / 2, span=300),
|
||||||
|
y=Extent(min=thp, max=1e6),
|
||||||
|
z=Extent(min=-1e6, max=0),
|
||||||
|
)
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
foreground=eps_ox,
|
||||||
|
x=Extent(min=width / 2, span=300),
|
||||||
|
y=Extent(min=thp, max=1e6),
|
||||||
|
z=Extent(min=-1e6, max=0),
|
||||||
|
)
|
||||||
|
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
foreground=eps_si,
|
||||||
|
x=Extent(max=-(width / 2 + 600), span=240),
|
||||||
|
y=Extent(min=0, max=thf),
|
||||||
|
z=Extent(min=0, max=1e6),
|
||||||
|
)
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
foreground=eps_si,
|
||||||
|
x=Extent(max=width / 2 + 600, span=240),
|
||||||
|
y=Extent(min=0, max=thf),
|
||||||
|
z=Extent(min=0, max=1e6),
|
||||||
|
)
|
||||||
|
|
||||||
|
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
||||||
|
dxes_2d = [[d[0], d[1]] for d in dxes]
|
||||||
|
return grid, epsilon, dxes_2d, omega
|
||||||
|
|
||||||
|
|
||||||
|
def solve_cross_section_modes(
|
||||||
|
epsilon_slice: numpy.ndarray,
|
||||||
|
*,
|
||||||
|
omega: float,
|
||||||
|
dxes_2d: list[list[numpy.ndarray]],
|
||||||
|
mode_numbers: numpy.ndarray = MODE_NUMBERS,
|
||||||
|
) -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], numpy.ndarray]:
|
||||||
|
e_xys, wavenumbers = waveguide_2d.solve_modes(
|
||||||
|
epsilon=epsilon_slice.ravel(),
|
||||||
|
omega=omega,
|
||||||
|
dxes=dxes_2d,
|
||||||
|
mode_numbers=mode_numbers,
|
||||||
|
)
|
||||||
|
eh_fields = [
|
||||||
|
waveguide_2d.normalized_fields_e(
|
||||||
|
e_xy,
|
||||||
|
wavenumber=wavenumber,
|
||||||
|
dxes=dxes_2d,
|
||||||
|
omega=omega,
|
||||||
|
epsilon=epsilon_slice.ravel(),
|
||||||
|
)
|
||||||
|
for e_xy, wavenumber in zip(e_xys, wavenumbers, strict=True)
|
||||||
|
]
|
||||||
|
return eh_fields, wavenumbers
|
||||||
|
|
||||||
|
|
||||||
|
def print_summary(ss: numpy.ndarray, wavenumbers_left: numpy.ndarray, wavenumbers_right: numpy.ndarray, omega: float) -> None:
|
||||||
|
n_left = len(wavenumbers_left)
|
||||||
|
left_neff = numpy.real(wavenumbers_left / omega)
|
||||||
|
right_neff = numpy.real(wavenumbers_right / omega)
|
||||||
|
|
||||||
|
print('left effective indices:', ', '.join(f'{value:.5f}' for value in left_neff[:4]))
|
||||||
|
print('right effective indices:', ', '.join(f'{value:.5f}' for value in right_neff[:4]))
|
||||||
|
|
||||||
|
reflection = abs(ss[0, 0]) ** 2
|
||||||
|
transmission = abs(ss[n_left, 0]) ** 2
|
||||||
|
total_output = numpy.sum(abs(ss[:, 0]) ** 2)
|
||||||
|
print(f'fundamental left-incident reflection |S_00|^2 = {reflection:.6f}')
|
||||||
|
print(f'fundamental left-to-right transmission |S_{n_left},0|^2 = {transmission:.6f}')
|
||||||
|
print(f'fundamental left-incident total output power = {total_output:.6f}')
|
||||||
|
|
||||||
|
strongest = numpy.argsort(abs(ss[n_left:, 0]) ** 2)[::-1][:3]
|
||||||
|
print('dominant transmitted right-side modes for left mode 0:')
|
||||||
|
for mode_index in strongest:
|
||||||
|
print(f' mode {mode_index}: |S|^2 = {abs(ss[n_left + mode_index, 0]) ** 2:.6f}')
|
||||||
|
|
||||||
|
|
||||||
|
def plot_results(
|
||||||
|
*,
|
||||||
|
pyplot: ModuleType,
|
||||||
|
ss: numpy.ndarray,
|
||||||
|
left_mode: tuple[numpy.ndarray, numpy.ndarray],
|
||||||
|
right_mode: tuple[numpy.ndarray, numpy.ndarray],
|
||||||
|
shape_2d: tuple[int, int],
|
||||||
|
) -> None:
|
||||||
|
fig_s, ax_s = pyplot.subplots()
|
||||||
|
image = ax_s.imshow(abs(ss) ** 2, origin='lower', cmap='magma')
|
||||||
|
fig_s.colorbar(image, ax=ax_s)
|
||||||
|
ax_s.set_title('Interface scattering magnitude |S|^2')
|
||||||
|
ax_s.set_xlabel('Incident mode index')
|
||||||
|
ax_s.set_ylabel('Outgoing mode index')
|
||||||
|
|
||||||
|
e_left = unvec(left_mode[0], shape_2d)
|
||||||
|
e_right = unvec(right_mode[0], shape_2d)
|
||||||
|
left_intensity = numpy.sum(abs(e_left) ** 2, axis=0).T
|
||||||
|
right_intensity = numpy.sum(abs(e_right) ** 2, axis=0).T
|
||||||
|
|
||||||
|
fig_modes, axes = pyplot.subplots(1, 2, figsize=(10, 4))
|
||||||
|
left_plot = axes[0].imshow(left_intensity, origin='lower', cmap='viridis')
|
||||||
|
fig_modes.colorbar(left_plot, ax=axes[0])
|
||||||
|
axes[0].set_title('Left fundamental mode |E|^2')
|
||||||
|
right_plot = axes[1].imshow(right_intensity, origin='lower', cmap='viridis')
|
||||||
|
fig_modes.colorbar(right_plot, ax=axes[1])
|
||||||
|
axes[1].set_title('Right fundamental mode |E|^2')
|
||||||
|
if pyplot.get_backend().lower().endswith('agg'):
|
||||||
|
pyplot.close(fig_s)
|
||||||
|
pyplot.close(fig_modes)
|
||||||
|
else:
|
||||||
|
pyplot.show()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
pyplot = require_optional('matplotlib.pyplot', package_name='matplotlib')
|
||||||
|
|
||||||
|
grid, epsilon, dxes_2d, omega = build_geometry()
|
||||||
|
left_slice = epsilon[:, :, :, 1]
|
||||||
|
right_slice = epsilon[:, :, :, -2]
|
||||||
|
|
||||||
|
left_modes, wavenumbers_left = solve_cross_section_modes(left_slice, omega=omega, dxes_2d=dxes_2d)
|
||||||
|
right_modes, wavenumbers_right = solve_cross_section_modes(right_slice, omega=omega, dxes_2d=dxes_2d)
|
||||||
|
|
||||||
|
ss = eme.get_s(left_modes, wavenumbers_left, right_modes, wavenumbers_right, dxes=dxes_2d)
|
||||||
|
|
||||||
|
print_summary(ss, wavenumbers_left, wavenumbers_right, omega)
|
||||||
|
plot_results(
|
||||||
|
pyplot=pyplot,
|
||||||
|
ss=ss,
|
||||||
|
left_mode=left_modes[0],
|
||||||
|
right_mode=right_modes[0],
|
||||||
|
shape_2d=grid.shape[:2],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
314
examples/eme_bend.py
Normal file
314
examples/eme_bend.py
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
"""
|
||||||
|
Mode-matching / EME example for a straight-to-bent waveguide interface.
|
||||||
|
|
||||||
|
This example demonstrates a cylindrical-waveguide EME workflow:
|
||||||
|
|
||||||
|
1. build a rib-waveguide cross-section,
|
||||||
|
2. solve straight port modes with `waveguide_2d`,
|
||||||
|
3. solve bend modes with `waveguide_cyl`,
|
||||||
|
4. assemble the straight-to-bend interface scattering matrix with
|
||||||
|
`meanas.fdfd.eme.get_s(...)`,
|
||||||
|
5. optionally cascade a straight section, bend section, and interface pair into
|
||||||
|
a compact multiport network using `scikit-rf`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
from scipy import sparse
|
||||||
|
|
||||||
|
import gridlock
|
||||||
|
from gridlock import Extent
|
||||||
|
|
||||||
|
from meanas.fdfd import eme, waveguide_2d, waveguide_cyl
|
||||||
|
from meanas.fdmath import unvec
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
WL = 1310.0
|
||||||
|
DX = 40.0
|
||||||
|
RADIUS = 6e3
|
||||||
|
WIDTH = 400.0
|
||||||
|
THF = 161.0
|
||||||
|
THP = 77.0
|
||||||
|
EPS_SI = 3.51 ** 2
|
||||||
|
EPS_OX = 1.453 ** 2
|
||||||
|
MODE_NUMBERS = numpy.array([0])
|
||||||
|
STRAIGHT_SECTION_LENGTH = 12e3
|
||||||
|
BEND_ANGLE = pi / 2
|
||||||
|
|
||||||
|
|
||||||
|
def require_optional(name: str, package_name: str | None = None) -> ModuleType:
|
||||||
|
package_name = package_name or name
|
||||||
|
try:
|
||||||
|
return importlib.import_module(name)
|
||||||
|
except ImportError as exc: # pragma: no cover - user environment guard
|
||||||
|
raise SystemExit(
|
||||||
|
f"This example requires the optional package '{package_name}'. "
|
||||||
|
"Install example dependencies with `pip install -e './meanas[examples]'`.",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def build_geometry(
|
||||||
|
*,
|
||||||
|
dx: float = DX,
|
||||||
|
width: float = WIDTH,
|
||||||
|
thf: float = THF,
|
||||||
|
thp: float = THP,
|
||||||
|
eps_si: float = EPS_SI,
|
||||||
|
eps_ox: float = EPS_OX,
|
||||||
|
) -> tuple[gridlock.Grid, numpy.ndarray, list[list[numpy.ndarray]], float]:
|
||||||
|
x0 = (width / 2) % dx
|
||||||
|
omega = 2 * pi / WL
|
||||||
|
|
||||||
|
grid = gridlock.Grid(
|
||||||
|
[
|
||||||
|
numpy.arange(-800, 800 + dx, dx),
|
||||||
|
numpy.arange(-400, 400 + dx, dx),
|
||||||
|
numpy.arange(-2 * dx, 2 * dx + dx, dx),
|
||||||
|
],
|
||||||
|
periodic=True,
|
||||||
|
)
|
||||||
|
epsilon = grid.allocate(eps_ox)
|
||||||
|
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
foreground=eps_si,
|
||||||
|
x=Extent(center=x0, span=width + 1200),
|
||||||
|
y=Extent(min=0, max=thf),
|
||||||
|
z=Extent(min=-1e6, max=0),
|
||||||
|
)
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
foreground=eps_ox,
|
||||||
|
x=Extent(max=-width / 2, span=300),
|
||||||
|
y=Extent(min=thp, max=1e6),
|
||||||
|
z=Extent(min=-1e6, center=0),
|
||||||
|
)
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
foreground=eps_ox,
|
||||||
|
x=Extent(min=width / 2, span=300),
|
||||||
|
y=Extent(min=thp, max=1e6),
|
||||||
|
z=Extent(min=-1e6, center=0),
|
||||||
|
)
|
||||||
|
|
||||||
|
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
||||||
|
dxes_2d = [[d[0], d[1]] for d in dxes]
|
||||||
|
return grid, epsilon, dxes_2d, omega
|
||||||
|
|
||||||
|
|
||||||
|
def solve_straight_modes(
|
||||||
|
epsilon_slice: numpy.ndarray,
|
||||||
|
*,
|
||||||
|
omega: float,
|
||||||
|
dxes_2d: list[list[numpy.ndarray]],
|
||||||
|
mode_numbers: numpy.ndarray = MODE_NUMBERS,
|
||||||
|
) -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], numpy.ndarray]:
|
||||||
|
e_xys, wavenumbers = waveguide_2d.solve_modes(
|
||||||
|
epsilon=epsilon_slice.ravel(),
|
||||||
|
omega=omega,
|
||||||
|
dxes=dxes_2d,
|
||||||
|
mode_numbers=mode_numbers,
|
||||||
|
)
|
||||||
|
eh_fields = [
|
||||||
|
waveguide_2d.normalized_fields_e(
|
||||||
|
e_xy,
|
||||||
|
wavenumber=wavenumber,
|
||||||
|
dxes=dxes_2d,
|
||||||
|
omega=omega,
|
||||||
|
epsilon=epsilon_slice.ravel(),
|
||||||
|
)
|
||||||
|
for e_xy, wavenumber in zip(e_xys, wavenumbers, strict=True)
|
||||||
|
]
|
||||||
|
return eh_fields, wavenumbers
|
||||||
|
|
||||||
|
|
||||||
|
def solve_bend_modes(
|
||||||
|
epsilon_slice: numpy.ndarray,
|
||||||
|
*,
|
||||||
|
omega: float,
|
||||||
|
dxes_2d: list[list[numpy.ndarray]],
|
||||||
|
rmin: float,
|
||||||
|
mode_numbers: numpy.ndarray = MODE_NUMBERS,
|
||||||
|
) -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], numpy.ndarray, numpy.ndarray]:
|
||||||
|
e_xys, angular_wavenumbers = waveguide_cyl.solve_modes(
|
||||||
|
epsilon=epsilon_slice.ravel(),
|
||||||
|
omega=omega,
|
||||||
|
dxes=dxes_2d,
|
||||||
|
mode_numbers=mode_numbers,
|
||||||
|
rmin=rmin,
|
||||||
|
)
|
||||||
|
linear_wavenumbers = waveguide_cyl.linear_wavenumbers(
|
||||||
|
e_xys=e_xys,
|
||||||
|
angular_wavenumbers=angular_wavenumbers,
|
||||||
|
rmin=rmin,
|
||||||
|
epsilon=epsilon_slice.ravel(),
|
||||||
|
dxes=dxes_2d,
|
||||||
|
)
|
||||||
|
eh_fields = [
|
||||||
|
waveguide_cyl.normalized_fields_e(
|
||||||
|
e_xy,
|
||||||
|
angular_wavenumber=angular_wavenumber,
|
||||||
|
dxes=dxes_2d,
|
||||||
|
omega=omega,
|
||||||
|
epsilon=epsilon_slice.ravel(),
|
||||||
|
rmin=rmin,
|
||||||
|
)
|
||||||
|
for e_xy, angular_wavenumber in zip(e_xys, angular_wavenumbers, strict=True)
|
||||||
|
]
|
||||||
|
return eh_fields, linear_wavenumbers, angular_wavenumbers
|
||||||
|
|
||||||
|
|
||||||
|
def build_cascaded_network(
|
||||||
|
skrf: ModuleType,
|
||||||
|
*,
|
||||||
|
interface_s: numpy.ndarray,
|
||||||
|
straight_wavenumbers: numpy.ndarray,
|
||||||
|
bend_angular_wavenumbers: numpy.ndarray,
|
||||||
|
n_modes: int,
|
||||||
|
) -> object:
|
||||||
|
net_sb = skrf.Network(f=[1 / WL], s=interface_s[numpy.newaxis, ...])
|
||||||
|
net_bs = net_sb.copy()
|
||||||
|
net_bs.renumber(numpy.arange(2 * n_modes), numpy.roll(numpy.arange(2 * n_modes), n_modes))
|
||||||
|
|
||||||
|
straight_phase = sparse.diags_array(numpy.exp(-1j * straight_wavenumbers[:n_modes] * STRAIGHT_SECTION_LENGTH))
|
||||||
|
bend_phase = sparse.diags_array(numpy.exp(-1j * bend_angular_wavenumbers[:n_modes] * BEND_ANGLE))
|
||||||
|
zero = numpy.zeros((n_modes, n_modes), dtype=complex)
|
||||||
|
|
||||||
|
straight_s = numpy.block([[zero, straight_phase.toarray()], [straight_phase.toarray(), zero]])
|
||||||
|
bend_s = numpy.block([[zero, bend_phase.toarray()], [bend_phase.toarray(), zero]])
|
||||||
|
net_straight = skrf.Network(f=[1 / WL], s=straight_s[numpy.newaxis, ...])
|
||||||
|
net_bend = skrf.Network(f=[1 / WL], s=bend_s[numpy.newaxis, ...])
|
||||||
|
|
||||||
|
return skrf.network.cascade_list([net_straight, net_sb, net_bend, net_bs, net_straight])
|
||||||
|
|
||||||
|
|
||||||
|
def print_summary(
|
||||||
|
interface_s: numpy.ndarray,
|
||||||
|
cascaded_s: numpy.ndarray,
|
||||||
|
straight_wavenumbers: numpy.ndarray,
|
||||||
|
bend_linear_wavenumbers: numpy.ndarray,
|
||||||
|
bend_angular_wavenumbers: numpy.ndarray,
|
||||||
|
omega: float,
|
||||||
|
n_modes: int,
|
||||||
|
) -> None:
|
||||||
|
straight_neff = numpy.real(straight_wavenumbers / omega)
|
||||||
|
bend_neff = numpy.real(bend_linear_wavenumbers / omega)
|
||||||
|
print('straight effective indices:', ', '.join(f'{value:.5f}' for value in straight_neff[:4]))
|
||||||
|
print('bend effective indices :', ', '.join(f'{value:.5f}' for value in bend_neff[:4]))
|
||||||
|
print('bend angular wavenumbers :', ', '.join(f'{value:.5e}' for value in bend_angular_wavenumbers[:4]))
|
||||||
|
|
||||||
|
interface_transmission = abs(interface_s[n_modes, 0]) ** 2
|
||||||
|
interface_reflection = abs(interface_s[0, 0]) ** 2
|
||||||
|
print(f'interface fundamental transmission |S_{n_modes},0|^2 = {interface_transmission:.6f}')
|
||||||
|
print(f'interface fundamental reflection |S_00|^2 = {interface_reflection:.6f}')
|
||||||
|
|
||||||
|
total_cascaded_output = numpy.sum(abs(cascaded_s[:, 0]) ** 2)
|
||||||
|
bend_through = abs(cascaded_s[n_modes, 0]) ** 2
|
||||||
|
bend_reflection = abs(cascaded_s[0, 0]) ** 2
|
||||||
|
print(f'cascaded bend through power |S_{n_modes},0|^2 = {bend_through:.6f}')
|
||||||
|
print(f'cascaded bend reflection |S_00|^2 = {bend_reflection:.6f}')
|
||||||
|
print(f'cascaded left-incident total output power = {total_cascaded_output:.6f}')
|
||||||
|
|
||||||
|
|
||||||
|
def plot_results(
|
||||||
|
*,
|
||||||
|
pyplot: ModuleType,
|
||||||
|
interface_s: numpy.ndarray,
|
||||||
|
cascaded_s: numpy.ndarray,
|
||||||
|
straight_mode: tuple[numpy.ndarray, numpy.ndarray],
|
||||||
|
bend_mode: tuple[numpy.ndarray, numpy.ndarray],
|
||||||
|
shape_2d: tuple[int, int],
|
||||||
|
) -> None:
|
||||||
|
fig_s, axes = pyplot.subplots(1, 2, figsize=(12, 4))
|
||||||
|
interface_plot = axes[0].imshow(abs(interface_s) ** 2, origin='lower', cmap='magma')
|
||||||
|
fig_s.colorbar(interface_plot, ax=axes[0])
|
||||||
|
axes[0].set_title('Straight-to-bend interface |S|^2')
|
||||||
|
axes[0].set_xlabel('Incident mode index')
|
||||||
|
axes[0].set_ylabel('Outgoing mode index')
|
||||||
|
|
||||||
|
cascaded_plot = axes[1].imshow(abs(cascaded_s) ** 2, origin='lower', cmap='magma')
|
||||||
|
fig_s.colorbar(cascaded_plot, ax=axes[1])
|
||||||
|
axes[1].set_title('Cascaded bend network |S|^2')
|
||||||
|
axes[1].set_xlabel('Incident mode index')
|
||||||
|
axes[1].set_ylabel('Outgoing mode index')
|
||||||
|
|
||||||
|
straight_e = unvec(straight_mode[0], shape_2d)
|
||||||
|
bend_e = unvec(bend_mode[0], shape_2d)
|
||||||
|
straight_intensity = numpy.sum(abs(straight_e) ** 2, axis=0).T
|
||||||
|
bend_intensity = numpy.sum(abs(bend_e) ** 2, axis=0).T
|
||||||
|
|
||||||
|
fig_modes, axes_modes = pyplot.subplots(1, 2, figsize=(10, 4))
|
||||||
|
straight_plot = axes_modes[0].imshow(straight_intensity, origin='lower', cmap='viridis')
|
||||||
|
fig_modes.colorbar(straight_plot, ax=axes_modes[0])
|
||||||
|
axes_modes[0].set_title('Straight fundamental mode |E|^2')
|
||||||
|
bend_plot = axes_modes[1].imshow(bend_intensity, origin='lower', cmap='viridis')
|
||||||
|
fig_modes.colorbar(bend_plot, ax=axes_modes[1])
|
||||||
|
axes_modes[1].set_title('Bent fundamental mode |E|^2')
|
||||||
|
if pyplot.get_backend().lower().endswith('agg'):
|
||||||
|
pyplot.close(fig_s)
|
||||||
|
pyplot.close(fig_modes)
|
||||||
|
else:
|
||||||
|
pyplot.show()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
pyplot = require_optional('matplotlib.pyplot', package_name='matplotlib')
|
||||||
|
skrf = require_optional('skrf', package_name='scikit-rf')
|
||||||
|
|
||||||
|
grid, epsilon, dxes_2d, omega = build_geometry()
|
||||||
|
epsilon_slice = epsilon[:, :, :, 2]
|
||||||
|
rmin = RADIUS + grid.xyz[0].min()
|
||||||
|
|
||||||
|
straight_modes, straight_wavenumbers = solve_straight_modes(epsilon_slice, omega=omega, dxes_2d=dxes_2d)
|
||||||
|
bend_modes, bend_linear_wavenumbers, bend_angular_wavenumbers = solve_bend_modes(
|
||||||
|
epsilon_slice,
|
||||||
|
omega=omega,
|
||||||
|
dxes_2d=dxes_2d,
|
||||||
|
rmin=rmin,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface_s = eme.get_s(
|
||||||
|
straight_modes,
|
||||||
|
straight_wavenumbers,
|
||||||
|
bend_modes,
|
||||||
|
bend_linear_wavenumbers,
|
||||||
|
dxes=dxes_2d,
|
||||||
|
)
|
||||||
|
cascaded = build_cascaded_network(
|
||||||
|
skrf,
|
||||||
|
interface_s=interface_s,
|
||||||
|
straight_wavenumbers=straight_wavenumbers,
|
||||||
|
bend_angular_wavenumbers=bend_angular_wavenumbers,
|
||||||
|
n_modes=len(MODE_NUMBERS),
|
||||||
|
)
|
||||||
|
cascaded_s = cascaded.s[0]
|
||||||
|
|
||||||
|
print_summary(
|
||||||
|
interface_s,
|
||||||
|
cascaded_s,
|
||||||
|
straight_wavenumbers,
|
||||||
|
bend_linear_wavenumbers,
|
||||||
|
bend_angular_wavenumbers,
|
||||||
|
omega,
|
||||||
|
len(MODE_NUMBERS),
|
||||||
|
)
|
||||||
|
plot_results(
|
||||||
|
pyplot=pyplot,
|
||||||
|
interface_s=interface_s,
|
||||||
|
cascaded_s=cascaded_s,
|
||||||
|
straight_mode=straight_modes[0],
|
||||||
|
bend_mode=bend_modes[0],
|
||||||
|
shape_2d=grid.shape[:2],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
103
examples/fdfd0.py
Normal file
103
examples/fdfd0.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import numpy
|
||||||
|
from numpy.linalg import norm
|
||||||
|
from matplotlib import pyplot, colors
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import meanas
|
||||||
|
from meanas import fdtd
|
||||||
|
from meanas.fdmath import vec, unvec
|
||||||
|
from meanas.fdfd import waveguide_3d, functional, scpml, operators
|
||||||
|
from meanas.fdfd.solvers import generic as generic_solver
|
||||||
|
|
||||||
|
import gridlock
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
def pcolor(ax, v) -> None:
|
||||||
|
mappable = ax.pcolor(v, cmap='seismic', norm=colors.CenteredNorm())
|
||||||
|
ax.axis('equal')
|
||||||
|
ax.get_figure().colorbar(mappable)
|
||||||
|
|
||||||
|
|
||||||
|
def test0(solver=generic_solver):
|
||||||
|
dx = 50 # discretization (nm/cell)
|
||||||
|
pml_thickness = 10 # (number of cells)
|
||||||
|
|
||||||
|
wl = 1550 # Excitation wavelength
|
||||||
|
omega = 2 * numpy.pi / wl
|
||||||
|
|
||||||
|
# Device design parameters
|
||||||
|
radii = (1, 0.6)
|
||||||
|
th = 220
|
||||||
|
center = [0, 0, 0]
|
||||||
|
|
||||||
|
# refractive indices
|
||||||
|
n_ring = numpy.sqrt(12.6) # ~Si
|
||||||
|
n_air = 4.0 # air
|
||||||
|
|
||||||
|
# Half-dimensions of the simulation grid
|
||||||
|
xyz_max = numpy.array([1.2, 1.2, 0.3]) * 1000 + pml_thickness * dx
|
||||||
|
|
||||||
|
# Coordinates of the edges of the cells.
|
||||||
|
half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max]
|
||||||
|
edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
|
||||||
|
|
||||||
|
# #### Create the grid, mask, and draw the device ####
|
||||||
|
grid = gridlock.Grid(edge_coords)
|
||||||
|
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
|
||||||
|
grid.draw_cylinder(
|
||||||
|
epsilon,
|
||||||
|
h = dict(axis='z', center=center[2], span=th),
|
||||||
|
radius = max(radii),
|
||||||
|
center2d = center[:2],
|
||||||
|
foreground = n_ring ** 2,
|
||||||
|
num_points = 24,
|
||||||
|
)
|
||||||
|
grid.draw_cylinder(
|
||||||
|
epsilon,
|
||||||
|
h = dict(axis='z', center=center[2], span=th * 1.1),
|
||||||
|
radius = min(radii),
|
||||||
|
center2d = center[:2],
|
||||||
|
foreground = n_air ** 2,
|
||||||
|
num_points = 24,
|
||||||
|
)
|
||||||
|
|
||||||
|
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
||||||
|
for a in (0, 1, 2):
|
||||||
|
for p in (-1, 1):
|
||||||
|
dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega,
|
||||||
|
thickness=pml_thickness)
|
||||||
|
|
||||||
|
J = [numpy.zeros_like(epsilon[0], dtype=complex) for _ in range(3)]
|
||||||
|
J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Solve!
|
||||||
|
#
|
||||||
|
sim_args = dict(
|
||||||
|
omega = omega,
|
||||||
|
dxes = dxes,
|
||||||
|
epsilon = vec(epsilon),
|
||||||
|
)
|
||||||
|
x = solver(J=vec(J), **sim_args)
|
||||||
|
|
||||||
|
A = operators.e_full(omega, dxes, vec(epsilon)).tocsr()
|
||||||
|
b = -1j * omega * vec(J)
|
||||||
|
print('Norm of the residual is ', norm(A @ x - b) / norm(b))
|
||||||
|
|
||||||
|
E = unvec(x, grid.shape)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Plot results
|
||||||
|
#
|
||||||
|
grid.visualize_slice(E.real, plane=dict(z=0), which_shifts=1, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test0()
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import importlib
|
import importlib
|
||||||
|
import logging
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.linalg import norm
|
from numpy.linalg import norm
|
||||||
|
from matplotlib import pyplot, colors
|
||||||
|
import logging
|
||||||
|
|
||||||
import meanas
|
import meanas
|
||||||
from meanas import fdtd
|
from meanas import fdtd
|
||||||
from meanas.fdmath import vec, unvec
|
from meanas.fdmath import vec, unvec, fdfield_t
|
||||||
from meanas.fdfd import waveguide_3d, functional, scpml, operators
|
from meanas.fdfd import waveguide_3d, functional, scpml, operators
|
||||||
from meanas.fdfd.solvers import generic as generic_solver
|
from meanas.fdfd.solvers import generic as generic_solver
|
||||||
|
|
||||||
|
|
@ -12,7 +15,6 @@ import gridlock
|
||||||
|
|
||||||
from matplotlib import pyplot
|
from matplotlib import pyplot
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
||||||
|
|
@ -20,82 +22,6 @@ logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
def test0(solver=generic_solver):
|
|
||||||
dx = 50 # discretization (nm/cell)
|
|
||||||
pml_thickness = 10 # (number of cells)
|
|
||||||
|
|
||||||
wl = 1550 # Excitation wavelength
|
|
||||||
omega = 2 * numpy.pi / wl
|
|
||||||
|
|
||||||
# Device design parameters
|
|
||||||
radii = (1, 0.6)
|
|
||||||
th = 220
|
|
||||||
center = [0, 0, 0]
|
|
||||||
|
|
||||||
# refractive indices
|
|
||||||
n_ring = numpy.sqrt(12.6) # ~Si
|
|
||||||
n_air = 4.0 # air
|
|
||||||
|
|
||||||
# Half-dimensions of the simulation grid
|
|
||||||
xyz_max = numpy.array([1.2, 1.2, 0.3]) * 1000 + pml_thickness * dx
|
|
||||||
|
|
||||||
# Coordinates of the edges of the cells.
|
|
||||||
half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max]
|
|
||||||
edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
|
|
||||||
|
|
||||||
# #### Create the grid, mask, and draw the device ####
|
|
||||||
grid = gridlock.Grid(edge_coords)
|
|
||||||
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
|
|
||||||
grid.draw_cylinder(epsilon,
|
|
||||||
surface_normal=2,
|
|
||||||
center=center,
|
|
||||||
radius=max(radii),
|
|
||||||
thickness=th,
|
|
||||||
eps=n_ring**2,
|
|
||||||
num_points=24)
|
|
||||||
grid.draw_cylinder(epsilon,
|
|
||||||
surface_normal=2,
|
|
||||||
center=center,
|
|
||||||
radius=min(radii),
|
|
||||||
thickness=th*1.1,
|
|
||||||
eps=n_air ** 2,
|
|
||||||
num_points=24)
|
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
|
||||||
for a in (0, 1, 2):
|
|
||||||
for p in (-1, 1):
|
|
||||||
dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega,
|
|
||||||
thickness=pml_thickness)
|
|
||||||
|
|
||||||
J = [numpy.zeros_like(epsilon[0], dtype=complex) for _ in range(3)]
|
|
||||||
J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Solve!
|
|
||||||
'''
|
|
||||||
sim_args = {
|
|
||||||
'omega': omega,
|
|
||||||
'dxes': dxes,
|
|
||||||
'epsilon': vec(epsilon),
|
|
||||||
}
|
|
||||||
x = solver(J=vec(J), **sim_args)
|
|
||||||
|
|
||||||
A = operators.e_full(omega, dxes, vec(epsilon)).tocsr()
|
|
||||||
b = -1j * omega * vec(J)
|
|
||||||
print('Norm of the residual is ', norm(A @ x - b))
|
|
||||||
|
|
||||||
E = unvec(x, grid.shape)
|
|
||||||
|
|
||||||
'''
|
|
||||||
Plot results
|
|
||||||
'''
|
|
||||||
pyplot.figure()
|
|
||||||
pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic')
|
|
||||||
pyplot.axis('equal')
|
|
||||||
pyplot.show()
|
|
||||||
|
|
||||||
|
|
||||||
def test1(solver=generic_solver):
|
def test1(solver=generic_solver):
|
||||||
dx = 40 # discretization (nm/cell)
|
dx = 40 # discretization (nm/cell)
|
||||||
pml_thickness = 10 # (number of cells)
|
pml_thickness = 10 # (number of cells)
|
||||||
|
|
@ -122,7 +48,7 @@ def test1(solver=generic_solver):
|
||||||
# #### Create the grid and draw the device ####
|
# #### Create the grid and draw the device ####
|
||||||
grid = gridlock.Grid(edge_coords)
|
grid = gridlock.Grid(edge_coords)
|
||||||
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
|
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
|
||||||
grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], eps=n_wg**2)
|
grid.draw_cuboid(epsilon, x=dict(center=0, span=8e3), y=dict(center=0, span=w), z=dict(center=0, span=th), foreground=n_wg**2)
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
||||||
for a in (0, 1, 2):
|
for a in (0, 1, 2):
|
||||||
|
|
@ -156,22 +82,14 @@ def test1(solver=generic_solver):
|
||||||
# grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
|
# grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
|
||||||
# grid.visualize_isosurface(pmcg)
|
# grid.visualize_isosurface(pmcg)
|
||||||
|
|
||||||
def pcolor(v) -> None:
|
grid.visualize_slice(J.imag, plane=dict(y=6*dx), which_shifts=1, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr'))
|
||||||
vmax = numpy.max(numpy.abs(v))
|
fig, ax = pyplot.subplots()
|
||||||
pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax)
|
ax.pcolormesh((numpy.abs(J).sum(axis=2).sum(axis=0) > 0).astype(float).T, cmap='hot')
|
||||||
pyplot.axis('equal')
|
|
||||||
pyplot.colorbar()
|
|
||||||
|
|
||||||
ss = (1, slice(None), J.shape[2]//2+6, slice(None))
|
|
||||||
# pyplot.figure()
|
|
||||||
# pcolor(J3[ss].T.imag)
|
|
||||||
# pyplot.figure()
|
|
||||||
# pcolor((numpy.abs(J3).sum(axis=2).sum(axis=0) > 0).astype(float).T)
|
|
||||||
pyplot.show(block=True)
|
pyplot.show(block=True)
|
||||||
|
|
||||||
'''
|
#
|
||||||
Solve!
|
# Solve!
|
||||||
'''
|
#
|
||||||
sim_args = {
|
sim_args = {
|
||||||
'omega': omega,
|
'omega': omega,
|
||||||
'dxes': dxes,
|
'dxes': dxes,
|
||||||
|
|
@ -188,20 +106,18 @@ def test1(solver=generic_solver):
|
||||||
|
|
||||||
E = unvec(x, grid.shape)
|
E = unvec(x, grid.shape)
|
||||||
|
|
||||||
'''
|
#
|
||||||
Plot results
|
# Plot results
|
||||||
'''
|
#
|
||||||
center = grid.pos2ind([0, 0, 0], None).astype(int)
|
center = grid.pos2ind([0, 0, 0], None).astype(int)
|
||||||
pyplot.figure()
|
fig, axes = pyplot.subplots(2, 2)
|
||||||
pyplot.subplot(2, 2, 1)
|
grid.visualize_slice(E.real, plane=dict(x=0), which_shifts=1, ax=axes[0, 0], finalize=False, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr'))
|
||||||
pcolor(numpy.real(E[1][center[0], :, :]).T)
|
grid.visualize_slice(E.real, plane=dict(z=0), which_shifts=1, ax=axes[0, 1], finalize=False, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr'))
|
||||||
pyplot.subplot(2, 2, 2)
|
# pcolor(axes[0, 0], numpy.real(E[1][center[0], :, :]).T)
|
||||||
pyplot.plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10))
|
# pcolor(axes[0, 1], numpy.real(E[1][:, :, center[2]]).T)
|
||||||
pyplot.grid(alpha=0.6)
|
axes[1, 0].plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10))
|
||||||
pyplot.ylabel('log10 of field')
|
axes[1, 0].grid(alpha=0.6)
|
||||||
pyplot.subplot(2, 2, 3)
|
axes[1, 0].set_ylabel('log10 of field')
|
||||||
pcolor(numpy.real(E[1][:, :, center[2]]).T)
|
|
||||||
pyplot.subplot(2, 2, 4)
|
|
||||||
|
|
||||||
def poyntings(E):
|
def poyntings(E):
|
||||||
H = functional.e2h(omega, dxes)(E)
|
H = functional.e2h(omega, dxes)(E)
|
||||||
|
|
@ -215,24 +131,28 @@ def test1(solver=generic_solver):
|
||||||
return s0, s1, s2
|
return s0, s1, s2
|
||||||
|
|
||||||
s0x, s1x, s2x = poyntings(E)
|
s0x, s1x, s2x = poyntings(E)
|
||||||
pyplot.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.')
|
ax = axes[1, 1]
|
||||||
pyplot.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.')
|
ax.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.')
|
||||||
pyplot.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.')
|
ax.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.')
|
||||||
pyplot.plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x')
|
ax.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.')
|
||||||
pyplot.grid(alpha=0.6)
|
ax.plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x')
|
||||||
pyplot.legend()
|
ax.grid(alpha=0.6)
|
||||||
pyplot.show()
|
ax.legend()
|
||||||
|
|
||||||
|
p_in = (-E * J.conj()).sum() / 2 * (dx * dx * dx)
|
||||||
|
print(f'{p_in=}')
|
||||||
|
|
||||||
q = []
|
q = []
|
||||||
for i in range(-5, 30):
|
for i in range(-5, 30):
|
||||||
e_ovl_rolled = numpy.roll(e_overlap, i, axis=1)
|
e_ovl_rolled = numpy.roll(e_overlap, i, axis=1)
|
||||||
q += [numpy.abs(vec(E) @ vec(e_ovl_rolled).conj())]
|
q += [numpy.abs(vec(E).conj() @ vec(e_ovl_rolled))]
|
||||||
pyplot.figure()
|
fig, ax = pyplot.subplots()
|
||||||
pyplot.plot(q, marker='.')
|
ax.plot(q, marker='.')
|
||||||
pyplot.grid(alpha=0.6)
|
ax.grid(alpha=0.6)
|
||||||
pyplot.title('Overlap with mode')
|
ax.set_title('Overlap with mode')
|
||||||
pyplot.show()
|
print('Average overlap with mode:', sum(q[8:32])/len(q[8:32]))
|
||||||
print('Average overlap with mode:', sum(q)/len(q))
|
|
||||||
|
pyplot.show(block=True)
|
||||||
|
|
||||||
|
|
||||||
def module_available(name):
|
def module_available(name):
|
||||||
|
|
@ -240,9 +160,6 @@ def module_available(name):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
#test0()
|
|
||||||
# test1()
|
|
||||||
|
|
||||||
if module_available('opencl_fdfd'):
|
if module_available('opencl_fdfd'):
|
||||||
from opencl_fdfd import cg_solver as opencl_solver
|
from opencl_fdfd import cg_solver as opencl_solver
|
||||||
test1(opencl_solver)
|
test1(opencl_solver)
|
||||||
|
|
@ -253,3 +170,4 @@ if __name__ == '__main__':
|
||||||
# test1(magma_solver)
|
# test1(magma_solver)
|
||||||
else:
|
else:
|
||||||
test1()
|
test1()
|
||||||
|
|
||||||
158
examples/fdtd.py
158
examples/fdtd.py
|
|
@ -1,18 +1,30 @@
|
||||||
"""
|
"""
|
||||||
Example code for running an OpenCL FDTD simulation
|
Example code for a broadband FDTD run with phasor extraction.
|
||||||
|
|
||||||
See main() for simulation setup.
|
This script shows the intended low-level workflow for:
|
||||||
|
|
||||||
|
1. building a Yee-grid simulation with CPML on all faces,
|
||||||
|
2. driving it with an electric-current pulse,
|
||||||
|
3. extracting a single-frequency phasor on the fly, and
|
||||||
|
4. checking that phasor against the matching stretched-grid FDFD operator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import copy
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import h5py
|
import h5py
|
||||||
|
from numpy.linalg import norm
|
||||||
|
|
||||||
from meanas import fdtd
|
from meanas import fdtd
|
||||||
from meanas.fdtd import cpml_params, updates_with_cpml
|
from meanas.fdtd import cpml_params, updates_with_cpml
|
||||||
from masque import Pattern, shapes
|
from meanas.fdtd.misc import gaussian_packet
|
||||||
|
|
||||||
|
from meanas.fdfd.operators import e_full
|
||||||
|
from meanas.fdfd.scpml import stretch_with_scpml
|
||||||
|
from meanas.fdmath import vec
|
||||||
|
from masque import Pattern, Circle, Polygon
|
||||||
import gridlock
|
import gridlock
|
||||||
import pcgen
|
import pcgen
|
||||||
|
|
||||||
|
|
@ -41,8 +53,7 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern:
|
||||||
`masque.Pattern` object containing the L3 design
|
`masque.Pattern` object containing the L3 design
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_args = {'hole_dose': 1,
|
default_args = {
|
||||||
'trench_dose': 1,
|
|
||||||
'hole_layer': 0,
|
'hole_layer': 0,
|
||||||
'trench_layer': 1,
|
'trench_layer': 1,
|
||||||
'shifts_a': (0.15, 0, 0.075),
|
'shifts_a': (0.15, 0, 0.075),
|
||||||
|
|
@ -53,38 +64,39 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern:
|
||||||
}
|
}
|
||||||
kwargs = {**default_args, **kwargs}
|
kwargs = {**default_args, **kwargs}
|
||||||
|
|
||||||
xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=kwargs['xy_size'],
|
xyr = pcgen.l3_shift_perturbed_defect(
|
||||||
|
mirror_dims=kwargs['xy_size'],
|
||||||
perturbed_radius=kwargs['perturbed_radius'],
|
perturbed_radius=kwargs['perturbed_radius'],
|
||||||
shifts_a=kwargs['shifts_a'],
|
shifts_a=kwargs['shifts_a'],
|
||||||
shifts_r=kwargs['shifts_r'])
|
shifts_r=kwargs['shifts_r'],
|
||||||
|
)
|
||||||
xyr *= a
|
xyr *= a
|
||||||
xyr[:, 2] *= radius
|
xyr[:, 2] *= radius
|
||||||
|
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
pat.name = f'L3p-a{a:g}r{radius:g}rp{kwargs["perturbed_radius"]:g}'
|
#pat.name = f'L3p-a{a:g}r{radius:g}rp{kwargs["perturbed_radius"]:g}'
|
||||||
pat.shapes += [shapes.Circle(radius=r, offset=(x, y),
|
pat.shapes[(kwargs['hole_layer'], 0)] += [
|
||||||
dose=kwargs['hole_dose'],
|
Circle(radius=r, offset=(x, y))
|
||||||
layer=kwargs['hole_layer'])
|
|
||||||
for x, y, r in xyr]
|
for x, y, r in xyr]
|
||||||
|
|
||||||
maxes = numpy.max(numpy.fabs(xyr), axis=0)
|
maxes = numpy.max(numpy.fabs(xyr), axis=0)
|
||||||
pat.shapes += [shapes.Polygon.rectangle(
|
pat.shapes[(kwargs['trench_layer'], 0)] += [
|
||||||
|
Polygon.rectangle(
|
||||||
lx=(2 * maxes[0]), ly=kwargs['trench_width'],
|
lx=(2 * maxes[0]), ly=kwargs['trench_width'],
|
||||||
offset=(0, s * (maxes[1] + a + kwargs['trench_width'] / 2)),
|
offset=(0, s * (maxes[1] + a + kwargs['trench_width'] / 2))
|
||||||
dose=kwargs['trench_dose'], layer=kwargs['trench_layer'])
|
)
|
||||||
for s in (-1, 1)]
|
for s in (-1, 1)]
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
dtype = numpy.float32
|
dtype = numpy.float32
|
||||||
max_t = 8000 # number of timesteps
|
max_t = 3600 # number of timesteps
|
||||||
|
|
||||||
dx = 40 # discretization (nm/cell)
|
dx = 40 # discretization (nm/cell)
|
||||||
pml_thickness = 8 # (number of cells)
|
pml_thickness = 8 # (number of cells)
|
||||||
|
|
||||||
wl = 1550 # Excitation wavelength and fwhm
|
wl = 1550 # Excitation wavelength and fwhm
|
||||||
dwl = 200
|
|
||||||
|
|
||||||
# Device design parameters
|
# Device design parameters
|
||||||
xy_size = numpy.array([10, 10])
|
xy_size = numpy.array([10, 10])
|
||||||
|
|
@ -107,69 +119,97 @@ def main():
|
||||||
|
|
||||||
# #### Create the grid, mask, and draw the device ####
|
# #### Create the grid, mask, and draw the device ####
|
||||||
grid = gridlock.Grid(edge_coords)
|
grid = gridlock.Grid(edge_coords)
|
||||||
epsilon = grid.allocate(n_air**2, dtype=dtype)
|
epsilon = grid.allocate(n_air ** 2, dtype=dtype)
|
||||||
grid.draw_slab(epsilon,
|
grid.draw_slab(
|
||||||
surface_normal=2,
|
epsilon,
|
||||||
center=[0, 0, 0],
|
slab = dict(axis='z', center=0, span=th),
|
||||||
thickness=th,
|
foreground = n_slab ** 2,
|
||||||
eps=n_slab**2)
|
)
|
||||||
|
|
||||||
mask = perturbed_l3(a, r)
|
mask = perturbed_l3(a, r)
|
||||||
|
grid.draw_polygons(
|
||||||
|
epsilon,
|
||||||
|
slab = dict(axis='z', center=0, span=2 * th),
|
||||||
|
foreground = n_air ** 2,
|
||||||
|
offset2d = (0, 0),
|
||||||
|
polygons = mask.as_polygons(library=None),
|
||||||
|
)
|
||||||
|
|
||||||
grid.draw_polygons(epsilon,
|
print(f'{grid.shape=}')
|
||||||
surface_normal=2,
|
|
||||||
center=[0, 0, 0],
|
|
||||||
thickness=2 * th,
|
|
||||||
eps=n_air**2,
|
|
||||||
polygons=mask.as_polygons())
|
|
||||||
|
|
||||||
print(grid.shape)
|
dt = dx * 0.99 / numpy.sqrt(3)
|
||||||
|
ee = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
dt = .99/numpy.sqrt(3)
|
hh = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
e = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)]
|
|
||||||
h = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)]
|
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
||||||
|
|
||||||
# PMLs in every direction
|
# PMLs in every direction
|
||||||
pml_params = [[cpml_params(axis=dd, polarity=pp, dt=dt,
|
pml_params = [
|
||||||
thickness=pml_thickness, epsilon_eff=1.0**2)
|
[cpml_params(axis=dd, polarity=pp, dt=dt, thickness=pml_thickness, epsilon_eff=n_air ** 2)
|
||||||
for pp in (-1, +1)]
|
for pp in (-1, +1)]
|
||||||
for dd in range(3)]
|
for dd in range(3)]
|
||||||
update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt,
|
update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, dxes=dxes, epsilon=epsilon, dtype=complex)
|
||||||
dxes=dxes, epsilon=epsilon)
|
|
||||||
|
|
||||||
# Source parameters and function
|
# sample_interval = numpy.floor(1 / (2 * 1 / wl * dt)).astype(int)
|
||||||
w = 2 * numpy.pi * dx / wl
|
# print(f'Save time interval would be {sample_interval} * dt = {sample_interval * dt:3g}')
|
||||||
fwhm = dwl * w * w / (2 * numpy.pi * dx)
|
|
||||||
alpha = (fwhm ** 2) / 8 * numpy.log(2)
|
|
||||||
delay = 7/numpy.sqrt(2 * alpha)
|
|
||||||
|
|
||||||
def field_source(i):
|
|
||||||
t0 = i * dt - delay
|
# Build the pulse directly at the current half-steps and normalize that
|
||||||
return numpy.sin(w * t0) * numpy.exp(-alpha * t0**2)
|
# scalar waveform so its extracted temporal phasor is exactly 1 at omega.
|
||||||
|
source_phasor, _delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5)
|
||||||
|
aa, cc, ss = source_phasor(numpy.arange(max_t) + 0.5)
|
||||||
|
source_waveform = aa * (cc + 1j * ss)
|
||||||
|
omega = 2 * numpy.pi / wl
|
||||||
|
pulse_scale = fdtd.temporal_phasor_scale(source_waveform, omega, dt, offset_steps=0.5)[0]
|
||||||
|
|
||||||
|
j_source = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
|
j_source[1, *(grid.shape // 2)] = epsilon[1, *(grid.shape // 2)]
|
||||||
|
jph = numpy.zeros((1, *epsilon.shape), dtype=complex)
|
||||||
|
eph = numpy.zeros((1, *epsilon.shape), dtype=complex)
|
||||||
|
hph = numpy.zeros((1, *epsilon.shape), dtype=complex)
|
||||||
|
|
||||||
# #### Run a bunch of iterations ####
|
# #### Run a bunch of iterations ####
|
||||||
output_file = h5py.File('simulation_output.h5', 'w')
|
output_file = h5py.File('simulation_output.h5', 'w')
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
for t in range(max_t):
|
for tt in range(max_t):
|
||||||
update_E(e, h, epsilon)
|
update_E(ee, hh, epsilon)
|
||||||
|
|
||||||
e[1][tuple(grid.shape//2)] += field_source(t)
|
# Electric-current injection uses E -= dt * J / epsilon, which is the
|
||||||
update_H(e, h)
|
# same sign convention used by the matching FDFD right-hand side.
|
||||||
|
j_step = pulse_scale * source_waveform[tt] * j_source
|
||||||
|
ee -= dt * j_step / epsilon
|
||||||
|
update_H(ee, hh)
|
||||||
|
|
||||||
avg_rate = (t + 1)/(time.perf_counter() - start))
|
avg_rate = (tt + 1) / (time.perf_counter() - start)
|
||||||
print(f'iteration {t}: average {avg_rate} iterations per sec')
|
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
if t % 20 == 0:
|
if tt % 200 == 0:
|
||||||
r = sum([(f * f * e).sum() for f, e in zip(e, epsilon)])
|
print(f'iteration {tt}: average {avg_rate} iterations per sec')
|
||||||
print('E sum', r)
|
E_energy_sum = (ee.conj() * ee * epsilon).sum().real
|
||||||
|
print(f'{E_energy_sum=}')
|
||||||
|
|
||||||
# Save field slices
|
# Save field slices
|
||||||
if (t % 20 == 0 and (max_t - t <= 1000 or t <= 2000)) or t == max_t-1:
|
if (tt % 20 == 0 and (max_t - tt <= 1000 or tt <= 2000)) or tt == max_t - 1:
|
||||||
print('saving E-field')
|
print(f'saving E-field at iteration {tt}')
|
||||||
for j, f in enumerate(e):
|
output_file[f'/E_t{tt}'] = ee[:, :, :, ee.shape[3] // 2]
|
||||||
output_file['/E{}_t{}'.format('xyz'[j], t)] = f[:, :, round(f.shape[2]/2)]
|
|
||||||
|
fdtd.accumulate_phasor_j(jph, omega, dt, j_step, tt)
|
||||||
|
fdtd.accumulate_phasor_e(eph, omega, dt, ee, tt + 1)
|
||||||
|
fdtd.accumulate_phasor_h(hph, omega, dt, hh, tt + 1)
|
||||||
|
|
||||||
|
Eph = eph[0]
|
||||||
|
Jph = jph[0]
|
||||||
|
b = -1j * omega * Jph
|
||||||
|
dxes_fdfd = copy.deepcopy(dxes)
|
||||||
|
for pp in (-1, +1):
|
||||||
|
for dd in range(3):
|
||||||
|
stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_air ** 2, thickness=pml_thickness)
|
||||||
|
# Compare the extracted phasor to the FDFD operator on the stretched grid,
|
||||||
|
# not the unstretched Yee spacings used by the raw time-domain update.
|
||||||
|
A = e_full(omega=omega, dxes=dxes_fdfd, epsilon=epsilon)
|
||||||
|
residual = norm(A @ vec(Eph) - vec(b)) / norm(vec(b))
|
||||||
|
print(f'FDFD residual is {residual}')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
342
examples/waveguide.py
Normal file
342
examples/waveguide.py
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
"""
|
||||||
|
Example code for guided-wave FDFD and FDTD comparison.
|
||||||
|
|
||||||
|
This example is the reference workflow for:
|
||||||
|
|
||||||
|
1. solving a waveguide port mode,
|
||||||
|
2. turning that mode into a one-sided source and overlap window,
|
||||||
|
3. comparing a direct FDFD solve against a time-domain phasor extracted from FDTD.
|
||||||
|
"""
|
||||||
|
from typing import Callable
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import copy
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import h5py
|
||||||
|
from numpy.linalg import norm
|
||||||
|
|
||||||
|
import gridlock
|
||||||
|
import meanas
|
||||||
|
from meanas import fdtd, fdfd
|
||||||
|
from meanas.fdtd import cpml_params, updates_with_cpml
|
||||||
|
from meanas.fdtd.misc import gaussian_packet
|
||||||
|
|
||||||
|
from meanas.fdmath import vec, unvec, vcfdfield_t, cfdfield_t, fdfield_t, dx_lists_t
|
||||||
|
from meanas.fdfd import waveguide_3d, functional, scpml, operators
|
||||||
|
from meanas.fdfd.solvers import generic as generic_solver
|
||||||
|
from meanas.fdfd.operators import e_full
|
||||||
|
from meanas.fdfd.scpml import stretch_with_scpml
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
for pp in ('matplotlib', 'PIL'):
|
||||||
|
logging.getLogger(pp).setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def pcolor(vv, fig=None, ax=None) -> None:
|
||||||
|
if fig is None:
|
||||||
|
assert ax is None
|
||||||
|
fig, ax = pyplot.subplots()
|
||||||
|
mb = ax.pcolor(vv, cmap='seismic', norm=colors.CenteredNorm())
|
||||||
|
fig.colorbar(mb)
|
||||||
|
ax.set_aspect('equal')
|
||||||
|
|
||||||
|
|
||||||
|
def draw_grid(
|
||||||
|
*,
|
||||||
|
dx: float,
|
||||||
|
pml_thickness: int,
|
||||||
|
n_wg: float = 3.476, # Si index @ 1550
|
||||||
|
n_cladding: float = 1.00, # Air index
|
||||||
|
wg_w: float = 400,
|
||||||
|
wg_th: float = 200,
|
||||||
|
) -> tuple[gridlock.Grid, fdfield_t]:
|
||||||
|
""" Create the grid and draw the device """
|
||||||
|
# Half-dimensions of the simulation grid
|
||||||
|
xyz_max = numpy.array([800, 900, 600]) + (pml_thickness + 2) * dx
|
||||||
|
|
||||||
|
# Coordinates of the edges of the cells.
|
||||||
|
half_edge_coords = [numpy.arange(dx / 2, m + dx / 2, step=dx) for m in xyz_max]
|
||||||
|
edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
|
||||||
|
|
||||||
|
grid = gridlock.Grid(edge_coords)
|
||||||
|
epsilon = grid.allocate(n_cladding**2, dtype=numpy.float32)
|
||||||
|
grid.draw_cuboid(
|
||||||
|
epsilon,
|
||||||
|
x = dict(center=0, span=8e3),
|
||||||
|
y = dict(center=0, span=wg_w),
|
||||||
|
z = dict(center=0, span=wg_th),
|
||||||
|
foreground = n_wg ** 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
return grid, epsilon
|
||||||
|
|
||||||
|
|
||||||
|
def get_waveguide_mode(
|
||||||
|
*,
|
||||||
|
grid: gridlock.Grid,
|
||||||
|
dxes: dx_lists_t,
|
||||||
|
omega: float,
|
||||||
|
epsilon: fdfield_t,
|
||||||
|
) -> tuple[vcfdfield_t, vcfdfield_t]:
|
||||||
|
"""Create a mode source and overlap window for one forward-going port."""
|
||||||
|
dims = numpy.array([[-10, -20, -15],
|
||||||
|
[-10, 20, 15]]) * [[numpy.median(numpy.real(dx)) for dx in dxes[0]]]
|
||||||
|
ind_dims = (grid.pos2ind(dims[0], which_shifts=None).astype(int),
|
||||||
|
grid.pos2ind(dims[1], which_shifts=None).astype(int))
|
||||||
|
wg_args = dict(
|
||||||
|
slices = [slice(i, f+1) for i, f in zip(*ind_dims)],
|
||||||
|
dxes = dxes,
|
||||||
|
axis = 0,
|
||||||
|
polarity = +1,
|
||||||
|
)
|
||||||
|
|
||||||
|
wg_results = waveguide_3d.solve_mode(mode_number=0, omega=omega, epsilon=epsilon, **wg_args)
|
||||||
|
J = waveguide_3d.compute_source(E=wg_results['E'], wavenumber=wg_results['wavenumber'],
|
||||||
|
omega=omega, epsilon=epsilon, **wg_args)
|
||||||
|
|
||||||
|
# compute_overlap_e() returns the normalized upstream overlap window used to
|
||||||
|
# project another field onto this same guided mode.
|
||||||
|
e_overlap = waveguide_3d.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args)
|
||||||
|
return J, e_overlap
|
||||||
|
|
||||||
|
|
||||||
|
def main(
|
||||||
|
*,
|
||||||
|
solver: Callable = generic_solver,
|
||||||
|
dx: float = 40, # discretization (nm / cell)
|
||||||
|
pml_thickness: int = 10, # (number of cells)
|
||||||
|
wl: float = 1550, # Excitation wavelength
|
||||||
|
wg_w: float = 600, # Waveguide width
|
||||||
|
wg_th: float = 220, # Waveguide thickness
|
||||||
|
):
|
||||||
|
omega = 2 * numpy.pi / wl
|
||||||
|
|
||||||
|
grid, epsilon = draw_grid(dx=dx, pml_thickness=pml_thickness)
|
||||||
|
|
||||||
|
# Add SCPML stretching to the FDFD grid; this matches the CPML-backed FDTD
|
||||||
|
# run below so the two solvers see the same absorbing boundary profile.
|
||||||
|
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
||||||
|
for a in (0, 1, 2):
|
||||||
|
for p in (-1, 1):
|
||||||
|
dxes = scpml.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, thickness=pml_thickness)
|
||||||
|
|
||||||
|
|
||||||
|
J, e_overlap = get_waveguide_mode(grid=grid, dxes=dxes, omega=omega, epsilon=epsilon)
|
||||||
|
|
||||||
|
|
||||||
|
pecg = numpy.zeros_like(epsilon)
|
||||||
|
# pecg.draw_cuboid(pecg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
|
||||||
|
# pecg.visualize_isosurface(pecg)
|
||||||
|
|
||||||
|
pmcg = numpy.zeros_like(epsilon)
|
||||||
|
# grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
|
||||||
|
# grid.visualize_isosurface(pmcg)
|
||||||
|
|
||||||
|
|
||||||
|
# ss = (1, slice(None), J.shape[2]//2+6, slice(None))
|
||||||
|
# pcolor(J3[ss].T.imag)
|
||||||
|
# pcolor((numpy.abs(J3).sum(axis=(0, 2)) > 0).astype(float).T)
|
||||||
|
# pyplot.show(block=True)
|
||||||
|
|
||||||
|
E_fdfd = fdfd_solve(
|
||||||
|
omega = omega,
|
||||||
|
dxes = dxes,
|
||||||
|
epsilon = epsilon,
|
||||||
|
J = J,
|
||||||
|
pec = pecg,
|
||||||
|
pmc = pmcg,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Plot results
|
||||||
|
#
|
||||||
|
center = grid.pos2ind([0, 0, 0], None).astype(int)
|
||||||
|
fig, axes = pyplot.subplots(2, 2)
|
||||||
|
pcolor(numpy.real(E[1][center[0], :, :]).T, fig=fig, ax=axes[0, 0])
|
||||||
|
axes[0, 1].plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10))
|
||||||
|
axes[0, 1].grid(alpha=0.6)
|
||||||
|
axes[0, 1].set_ylabel('log10 of field')
|
||||||
|
pcolor(numpy.real(E[1][:, :, center[2]]).T, fig=fig, ax=axes[1, 0])
|
||||||
|
|
||||||
|
def poyntings(E):
|
||||||
|
H = functional.e2h(omega, dxes)(E)
|
||||||
|
poynting = fdtd.poynting(e=E, h=H.conj(), dxes=dxes)
|
||||||
|
cross1 = operators.poynting_e_cross(vec(E), dxes) @ vec(H).conj()
|
||||||
|
cross2 = operators.poynting_h_cross(vec(H), dxes) @ vec(E).conj() * -1
|
||||||
|
s1 = 0.5 * unvec(numpy.real(cross1), grid.shape)
|
||||||
|
s2 = 0.5 * unvec(numpy.real(cross2), grid.shape)
|
||||||
|
s0 = 0.5 * poynting.real
|
||||||
|
# s2 = poynting.imag
|
||||||
|
return s0, s1, s2
|
||||||
|
|
||||||
|
s0x, s1x, s2x = poyntings(E)
|
||||||
|
axes[1, 1].plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.')
|
||||||
|
axes[1, 1].plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.')
|
||||||
|
axes[1, 1].plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.')
|
||||||
|
axes[1, 1].plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x')
|
||||||
|
axes[1, 1].grid(alpha=0.6)
|
||||||
|
axes[1, 1].legend()
|
||||||
|
|
||||||
|
q = []
|
||||||
|
for i in range(-5, 30):
|
||||||
|
e_ovl_rolled = numpy.roll(e_overlap, i, axis=1)
|
||||||
|
q += [numpy.abs(vec(E) @ vec(e_ovl_rolled).conj())]
|
||||||
|
fig, ax = pyplot.subplots()
|
||||||
|
ax.plot(q, marker='.')
|
||||||
|
ax.grid(alpha=0.6)
|
||||||
|
ax.set_title('Overlap with mode')
|
||||||
|
|
||||||
|
logger.info('Average overlap with mode:', sum(q[8:32]) / len(q[8:32]))
|
||||||
|
|
||||||
|
pyplot.show(block=True)
|
||||||
|
|
||||||
|
|
||||||
|
def fdfd_solve(
|
||||||
|
*,
|
||||||
|
omega: float,
|
||||||
|
dxes = dx_lists_t,
|
||||||
|
epsilon: fdfield_t,
|
||||||
|
J: cfdfield_t,
|
||||||
|
pec: fdfield_t,
|
||||||
|
pmc: fdfield_t,
|
||||||
|
) -> cfdfield_t:
|
||||||
|
""" Construct and run the solve """
|
||||||
|
sim_args = dict(
|
||||||
|
omega = omega,
|
||||||
|
dxes = dxes,
|
||||||
|
epsilon = vec(epsilon),
|
||||||
|
pec = vec(pecg),
|
||||||
|
pmc = vec(pmcg),
|
||||||
|
)
|
||||||
|
|
||||||
|
x = solver(J=vec(J), **sim_args)
|
||||||
|
|
||||||
|
b = -1j * omega * vec(J)
|
||||||
|
A = operators.e_full(**sim_args).tocsr()
|
||||||
|
logger.info('Norm of the residual is ', norm(A @ x - b) / norm(b))
|
||||||
|
|
||||||
|
E = unvec(x, epsilon.shape[1:])
|
||||||
|
return E
|
||||||
|
|
||||||
|
|
||||||
|
def main2():
|
||||||
|
dtype = numpy.float32
|
||||||
|
max_t = 3600 # number of timesteps
|
||||||
|
|
||||||
|
dx = 40 # discretization (nm/cell)
|
||||||
|
pml_thickness = 8 # (number of cells)
|
||||||
|
|
||||||
|
wl = 1550 # Excitation wavelength and fwhm
|
||||||
|
dwl = 100
|
||||||
|
|
||||||
|
# Device design parameters
|
||||||
|
xy_size = numpy.array([10, 10])
|
||||||
|
a = 430
|
||||||
|
r = 0.285
|
||||||
|
th = 170
|
||||||
|
|
||||||
|
# refractive indices
|
||||||
|
n_slab = 3.408 # InGaAsP(80, 50) @ 1550nm
|
||||||
|
n_cladding = 1.0 # air
|
||||||
|
|
||||||
|
# Half-dimensions of the simulation grid
|
||||||
|
xy_max = (xy_size + 1) * a * [1, numpy.sqrt(3)/2]
|
||||||
|
z_max = 1.6 * a
|
||||||
|
xyz_max = numpy.hstack((xy_max, z_max)) + pml_thickness * dx
|
||||||
|
|
||||||
|
# Coordinates of the edges of the cells. The fdtd package can only do square grids at the moment.
|
||||||
|
half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max]
|
||||||
|
edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
|
||||||
|
|
||||||
|
# #### Create the grid, mask, and draw the device ####
|
||||||
|
grid = gridlock.Grid(edge_coords)
|
||||||
|
epsilon = grid.allocate(n_cladding ** 2, dtype=dtype)
|
||||||
|
grid.draw_slab(
|
||||||
|
epsilon,
|
||||||
|
slab = dict(axis='z', center=0, span=th),
|
||||||
|
foreground = n_slab ** 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print(f'{grid.shape=}')
|
||||||
|
|
||||||
|
dt = dx * 0.99 / numpy.sqrt(3)
|
||||||
|
ee = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
|
hh = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
|
|
||||||
|
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
||||||
|
|
||||||
|
# PMLs in every direction
|
||||||
|
pml_params = [
|
||||||
|
[cpml_params(axis=dd, polarity=pp, dt=dt, thickness=pml_thickness, epsilon_eff=n_cladding ** 2)
|
||||||
|
for pp in (-1, +1)]
|
||||||
|
for dd in range(3)]
|
||||||
|
update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, dxes=dxes, epsilon=epsilon, dtype=complex)
|
||||||
|
|
||||||
|
# sample_interval = numpy.floor(1 / (2 * 1 / wl * dt)).astype(int)
|
||||||
|
# print(f'Save time interval would be {sample_interval} * dt = {sample_interval * dt:3g}')
|
||||||
|
|
||||||
|
|
||||||
|
# Sample the pulse at the current half-steps and normalize that scalar
|
||||||
|
# waveform so the extracted temporal phasor is exactly 1 at omega.
|
||||||
|
source_phasor, _delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5)
|
||||||
|
aa, cc, ss = source_phasor(numpy.arange(max_t) + 0.5)
|
||||||
|
source_waveform = aa * (cc + 1j * ss)
|
||||||
|
omega = 2 * numpy.pi / wl
|
||||||
|
pulse_scale = fdtd.temporal_phasor_scale(source_waveform, omega, dt, offset_steps=0.5)[0]
|
||||||
|
|
||||||
|
j_source = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
|
j_source[1, *(grid.shape // 2)] = epsilon[1, *(grid.shape // 2)]
|
||||||
|
jph = numpy.zeros((1, *epsilon.shape), dtype=complex)
|
||||||
|
eph = numpy.zeros((1, *epsilon.shape), dtype=complex)
|
||||||
|
hph = numpy.zeros((1, *epsilon.shape), dtype=complex)
|
||||||
|
|
||||||
|
# #### Run a bunch of iterations ####
|
||||||
|
output_file = h5py.File('simulation_output.h5', 'w')
|
||||||
|
start = time.perf_counter()
|
||||||
|
for tt in range(max_t):
|
||||||
|
update_E(ee, hh, epsilon)
|
||||||
|
|
||||||
|
# Electric-current injection uses E -= dt * J / epsilon, which is the
|
||||||
|
# sign convention matched by the FDFD source term -1j * omega * J.
|
||||||
|
j_step = pulse_scale * source_waveform[tt] * j_source
|
||||||
|
ee -= dt * j_step / epsilon
|
||||||
|
update_H(ee, hh)
|
||||||
|
|
||||||
|
avg_rate = (tt + 1) / (time.perf_counter() - start)
|
||||||
|
|
||||||
|
if tt % 200 == 0:
|
||||||
|
print(f'iteration {tt}: average {avg_rate} iterations per sec')
|
||||||
|
E_energy_sum = (ee.conj() * ee * epsilon).sum().real
|
||||||
|
print(f'{E_energy_sum=}')
|
||||||
|
|
||||||
|
# Save field slices
|
||||||
|
if (tt % 20 == 0 and (max_t - tt <= 1000 or tt <= 2000)) or tt == max_t - 1:
|
||||||
|
print(f'saving E-field at iteration {tt}')
|
||||||
|
output_file[f'/E_t{tt}'] = ee[:, :, :, ee.shape[3] // 2]
|
||||||
|
|
||||||
|
fdtd.accumulate_phasor_j(jph, omega, dt, j_step, tt)
|
||||||
|
fdtd.accumulate_phasor_e(eph, omega, dt, ee, tt + 1)
|
||||||
|
fdtd.accumulate_phasor_h(hph, omega, dt, hh, tt + 1)
|
||||||
|
|
||||||
|
Eph = eph[0]
|
||||||
|
Jph = jph[0]
|
||||||
|
b = -1j * omega * Jph
|
||||||
|
dxes_fdfd = copy.deepcopy(dxes)
|
||||||
|
for pp in (-1, +1):
|
||||||
|
for dd in range(3):
|
||||||
|
stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_cladding ** 2, thickness=pml_thickness)
|
||||||
|
# Residuals must be checked on the stretched FDFD grid, because the FDTD run
|
||||||
|
# already includes those same absorbing layers through CPML.
|
||||||
|
A = e_full(omega=omega, dxes=dxes_fdfd, epsilon=epsilon)
|
||||||
|
residual = norm(A @ vec(Eph) - vec(b)) / norm(vec(b))
|
||||||
|
print(f'FDFD residual is {residual}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
219
examples/waveguide_real.py
Normal file
219
examples/waveguide_real.py
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
"""
|
||||||
|
Real-valued straight-waveguide FDTD/FDFD comparison.
|
||||||
|
|
||||||
|
This example shows the user-facing "compare real FDTD against reconstructed real
|
||||||
|
FDFD" workflow:
|
||||||
|
|
||||||
|
1. build a straight waveguide on a uniform Yee grid,
|
||||||
|
2. drive it with a real-valued continuous-wave mode source,
|
||||||
|
3. solve the matching FDFD problem from the analytic source phasor, and
|
||||||
|
4. compare late real monitor slices against `fdtd.reconstruct_real_e/h(...)`.
|
||||||
|
|
||||||
|
Unlike the phasor-based examples, this script does not use extracted phasors as
|
||||||
|
the main output. It is a stricter diagnostic: the comparison target is the raw
|
||||||
|
real field itself, with full-plane, mode-weighted, guided-mode, and orthogonal-
|
||||||
|
residual errors reported. Strong phasor agreement can coexist with visibly
|
||||||
|
larger raw-snapshot error because the latter includes weak nonguided tails on
|
||||||
|
the monitor plane.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from meanas import fdfd, fdtd
|
||||||
|
from meanas.fdfd import functional, scpml, waveguide_3d
|
||||||
|
from meanas.fdmath import vec, unvec
|
||||||
|
|
||||||
|
|
||||||
|
DT = 0.25
|
||||||
|
PERIOD_STEPS = 64
|
||||||
|
OMEGA = 2 * numpy.pi / (PERIOD_STEPS * DT)
|
||||||
|
CPML_THICKNESS = 3
|
||||||
|
SHAPE = (3, 37, 13, 13)
|
||||||
|
SOURCE_SLICES = (slice(5, 6), slice(None), slice(None))
|
||||||
|
MONITOR_SLICES = (slice(30, 31), slice(None), slice(None))
|
||||||
|
WARMUP_PERIODS = 16
|
||||||
|
SOURCE_PHASE = 0.4
|
||||||
|
CORE_SLICES = (slice(None), slice(None), slice(4, 9), slice(4, 9))
|
||||||
|
|
||||||
|
|
||||||
|
def build_uniform_dxes(shape: tuple[int, int, int, int]) -> list[list[numpy.ndarray]]:
|
||||||
|
return [[numpy.ones(shape[axis + 1]) for axis in range(3)] for _ in range(2)]
|
||||||
|
|
||||||
|
|
||||||
|
def build_epsilon(shape: tuple[int, int, int, int]) -> numpy.ndarray:
|
||||||
|
epsilon = numpy.ones(shape, dtype=float)
|
||||||
|
y0 = (shape[2] - 3) // 2
|
||||||
|
z0 = (shape[3] - 3) // 2
|
||||||
|
epsilon[:, :, y0:y0 + 3, z0:z0 + 3] = 12.0
|
||||||
|
return epsilon
|
||||||
|
|
||||||
|
|
||||||
|
def build_stretched_dxes(base_dxes: list[list[numpy.ndarray]]) -> list[list[numpy.ndarray]]:
|
||||||
|
stretched_dxes = [[dx.copy() for dx in group] for group in base_dxes]
|
||||||
|
for axis in (0, 1, 2):
|
||||||
|
for polarity in (-1, 1):
|
||||||
|
stretched_dxes = scpml.stretch_with_scpml(
|
||||||
|
stretched_dxes,
|
||||||
|
axis=axis,
|
||||||
|
polarity=polarity,
|
||||||
|
omega=OMEGA,
|
||||||
|
epsilon_effective=1.0,
|
||||||
|
thickness=CPML_THICKNESS,
|
||||||
|
)
|
||||||
|
return stretched_dxes
|
||||||
|
|
||||||
|
|
||||||
|
def build_cpml_params() -> list[list[dict[str, numpy.ndarray | float]]]:
|
||||||
|
return [
|
||||||
|
[fdtd.cpml_params(axis=axis, polarity=polarity, dt=DT, thickness=CPML_THICKNESS, epsilon_eff=1.0)
|
||||||
|
for polarity in (-1, 1)]
|
||||||
|
for axis in range(3)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def weighted_rel_err(observed: numpy.ndarray, reference: numpy.ndarray, weight: numpy.ndarray) -> float:
|
||||||
|
return numpy.linalg.norm((observed - reference) * weight) / numpy.linalg.norm(reference * weight)
|
||||||
|
|
||||||
|
|
||||||
|
def project_onto_mode(observed: numpy.ndarray, mode: numpy.ndarray) -> tuple[complex, numpy.ndarray, numpy.ndarray]:
|
||||||
|
coefficient = numpy.vdot(mode, observed) / numpy.vdot(mode, mode)
|
||||||
|
guided = coefficient * mode
|
||||||
|
residual = observed - guided
|
||||||
|
return coefficient, guided, residual
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
epsilon = build_epsilon(SHAPE)
|
||||||
|
base_dxes = build_uniform_dxes(SHAPE)
|
||||||
|
stretched_dxes = build_stretched_dxes(base_dxes)
|
||||||
|
|
||||||
|
source_mode = waveguide_3d.solve_mode(
|
||||||
|
0,
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=base_dxes,
|
||||||
|
axis=0,
|
||||||
|
polarity=1,
|
||||||
|
slices=SOURCE_SLICES,
|
||||||
|
epsilon=epsilon,
|
||||||
|
)
|
||||||
|
j_mode = waveguide_3d.compute_source(
|
||||||
|
E=source_mode['E'],
|
||||||
|
wavenumber=source_mode['wavenumber'],
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=base_dxes,
|
||||||
|
axis=0,
|
||||||
|
polarity=1,
|
||||||
|
slices=SOURCE_SLICES,
|
||||||
|
epsilon=epsilon,
|
||||||
|
)
|
||||||
|
# A small global phase aligns the real-valued source with the late-cycle
|
||||||
|
# raw-snapshot diagnostic. The underlying phasor problem is unchanged.
|
||||||
|
j_mode *= numpy.exp(1j * SOURCE_PHASE)
|
||||||
|
monitor_mode = waveguide_3d.solve_mode(
|
||||||
|
0,
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=base_dxes,
|
||||||
|
axis=0,
|
||||||
|
polarity=1,
|
||||||
|
slices=MONITOR_SLICES,
|
||||||
|
epsilon=epsilon,
|
||||||
|
)
|
||||||
|
e_weight = numpy.abs(monitor_mode['E'][:, MONITOR_SLICES[0], :, :])
|
||||||
|
h_weight = numpy.abs(monitor_mode['H'][:, MONITOR_SLICES[0], :, :])
|
||||||
|
e_mode = monitor_mode['E'][:, MONITOR_SLICES[0], :, :]
|
||||||
|
h_mode = monitor_mode['H'][:, MONITOR_SLICES[0], :, :]
|
||||||
|
|
||||||
|
e_fdfd = unvec(
|
||||||
|
fdfd.solvers.generic(
|
||||||
|
J=vec(j_mode),
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=stretched_dxes,
|
||||||
|
epsilon=vec(epsilon),
|
||||||
|
matrix_solver_opts={'atol': 1e-10, 'rtol': 1e-7},
|
||||||
|
),
|
||||||
|
SHAPE[1:],
|
||||||
|
)
|
||||||
|
h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd)
|
||||||
|
|
||||||
|
update_e, update_h = fdtd.updates_with_cpml(
|
||||||
|
cpml_params=build_cpml_params(),
|
||||||
|
dt=DT,
|
||||||
|
dxes=base_dxes,
|
||||||
|
epsilon=epsilon,
|
||||||
|
)
|
||||||
|
|
||||||
|
e_field = numpy.zeros_like(epsilon)
|
||||||
|
h_field = numpy.zeros_like(epsilon)
|
||||||
|
total_steps = (WARMUP_PERIODS + 1) * PERIOD_STEPS
|
||||||
|
e_errors: list[float] = []
|
||||||
|
h_errors: list[float] = []
|
||||||
|
e_core_errors: list[float] = []
|
||||||
|
h_core_errors: list[float] = []
|
||||||
|
e_weighted_errors: list[float] = []
|
||||||
|
h_weighted_errors: list[float] = []
|
||||||
|
e_guided_errors: list[float] = []
|
||||||
|
h_guided_errors: list[float] = []
|
||||||
|
e_residual_errors: list[float] = []
|
||||||
|
h_residual_errors: list[float] = []
|
||||||
|
|
||||||
|
for step in range(total_steps):
|
||||||
|
update_e(e_field, h_field, epsilon)
|
||||||
|
|
||||||
|
# Real-valued FDTD uses the real part of the analytic mode source.
|
||||||
|
t_half = (step + 0.5) * DT
|
||||||
|
j_real = (j_mode.real * numpy.cos(OMEGA * t_half) - j_mode.imag * numpy.sin(OMEGA * t_half)).real
|
||||||
|
e_field -= DT * j_real / epsilon
|
||||||
|
|
||||||
|
update_h(e_field, h_field)
|
||||||
|
|
||||||
|
if step >= total_steps - PERIOD_STEPS // 4:
|
||||||
|
reconstructed_e = fdtd.reconstruct_real_e(
|
||||||
|
e_fdfd[:, MONITOR_SLICES[0], :, :],
|
||||||
|
OMEGA,
|
||||||
|
DT,
|
||||||
|
step + 1,
|
||||||
|
)
|
||||||
|
reconstructed_h = fdtd.reconstruct_real_h(
|
||||||
|
h_fdfd[:, MONITOR_SLICES[0], :, :],
|
||||||
|
OMEGA,
|
||||||
|
DT,
|
||||||
|
step + 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
e_monitor = e_field[:, MONITOR_SLICES[0], :, :]
|
||||||
|
h_monitor = h_field[:, MONITOR_SLICES[0], :, :]
|
||||||
|
e_errors.append(numpy.linalg.norm(e_monitor - reconstructed_e) / numpy.linalg.norm(reconstructed_e))
|
||||||
|
h_errors.append(numpy.linalg.norm(h_monitor - reconstructed_h) / numpy.linalg.norm(reconstructed_h))
|
||||||
|
e_core_errors.append(
|
||||||
|
numpy.linalg.norm(e_monitor[CORE_SLICES] - reconstructed_e[CORE_SLICES])
|
||||||
|
/ numpy.linalg.norm(reconstructed_e[CORE_SLICES]),
|
||||||
|
)
|
||||||
|
h_core_errors.append(
|
||||||
|
numpy.linalg.norm(h_monitor[CORE_SLICES] - reconstructed_h[CORE_SLICES])
|
||||||
|
/ numpy.linalg.norm(reconstructed_h[CORE_SLICES]),
|
||||||
|
)
|
||||||
|
e_weighted_errors.append(weighted_rel_err(e_monitor, reconstructed_e, e_weight))
|
||||||
|
h_weighted_errors.append(weighted_rel_err(h_monitor, reconstructed_h, h_weight))
|
||||||
|
e_guided_coeff, _, e_residual = project_onto_mode(e_monitor, e_mode)
|
||||||
|
e_guided_coeff_ref, _, e_residual_ref = project_onto_mode(reconstructed_e, e_mode)
|
||||||
|
h_guided_coeff, _, h_residual = project_onto_mode(h_monitor, h_mode)
|
||||||
|
h_guided_coeff_ref, _, h_residual_ref = project_onto_mode(reconstructed_h, h_mode)
|
||||||
|
e_guided_errors.append(abs(e_guided_coeff - e_guided_coeff_ref) / abs(e_guided_coeff_ref))
|
||||||
|
h_guided_errors.append(abs(h_guided_coeff - h_guided_coeff_ref) / abs(h_guided_coeff_ref))
|
||||||
|
e_residual_errors.append(numpy.linalg.norm(e_residual - e_residual_ref) / numpy.linalg.norm(e_residual_ref))
|
||||||
|
h_residual_errors.append(numpy.linalg.norm(h_residual - h_residual_ref) / numpy.linalg.norm(h_residual_ref))
|
||||||
|
|
||||||
|
print(f'late-cycle monitor E errors: min={min(e_errors):.4f} max={max(e_errors):.4f}')
|
||||||
|
print(f'late-cycle monitor H errors: min={min(h_errors):.4f} max={max(h_errors):.4f}')
|
||||||
|
print(f'late-cycle core-window E errors: min={min(e_core_errors):.4f} max={max(e_core_errors):.4f}')
|
||||||
|
print(f'late-cycle core-window H errors: min={min(h_core_errors):.4f} max={max(h_core_errors):.4f}')
|
||||||
|
print(f'late-cycle mode-weighted E errors: min={min(e_weighted_errors):.4f} max={max(e_weighted_errors):.4f}')
|
||||||
|
print(f'late-cycle mode-weighted H errors: min={min(h_weighted_errors):.4f} max={max(h_weighted_errors):.4f}')
|
||||||
|
print(f'late-cycle guided-mode E coefficient errors: min={min(e_guided_errors):.4f} max={max(e_guided_errors):.4f}')
|
||||||
|
print(f'late-cycle guided-mode H coefficient errors: min={min(h_guided_errors):.4f} max={max(h_guided_errors):.4f}')
|
||||||
|
print(f'late-cycle orthogonal-residual E errors: min={min(e_residual_errors):.4f} max={max(e_residual_errors):.4f}')
|
||||||
|
print(f'late-cycle orthogonal-residual H errors: min={min(h_residual_errors):.4f} max={max(h_residual_errors):.4f}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
26
make_docs.sh
26
make_docs.sh
|
|
@ -2,18 +2,20 @@
|
||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
cd ~/projects/meanas
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
# Approach 1: pdf to html?
|
DOCS_TMP="$(mktemp -d)"
|
||||||
#pdoc3 --pdf --force --template-dir pdoc_templates -o doc . | \
|
cleanup() {
|
||||||
# pandoc --metadata=title:"meanas" --toc --toc-depth=4 --from=markdown+abbreviations --to=html --output=doc.html --gladtex -s -
|
rm -rf "$DOCS_TMP"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
# Approach 2: pdf to html with gladtex
|
python3 "$ROOT/scripts/prepare_docs_sources.py" "$ROOT/meanas" "$DOCS_TMP"
|
||||||
rm -rf _doc_mathimg
|
|
||||||
pdoc --pdf --force --template-dir pdoc_templates -o doc . > doc.md
|
|
||||||
pandoc --metadata=title:"meanas" --from=markdown+abbreviations --to=html --output=doc.htex --gladtex -s --css pdoc_templates/pdoc.css doc.md
|
|
||||||
gladtex -a -n -d _doc_mathimg -c white -b black doc.htex
|
|
||||||
|
|
||||||
# Approach 3: html with gladtex
|
MKDOCSTRINGS_PYTHON_PATH="$DOCS_TMP" mkdocs build --clean
|
||||||
#pdoc3 --html --force --template-dir pdoc_templates -o doc .
|
|
||||||
#find doc -iname '*.html' -exec gladtex -a -n -d _mathimg -c white {} \;
|
PRINT_PAGE='site/print_page/index.html'
|
||||||
|
if [[ -f "$PRINT_PAGE" ]] && command -v htmlark >/dev/null 2>&1; then
|
||||||
|
htmlark "$PRINT_PAGE" -o site/standalone.html
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
"""
|
"""
|
||||||
Electromagnetic simulation tools
|
Electromagnetic simulation tools
|
||||||
|
|
||||||
See the readme or `import meanas; help(meanas)` for more info.
|
See the tracked examples for end-to-end workflows, and `help(meanas)` for the
|
||||||
|
toolbox overview and API derivations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
__version__ = '0.9'
|
__version__ = '0.12'
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(pathlib.Path(__file__).parent / 'README.md', 'r') as f:
|
readme_path = pathlib.Path(__file__).parent / 'README.md'
|
||||||
|
with readme_path.open('r') as f:
|
||||||
__doc__ = f.read()
|
__doc__ = f.read()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
"""
|
"""
|
||||||
Solvers for eigenvalue / eigenvector problems
|
Solvers for eigenvalue / eigenvector problems
|
||||||
"""
|
"""
|
||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
from numpy.linalg import norm
|
from numpy.linalg import norm
|
||||||
from scipy import sparse # type: ignore
|
from scipy import sparse
|
||||||
import scipy.sparse.linalg as spalg # type: ignore
|
import scipy.sparse.linalg as spalg
|
||||||
|
|
||||||
|
|
||||||
def power_iteration(
|
def power_iteration(
|
||||||
|
|
@ -25,8 +25,9 @@ def power_iteration(
|
||||||
Returns:
|
Returns:
|
||||||
(Largest-magnitude eigenvalue, Corresponding eigenvector estimate)
|
(Largest-magnitude eigenvalue, Corresponding eigenvector estimate)
|
||||||
"""
|
"""
|
||||||
|
rng = numpy.random.default_rng()
|
||||||
if guess_vector is None:
|
if guess_vector is None:
|
||||||
v = numpy.random.rand(operator.shape[0]) + 1j * numpy.random.rand(operator.shape[0])
|
v = rng.random(operator.shape[0]) + 1j * rng.random(operator.shape[0])
|
||||||
else:
|
else:
|
||||||
v = guess_vector
|
v = guess_vector
|
||||||
|
|
||||||
|
|
@ -63,10 +64,10 @@ def rayleigh_quotient_iteration(
|
||||||
(eigenvalues, eigenvectors)
|
(eigenvalues, eigenvectors)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
(operator - sparse.eye(operator.shape[0]))
|
(operator - sparse.eye_array(operator.shape[0]))
|
||||||
|
|
||||||
def shift(eigval: float) -> sparse:
|
def shift(eigval: float) -> sparse.sparray:
|
||||||
return eigval * sparse.eye(operator.shape[0])
|
return eigval * sparse.eye_array(operator.shape[0])
|
||||||
|
|
||||||
if solver is None:
|
if solver is None:
|
||||||
solver = spalg.spsolve
|
solver = spalg.spsolve
|
||||||
|
|
@ -129,12 +130,12 @@ def signed_eigensolve(
|
||||||
|
|
||||||
# Try to combine, use general LinearOperator if we fail
|
# Try to combine, use general LinearOperator if we fail
|
||||||
try:
|
try:
|
||||||
shifted_operator = operator + shift * sparse.eye(operator.shape[0])
|
shifted_operator = operator + shift * sparse.eye_array(operator.shape[0])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
shifted_operator = operator + spalg.LinearOperator(shape=operator.shape,
|
shifted_operator = operator + spalg.LinearOperator(shape=operator.shape,
|
||||||
matvec=lambda v: shift * v)
|
matvec=lambda v: shift * v)
|
||||||
|
|
||||||
shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=50)
|
shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=2 * how_many + 50)
|
||||||
eigenvalues = shifted_eigenvalues - shift
|
eigenvalues = shifted_eigenvalues - shift
|
||||||
|
|
||||||
k = eigenvalues.argsort()
|
k = eigenvalues.argsort()
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@ Submodules:
|
||||||
|
|
||||||
- `operators`, `functional`: General FDFD problem setup.
|
- `operators`, `functional`: General FDFD problem setup.
|
||||||
- `solvers`: Solver interface and reference implementation.
|
- `solvers`: Solver interface and reference implementation.
|
||||||
- `scpml`: Stretched-coordinate perfectly matched layer (scpml) boundary conditions
|
- `scpml`: Stretched-coordinate perfectly matched layer (SCPML) boundary conditions.
|
||||||
- `waveguide_2d`: Operators and mode-solver for waveguides with constant cross-section.
|
- `waveguide_2d`: Operators and mode-solver for waveguides with constant cross-section.
|
||||||
- `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D.
|
- `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D,
|
||||||
|
including mode-source and overlap-window construction.
|
||||||
|
- `farfield`, `bloch`, `eme`: specialized helper modules for near/far transforms,
|
||||||
|
Bloch-periodic problems, and eigenmode expansion.
|
||||||
|
|
||||||
|
|
||||||
================================================================
|
================================================================
|
||||||
|
|
@ -86,10 +89,13 @@ $$
|
||||||
-\omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \omega \tilde{J}_{\vec{r}} \\
|
-\omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \omega \tilde{J}_{\vec{r}} \\
|
||||||
$$
|
$$
|
||||||
|
|
||||||
# TODO FDFD?
|
|
||||||
# TODO PML
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from . import solvers, operators, functional, scpml, waveguide_2d, waveguide_3d
|
from . import (
|
||||||
|
solvers as solvers,
|
||||||
|
operators as operators,
|
||||||
|
functional as functional,
|
||||||
|
scpml as scpml,
|
||||||
|
waveguide_2d as waveguide_2d,
|
||||||
|
waveguide_3d as waveguide_3d,
|
||||||
|
)
|
||||||
# from . import farfield, bloch TODO
|
# from . import farfield, bloch TODO
|
||||||
|
|
|
||||||
|
|
@ -94,18 +94,19 @@ This module contains functions for generating and solving the
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Callable, Any, cast, Sequence
|
from typing import Any, cast
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
import logging
|
import logging
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi, real, trace
|
from numpy import pi, real, trace
|
||||||
from numpy.fft import fftfreq
|
from numpy.fft import fftfreq
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
import scipy # type: ignore
|
import scipy
|
||||||
import scipy.optimize # type: ignore
|
import scipy.optimize
|
||||||
from scipy.linalg import norm # type: ignore
|
from scipy.linalg import norm
|
||||||
import scipy.sparse.linalg as spalg # type: ignore
|
import scipy.sparse.linalg as spalg
|
||||||
|
|
||||||
from ..fdmath import fdfield_t, cfdfield_t
|
from ..fdmath import fdfield, cfdfield, cfdfield_t
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -114,7 +115,6 @@ logger = logging.getLogger(__name__)
|
||||||
try:
|
try:
|
||||||
import pyfftw.interfaces.numpy_fft # type: ignore
|
import pyfftw.interfaces.numpy_fft # type: ignore
|
||||||
import pyfftw.interfaces # type: ignore
|
import pyfftw.interfaces # type: ignore
|
||||||
import multiprocessing
|
|
||||||
logger.info('Using pyfftw')
|
logger.info('Using pyfftw')
|
||||||
|
|
||||||
pyfftw.interfaces.cache.enable()
|
pyfftw.interfaces.cache.enable()
|
||||||
|
|
@ -136,6 +136,14 @@ except ImportError:
|
||||||
logger.info('Using numpy fft')
|
logger.info('Using numpy fft')
|
||||||
|
|
||||||
|
|
||||||
|
def _assemble_hmn_vector(
|
||||||
|
h_m: NDArray[numpy.complex128],
|
||||||
|
h_n: NDArray[numpy.complex128],
|
||||||
|
) -> NDArray[numpy.complex128]:
|
||||||
|
stacked = numpy.concatenate((numpy.ravel(h_m), numpy.ravel(h_n)))
|
||||||
|
return stacked[:, None]
|
||||||
|
|
||||||
|
|
||||||
def generate_kmn(
|
def generate_kmn(
|
||||||
k0: ArrayLike,
|
k0: ArrayLike,
|
||||||
G_matrix: ArrayLike,
|
G_matrix: ArrayLike,
|
||||||
|
|
@ -155,7 +163,7 @@ def generate_kmn(
|
||||||
All are given in the xyz basis (e.g. `|k|[0,0,0] = norm(G_matrix @ k0)`).
|
All are given in the xyz basis (e.g. `|k|[0,0,0] = norm(G_matrix @ k0)`).
|
||||||
"""
|
"""
|
||||||
k0 = numpy.array(k0)
|
k0 = numpy.array(k0)
|
||||||
G_matrix = numpy.array(G_matrix, copy=False)
|
G_matrix = numpy.asarray(G_matrix)
|
||||||
|
|
||||||
Gi_grids = numpy.array(numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij'))
|
Gi_grids = numpy.array(numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij'))
|
||||||
Gi = numpy.moveaxis(Gi_grids, 0, -1)
|
Gi = numpy.moveaxis(Gi_grids, 0, -1)
|
||||||
|
|
@ -183,8 +191,8 @@ def generate_kmn(
|
||||||
def maxwell_operator(
|
def maxwell_operator(
|
||||||
k0: ArrayLike,
|
k0: ArrayLike,
|
||||||
G_matrix: ArrayLike,
|
G_matrix: ArrayLike,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None
|
mu: fdfield | None = None
|
||||||
) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
|
) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
|
||||||
"""
|
"""
|
||||||
Generate the Maxwell operator
|
Generate the Maxwell operator
|
||||||
|
|
@ -232,13 +240,13 @@ def maxwell_operator(
|
||||||
Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)), returned
|
Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)), returned
|
||||||
and overwritten in-place of `h`.
|
and overwritten in-place of `h`.
|
||||||
"""
|
"""
|
||||||
hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
|
hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
|
||||||
|
|
||||||
#{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis
|
#{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis
|
||||||
|
|
||||||
# cross product and transform into xyz basis
|
# cross product and transform into xyz basis
|
||||||
d_xyz = (n * hin_m
|
d_xyz = (n * hin_m
|
||||||
- m * hin_n) * k_mag # noqa: E128
|
- m * hin_n) * k_mag
|
||||||
|
|
||||||
# divide by epsilon
|
# divide by epsilon
|
||||||
temp = ifftn(d_xyz, axes=range(3)) # reuses d_xyz if using pyfftw
|
temp = ifftn(d_xyz, axes=range(3)) # reuses d_xyz if using pyfftw
|
||||||
|
|
@ -253,8 +261,8 @@ def maxwell_operator(
|
||||||
h_m, h_n = b_m, b_n
|
h_m, h_n = b_m, b_n
|
||||||
else:
|
else:
|
||||||
# transform from mn to xyz
|
# transform from mn to xyz
|
||||||
b_xyz = (m * b_m[:, :, :, None]
|
b_xyz = (m * b_m
|
||||||
+ n * b_n[:, :, :, None]) # noqa: E128
|
+ n * b_n) # noqa
|
||||||
|
|
||||||
# divide by mu
|
# divide by mu
|
||||||
temp = ifftn(b_xyz, axes=range(3))
|
temp = ifftn(b_xyz, axes=range(3))
|
||||||
|
|
@ -265,10 +273,7 @@ def maxwell_operator(
|
||||||
h_m = numpy.sum(h_xyz * m, axis=3)
|
h_m = numpy.sum(h_xyz * m, axis=3)
|
||||||
h_n = numpy.sum(h_xyz * n, axis=3)
|
h_n = numpy.sum(h_xyz * n, axis=3)
|
||||||
|
|
||||||
h.shape = (h.size,)
|
return _assemble_hmn_vector(h_m, h_n)
|
||||||
h = numpy.concatenate((h_m.ravel(), h_n.ravel()), axis=None, out=h) # ravel and merge
|
|
||||||
h.shape = (h.size, 1)
|
|
||||||
return h
|
|
||||||
|
|
||||||
return operator
|
return operator
|
||||||
|
|
||||||
|
|
@ -276,7 +281,7 @@ def maxwell_operator(
|
||||||
def hmn_2_exyz(
|
def hmn_2_exyz(
|
||||||
k0: ArrayLike,
|
k0: ArrayLike,
|
||||||
G_matrix: ArrayLike,
|
G_matrix: ArrayLike,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
|
) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
|
||||||
"""
|
"""
|
||||||
Generate an operator which converts a vectorized spatial-frequency-space
|
Generate an operator which converts a vectorized spatial-frequency-space
|
||||||
|
|
@ -303,12 +308,13 @@ def hmn_2_exyz(
|
||||||
k_mag, m, n = generate_kmn(k0, G_matrix, shape)
|
k_mag, m, n = generate_kmn(k0, G_matrix, shape)
|
||||||
|
|
||||||
def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
|
def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
|
||||||
hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
|
hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
|
||||||
d_xyz = (n * hin_m
|
d_xyz = (n * hin_m
|
||||||
- m * hin_n) * k_mag # noqa: E128
|
- m * hin_n) * k_mag
|
||||||
|
|
||||||
# divide by epsilon
|
# divide by epsilon
|
||||||
return numpy.array([ei for ei in numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)]) # TODO avoid copy
|
exyz = numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)
|
||||||
|
return cfdfield_t(exyz)
|
||||||
|
|
||||||
return operator
|
return operator
|
||||||
|
|
||||||
|
|
@ -316,7 +322,7 @@ def hmn_2_exyz(
|
||||||
def hmn_2_hxyz(
|
def hmn_2_hxyz(
|
||||||
k0: ArrayLike,
|
k0: ArrayLike,
|
||||||
G_matrix: ArrayLike,
|
G_matrix: ArrayLike,
|
||||||
epsilon: fdfield_t
|
epsilon: fdfield,
|
||||||
) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
|
) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
|
||||||
"""
|
"""
|
||||||
Generate an operator which converts a vectorized spatial-frequency-space
|
Generate an operator which converts a vectorized spatial-frequency-space
|
||||||
|
|
@ -341,10 +347,10 @@ def hmn_2_hxyz(
|
||||||
_k_mag, m, n = generate_kmn(k0, G_matrix, shape)
|
_k_mag, m, n = generate_kmn(k0, G_matrix, shape)
|
||||||
|
|
||||||
def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
|
def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
|
||||||
hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
|
hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
|
||||||
h_xyz = (m * hin_m
|
h_xyz = (m * hin_m
|
||||||
+ n * hin_n) # noqa: E128
|
+ n * hin_n)
|
||||||
return numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)])
|
return cfdfield_t(numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)]))
|
||||||
|
|
||||||
return operator
|
return operator
|
||||||
|
|
||||||
|
|
@ -352,8 +358,8 @@ def hmn_2_hxyz(
|
||||||
def inverse_maxwell_operator_approx(
|
def inverse_maxwell_operator_approx(
|
||||||
k0: ArrayLike,
|
k0: ArrayLike,
|
||||||
G_matrix: ArrayLike,
|
G_matrix: ArrayLike,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
|
) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
|
||||||
"""
|
"""
|
||||||
Generate an approximate inverse of the Maxwell operator,
|
Generate an approximate inverse of the Maxwell operator,
|
||||||
|
|
@ -394,7 +400,7 @@ def inverse_maxwell_operator_approx(
|
||||||
Returns:
|
Returns:
|
||||||
Raveled ik x conv(eps_k, ik x conv(mu_k, h_mn))
|
Raveled ik x conv(eps_k, ik x conv(mu_k, h_mn))
|
||||||
"""
|
"""
|
||||||
hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
|
hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
|
||||||
|
|
||||||
#{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis
|
#{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis
|
||||||
|
|
||||||
|
|
@ -402,8 +408,8 @@ def inverse_maxwell_operator_approx(
|
||||||
b_m, b_n = hin_m, hin_n
|
b_m, b_n = hin_m, hin_n
|
||||||
else:
|
else:
|
||||||
# transform from mn to xyz
|
# transform from mn to xyz
|
||||||
h_xyz = (m * hin_m[:, :, :, None]
|
h_xyz = (m * hin_m
|
||||||
+ n * hin_n[:, :, :, None]) # noqa: E128
|
+ n * hin_n) # noqa
|
||||||
|
|
||||||
# multiply by mu
|
# multiply by mu
|
||||||
temp = ifftn(h_xyz, axes=range(3))
|
temp = ifftn(h_xyz, axes=range(3))
|
||||||
|
|
@ -411,12 +417,12 @@ def inverse_maxwell_operator_approx(
|
||||||
b_xyz = fftn(temp, axes=range(3))
|
b_xyz = fftn(temp, axes=range(3))
|
||||||
|
|
||||||
# transform back to mn
|
# transform back to mn
|
||||||
b_m = numpy.sum(b_xyz * m, axis=3)
|
b_m = numpy.sum(b_xyz * m, axis=3, keepdims=True)
|
||||||
b_n = numpy.sum(b_xyz * n, axis=3)
|
b_n = numpy.sum(b_xyz * n, axis=3, keepdims=True)
|
||||||
|
|
||||||
# cross product and transform into xyz basis
|
# cross product and transform into xyz basis
|
||||||
e_xyz = (n * b_m
|
e_xyz = (n * b_m
|
||||||
- m * b_n) / k_mag # noqa: E128
|
- m * b_n) / k_mag
|
||||||
|
|
||||||
# multiply by epsilon
|
# multiply by epsilon
|
||||||
temp = ifftn(e_xyz, axes=range(3))
|
temp = ifftn(e_xyz, axes=range(3))
|
||||||
|
|
@ -427,10 +433,7 @@ def inverse_maxwell_operator_approx(
|
||||||
h_m = numpy.sum(d_xyz * n, axis=3, keepdims=True) / +k_mag
|
h_m = numpy.sum(d_xyz * n, axis=3, keepdims=True) / +k_mag
|
||||||
h_n = numpy.sum(d_xyz * m, axis=3, keepdims=True) / -k_mag
|
h_n = numpy.sum(d_xyz * m, axis=3, keepdims=True) / -k_mag
|
||||||
|
|
||||||
h.shape = (h.size,)
|
return _assemble_hmn_vector(h_m, h_n)
|
||||||
h = numpy.concatenate((h_m, h_n), axis=None, out=h)
|
|
||||||
h.shape = (h.size, 1)
|
|
||||||
return h
|
|
||||||
|
|
||||||
return operator
|
return operator
|
||||||
|
|
||||||
|
|
@ -440,15 +443,15 @@ def find_k(
|
||||||
tolerance: float,
|
tolerance: float,
|
||||||
direction: ArrayLike,
|
direction: ArrayLike,
|
||||||
G_matrix: ArrayLike,
|
G_matrix: ArrayLike,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
band: int = 0,
|
band: int = 0,
|
||||||
k_bounds: tuple[float, float] = (0, 0.5),
|
k_bounds: tuple[float, float] = (0, 0.5),
|
||||||
k_guess: float | None = None,
|
k_guess: float | None = None,
|
||||||
solve_callback: Callable[..., None] | None = None,
|
solve_callback: Callable[..., None] | None = None,
|
||||||
iter_callback: Callable[..., None] | None = None,
|
iter_callback: Callable[..., None] | None = None,
|
||||||
v0: NDArray[numpy.complex128] | None = None,
|
v0: NDArray[numpy.complex128] | None = None,
|
||||||
) -> tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
|
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
|
||||||
"""
|
"""
|
||||||
Search for a bloch vector that has a given frequency.
|
Search for a bloch vector that has a given frequency.
|
||||||
|
|
||||||
|
|
@ -471,7 +474,7 @@ def find_k(
|
||||||
`(k, actual_frequency, eigenvalues, eigenvectors)`
|
`(k, actual_frequency, eigenvalues, eigenvectors)`
|
||||||
The found k-vector and its frequency, along with all eigenvalues and eigenvectors.
|
The found k-vector and its frequency, along with all eigenvalues and eigenvectors.
|
||||||
"""
|
"""
|
||||||
direction = numpy.array(direction) / norm(direction)
|
direction = numpy.array(direction) / norm(direction) # type: ignore[operator]
|
||||||
|
|
||||||
k_bounds = tuple(sorted(k_bounds)) # type: ignore # we know the length already...
|
k_bounds = tuple(sorted(k_bounds)) # type: ignore # we know the length already...
|
||||||
assert len(k_bounds) == 2
|
assert len(k_bounds) == 2
|
||||||
|
|
@ -493,23 +496,23 @@ def find_k(
|
||||||
|
|
||||||
res = scipy.optimize.minimize_scalar(
|
res = scipy.optimize.minimize_scalar(
|
||||||
lambda x: abs(get_f(x, band) - frequency),
|
lambda x: abs(get_f(x, band) - frequency),
|
||||||
k_guess,
|
method='bounded',
|
||||||
method='Bounded',
|
|
||||||
bounds=k_bounds,
|
bounds=k_bounds,
|
||||||
options={'xatol': abs(tolerance)},
|
options={'xatol': abs(tolerance)},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert n is not None
|
assert n is not None
|
||||||
assert v is not None
|
assert v is not None
|
||||||
return float(res.x * direction), float(res.fun + frequency), n, v
|
actual_frequency = get_f(float(res.x), band)
|
||||||
|
return direction * float(res.x), float(actual_frequency), n, v # type: ignore[operator,return-value]
|
||||||
|
|
||||||
|
|
||||||
def eigsolve(
|
def eigsolve(
|
||||||
num_modes: int,
|
num_modes: int,
|
||||||
k0: ArrayLike,
|
k0: ArrayLike,
|
||||||
G_matrix: ArrayLike,
|
G_matrix: ArrayLike,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
tolerance: float = 1e-7,
|
tolerance: float = 1e-7,
|
||||||
max_iters: int = 10000,
|
max_iters: int = 10000,
|
||||||
reset_iters: int = 100,
|
reset_iters: int = 100,
|
||||||
|
|
@ -538,7 +541,7 @@ def eigsolve(
|
||||||
`(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the
|
`(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the
|
||||||
vector `eigenvectors[i, :]`
|
vector `eigenvectors[i, :]`
|
||||||
"""
|
"""
|
||||||
k0 = numpy.array(k0, copy=False)
|
k0 = numpy.asarray(k0)
|
||||||
|
|
||||||
h_size = 2 * epsilon[0].size
|
h_size = 2 * epsilon[0].size
|
||||||
|
|
||||||
|
|
@ -561,11 +564,12 @@ def eigsolve(
|
||||||
prev_theta = 0.5
|
prev_theta = 0.5
|
||||||
D = numpy.zeros(shape=y_shape, dtype=complex)
|
D = numpy.zeros(shape=y_shape, dtype=complex)
|
||||||
|
|
||||||
|
rng = numpy.random.default_rng()
|
||||||
Z: NDArray[numpy.complex128]
|
Z: NDArray[numpy.complex128]
|
||||||
if y0 is None:
|
if y0 is None:
|
||||||
Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape)
|
Z = rng.random(y_shape) + 1j * rng.random(y_shape)
|
||||||
else:
|
else:
|
||||||
Z = numpy.array(y0, copy=False).T
|
Z = numpy.asarray(y0).T
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
Z *= num_modes / norm(Z)
|
Z *= num_modes / norm(Z)
|
||||||
|
|
@ -573,7 +577,7 @@ def eigsolve(
|
||||||
try:
|
try:
|
||||||
U = numpy.linalg.inv(ZtZ)
|
U = numpy.linalg.inv(ZtZ)
|
||||||
except numpy.linalg.LinAlgError:
|
except numpy.linalg.LinAlgError:
|
||||||
Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape)
|
Z = rng.random(y_shape) + 1j * rng.random(y_shape)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
trace_U = real(trace(U))
|
trace_U = real(trace(U))
|
||||||
|
|
@ -646,17 +650,16 @@ def eigsolve(
|
||||||
|
|
||||||
Qi_memo: list[float | None] = [None, None]
|
Qi_memo: list[float | None] = [None, None]
|
||||||
|
|
||||||
def Qi_func(theta: float) -> float:
|
def Qi_func(theta: float, Qi_memo=Qi_memo, ZtZ=ZtZ, DtD=DtD, symZtD=symZtD) -> float: # noqa: ANN001
|
||||||
nonlocal Qi_memo
|
|
||||||
if Qi_memo[0] == theta:
|
if Qi_memo[0] == theta:
|
||||||
return cast(float, Qi_memo[1])
|
return cast('float', Qi_memo[1])
|
||||||
|
|
||||||
c = numpy.cos(theta)
|
c = numpy.cos(theta)
|
||||||
s = numpy.sin(theta)
|
s = numpy.sin(theta)
|
||||||
Q = c * c * ZtZ + s * s * DtD + 2 * s * c * symZtD
|
Q = c * c * ZtZ + s * s * DtD + 2 * s * c * symZtD
|
||||||
try:
|
try:
|
||||||
Qi = numpy.linalg.inv(Q)
|
Qi = numpy.linalg.inv(Q)
|
||||||
except numpy.linalg.LinAlgError:
|
except numpy.linalg.LinAlgError as err:
|
||||||
logger.info('taylor Qi')
|
logger.info('taylor Qi')
|
||||||
# if c or s small, taylor expand
|
# if c or s small, taylor expand
|
||||||
if c < 1e-4 * s and c != 0:
|
if c < 1e-4 * s and c != 0:
|
||||||
|
|
@ -666,12 +669,12 @@ def eigsolve(
|
||||||
ZtZi = numpy.linalg.inv(ZtZ)
|
ZtZi = numpy.linalg.inv(ZtZ)
|
||||||
Qi = ZtZi / (c * c) - 2 * s / (c * c * c) * (ZtZi @ (ZtZi @ symZtD).conj().T)
|
Qi = ZtZi / (c * c) - 2 * s / (c * c * c) * (ZtZi @ (ZtZi @ symZtD).conj().T)
|
||||||
else:
|
else:
|
||||||
raise Exception('Inexplicable singularity in trace_func')
|
raise Exception('Inexplicable singularity in trace_func') from err
|
||||||
Qi_memo[0] = theta
|
Qi_memo[0] = theta
|
||||||
Qi_memo[1] = cast(float, Qi)
|
Qi_memo[1] = cast('float', Qi)
|
||||||
return cast(float, Qi)
|
return cast('float', Qi)
|
||||||
|
|
||||||
def trace_func(theta: float) -> float:
|
def trace_func(theta: float, ZtAZ=ZtAZ, DtAD=DtAD, symZtAD=symZtAD) -> float: # noqa: ANN001
|
||||||
c = numpy.cos(theta)
|
c = numpy.cos(theta)
|
||||||
s = numpy.sin(theta)
|
s = numpy.sin(theta)
|
||||||
Qi = Qi_func(theta)
|
Qi = Qi_func(theta)
|
||||||
|
|
@ -680,7 +683,16 @@ def eigsolve(
|
||||||
return numpy.abs(trace)
|
return numpy.abs(trace)
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
def trace_deriv(theta):
|
def trace_deriv(
|
||||||
|
theta: float,
|
||||||
|
sgn: int = sgn,
|
||||||
|
ZtAZ=ZtAZ, # noqa: ANN001
|
||||||
|
DtAD=DtAD, # noqa: ANN001
|
||||||
|
symZtD=symZtD, # noqa: ANN001
|
||||||
|
symZtAD=symZtAD, # noqa: ANN001
|
||||||
|
ZtZ=ZtZ, # noqa: ANN001
|
||||||
|
DtD=DtD, # noqa: ANN001
|
||||||
|
) -> float:
|
||||||
Qi = Qi_func(theta)
|
Qi = Qi_func(theta)
|
||||||
c2 = numpy.cos(2 * theta)
|
c2 = numpy.cos(2 * theta)
|
||||||
s2 = numpy.sin(2 * theta)
|
s2 = numpy.sin(2 * theta)
|
||||||
|
|
@ -722,7 +734,12 @@ def eigsolve(
|
||||||
amax=pi,
|
amax=pi,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = scipy.optimize.minimize_scalar(trace_func, bounds=(0, pi), tol=tolerance)
|
result = scipy.optimize.minimize_scalar(
|
||||||
|
trace_func,
|
||||||
|
method='bounded',
|
||||||
|
bounds=(0, pi),
|
||||||
|
options={'xatol': tolerance},
|
||||||
|
)
|
||||||
new_E = result.fun
|
new_E = result.fun
|
||||||
theta = result.x
|
theta = result.x
|
||||||
|
|
||||||
|
|
@ -751,7 +768,7 @@ def eigsolve(
|
||||||
v = eigvecs[:, i]
|
v = eigvecs[:, i]
|
||||||
n = eigvals[i]
|
n = eigvals[i]
|
||||||
v /= norm(v)
|
v /= norm(v)
|
||||||
Av = (scipy_op @ v.copy())[:, 0]
|
Av = numpy.asarray(scipy_op @ v.copy()).reshape(-1)
|
||||||
eigness = norm(Av - (v.conj() @ Av) * v)
|
eigness = norm(Av - (v.conj() @ Av) * v)
|
||||||
f = numpy.sqrt(-numpy.real(n))
|
f = numpy.sqrt(-numpy.real(n))
|
||||||
df = numpy.sqrt(-numpy.real(n) + eigness)
|
df = numpy.sqrt(-numpy.real(n) + eigness)
|
||||||
|
|
@ -799,3 +816,62 @@ def _rtrace_AtB(
|
||||||
def _symmetrize(A: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]:
|
def _symmetrize(A: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]:
|
||||||
return (A + A.conj().T) * 0.5
|
return (A + A.conj().T) * 0.5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def inner_product(
|
||||||
|
eL: cfdfield,
|
||||||
|
hL: cfdfield,
|
||||||
|
eR: cfdfield,
|
||||||
|
hR: cfdfield,
|
||||||
|
) -> complex:
|
||||||
|
# assumes x-axis propagation
|
||||||
|
|
||||||
|
assert numpy.array_equal(eR.shape, hR.shape)
|
||||||
|
assert numpy.array_equal(eL.shape, hL.shape)
|
||||||
|
assert numpy.array_equal(eR.shape, eL.shape)
|
||||||
|
|
||||||
|
# Cross product, times 2 since it's <p | n>, then divide by 4. # TODO might want to abs() this?
|
||||||
|
norm2R = (eR[1] * hR[2] - eR[2] * hR[1]).sum() / 2
|
||||||
|
norm2L = (eL[1] * hL[2] - eL[2] * hL[1]).sum() / 2
|
||||||
|
|
||||||
|
# eRxhR_x = numpy.cross(eR.reshape(3, -1), hR.reshape(3, -1), axis=0).reshape(eR.shape)[0] / normR
|
||||||
|
# logger.info(f'power {eRxhR_x.sum() / 2})
|
||||||
|
|
||||||
|
eR_norm = eR / numpy.sqrt(abs(norm2R))
|
||||||
|
hR_norm = hR / numpy.sqrt(abs(norm2R))
|
||||||
|
eL_norm = eL / numpy.sqrt(abs(norm2L))
|
||||||
|
hL_norm = hL / numpy.sqrt(abs(norm2L))
|
||||||
|
|
||||||
|
# (eR x hL)[0] and (eL x hR)[0]
|
||||||
|
eRxhL_x = eR_norm[1] * hL_norm[2] - eR_norm[2] * hL_norm[1]
|
||||||
|
eLxhR_x = eL_norm[1] * hR_norm[2] - eL_norm[2] * hR_norm[1]
|
||||||
|
|
||||||
|
#return 1j * (eRxhL_x - eLxhR_x).sum() / numpy.sqrt(norm2R * norm2L)
|
||||||
|
#return (eRxhL_x.sum() - eLxhR_x.sum()) / numpy.sqrt(norm2R * norm2L)
|
||||||
|
return eLxhR_x.sum() - eRxhL_x.sum()
|
||||||
|
|
||||||
|
|
||||||
|
def trq(
|
||||||
|
eI: cfdfield,
|
||||||
|
hI: cfdfield,
|
||||||
|
eO: cfdfield,
|
||||||
|
hO: cfdfield,
|
||||||
|
) -> tuple[complex, complex]:
|
||||||
|
pp = inner_product(eO, hO, eI, hI)
|
||||||
|
pn = inner_product(eO, hO, eI, -hI)
|
||||||
|
np = inner_product(eO, -hO, eI, hI)
|
||||||
|
nn = inner_product(eO, -hO, eI, -hI)
|
||||||
|
|
||||||
|
assert numpy.allclose(pp, -nn, atol=1e-12, rtol=1e-12)
|
||||||
|
assert numpy.allclose(pn, -np, atol=1e-12, rtol=1e-12)
|
||||||
|
|
||||||
|
logger.info(f'''
|
||||||
|
{pp=:4g} {pn=:4g}
|
||||||
|
{nn=:4g} {np=:4g}
|
||||||
|
{nn * pp / pn=:4g} {-np=:4g}
|
||||||
|
''')
|
||||||
|
|
||||||
|
r = -pp / pn # -<Pp|Bp>/<Pn/Bp> = -(-pp) / (-pn)
|
||||||
|
t = (np - nn * pp / pn) / 4
|
||||||
|
|
||||||
|
return t, r
|
||||||
|
|
|
||||||
190
meanas/fdfd/eme.py
Normal file
190
meanas/fdfd/eme.py
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
"""
|
||||||
|
Low-level mode-matching helpers for waveguide / EME workflows.
|
||||||
|
|
||||||
|
These helpers operate on already-solved and already-normalized port fields.
|
||||||
|
They do not build geometries or solve modes themselves; downstream users are
|
||||||
|
expected to supply compatible `(E, H)` modal field pairs from
|
||||||
|
`waveguide_2d`, `waveguide_3d`, or `waveguide_cyl`.
|
||||||
|
|
||||||
|
The returned matrices follow the usual port ordering:
|
||||||
|
|
||||||
|
- `get_tr(...)` returns `(T, R)` for left-incident modes.
|
||||||
|
- `get_abcd(...)` returns the 2-port block transfer matrix built from the two
|
||||||
|
directional `T/R` solves.
|
||||||
|
- `get_s(...)` returns the full block scattering matrix
|
||||||
|
`[[R12, T12], [T21, R21]]`.
|
||||||
|
|
||||||
|
This module is intentionally a thin library layer rather than an integrated
|
||||||
|
simulation suite. It provides the overlap algebra that downstream users can
|
||||||
|
compose into larger workflows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
from scipy import sparse
|
||||||
|
|
||||||
|
from ..fdmath import dx_lists2_t, vcfdfield2
|
||||||
|
from .waveguide_2d import inner_product
|
||||||
|
|
||||||
|
type wavenumber_seq = Sequence[complex] | NDArray[numpy.complexfloating] | NDArray[numpy.floating]
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_port_modes(
|
||||||
|
name: str,
|
||||||
|
ehs: Sequence[Sequence[vcfdfield2]],
|
||||||
|
wavenumbers: wavenumber_seq,
|
||||||
|
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
||||||
|
if len(ehs) != len(wavenumbers):
|
||||||
|
raise ValueError(f'{name} mode list and wavenumber list must have the same length')
|
||||||
|
if not ehs:
|
||||||
|
raise ValueError(f'{name} must contain at least one mode')
|
||||||
|
|
||||||
|
e_shape: tuple[int, ...] | None = None
|
||||||
|
h_shape: tuple[int, ...] | None = None
|
||||||
|
for index, mode in enumerate(ehs):
|
||||||
|
if len(mode) != 2:
|
||||||
|
raise ValueError(f'{name}[{index}] must be a 2-tuple of (E, H) modal fields')
|
||||||
|
e_field, h_field = mode
|
||||||
|
mode_e_shape = numpy.shape(e_field)
|
||||||
|
mode_h_shape = numpy.shape(h_field)
|
||||||
|
if mode_e_shape != mode_h_shape:
|
||||||
|
raise ValueError(f'{name}[{index}] has mismatched E/H field shapes')
|
||||||
|
if e_shape is None:
|
||||||
|
e_shape = mode_e_shape
|
||||||
|
h_shape = mode_h_shape
|
||||||
|
elif mode_e_shape != e_shape or mode_h_shape != h_shape:
|
||||||
|
raise ValueError(f'{name} modal fields must all share the same shape')
|
||||||
|
|
||||||
|
assert e_shape is not None
|
||||||
|
assert h_shape is not None
|
||||||
|
return e_shape, h_shape
|
||||||
|
|
||||||
|
|
||||||
|
def get_tr(
|
||||||
|
ehLs: Sequence[Sequence[vcfdfield2]],
|
||||||
|
wavenumbers_L: wavenumber_seq,
|
||||||
|
ehRs: Sequence[Sequence[vcfdfield2]],
|
||||||
|
wavenumbers_R: wavenumber_seq,
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
|
||||||
|
"""
|
||||||
|
Compute left-incident transmission and reflection matrices.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ehLs: Left-port modes as `(E, H)` field pairs.
|
||||||
|
wavenumbers_L: Propagation constants for `ehLs`.
|
||||||
|
ehRs: Right-port modes as `(E, H)` field pairs.
|
||||||
|
wavenumbers_R: Propagation constants for `ehRs`.
|
||||||
|
dxes: Two-dimensional Yee-cell edge lengths for the shared port plane.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`(T12, R12)` where columns index left-incident modes and rows index
|
||||||
|
outgoing right-going / left-going modes respectively.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the port mode lists are empty, malformed, or defined on
|
||||||
|
incompatible field shapes.
|
||||||
|
"""
|
||||||
|
left_e_shape, left_h_shape = _validate_port_modes('ehLs', ehLs, wavenumbers_L)
|
||||||
|
right_e_shape, right_h_shape = _validate_port_modes('ehRs', ehRs, wavenumbers_R)
|
||||||
|
if left_e_shape != right_e_shape or left_h_shape != right_h_shape:
|
||||||
|
raise ValueError('left and right modal fields must share the same E/H shapes')
|
||||||
|
|
||||||
|
nL = len(wavenumbers_L)
|
||||||
|
nR = len(wavenumbers_R)
|
||||||
|
A12 = numpy.zeros((nL, nR), dtype=complex)
|
||||||
|
A21 = numpy.zeros((nL, nR), dtype=complex)
|
||||||
|
B11 = numpy.zeros((nL,), dtype=complex)
|
||||||
|
for ll in range(nL):
|
||||||
|
eL, hL = ehLs[ll]
|
||||||
|
B11[ll] = inner_product(eL, hL, dxes=dxes, conj_h=False)
|
||||||
|
for rr in range(nR):
|
||||||
|
eR, hR = ehRs[rr]
|
||||||
|
A12[ll, rr] = inner_product(eL, hR, dxes=dxes, conj_h=False) # TODO optimize loop?
|
||||||
|
A21[ll, rr] = inner_product(eR, hL, dxes=dxes, conj_h=False)
|
||||||
|
|
||||||
|
# tt0 = 2 * numpy.linalg.pinv(A21 + numpy.conj(A12))
|
||||||
|
tt0, _resid, _rank, _sing = numpy.linalg.lstsq(A21 + A12, numpy.diag(2 * B11), rcond=None)
|
||||||
|
|
||||||
|
U, st, V = numpy.linalg.svd(tt0)
|
||||||
|
gain = st > 1
|
||||||
|
st[gain] = 1 / st[gain]
|
||||||
|
tt = U @ numpy.diag(st) @ V
|
||||||
|
|
||||||
|
# rr = 0.5 * (A21 - numpy.conj(A12)) @ tt
|
||||||
|
rr = numpy.diag(0.5 / B11) @ (A21 - A12) @ tt
|
||||||
|
|
||||||
|
return tt, rr
|
||||||
|
|
||||||
|
|
||||||
|
def get_abcd(
|
||||||
|
ehLs: Sequence[Sequence[vcfdfield2]],
|
||||||
|
wavenumbers_L: wavenumber_seq,
|
||||||
|
ehRs: Sequence[Sequence[vcfdfield2]],
|
||||||
|
wavenumbers_R: wavenumber_seq,
|
||||||
|
**kwargs,
|
||||||
|
) -> sparse.sparray:
|
||||||
|
"""
|
||||||
|
Build the 2-port block transfer matrix for an interface.
|
||||||
|
|
||||||
|
The blocks are assembled from the forward and reverse `get_tr(...)`
|
||||||
|
solutions using the standard
|
||||||
|
|
||||||
|
`[[A, B], [C, D]] = [[T12 - R21 T21^-1 R12, R21 T21^-1], [-T21^-1 R12, T21^-1]]`
|
||||||
|
|
||||||
|
convention.
|
||||||
|
"""
|
||||||
|
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
|
||||||
|
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
|
||||||
|
t21i = numpy.linalg.pinv(t21)
|
||||||
|
A = t12 - r21 @ t21i @ r12
|
||||||
|
B = r21 @ t21i
|
||||||
|
C = -t21i @ r12
|
||||||
|
D = t21i
|
||||||
|
return sparse.block_array(
|
||||||
|
[
|
||||||
|
[sparse.csr_array(A), sparse.csr_array(B)],
|
||||||
|
[sparse.csr_array(C), sparse.csr_array(D)],
|
||||||
|
],
|
||||||
|
format='csr',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_s(
|
||||||
|
ehLs: Sequence[Sequence[vcfdfield2]],
|
||||||
|
wavenumbers_L: wavenumber_seq,
|
||||||
|
ehRs: Sequence[Sequence[vcfdfield2]],
|
||||||
|
wavenumbers_R: wavenumber_seq,
|
||||||
|
force_nogain: bool = False,
|
||||||
|
force_reciprocal: bool = False,
|
||||||
|
**kwargs,
|
||||||
|
) -> NDArray[numpy.complex128]:
|
||||||
|
"""
|
||||||
|
Build the full block scattering matrix for a two-sided interface.
|
||||||
|
|
||||||
|
The returned matrix is ordered as `[[R12, T12], [T21, R21]]`, where the
|
||||||
|
first block-row/column corresponds to the left port and the second to the
|
||||||
|
right port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force_nogain: If `True`, clamp singular values of the assembled
|
||||||
|
scattering matrix to at most one.
|
||||||
|
force_reciprocal: If `True`, symmetrize the assembled matrix as
|
||||||
|
`0.5 * (S + S.T)`.
|
||||||
|
"""
|
||||||
|
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
|
||||||
|
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
|
||||||
|
|
||||||
|
ss = numpy.block([[r12, t12],
|
||||||
|
[t21, r21]])
|
||||||
|
|
||||||
|
if force_nogain:
|
||||||
|
# force S @ S.H diagonal
|
||||||
|
U, sing, Vh = numpy.linalg.svd(ss)
|
||||||
|
ss = U @ numpy.diag(numpy.minimum(sing, 1.0)) @ Vh
|
||||||
|
|
||||||
|
if force_reciprocal:
|
||||||
|
ss = 0.5 * (ss + ss.T)
|
||||||
|
|
||||||
|
return ss
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
"""
|
"""
|
||||||
Functions for performing near-to-farfield transformation (and the reverse).
|
Functions for performing near-to-farfield transformation (and the reverse).
|
||||||
"""
|
"""
|
||||||
from typing import Any, Sequence, cast
|
from typing import Any, cast
|
||||||
|
from collections.abc import Sequence
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
|
from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
from numpy import complexfloating
|
||||||
|
|
||||||
from ..fdmath import cfdfield_t
|
type farfield_slice = NDArray[complexfloating]
|
||||||
|
type transverse_slice_pair = Sequence[farfield_slice]
|
||||||
|
|
||||||
|
|
||||||
def near_to_farfield(
|
def near_to_farfield(
|
||||||
E_near: cfdfield_t,
|
E_near: transverse_slice_pair,
|
||||||
H_near: cfdfield_t,
|
H_near: transverse_slice_pair,
|
||||||
dx: float,
|
dx: float,
|
||||||
dy: float,
|
dy: float,
|
||||||
padded_size: list[int] | int | None = None
|
padded_size: Sequence[int] | int | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Compute the farfield, i.e. the distribution of the fields after propagation
|
Compute the farfield, i.e. the distribution of the fields after propagation
|
||||||
|
|
@ -55,14 +59,14 @@ def near_to_farfield(
|
||||||
raise Exception('H_near must be a length-2 list of ndarrays')
|
raise Exception('H_near must be a length-2 list of ndarrays')
|
||||||
|
|
||||||
s = E_near[0].shape
|
s = E_near[0].shape
|
||||||
if not all(s == f.shape for f in E_near + H_near):
|
if not all(s == f.shape for f in [*E_near, *H_near]):
|
||||||
raise Exception('All fields must be the same shape!')
|
raise Exception('All fields must be the same shape!')
|
||||||
|
|
||||||
if padded_size is None:
|
if padded_size is None:
|
||||||
padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int)
|
padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int)
|
||||||
if not hasattr(padded_size, '__len__'):
|
if not hasattr(padded_size, '__len__'):
|
||||||
padded_size = (padded_size, padded_size) # type: ignore # checked if sequence
|
padded_size = (padded_size, padded_size) # type: ignore # checked if sequence
|
||||||
padded_shape = cast(Sequence[int], padded_size)
|
padded_shape = cast('Sequence[int]', padded_size)
|
||||||
|
|
||||||
En_fft = [fftshift(fft2(fftshift(Eni), s=padded_shape)) for Eni in E_near]
|
En_fft = [fftshift(fft2(fftshift(Eni), s=padded_shape)) for Eni in E_near]
|
||||||
Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_shape)) for Hni in H_near]
|
Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_shape)) for Hni in H_near]
|
||||||
|
|
@ -75,25 +79,22 @@ def near_to_farfield(
|
||||||
kx, ky = numpy.meshgrid(kxx, kyy, indexing='ij')
|
kx, ky = numpy.meshgrid(kxx, kyy, indexing='ij')
|
||||||
kxy2 = kx * kx + ky * ky
|
kxy2 = kx * kx + ky * ky
|
||||||
kxy = numpy.sqrt(kxy2)
|
kxy = numpy.sqrt(kxy2)
|
||||||
kz = numpy.sqrt(k * k - kxy2)
|
kz = numpy.sqrt(numpy.maximum(0, k * k - kxy2))
|
||||||
|
|
||||||
sin_th = ky / kxy
|
sin_th = numpy.divide(ky, kxy, out=numpy.zeros_like(ky), where=kxy != 0)
|
||||||
cos_th = kx / kxy
|
cos_th = numpy.divide(kx, kxy, out=numpy.ones_like(kx), where=kxy != 0)
|
||||||
cos_phi = kz / k
|
cos_phi = kz / k
|
||||||
|
|
||||||
sin_th[numpy.logical_and(kx == 0, ky == 0)] = 0
|
|
||||||
cos_th[numpy.logical_and(kx == 0, ky == 0)] = 1
|
|
||||||
|
|
||||||
# Normalized vector potentials N, L
|
# Normalized vector potentials N, L
|
||||||
N = [-Hn_fft[1] * cos_phi * cos_th + Hn_fft[0] * cos_phi * sin_th,
|
N = [-Hn_fft[1] * cos_phi * cos_th + Hn_fft[0] * cos_phi * sin_th,
|
||||||
Hn_fft[1] * sin_th + Hn_fft[0] * cos_th] # noqa: E127
|
Hn_fft[1] * sin_th + Hn_fft[0] * cos_th] # noqa
|
||||||
L = [ En_fft[1] * cos_phi * cos_th - En_fft[0] * cos_phi * sin_th,
|
L = [ En_fft[1] * cos_phi * cos_th - En_fft[0] * cos_phi * sin_th,
|
||||||
-En_fft[1] * sin_th - En_fft[0] * cos_th] # noqa: E128
|
-En_fft[1] * sin_th - En_fft[0] * cos_th] # noqa
|
||||||
|
|
||||||
E_far = [-L[1] - N[0],
|
E_far = [-L[1] - N[0],
|
||||||
L[0] - N[1]] # noqa: E127
|
L[0] - N[1]] # noqa
|
||||||
H_far = [-E_far[1],
|
H_far = [-E_far[1],
|
||||||
E_far[0]] # noqa: E127
|
E_far[0]] # noqa
|
||||||
|
|
||||||
theta = numpy.arctan2(ky, kx)
|
theta = numpy.arctan2(ky, kx)
|
||||||
phi = numpy.arccos(cos_phi)
|
phi = numpy.arccos(cos_phi)
|
||||||
|
|
@ -111,8 +112,8 @@ def near_to_farfield(
|
||||||
outputs = {
|
outputs = {
|
||||||
'E': E_far,
|
'E': E_far,
|
||||||
'H': H_far,
|
'H': H_far,
|
||||||
'dkx': kx[1] - kx[0],
|
'dkx': float(kxx[1] - kxx[0]),
|
||||||
'dky': ky[1] - ky[0],
|
'dky': float(kyy[1] - kyy[0]),
|
||||||
'kx': kx,
|
'kx': kx,
|
||||||
'ky': ky,
|
'ky': ky,
|
||||||
'theta': theta,
|
'theta': theta,
|
||||||
|
|
@ -123,11 +124,11 @@ def near_to_farfield(
|
||||||
|
|
||||||
|
|
||||||
def far_to_nearfield(
|
def far_to_nearfield(
|
||||||
E_far: cfdfield_t,
|
E_far: transverse_slice_pair,
|
||||||
H_far: cfdfield_t,
|
H_far: transverse_slice_pair,
|
||||||
dkx: float,
|
dkx: float,
|
||||||
dky: float,
|
dky: float,
|
||||||
padded_size: list[int] | int | None = None
|
padded_size: Sequence[int] | int | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Compute the farfield, i.e. the distribution of the fields after propagation
|
Compute the farfield, i.e. the distribution of the fields after propagation
|
||||||
|
|
@ -164,32 +165,29 @@ def far_to_nearfield(
|
||||||
raise Exception('H_far must be a length-2 list of ndarrays')
|
raise Exception('H_far must be a length-2 list of ndarrays')
|
||||||
|
|
||||||
s = E_far[0].shape
|
s = E_far[0].shape
|
||||||
if not all(s == f.shape for f in E_far + H_far):
|
if not all(s == f.shape for f in [*E_far, *H_far]):
|
||||||
raise Exception('All fields must be the same shape!')
|
raise Exception('All fields must be the same shape!')
|
||||||
|
|
||||||
if padded_size is None:
|
if padded_size is None:
|
||||||
padded_size = (2 ** numpy.ceil(numpy.log2(s))).astype(int)
|
padded_size = (2 ** numpy.ceil(numpy.log2(s))).astype(int)
|
||||||
if not hasattr(padded_size, '__len__'):
|
if not hasattr(padded_size, '__len__'):
|
||||||
padded_size = (padded_size, padded_size) # type: ignore # checked if sequence
|
padded_size = (padded_size, padded_size) # type: ignore # checked if sequence
|
||||||
padded_shape = cast(Sequence[int], padded_size)
|
padded_shape = cast('Sequence[int]', padded_size)
|
||||||
|
|
||||||
k = 2 * pi
|
k = 2 * pi
|
||||||
kxs = fftshift(fftfreq(s[0], 1 / (s[0] * dkx)))
|
kxs = dkx * fftshift(fftfreq(s[0], d=1 / s[0]))
|
||||||
kys = fftshift(fftfreq(s[0], 1 / (s[1] * dky)))
|
kys = dky * fftshift(fftfreq(s[1], d=1 / s[1]))
|
||||||
|
|
||||||
kx, ky = numpy.meshgrid(kxs, kys, indexing='ij')
|
kx, ky = numpy.meshgrid(kxs, kys, indexing='ij')
|
||||||
kxy2 = kx * kx + ky * ky
|
kxy2 = kx * kx + ky * ky
|
||||||
kxy = numpy.sqrt(kxy2)
|
kxy = numpy.sqrt(kxy2)
|
||||||
|
|
||||||
kz = numpy.sqrt(k * k - kxy2)
|
kz = numpy.sqrt(numpy.maximum(0, k * k - kxy2))
|
||||||
|
|
||||||
sin_th = ky / kxy
|
sin_th = numpy.divide(ky, kxy, out=numpy.zeros_like(ky), where=kxy != 0)
|
||||||
cos_th = kx / kxy
|
cos_th = numpy.divide(kx, kxy, out=numpy.ones_like(kx), where=kxy != 0)
|
||||||
cos_phi = kz / k
|
cos_phi = kz / k
|
||||||
|
|
||||||
sin_th[numpy.logical_and(kx == 0, ky == 0)] = 0
|
|
||||||
cos_th[numpy.logical_and(kx == 0, ky == 0)] = 1
|
|
||||||
|
|
||||||
theta = numpy.arctan2(ky, kx)
|
theta = numpy.arctan2(ky, kx)
|
||||||
phi = numpy.arccos(cos_phi)
|
phi = numpy.arccos(cos_phi)
|
||||||
theta[numpy.logical_and(kx == 0, ky == 0)] = 0
|
theta[numpy.logical_and(kx == 0, ky == 0)] = 0
|
||||||
|
|
@ -205,25 +203,45 @@ def far_to_nearfield(
|
||||||
|
|
||||||
# Normalized vector potentials N, L
|
# Normalized vector potentials N, L
|
||||||
L = [0.5 * E_far[1],
|
L = [0.5 * E_far[1],
|
||||||
-0.5 * E_far[0]] # noqa: E128
|
-0.5 * E_far[0]] # noqa
|
||||||
N = [L[1],
|
N = [L[1],
|
||||||
-L[0]] # noqa: E128
|
-L[0]] # noqa
|
||||||
|
|
||||||
En_fft = [-( L[0] * sin_th + L[1] * cos_phi * cos_th) / cos_phi,
|
En_fft = [
|
||||||
-(-L[0] * cos_th + L[1] * cos_phi * sin_th) / cos_phi]
|
numpy.divide(
|
||||||
|
-(L[0] * sin_th + L[1] * cos_phi * cos_th),
|
||||||
|
cos_phi,
|
||||||
|
out=numpy.zeros_like(L[0]),
|
||||||
|
where=cos_phi != 0,
|
||||||
|
),
|
||||||
|
numpy.divide(
|
||||||
|
-(-L[0] * cos_th + L[1] * cos_phi * sin_th),
|
||||||
|
cos_phi,
|
||||||
|
out=numpy.zeros_like(L[0]),
|
||||||
|
where=cos_phi != 0,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
Hn_fft = [( N[0] * sin_th + N[1] * cos_phi * cos_th) / cos_phi,
|
Hn_fft = [
|
||||||
(-N[0] * cos_th + N[1] * cos_phi * sin_th) / cos_phi]
|
numpy.divide(
|
||||||
|
N[0] * sin_th + N[1] * cos_phi * cos_th,
|
||||||
for i in range(2):
|
cos_phi,
|
||||||
En_fft[i][cos_phi == 0] = 0
|
out=numpy.zeros_like(N[0]),
|
||||||
Hn_fft[i][cos_phi == 0] = 0
|
where=cos_phi != 0,
|
||||||
|
),
|
||||||
|
numpy.divide(
|
||||||
|
-N[0] * cos_th + N[1] * cos_phi * sin_th,
|
||||||
|
cos_phi,
|
||||||
|
out=numpy.zeros_like(N[0]),
|
||||||
|
where=cos_phi != 0,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
E_near = [ifftshift(ifft2(ifftshift(Ei), s=padded_shape)) for Ei in En_fft]
|
E_near = [ifftshift(ifft2(ifftshift(Ei), s=padded_shape)) for Ei in En_fft]
|
||||||
H_near = [ifftshift(ifft2(ifftshift(Hi), s=padded_shape)) for Hi in Hn_fft]
|
H_near = [ifftshift(ifft2(ifftshift(Hi), s=padded_shape)) for Hi in Hn_fft]
|
||||||
|
|
||||||
dx = 2 * pi / (s[0] * dkx)
|
dx = 2 * pi / (s[0] * dkx)
|
||||||
dy = 2 * pi / (s[0] * dky)
|
dy = 2 * pi / (s[1] * dky)
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
'E': E_near,
|
'E': E_near,
|
||||||
|
|
@ -233,4 +251,3 @@ def far_to_nearfield(
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputs
|
return outputs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ Functional versions of many FDFD operators. These can be useful for performing
|
||||||
The functions generated here expect `cfdfield_t` inputs with shape (3, X, Y, Z),
|
The functions generated here expect `cfdfield_t` inputs with shape (3, X, Y, Z),
|
||||||
e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z)
|
e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z)
|
||||||
"""
|
"""
|
||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
from ..fdmath import dx_lists_t, fdfield_t, cfdfield_t, cfdfield_updater_t
|
from ..fdmath import dx_lists_t, cfdfield_t, fdfield, cfdfield, cfdfield_updater_t
|
||||||
from ..fdmath.functional import curl_forward, curl_back
|
from ..fdmath.functional import curl_forward, curl_back
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,8 +18,8 @@ __author__ = 'Jan Petykiewicz'
|
||||||
def e_full(
|
def e_full(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
) -> cfdfield_updater_t:
|
) -> cfdfield_updater_t:
|
||||||
"""
|
"""
|
||||||
Wave operator for use with E-field. See `operators.e_full` for details.
|
Wave operator for use with E-field. See `operators.e_full` for details.
|
||||||
|
|
@ -37,26 +37,25 @@ def e_full(
|
||||||
ch = curl_back(dxes[1])
|
ch = curl_back(dxes[1])
|
||||||
ce = curl_forward(dxes[0])
|
ce = curl_forward(dxes[0])
|
||||||
|
|
||||||
def op_1(e: cfdfield_t) -> cfdfield_t:
|
def op_1(e: cfdfield) -> cfdfield_t:
|
||||||
curls = ch(ce(e))
|
curls = ch(ce(e))
|
||||||
return curls - omega ** 2 * epsilon * e
|
return cfdfield_t(curls - omega ** 2 * epsilon * e)
|
||||||
|
|
||||||
def op_mu(e: cfdfield_t) -> cfdfield_t:
|
def op_mu(e: cfdfield_t) -> cfdfield_t:
|
||||||
curls = ch(mu * ce(e)) # type: ignore # mu = None ok because we don't return the function
|
curls = ch(ce(e) / mu) # type: ignore # mu = None ok because we don't return the function
|
||||||
return curls - omega ** 2 * epsilon * e
|
return cfdfield_t(curls - omega ** 2 * epsilon * e)
|
||||||
|
|
||||||
if mu is None:
|
if mu is None:
|
||||||
return op_1
|
return op_1
|
||||||
else:
|
|
||||||
return op_mu
|
return op_mu
|
||||||
|
|
||||||
|
|
||||||
def eh_full(
|
def eh_full(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
) -> Callable[[cfdfield_t, cfdfield_t], tuple[cfdfield_t, cfdfield_t]]:
|
) -> Callable[[cfdfield, cfdfield], tuple[cfdfield_t, cfdfield_t]]:
|
||||||
"""
|
"""
|
||||||
Wave operator for full (both E and H) field representation.
|
Wave operator for full (both E and H) field representation.
|
||||||
See `operators.eh_full`.
|
See `operators.eh_full`.
|
||||||
|
|
@ -74,24 +73,23 @@ def eh_full(
|
||||||
ch = curl_back(dxes[1])
|
ch = curl_back(dxes[1])
|
||||||
ce = curl_forward(dxes[0])
|
ce = curl_forward(dxes[0])
|
||||||
|
|
||||||
def op_1(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]:
|
def op_1(e: cfdfield, h: cfdfield) -> tuple[cfdfield_t, cfdfield_t]:
|
||||||
return (ch(h) - 1j * omega * epsilon * e,
|
return (cfdfield_t(ch(h) - 1j * omega * epsilon * e),
|
||||||
ce(e) + 1j * omega * h)
|
cfdfield_t(ce(e) + 1j * omega * h))
|
||||||
|
|
||||||
def op_mu(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]:
|
def op_mu(e: cfdfield, h: cfdfield) -> tuple[cfdfield_t, cfdfield_t]:
|
||||||
return (ch(h) - 1j * omega * epsilon * e,
|
return (cfdfield_t(ch(h) - 1j * omega * epsilon * e),
|
||||||
ce(e) + 1j * omega * mu * h) # type: ignore # mu=None ok
|
cfdfield_t(ce(e) + 1j * omega * mu * h)) # type: ignore # mu=None ok
|
||||||
|
|
||||||
if mu is None:
|
if mu is None:
|
||||||
return op_1
|
return op_1
|
||||||
else:
|
|
||||||
return op_mu
|
return op_mu
|
||||||
|
|
||||||
|
|
||||||
def e2h(
|
def e2h(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
) -> cfdfield_updater_t:
|
) -> cfdfield_updater_t:
|
||||||
"""
|
"""
|
||||||
Utility operator for converting the `E` field into the `H` field.
|
Utility operator for converting the `E` field into the `H` field.
|
||||||
|
|
@ -108,22 +106,21 @@ def e2h(
|
||||||
"""
|
"""
|
||||||
ce = curl_forward(dxes[0])
|
ce = curl_forward(dxes[0])
|
||||||
|
|
||||||
def e2h_1_1(e: cfdfield_t) -> cfdfield_t:
|
def e2h_1_1(e: cfdfield) -> cfdfield_t:
|
||||||
return ce(e) / (-1j * omega)
|
return cfdfield_t(ce(e) / (-1j * omega))
|
||||||
|
|
||||||
def e2h_mu(e: cfdfield_t) -> cfdfield_t:
|
def e2h_mu(e: cfdfield) -> cfdfield_t:
|
||||||
return ce(e) / (-1j * omega * mu) # type: ignore # mu=None ok
|
return cfdfield_t(ce(e) / (-1j * omega * mu)) # type: ignore # mu=None ok
|
||||||
|
|
||||||
if mu is None:
|
if mu is None:
|
||||||
return e2h_1_1
|
return e2h_1_1
|
||||||
else:
|
|
||||||
return e2h_mu
|
return e2h_mu
|
||||||
|
|
||||||
|
|
||||||
def m2j(
|
def m2j(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
) -> cfdfield_updater_t:
|
) -> cfdfield_updater_t:
|
||||||
"""
|
"""
|
||||||
Utility operator for converting magnetic current `M` distribution
|
Utility operator for converting magnetic current `M` distribution
|
||||||
|
|
@ -142,30 +139,42 @@ def m2j(
|
||||||
ch = curl_back(dxes[1])
|
ch = curl_back(dxes[1])
|
||||||
|
|
||||||
def m2j_mu(m: cfdfield_t) -> cfdfield_t:
|
def m2j_mu(m: cfdfield_t) -> cfdfield_t:
|
||||||
J = ch(m / mu) / (-1j * omega) # type: ignore # mu=None ok
|
J = ch(m / mu) / (1j * omega) # type: ignore # mu=None ok
|
||||||
return J
|
return cfdfield_t(J)
|
||||||
|
|
||||||
def m2j_1(m: cfdfield_t) -> cfdfield_t:
|
def m2j_1(m: cfdfield_t) -> cfdfield_t:
|
||||||
J = ch(m) / (-1j * omega)
|
J = ch(m) / (1j * omega)
|
||||||
return J
|
return cfdfield_t(J)
|
||||||
|
|
||||||
if mu is None:
|
if mu is None:
|
||||||
return m2j_1
|
return m2j_1
|
||||||
else:
|
|
||||||
return m2j_mu
|
return m2j_mu
|
||||||
|
|
||||||
|
|
||||||
def e_tfsf_source(
|
def e_tfsf_source(
|
||||||
TF_region: fdfield_t,
|
TF_region: fdfield,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
) -> cfdfield_updater_t:
|
) -> cfdfield_updater_t:
|
||||||
"""
|
r"""
|
||||||
Operator that turns an E-field distribution into a total-field/scattered-field
|
Operator that turns an E-field distribution into a total-field/scattered-field
|
||||||
(TFSF) source.
|
(TFSF) source.
|
||||||
|
|
||||||
|
If `A` is the full wave operator from `e_full(...)` and `Q` is the diagonal
|
||||||
|
mask selecting the total-field region, then the TFSF source is the commutator
|
||||||
|
|
||||||
|
$$
|
||||||
|
\frac{A Q - Q A}{-i \omega} E.
|
||||||
|
$$
|
||||||
|
|
||||||
|
This vanishes in the interior of the total-field and scattered-field regions
|
||||||
|
and is supported only at their shared boundary, where the mask discontinuity
|
||||||
|
makes `A` and `Q` fail to commute. The returned current is therefore the
|
||||||
|
distributed source needed to inject the desired total field without also
|
||||||
|
forcing the scattered-field region.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
TF_region: mask which is set to 1 in the total-field region, and 0 elsewhere
|
TF_region: mask which is set to 1 in the total-field region, and 0 elsewhere
|
||||||
(i.e. in the scattered-field region).
|
(i.e. in the scattered-field region).
|
||||||
|
|
@ -179,20 +188,25 @@ def e_tfsf_source(
|
||||||
Function `f` which takes an E field and returns a current distribution,
|
Function `f` which takes an E field and returns a current distribution,
|
||||||
`f(E)` -> `J`
|
`f(E)` -> `J`
|
||||||
"""
|
"""
|
||||||
# TODO documentation
|
|
||||||
A = e_full(omega, dxes, epsilon, mu)
|
A = e_full(omega, dxes, epsilon, mu)
|
||||||
|
|
||||||
def op(e: cfdfield_t) -> cfdfield_t:
|
def op(e: cfdfield) -> cfdfield_t:
|
||||||
neg_iwj = A(TF_region * e) - TF_region * A(e)
|
neg_iwj = A(TF_region * e) - TF_region * A(e)
|
||||||
return neg_iwj / (-1j * omega)
|
return cfdfield_t(neg_iwj / (-1j * omega))
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
||||||
def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], cfdfield_t]:
|
def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfield_t]:
|
||||||
r"""
|
r"""
|
||||||
Generates a function that takes the single-frequency `E` and `H` fields
|
Generates a function that takes the single-frequency `E` and `H` fields
|
||||||
and calculates the cross product `E` x `H` = $E \times H$ as required
|
and calculates the cross product `E` x `H` = $E \times H$ as required
|
||||||
for the Poynting vector, $S = E \times H$
|
for the Poynting vector, $S = E \times H$.
|
||||||
|
|
||||||
|
On the Yee grid, the electric and magnetic components are not stored at the
|
||||||
|
same locations. This helper therefore applies the same one-cell electric-field
|
||||||
|
shifts used by the sparse `operators.poynting_e_cross(...)` construction so
|
||||||
|
that the discrete cross product matches the face-centered energy flux used in
|
||||||
|
`meanas.fdtd.energy.poynting(...)`.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
This function also shifts the input `E` field by one cell as required
|
This function also shifts the input `E` field by one cell as required
|
||||||
|
|
@ -208,9 +222,10 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c
|
||||||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Function `f` that returns E x H as required for the poynting vector.
|
Function `f` that returns the staggered-grid cross product `E \times H`.
|
||||||
|
For time-average power, call it as `f(E, H.conj())` and take `Re(...) / 2`.
|
||||||
"""
|
"""
|
||||||
def exh(e: cfdfield_t, h: cfdfield_t) -> cfdfield_t:
|
def exh(e: cfdfield, h: cfdfield) -> cfdfield_t:
|
||||||
s = numpy.empty_like(e)
|
s = numpy.empty_like(e)
|
||||||
ex = e[0] * dxes[0][0][:, None, None]
|
ex = e[0] * dxes[0][0][:, None, None]
|
||||||
ey = e[1] * dxes[0][1][None, :, None]
|
ey = e[1] * dxes[0][1][None, :, None]
|
||||||
|
|
@ -221,5 +236,5 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c
|
||||||
s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy
|
s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy
|
||||||
s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz
|
s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz
|
||||||
s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx
|
s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx
|
||||||
return s
|
return cfdfield_t(s)
|
||||||
return exh
|
return exh
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Sparse matrix operators for use with electromagnetic wave equations.
|
Sparse matrix operators for use with electromagnetic wave equations.
|
||||||
|
|
||||||
These functions return sparse-matrix (`scipy.sparse.spmatrix`) representations of
|
These functions return sparse-matrix (`scipy.sparse.sparray`) representations of
|
||||||
a variety of operators, intended for use with E and H fields vectorized using the
|
a variety of operators, intended for use with E and H fields vectorized using the
|
||||||
`meanas.fdmath.vectorization.vec()` and `meanas.fdmath.vectorization.unvec()` functions.
|
`meanas.fdmath.vectorization.vec()` and `meanas.fdmath.vectorization.unvec()` functions.
|
||||||
|
|
||||||
|
|
@ -28,9 +28,9 @@ The following operators are included:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import scipy.sparse as sparse # type: ignore
|
from scipy import sparse
|
||||||
|
|
||||||
from ..fdmath import vec, dx_lists_t, vfdfield_t, vcfdfield_t
|
from ..fdmath import vec, dx_lists_t, vfdfield, vcfdfield
|
||||||
from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back
|
from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -40,11 +40,11 @@ __author__ = 'Jan Petykiewicz'
|
||||||
def e_full(
|
def e_full(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdfield | vcfdfield,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdfield | None = None,
|
||||||
pec: vfdfield_t | None = None,
|
pec: vfdfield | None = None,
|
||||||
pmc: vfdfield_t | None = None,
|
pmc: vfdfield | None = None,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
r"""
|
r"""
|
||||||
Wave operator
|
Wave operator
|
||||||
$$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$
|
$$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$
|
||||||
|
|
@ -77,20 +77,20 @@ def e_full(
|
||||||
ce = curl_forward(dxes[0])
|
ce = curl_forward(dxes[0])
|
||||||
|
|
||||||
if pec is None:
|
if pec is None:
|
||||||
pe = sparse.eye(epsilon.size)
|
pe = sparse.eye_array(epsilon.size)
|
||||||
else:
|
else:
|
||||||
pe = sparse.diags(numpy.where(pec, 0, 1)) # Set pe to (not PEC)
|
pe = sparse.diags_array(numpy.where(pec, 0, 1)) # Set pe to (not PEC)
|
||||||
|
|
||||||
if pmc is None:
|
if pmc is None:
|
||||||
pm = sparse.eye(epsilon.size)
|
pm = sparse.eye_array(epsilon.size)
|
||||||
else:
|
else:
|
||||||
pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
|
pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
|
||||||
|
|
||||||
e = sparse.diags(epsilon)
|
e = sparse.diags_array(epsilon)
|
||||||
if mu is None:
|
if mu is None:
|
||||||
m_div = sparse.eye(epsilon.size)
|
m_div = sparse.eye_array(epsilon.size)
|
||||||
else:
|
else:
|
||||||
m_div = sparse.diags(1 / mu)
|
m_div = sparse.diags_array(1 / mu)
|
||||||
|
|
||||||
op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe
|
op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe
|
||||||
return op
|
return op
|
||||||
|
|
@ -98,7 +98,7 @@ def e_full(
|
||||||
|
|
||||||
def e_full_preconditioners(
|
def e_full_preconditioners(
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
) -> tuple[sparse.spmatrix, sparse.spmatrix]:
|
) -> tuple[sparse.sparray, sparse.sparray]:
|
||||||
"""
|
"""
|
||||||
Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator.
|
Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator.
|
||||||
|
|
||||||
|
|
@ -118,19 +118,19 @@ def e_full_preconditioners(
|
||||||
dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[0][2][None, None, :]]
|
dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[0][2][None, None, :]]
|
||||||
|
|
||||||
p_vector = numpy.sqrt(vec(p_squared))
|
p_vector = numpy.sqrt(vec(p_squared))
|
||||||
P_left = sparse.diags(p_vector)
|
P_left = sparse.diags_array(p_vector)
|
||||||
P_right = sparse.diags(1 / p_vector)
|
P_right = sparse.diags_array(1 / p_vector)
|
||||||
return P_left, P_right
|
return P_left, P_right
|
||||||
|
|
||||||
|
|
||||||
def h_full(
|
def h_full(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdfield,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdfield | None = None,
|
||||||
pec: vfdfield_t | None = None,
|
pec: vfdfield | None = None,
|
||||||
pmc: vfdfield_t | None = None,
|
pmc: vfdfield | None = None,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
r"""
|
r"""
|
||||||
Wave operator
|
Wave operator
|
||||||
$$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$
|
$$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$
|
||||||
|
|
@ -161,20 +161,20 @@ def h_full(
|
||||||
ce = curl_forward(dxes[0])
|
ce = curl_forward(dxes[0])
|
||||||
|
|
||||||
if pec is None:
|
if pec is None:
|
||||||
pe = sparse.eye(epsilon.size)
|
pe = sparse.eye_array(epsilon.size)
|
||||||
else:
|
else:
|
||||||
pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC)
|
pe = sparse.diags_array(numpy.where(pec, 0, 1)) # set pe to (not PEC)
|
||||||
|
|
||||||
if pmc is None:
|
if pmc is None:
|
||||||
pm = sparse.eye(epsilon.size)
|
pm = sparse.eye_array(epsilon.size)
|
||||||
else:
|
else:
|
||||||
pm = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC)
|
pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # Set pe to (not PMC)
|
||||||
|
|
||||||
e_div = sparse.diags(1 / epsilon)
|
e_div = sparse.diags_array(1 / epsilon)
|
||||||
if mu is None:
|
if mu is None:
|
||||||
m = sparse.eye(epsilon.size)
|
m = sparse.eye_array(epsilon.size)
|
||||||
else:
|
else:
|
||||||
m = sparse.diags(mu)
|
m = sparse.diags_array(mu)
|
||||||
|
|
||||||
A = pm @ (ce @ pe @ e_div @ ch - omega**2 * m) @ pm
|
A = pm @ (ce @ pe @ e_div @ ch - omega**2 * m) @ pm
|
||||||
return A
|
return A
|
||||||
|
|
@ -183,11 +183,11 @@ def h_full(
|
||||||
def eh_full(
|
def eh_full(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdfield,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdfield | None = None,
|
||||||
pec: vfdfield_t | None = None,
|
pec: vfdfield | None = None,
|
||||||
pmc: vfdfield_t | None = None,
|
pmc: vfdfield | None = None,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
r"""
|
r"""
|
||||||
Wave operator for `[E, H]` field representation. This operator implements Maxwell's
|
Wave operator for `[E, H]` field representation. This operator implements Maxwell's
|
||||||
equations without cancelling out either E or H. The operator is
|
equations without cancelling out either E or H. The operator is
|
||||||
|
|
@ -227,25 +227,27 @@ def eh_full(
|
||||||
Sparse matrix containing the wave operator.
|
Sparse matrix containing the wave operator.
|
||||||
"""
|
"""
|
||||||
if pec is None:
|
if pec is None:
|
||||||
pe = sparse.eye(epsilon.size)
|
pe = sparse.eye_array(epsilon.size)
|
||||||
else:
|
else:
|
||||||
pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC)
|
pe = sparse.diags_array(numpy.where(pec, 0, 1)) # set pe to (not PEC)
|
||||||
|
|
||||||
if pmc is None:
|
if pmc is None:
|
||||||
pm = sparse.eye(epsilon.size)
|
pm = sparse.eye_array(epsilon.size)
|
||||||
else:
|
else:
|
||||||
pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
|
pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
|
||||||
|
|
||||||
iwe = pe @ (1j * omega * sparse.diags(epsilon)) @ pe
|
iwe = pe @ (1j * omega * sparse.diags(epsilon)) @ pe
|
||||||
iwm = 1j * omega
|
if mu is None:
|
||||||
if mu is not None:
|
iwm = 1j * omega * sparse.eye(epsilon.size)
|
||||||
iwm *= sparse.diags(mu)
|
else:
|
||||||
|
iwm = 1j * omega * sparse.diags(mu)
|
||||||
|
|
||||||
iwm = pm @ iwm @ pm
|
iwm = pm @ iwm @ pm
|
||||||
|
|
||||||
A1 = pe @ curl_back(dxes[1]) @ pm
|
A1 = pe @ curl_back(dxes[1]) @ pm
|
||||||
A2 = pm @ curl_forward(dxes[0]) @ pe
|
A2 = pm @ curl_forward(dxes[0]) @ pe
|
||||||
|
|
||||||
A = sparse.bmat([[-iwe, A1],
|
A = sparse.block_array([[-iwe, A1],
|
||||||
[A2, iwm]])
|
[A2, iwm]])
|
||||||
return A
|
return A
|
||||||
|
|
||||||
|
|
@ -253,9 +255,9 @@ def eh_full(
|
||||||
def e2h(
|
def e2h(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdfield | None = None,
|
||||||
pmc: vfdfield_t | None = None,
|
pmc: vfdfield | None = None,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Utility operator for converting the E field into the H field.
|
Utility operator for converting the E field into the H field.
|
||||||
For use with `e_full()` -- assumes that there is no magnetic current M.
|
For use with `e_full()` -- assumes that there is no magnetic current M.
|
||||||
|
|
@ -274,10 +276,10 @@ def e2h(
|
||||||
op = curl_forward(dxes[0]) / (-1j * omega)
|
op = curl_forward(dxes[0]) / (-1j * omega)
|
||||||
|
|
||||||
if mu is not None:
|
if mu is not None:
|
||||||
op = sparse.diags(1 / mu) @ op
|
op = sparse.diags_array(1 / mu) @ op
|
||||||
|
|
||||||
if pmc is not None:
|
if pmc is not None:
|
||||||
op = sparse.diags(numpy.where(pmc, 0, 1)) @ op
|
op = sparse.diags_array(numpy.where(pmc, 0, 1)) @ op
|
||||||
|
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
@ -285,8 +287,8 @@ def e2h(
|
||||||
def m2j(
|
def m2j(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdfield | None = None,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Operator for converting a magnetic current M into an electric current J.
|
Operator for converting a magnetic current M into an electric current J.
|
||||||
For use with eg. `e_full()`.
|
For use with eg. `e_full()`.
|
||||||
|
|
@ -302,79 +304,108 @@ def m2j(
|
||||||
op = curl_back(dxes[1]) / (1j * omega)
|
op = curl_back(dxes[1]) / (1j * omega)
|
||||||
|
|
||||||
if mu is not None:
|
if mu is not None:
|
||||||
op = op @ sparse.diags(1 / mu)
|
op = op @ sparse.diags_array(1 / mu)
|
||||||
|
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
||||||
def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
|
def poynting_e_cross(e: vcfdfield, dxes: dx_lists_t) -> sparse.sparray:
|
||||||
"""
|
r"""
|
||||||
Operator for computing the Poynting vector, containing the
|
Operator for computing the staggered-grid `(E \times)` part of the Poynting vector.
|
||||||
(E x) portion of the Poynting vector.
|
|
||||||
|
On the Yee grid the E and H components live on different edges, so the
|
||||||
|
electric field must be shifted by one cell in the appropriate direction
|
||||||
|
before forming the discrete cross product. This sparse operator encodes that
|
||||||
|
shifted cross product directly and is the matrix equivalent of
|
||||||
|
`functional.poynting_e_cross_h(...)`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
e: Vectorized E-field for the ExH cross product
|
e: Vectorized E-field for the ExH cross product
|
||||||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Sparse matrix containing (E x) portion of Poynting cross product.
|
Sparse matrix containing the `(E \times)` part of the staggered Poynting
|
||||||
|
cross product.
|
||||||
"""
|
"""
|
||||||
shape = [len(dx) for dx in dxes[0]]
|
shape = [len(dx) for dx in dxes[0]]
|
||||||
|
|
||||||
fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)]
|
fx, fy, fz = (shift_circ(i, shape, 1) for i in range(3))
|
||||||
|
|
||||||
dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
|
dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
|
||||||
dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
|
dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
|
||||||
Ex, Ey, Ez = [ei * da for ei, da in zip(numpy.split(e, 3), dxag)]
|
Ex, Ey, Ez = (ei * da for ei, da in zip(numpy.split(e, 3), dxag, strict=True))
|
||||||
|
|
||||||
block_diags = [[ None, fx @ -Ez, fx @ Ey],
|
block_diags = [[ None, fx @ -Ez, fx @ Ey],
|
||||||
[ fy @ Ez, None, fy @ -Ex],
|
[ fy @ Ez, None, fy @ -Ex],
|
||||||
[ fz @ -Ey, fz @ Ex, None]]
|
[ fz @ -Ey, fz @ Ex, None]]
|
||||||
block_matrix = sparse.bmat([[sparse.diags(x) if x is not None else None for x in row]
|
block_matrix = sparse.block_array([[sparse.diags_array(x) if x is not None else None for x in row]
|
||||||
for row in block_diags])
|
for row in block_diags])
|
||||||
P = block_matrix @ sparse.diags(numpy.concatenate(dxbg))
|
P = block_matrix @ sparse.diags_array(numpy.concatenate(dxbg))
|
||||||
return P
|
return P
|
||||||
|
|
||||||
|
|
||||||
def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
|
def poynting_h_cross(h: vcfdfield, dxes: dx_lists_t) -> sparse.sparray:
|
||||||
"""
|
r"""
|
||||||
Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector.
|
Operator for computing the staggered-grid `(H \times)` part of the Poynting vector.
|
||||||
|
|
||||||
|
Together with `poynting_e_cross(...)`, this provides the matrix form of the
|
||||||
|
Yee-grid cross product used in the flux helpers. The two are related by the
|
||||||
|
usual antisymmetry of the cross product,
|
||||||
|
|
||||||
|
$$
|
||||||
|
H \times E = -(E \times H),
|
||||||
|
$$
|
||||||
|
|
||||||
|
once the same staggered field placement is used on both sides.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
h: Vectorized H-field for the HxE cross product
|
h: Vectorized H-field for the HxE cross product
|
||||||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Sparse matrix containing (H x) portion of Poynting cross product.
|
Sparse matrix containing the `(H \times)` part of the staggered Poynting
|
||||||
|
cross product.
|
||||||
"""
|
"""
|
||||||
shape = [len(dx) for dx in dxes[0]]
|
shape = [len(dx) for dx in dxes[0]]
|
||||||
|
|
||||||
fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)]
|
fx, fy, fz = (shift_circ(i, shape, 1) for i in range(3))
|
||||||
|
|
||||||
dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
|
dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
|
||||||
dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
|
dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
|
||||||
Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)]
|
Hx, Hy, Hz = (sparse.diags_array(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg, strict=True))
|
||||||
|
|
||||||
P = (sparse.bmat(
|
P = (sparse.block_array(
|
||||||
[[ None, -Hz @ fx, Hy @ fx],
|
[[ None, -Hz @ fx, Hy @ fx],
|
||||||
[ Hz @ fy, None, -Hx @ fy],
|
[ Hz @ fy, None, -Hx @ fy],
|
||||||
[-Hy @ fz, Hx @ fz, None]])
|
[-Hy @ fz, Hx @ fz, None]])
|
||||||
@ sparse.diags(numpy.concatenate(dxag)))
|
@ sparse.diags_array(numpy.concatenate(dxag)))
|
||||||
return P
|
return P
|
||||||
|
|
||||||
|
|
||||||
def e_tfsf_source(
|
def e_tfsf_source(
|
||||||
TF_region: vfdfield_t,
|
TF_region: vfdfield,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdfield,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdfield | None = None,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
r"""
|
||||||
Operator that turns a desired E-field distribution into a
|
Operator that turns a desired E-field distribution into a
|
||||||
total-field/scattered-field (TFSF) source.
|
total-field/scattered-field (TFSF) source.
|
||||||
|
|
||||||
TODO: Reference Rumpf paper
|
Let `A` be the full wave operator from `e_full(...)`, and let
|
||||||
|
`Q = \mathrm{diag}(TF_region)` be the projector onto the total-field region.
|
||||||
|
Then the TFSF current operator is the commutator
|
||||||
|
|
||||||
|
$$
|
||||||
|
\frac{A Q - Q A}{-i \omega}.
|
||||||
|
$$
|
||||||
|
|
||||||
|
Inside regions where `Q` is locally constant, `A` and `Q` commute and the
|
||||||
|
source vanishes. Only cells at the TF/SF boundary contribute nonzero current,
|
||||||
|
which is exactly the desired distributed source for injecting the chosen
|
||||||
|
field into the total-field region without directly forcing the
|
||||||
|
scattered-field region.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
TF_region: Mask, which is set to 1 inside the total-field region and 0 in the
|
TF_region: Mask, which is set to 1 inside the total-field region and 0 in the
|
||||||
|
|
@ -386,27 +417,31 @@ def e_tfsf_source(
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Sparse matrix that turns an E-field into a current (J) distribution.
|
Sparse matrix that turns an E-field into a current (J) distribution.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# TODO documentation
|
|
||||||
A = e_full(omega, dxes, epsilon, mu)
|
A = e_full(omega, dxes, epsilon, mu)
|
||||||
Q = sparse.diags(TF_region)
|
Q = sparse.diags_array(TF_region)
|
||||||
return (A @ Q - Q @ A) / (-1j * omega)
|
return (A @ Q - Q @ A) / (-1j * omega)
|
||||||
|
|
||||||
|
|
||||||
def e_boundary_source(
|
def e_boundary_source(
|
||||||
mask: vfdfield_t,
|
mask: vfdfield,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdfield,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdfield | None = None,
|
||||||
periodic_mask_edges: bool = False,
|
periodic_mask_edges: bool = False,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
r"""
|
||||||
Operator that turns an E-field distrubtion into a current (J) distribution
|
Operator that turns an E-field distrubtion into a current (J) distribution
|
||||||
along the edges (external and internal) of the provided mask. This is just an
|
along the edges (external and internal) of the provided mask. This is just an
|
||||||
`e_tfsf_source()` with an additional masking step.
|
`e_tfsf_source()` with an additional masking step.
|
||||||
|
|
||||||
|
Equivalently, this helper first constructs the TFSF commutator source for the
|
||||||
|
full mask and then zeroes out all cells except the mask boundary. The
|
||||||
|
boundary is defined as the set of cells whose mask value changes under a
|
||||||
|
one-cell shift in any Cartesian direction. With `periodic_mask_edges=False`
|
||||||
|
the shifts mirror at the domain boundary; with `True` they wrap periodically.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mask: The current distribution is generated at the edges of the mask,
|
mask: The current distribution is generated at the edges of the mask,
|
||||||
i.e. any points where shifting the mask by one cell in any direction
|
i.e. any points where shifting the mask by one cell in any direction
|
||||||
|
|
@ -424,10 +459,10 @@ def e_boundary_source(
|
||||||
shape = [len(dxe) for dxe in dxes[0]]
|
shape = [len(dxe) for dxe in dxes[0]]
|
||||||
jmask = numpy.zeros_like(mask, dtype=bool)
|
jmask = numpy.zeros_like(mask, dtype=bool)
|
||||||
|
|
||||||
def shift_rot(axis: int, polarity: int) -> sparse.spmatrix:
|
def shift_rot(axis: int, polarity: int) -> sparse.sparray:
|
||||||
return shift_circ(axis=axis, shape=shape, shift_distance=polarity)
|
return shift_circ(axis=axis, shape=shape, shift_distance=polarity)
|
||||||
|
|
||||||
def shift_mir(axis: int, polarity: int) -> sparse.spmatrix:
|
def shift_mir(axis: int, polarity: int) -> sparse.sparray:
|
||||||
return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity)
|
return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity)
|
||||||
|
|
||||||
shift = shift_rot if periodic_mask_edges else shift_mir
|
shift = shift_rot if periodic_mask_edges else shift_mir
|
||||||
|
|
@ -436,7 +471,7 @@ def e_boundary_source(
|
||||||
if shape[axis] == 1:
|
if shape[axis] == 1:
|
||||||
continue
|
continue
|
||||||
for polarity in (-1, +1):
|
for polarity in (-1, +1):
|
||||||
r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original
|
r = shift(axis, polarity) - sparse.eye_array(numpy.prod(shape)) # shifted minus original
|
||||||
r3 = sparse.block_diag((r, r, r))
|
r3 = sparse.block_diag((r, r, r))
|
||||||
jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask))
|
jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask))
|
||||||
|
|
||||||
|
|
@ -447,5 +482,4 @@ def e_boundary_source(
|
||||||
# (numpy.roll(mask, -1, axis=2) != mask) |
|
# (numpy.roll(mask, -1, axis=2) != mask) |
|
||||||
# (numpy.roll(mask, +1, axis=2) != mask))
|
# (numpy.roll(mask, +1, axis=2) != mask))
|
||||||
|
|
||||||
return sparse.diags(jmask.astype(int)) @ full
|
return sparse.diags_array(jmask.astype(int)) @ full
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
Functions for creating stretched coordinate perfectly matched layer (PML) absorbers.
|
Functions for creating stretched coordinate perfectly matched layer (PML) absorbers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Callable
|
from collections.abc import Sequence, Callable
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
@ -128,6 +128,11 @@ def stretch_with_scpml(
|
||||||
dx_ai = dxes[0][axis].astype(complex)
|
dx_ai = dxes[0][axis].astype(complex)
|
||||||
dx_bi = dxes[1][axis].astype(complex)
|
dx_bi = dxes[1][axis].astype(complex)
|
||||||
|
|
||||||
|
if thickness == 0:
|
||||||
|
dxes[0][axis] = dx_ai
|
||||||
|
dxes[1][axis] = dx_bi
|
||||||
|
return dxes
|
||||||
|
|
||||||
pos = numpy.hstack((0, dx_ai.cumsum()))
|
pos = numpy.hstack((0, dx_ai.cumsum()))
|
||||||
pos_a = (pos[:-1] + pos[1:]) / 2
|
pos_a = (pos[:-1] + pos[1:]) / 2
|
||||||
pos_b = pos[:-1]
|
pos_b = pos[:-1]
|
||||||
|
|
@ -153,9 +158,6 @@ def stretch_with_scpml(
|
||||||
def l_d(x: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
|
def l_d(x: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
|
||||||
return (x - bound) / (pos[-1] - bound)
|
return (x - bound) / (pos[-1] - bound)
|
||||||
|
|
||||||
if thickness == 0:
|
|
||||||
slc = slice(None)
|
|
||||||
else:
|
|
||||||
slc = slice(-thickness, None)
|
slc = slice(-thickness, None)
|
||||||
|
|
||||||
dx_ai[slc] *= 1 + 1j * s_function(l_d(pos_a[slc])) / d / s_correction
|
dx_ai[slc] *= 1 + 1j * s_function(l_d(pos_a[slc])) / d / s_correction
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,16 @@
|
||||||
Solvers and solver interface for FDFD problems.
|
Solvers and solver interface for FDFD problems.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Callable, Dict, Any, Optional
|
from typing import Any
|
||||||
|
from collections.abc import Callable
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
from numpy.linalg import norm
|
from numpy.linalg import norm
|
||||||
import scipy.sparse.linalg # type: ignore
|
import scipy.sparse.linalg
|
||||||
|
|
||||||
from ..fdmath import dx_lists_t, vfdfield_t, vcfdfield_t
|
from ..fdmath import dx_lists_t, vfdfield, vcfdfield, vcfdfield_t
|
||||||
from . import operators
|
from . import operators
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,7 +19,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _scipy_qmr(
|
def _scipy_qmr(
|
||||||
A: scipy.sparse.csr_matrix,
|
A: scipy.sparse.csr_array,
|
||||||
b: ArrayLike,
|
b: ArrayLike,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> NDArray[numpy.float64]:
|
) -> NDArray[numpy.float64]:
|
||||||
|
|
@ -34,31 +35,32 @@ def _scipy_qmr(
|
||||||
Guess for solution (returned even if didn't converge)
|
Guess for solution (returned even if didn't converge)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
'''
|
#
|
||||||
Report on our progress
|
#Report on our progress
|
||||||
'''
|
#
|
||||||
ii = 0
|
ii = 0
|
||||||
|
|
||||||
def log_residual(xk: ArrayLike) -> None:
|
def log_residual(xk: ArrayLike) -> None:
|
||||||
nonlocal ii
|
nonlocal ii
|
||||||
ii += 1
|
ii += 1
|
||||||
if ii % 100 == 0:
|
if ii % 100 == 0:
|
||||||
cur_norm = norm(A @ xk - b)
|
cur_norm = norm(A @ xk - b) / norm(b)
|
||||||
logger.info(f'Solver residual at iteration {ii} : {cur_norm}')
|
logger.info(f'Solver residual at iteration {ii} : {cur_norm}')
|
||||||
|
|
||||||
if 'callback' in kwargs:
|
if 'callback' in kwargs:
|
||||||
|
callback = kwargs['callback']
|
||||||
|
|
||||||
def augmented_callback(xk: ArrayLike) -> None:
|
def augmented_callback(xk: ArrayLike) -> None:
|
||||||
log_residual(xk)
|
log_residual(xk)
|
||||||
kwargs['callback'](xk)
|
callback(xk)
|
||||||
|
|
||||||
kwargs['callback'] = augmented_callback
|
kwargs['callback'] = augmented_callback
|
||||||
else:
|
else:
|
||||||
kwargs['callback'] = log_residual
|
kwargs['callback'] = log_residual
|
||||||
|
|
||||||
'''
|
#
|
||||||
Run the actual solve
|
# Run the actual solve
|
||||||
'''
|
#
|
||||||
|
|
||||||
x, _ = scipy.sparse.linalg.qmr(A, b, **kwargs)
|
x, _ = scipy.sparse.linalg.qmr(A, b, **kwargs)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
@ -66,14 +68,16 @@ def _scipy_qmr(
|
||||||
def generic(
|
def generic(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
J: vcfdfield_t,
|
J: vcfdfield,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdfield,
|
||||||
mu: Optional[vfdfield_t] = None,
|
mu: vfdfield | None = None,
|
||||||
pec: Optional[vfdfield_t] = None,
|
*,
|
||||||
pmc: Optional[vfdfield_t] = None,
|
pec: vfdfield | None = None,
|
||||||
|
pmc: vfdfield | None = None,
|
||||||
adjoint: bool = False,
|
adjoint: bool = False,
|
||||||
matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
|
matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
|
||||||
matrix_solver_opts: Optional[Dict[str, Any]] = None,
|
matrix_solver_opts: dict[str, Any] | None = None,
|
||||||
|
E_guess: vcfdfield | None = None,
|
||||||
) -> vcfdfield_t:
|
) -> vcfdfield_t:
|
||||||
"""
|
"""
|
||||||
Conjugate gradient FDFD solver using CSR sparse matrices.
|
Conjugate gradient FDFD solver using CSR sparse matrices.
|
||||||
|
|
@ -93,13 +97,15 @@ def generic(
|
||||||
(at H-field locations; non-zero value indicates PMC is present)
|
(at H-field locations; non-zero value indicates PMC is present)
|
||||||
adjoint: If true, solves the adjoint problem.
|
adjoint: If true, solves the adjoint problem.
|
||||||
matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`,
|
matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`,
|
||||||
where `A`: `scipy.sparse.csr_matrix`;
|
where `A`: `scipy.sparse.csr_array`;
|
||||||
`b`: `ArrayLike`;
|
`b`: `ArrayLike`;
|
||||||
`x`: `ArrayLike`;
|
`x`: `ArrayLike`;
|
||||||
Default is a wrapped version of `scipy.sparse.linalg.qmr()`
|
Default is a wrapped version of `scipy.sparse.linalg.qmr()`
|
||||||
which doesn't return convergence info and logs the residual
|
which doesn't return convergence info and logs the residual
|
||||||
every 100 iterations.
|
every 100 iterations.
|
||||||
matrix_solver_opts: Passed as kwargs to `matrix_solver(...)`
|
matrix_solver_opts: Passed as kwargs to `matrix_solver(...)`
|
||||||
|
E_guess: Guess at the solution E-field. `matrix_solver` must accept an
|
||||||
|
`x0` argument with the same purpose.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
E-field which solves the system.
|
E-field which solves the system.
|
||||||
|
|
@ -114,17 +120,24 @@ def generic(
|
||||||
Pl, Pr = operators.e_full_preconditioners(dxes)
|
Pl, Pr = operators.e_full_preconditioners(dxes)
|
||||||
|
|
||||||
if adjoint:
|
if adjoint:
|
||||||
A = (Pl @ A0 @ Pr).H
|
A = (Pl @ A0 @ Pr).T.conjugate()
|
||||||
b = Pr.H @ b0
|
b = Pr.T.conjugate() @ b0
|
||||||
else:
|
else:
|
||||||
A = Pl @ A0 @ Pr
|
A = Pl @ A0 @ Pr
|
||||||
b = Pl @ b0
|
b = Pl @ b0
|
||||||
|
|
||||||
|
if E_guess is not None:
|
||||||
|
if adjoint:
|
||||||
|
x0 = Pr.T.conjugate() @ E_guess
|
||||||
|
else:
|
||||||
|
x0 = Pl @ E_guess
|
||||||
|
matrix_solver_opts['x0'] = x0
|
||||||
|
|
||||||
x = matrix_solver(A.tocsr(), b, **matrix_solver_opts)
|
x = matrix_solver(A.tocsr(), b, **matrix_solver_opts)
|
||||||
|
|
||||||
if adjoint:
|
if adjoint:
|
||||||
x0 = Pl.H @ x
|
x0 = Pl.T.conjugate() @ x
|
||||||
else:
|
else:
|
||||||
x0 = Pr @ x
|
x0 = Pr @ x
|
||||||
|
|
||||||
return x0
|
return vcfdfield_t(x0)
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ $$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
\nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\
|
\nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\
|
||||||
\nabla \times \vec{H}(x, y, z) &= \imath \omega \epsilon \vec{E} \\
|
\nabla \times \vec{H}(x, y, z) &= \imath \omega \epsilon \vec{E} \\
|
||||||
\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\gamma z} \\
|
\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\imath \beta z} \\
|
||||||
\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\gamma z} \\
|
\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\imath \beta z} \\
|
||||||
\end{aligned}
|
\end{aligned}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
|
|
@ -40,56 +40,57 @@ Substituting in our expressions for $\vec{E}$, $\vec{H}$ and discretizing:
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \gamma E_y \\
|
-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta E_y \\
|
||||||
-\imath \omega \mu_{yy} H_y &= -\gamma E_x - \tilde{\partial}_x E_z \\
|
-\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \tilde{\partial}_x E_z \\
|
||||||
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\
|
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\
|
||||||
\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \gamma H_y \\
|
\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta H_y \\
|
||||||
\imath \omega \epsilon_{yy} E_y &= -\gamma H_x - \hat{\partial}_x H_z \\
|
\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \hat{\partial}_x H_z \\
|
||||||
\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
|
\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
|
||||||
\end{aligned}
|
\end{aligned}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
Rewrite the last three equations as
|
Rewrite the last three equations as
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
\gamma H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
|
\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
|
||||||
\gamma H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
|
\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
|
||||||
\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
|
\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
|
||||||
\end{aligned}
|
\end{aligned}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
Now apply $\gamma \tilde{\partial}_x$ to the last equation,
|
Now apply $\imath \beta \tilde{\partial}_x$ to the last equation,
|
||||||
then substitute in for $\gamma H_x$ and $\gamma H_y$:
|
then substitute in for $\imath \beta H_x$ and $\imath \beta H_y$:
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
\gamma \tilde{\partial}_x \imath \omega E_z &= \gamma \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y
|
\imath \beta \tilde{\partial}_x \imath \omega E_z &= \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y
|
||||||
- \gamma \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
|
- \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
|
||||||
&= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z)
|
&= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z)
|
||||||
- \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
|
- \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
|
||||||
&= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x)
|
&= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x)
|
||||||
- \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y) \\
|
- \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y) \\
|
||||||
\gamma \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
\imath \beta \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
||||||
+ \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
|
+ \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
|
||||||
\end{aligned}
|
\end{aligned}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
With a similar approach (but using $\gamma \tilde{\partial}_y$ instead), we can get
|
With a similar approach (but using $\imath \beta \tilde{\partial}_y$ instead), we can get
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
\gamma \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
\imath \beta \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
||||||
+ \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
|
+ \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
|
||||||
\end{aligned}
|
\end{aligned}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
We can combine this equation for $\gamma \tilde{\partial}_y E_z$ with
|
We can combine this equation for $\imath \beta \tilde{\partial}_y E_z$ with
|
||||||
the unused $\imath \omega \mu_{xx} H_x$ and $\imath \omega \mu_{yy} H_y$ equations to get
|
the unused $\imath \omega \mu_{xx} H_x$ and $\imath \omega \mu_{yy} H_y$ equations to get
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
-\imath \omega \mu_{xx} \gamma H_x &= \gamma^2 E_y + \gamma \tilde{\partial}_y E_z \\
|
-\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \imath \beta \tilde{\partial}_y E_z \\
|
||||||
-\imath \omega \mu_{xx} \gamma H_x &= \gamma^2 E_y + \tilde{\partial}_y (
|
-\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \tilde{\partial}_y (
|
||||||
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
||||||
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
|
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
|
||||||
)\\
|
)\\
|
||||||
|
|
@ -100,22 +101,21 @@ and
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
-\imath \omega \mu_{yy} \gamma H_y &= -\gamma^2 E_x - \gamma \tilde{\partial}_x E_z \\
|
-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \imath \beta \tilde{\partial}_x E_z \\
|
||||||
-\imath \omega \mu_{yy} \gamma H_y &= -\gamma^2 E_x - \tilde{\partial}_x (
|
-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \tilde{\partial}_x (
|
||||||
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
||||||
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
|
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
|
||||||
)\\
|
)\\
|
||||||
\end{aligned}
|
\end{aligned}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
However, based on our rewritten equation for $\gamma H_x$ and the so-far unused
|
However, based on our rewritten equation for $\imath \beta H_x$ and the so-far unused
|
||||||
equation for $\imath \omega \mu_{zz} H_z$ we can also write
|
equation for $\imath \omega \mu_{zz} H_z$ we can also write
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
-\imath \omega \mu_{xx} (\gamma H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
|
-\imath \omega \mu_{xx} (\imath \beta H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
|
||||||
&= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
|
&= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + \imath \omega \mu_{xx} \hat{\partial}_x (
|
||||||
+\imath \omega \mu_{xx} \hat{\partial}_x (
|
|
||||||
\frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\
|
\frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\
|
||||||
&= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
|
&= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
|
||||||
-\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
|
-\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
|
||||||
|
|
@ -126,7 +126,7 @@ and, similarly,
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
-\imath \omega \mu_{yy} (\gamma H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
|
-\imath \omega \mu_{yy} (\imath \beta H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
|
||||||
+\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
|
+\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
|
||||||
\end{aligned}
|
\end{aligned}
|
||||||
$$
|
$$
|
||||||
|
|
@ -135,12 +135,12 @@ By combining both pairs of expressions, we get
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
-\gamma^2 E_x - \tilde{\partial}_x (
|
\beta^2 E_x - \tilde{\partial}_x (
|
||||||
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
||||||
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
|
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
|
||||||
) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
|
) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
|
||||||
+\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
|
+\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
|
||||||
\gamma^2 E_y + \tilde{\partial}_y (
|
-\beta^2 E_y + \tilde{\partial}_y (
|
||||||
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
|
||||||
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
|
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
|
||||||
) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
|
) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
|
||||||
|
|
@ -165,27 +165,25 @@ $$
|
||||||
E_y \end{bmatrix}
|
E_y \end{bmatrix}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
where $\gamma = \imath\beta$. In the literature, $\beta$ is usually used to denote
|
In the literature, $\beta$ is usually used to denote the lossless/real part of the propagation constant,
|
||||||
the lossless/real part of the propagation constant, but in `meanas` it is allowed to
|
but in `meanas` it is allowed to be complex.
|
||||||
be complex.
|
|
||||||
|
|
||||||
An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient.
|
An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient.
|
||||||
|
|
||||||
Note that $E_z$ was never discretized, so $\gamma$ and $\beta$ will need adjustment
|
Note that $E_z$ was never discretized, so $\beta$ will need adjustment to account for numerical dispersion
|
||||||
to account for numerical dispersion if the result is introduced into a space with a discretized z-axis.
|
if the result is introduced into a space with a discretized z-axis.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# TODO update module docs
|
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from collections.abc import Sequence
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray
|
||||||
from numpy.linalg import norm
|
from numpy.linalg import norm
|
||||||
import scipy.sparse as sparse # type: ignore
|
from scipy import sparse
|
||||||
|
|
||||||
from ..fdmath.operators import deriv_forward, deriv_back, cross
|
from ..fdmath.operators import deriv_forward, deriv_back, cross
|
||||||
from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
|
from ..fdmath import vec, unvec, dx_lists2_t, vcfdfield2_t, vcfdslice_t, vcfdfield2, vfdslice, vcfdslice
|
||||||
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
|
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -194,10 +192,10 @@ __author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
def operator_e(
|
def operator_e(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdslice | None = None,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
r"""
|
r"""
|
||||||
Waveguide operator of the form
|
Waveguide operator of the form
|
||||||
|
|
||||||
|
|
@ -246,12 +244,12 @@ def operator_e(
|
||||||
Dbx, Dby = deriv_back(dxes[1])
|
Dbx, Dby = deriv_back(dxes[1])
|
||||||
|
|
||||||
eps_parts = numpy.split(epsilon, 3)
|
eps_parts = numpy.split(epsilon, 3)
|
||||||
eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1])))
|
eps_xy = sparse.diags_array(numpy.hstack((eps_parts[0], eps_parts[1])))
|
||||||
eps_z_inv = sparse.diags(1 / eps_parts[2])
|
eps_z_inv = sparse.diags_array(1 / eps_parts[2])
|
||||||
|
|
||||||
mu_parts = numpy.split(mu, 3)
|
mu_parts = numpy.split(mu, 3)
|
||||||
mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0])))
|
mu_yx = sparse.diags_array(numpy.hstack((mu_parts[1], mu_parts[0])))
|
||||||
mu_z_inv = sparse.diags(1 / mu_parts[2])
|
mu_z_inv = sparse.diags_array(1 / mu_parts[2])
|
||||||
|
|
||||||
op = (
|
op = (
|
||||||
omega * omega * mu_yx @ eps_xy
|
omega * omega * mu_yx @ eps_xy
|
||||||
|
|
@ -263,10 +261,10 @@ def operator_e(
|
||||||
|
|
||||||
def operator_h(
|
def operator_h(
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdslice | None = None,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
r"""
|
r"""
|
||||||
Waveguide operator of the form
|
Waveguide operator of the form
|
||||||
|
|
||||||
|
|
@ -315,12 +313,12 @@ def operator_h(
|
||||||
Dbx, Dby = deriv_back(dxes[1])
|
Dbx, Dby = deriv_back(dxes[1])
|
||||||
|
|
||||||
eps_parts = numpy.split(epsilon, 3)
|
eps_parts = numpy.split(epsilon, 3)
|
||||||
eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0])))
|
eps_yx = sparse.diags_array(numpy.hstack((eps_parts[1], eps_parts[0])))
|
||||||
eps_z_inv = sparse.diags(1 / eps_parts[2])
|
eps_z_inv = sparse.diags_array(1 / eps_parts[2])
|
||||||
|
|
||||||
mu_parts = numpy.split(mu, 3)
|
mu_parts = numpy.split(mu, 3)
|
||||||
mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
|
mu_xy = sparse.diags_array(numpy.hstack((mu_parts[0], mu_parts[1])))
|
||||||
mu_z_inv = sparse.diags(1 / mu_parts[2])
|
mu_z_inv = sparse.diags_array(1 / mu_parts[2])
|
||||||
|
|
||||||
op = (
|
op = (
|
||||||
omega * omega * eps_yx @ mu_xy
|
omega * omega * eps_yx @ mu_xy
|
||||||
|
|
@ -331,15 +329,15 @@ def operator_h(
|
||||||
|
|
||||||
|
|
||||||
def normalized_fields_e(
|
def normalized_fields_e(
|
||||||
e_xy: ArrayLike,
|
e_xy: vcfdfield2,
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdslice | None = None,
|
||||||
prop_phase: float = 0,
|
prop_phase: float = 0,
|
||||||
) -> tuple[vcfdfield_t, vcfdfield_t]:
|
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||||
"""
|
r"""
|
||||||
Given a vector `e_xy` containing the vectorized E_x and E_y fields,
|
Given a vector `e_xy` containing the vectorized E_x and E_y fields,
|
||||||
returns normalized, vectorized E and H fields for the system.
|
returns normalized, vectorized E and H fields for the system.
|
||||||
|
|
||||||
|
|
@ -357,24 +355,40 @@ def normalized_fields_e(
|
||||||
Returns:
|
Returns:
|
||||||
`(e, h)`, where each field is vectorized, normalized,
|
`(e, h)`, where each field is vectorized, normalized,
|
||||||
and contains all three vector components.
|
and contains all three vector components.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
`e_xy` is only the transverse electric eigenvector. This helper first
|
||||||
|
reconstructs the full three-component `E` and `H` fields with `exy2e(...)`
|
||||||
|
and `exy2h(...)`, then normalizes them to unit forward power using
|
||||||
|
`_normalized_fields(...)`.
|
||||||
|
|
||||||
|
The normalization target is
|
||||||
|
|
||||||
|
$$
|
||||||
|
\Re\left[\mathrm{inner\_product}(e, h, \mathrm{conj\_h}=True)\right] = 1,
|
||||||
|
$$
|
||||||
|
|
||||||
|
so the returned fields represent a unit-power forward mode under the
|
||||||
|
discrete Yee-grid Poynting inner product.
|
||||||
"""
|
"""
|
||||||
e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy
|
e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy
|
||||||
h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy
|
h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy
|
||||||
e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon,
|
e_norm, h_norm = _normalized_fields(
|
||||||
mu=mu, prop_phase=prop_phase)
|
e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase,
|
||||||
|
)
|
||||||
return e_norm, h_norm
|
return e_norm, h_norm
|
||||||
|
|
||||||
|
|
||||||
def normalized_fields_h(
|
def normalized_fields_h(
|
||||||
h_xy: ArrayLike,
|
h_xy: vcfdfield2,
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdslice | None = None,
|
||||||
prop_phase: float = 0,
|
prop_phase: float = 0,
|
||||||
) -> tuple[vcfdfield_t, vcfdfield_t]:
|
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||||
"""
|
r"""
|
||||||
Given a vector `h_xy` containing the vectorized H_x and H_y fields,
|
Given a vector `h_xy` containing the vectorized H_x and H_y fields,
|
||||||
returns normalized, vectorized E and H fields for the system.
|
returns normalized, vectorized E and H fields for the system.
|
||||||
|
|
||||||
|
|
@ -392,39 +406,55 @@ def normalized_fields_h(
|
||||||
Returns:
|
Returns:
|
||||||
`(e, h)`, where each field is vectorized, normalized,
|
`(e, h)`, where each field is vectorized, normalized,
|
||||||
and contains all three vector components.
|
and contains all three vector components.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This is the `H_x/H_y` analogue of `normalized_fields_e(...)`. The final
|
||||||
|
normalized mode should describe the same physical solution, but because
|
||||||
|
the overall complex phase and sign are chosen heuristically,
|
||||||
|
`normalized_fields_e(...)` and `normalized_fields_h(...)` need not return
|
||||||
|
identical representatives for nearly symmetric modes.
|
||||||
"""
|
"""
|
||||||
e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy
|
e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy
|
||||||
h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy
|
h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy
|
||||||
e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon,
|
e_norm, h_norm = _normalized_fields(
|
||||||
mu=mu, prop_phase=prop_phase)
|
e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase,
|
||||||
|
)
|
||||||
return e_norm, h_norm
|
return e_norm, h_norm
|
||||||
|
|
||||||
|
|
||||||
def _normalized_fields(
|
def _normalized_fields(
|
||||||
e: vcfdfield_t,
|
e: vcfdslice,
|
||||||
h: vcfdfield_t,
|
h: vcfdslice,
|
||||||
omega: complex,
|
dxes: dx_lists2_t,
|
||||||
dxes: dx_lists_t,
|
epsilon: vfdslice,
|
||||||
epsilon: vfdfield_t,
|
|
||||||
mu: vfdfield_t | None = None,
|
|
||||||
prop_phase: float = 0,
|
prop_phase: float = 0,
|
||||||
) -> tuple[vcfdfield_t, vcfdfield_t]:
|
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||||
# TODO documentation
|
r"""
|
||||||
shape = [s.size for s in dxes[0]]
|
Normalize a reconstructed waveguide mode to unit forward power.
|
||||||
dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
|
|
||||||
|
|
||||||
E = unvec(e, shape)
|
The eigenproblem solved by `solve_mode(s)` determines only the mode shape and
|
||||||
H = unvec(h, shape)
|
propagation constant. The overall complex amplitude and sign are still free.
|
||||||
|
This helper fixes those remaining degrees of freedom in two steps:
|
||||||
|
|
||||||
|
1. Compute the discrete longitudinal Poynting flux with
|
||||||
|
`inner_product(e, h, conj_h=True)`, including the half-cell longitudinal
|
||||||
|
phase adjustment controlled by `prop_phase`.
|
||||||
|
2. Multiply both fields by a scalar chosen so that the real forward power is
|
||||||
|
`1`, then choose a reproducible phase/sign representative by making a
|
||||||
|
dominant-energy sample real and using a weighted quadrant sum to break
|
||||||
|
mirror-symmetry ties.
|
||||||
|
|
||||||
|
The sign heuristic is intentionally pragmatic rather than fundamental: it is
|
||||||
|
only there to make downstream tests and source/overlap construction choose a
|
||||||
|
consistent representative when the physical mode is symmetric.
|
||||||
|
"""
|
||||||
|
shape = [s.size for s in dxes[0]]
|
||||||
|
|
||||||
# Find time-averaged Sz and normalize to it
|
# Find time-averaged Sz and normalize to it
|
||||||
# H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
|
Sz_tavg = inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real
|
||||||
phase = numpy.exp(-1j * -prop_phase / 2)
|
|
||||||
Sz_a = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0]
|
|
||||||
Sz_b = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1]
|
|
||||||
Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5 # 0.5 since E, H are assumed to be peak (not RMS) amplitudes
|
|
||||||
assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
|
assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
|
||||||
|
|
||||||
energy = epsilon * e.conj() * e
|
energy = numpy.real(epsilon * e.conj() * e)
|
||||||
|
|
||||||
norm_amplitude = 1 / numpy.sqrt(Sz_tavg)
|
norm_amplitude = 1 / numpy.sqrt(Sz_tavg)
|
||||||
norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric
|
norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric
|
||||||
|
|
@ -434,22 +464,23 @@ def _normalized_fields(
|
||||||
sign = numpy.sign(E_weighted[:,
|
sign = numpy.sign(E_weighted[:,
|
||||||
:max(shape[0] // 2, 1),
|
:max(shape[0] // 2, 1),
|
||||||
:max(shape[1] // 2, 1)].real.sum())
|
:max(shape[1] // 2, 1)].real.sum())
|
||||||
|
assert sign != 0
|
||||||
|
|
||||||
norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle)
|
norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle)
|
||||||
|
|
||||||
e *= norm_factor
|
e *= norm_factor
|
||||||
h *= norm_factor
|
h *= norm_factor
|
||||||
|
|
||||||
return e, h
|
return vcfdslice_t(e), vcfdslice_t(h)
|
||||||
|
|
||||||
|
|
||||||
def exy2h(
|
def exy2h(
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None
|
mu: vfdslice | None = None
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
|
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
|
||||||
into a vectorized H containing all three H components
|
into a vectorized H containing all three H components
|
||||||
|
|
@ -472,10 +503,10 @@ def exy2h(
|
||||||
def hxy2e(
|
def hxy2e(
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None
|
mu: vfdslice | None = None
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
|
Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
|
||||||
into a vectorized E containing all three E components
|
into a vectorized E containing all three E components
|
||||||
|
|
@ -497,9 +528,9 @@ def hxy2e(
|
||||||
|
|
||||||
def hxy2h(
|
def hxy2h(
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
mu: vfdfield_t | None = None
|
mu: vfdslice | None = None
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
|
Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
|
||||||
into a vectorized H containing all three H components
|
into a vectorized H containing all three H components
|
||||||
|
|
@ -518,26 +549,53 @@ def hxy2h(
|
||||||
|
|
||||||
if mu is not None:
|
if mu is not None:
|
||||||
mu_parts = numpy.split(mu, 3)
|
mu_parts = numpy.split(mu, 3)
|
||||||
mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
|
mu_xy = sparse.diags_array(numpy.hstack((mu_parts[0], mu_parts[1])))
|
||||||
mu_z_inv = sparse.diags(1 / mu_parts[2])
|
mu_z_inv = sparse.diags_array(1 / mu_parts[2])
|
||||||
|
|
||||||
hxy2hz = mu_z_inv @ hxy2hz @ mu_xy
|
hxy2hz = mu_z_inv @ hxy2hz @ mu_xy
|
||||||
|
|
||||||
n_pts = dxes[1][0].size * dxes[1][1].size
|
n_pts = dxes[1][0].size * dxes[1][1].size
|
||||||
op = sparse.vstack((sparse.eye(2 * n_pts),
|
op = sparse.vstack((sparse.eye_array(2 * n_pts),
|
||||||
hxy2hz))
|
hxy2hz))
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
||||||
def exy2e(
|
def exy2e(
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
r"""
|
||||||
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
|
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
|
||||||
into a vectorized E containing all three E components
|
into a vectorized E containing all three E components
|
||||||
|
|
||||||
|
From the operator derivation (see module docs), we have
|
||||||
|
|
||||||
|
$$
|
||||||
|
\imath \omega \epsilon_{zz} E_z = \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
|
||||||
|
$$
|
||||||
|
|
||||||
|
as well as the intermediate equations
|
||||||
|
|
||||||
|
$$
|
||||||
|
\begin{aligned}
|
||||||
|
\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
|
||||||
|
\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
|
||||||
|
\end{aligned}
|
||||||
|
$$
|
||||||
|
|
||||||
|
Combining these, we get
|
||||||
|
|
||||||
|
$$
|
||||||
|
\begin{aligned}
|
||||||
|
E_z &= \frac{1}{- \omega \beta \epsilon_{zz}} ((
|
||||||
|
\hat{\partial}_y \hat{\partial}_x H_z
|
||||||
|
-\hat{\partial}_x \hat{\partial}_y H_z)
|
||||||
|
+ \imath \omega (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y))
|
||||||
|
&= \frac{1}{\imath \beta \epsilon_{zz}} (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y)
|
||||||
|
\end{aligned}
|
||||||
|
$$
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
||||||
It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy`
|
It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy`
|
||||||
|
|
@ -552,13 +610,13 @@ def exy2e(
|
||||||
|
|
||||||
if epsilon is not None:
|
if epsilon is not None:
|
||||||
epsilon_parts = numpy.split(epsilon, 3)
|
epsilon_parts = numpy.split(epsilon, 3)
|
||||||
epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1])))
|
epsilon_xy = sparse.diags_array(numpy.hstack((epsilon_parts[0], epsilon_parts[1])))
|
||||||
epsilon_z_inv = sparse.diags(1 / epsilon_parts[2])
|
epsilon_z_inv = sparse.diags_array(1 / epsilon_parts[2])
|
||||||
|
|
||||||
exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy
|
exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy
|
||||||
|
|
||||||
n_pts = dxes[0][0].size * dxes[0][1].size
|
n_pts = dxes[0][0].size * dxes[0][1].size
|
||||||
op = sparse.vstack((sparse.eye(2 * n_pts),
|
op = sparse.vstack((sparse.eye_array(2 * n_pts),
|
||||||
exy2ez))
|
exy2ez))
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
@ -566,12 +624,12 @@ def exy2e(
|
||||||
def e2h(
|
def e2h(
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
mu: vfdfield_t | None = None
|
mu: vfdslice | None = None
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Returns an operator which, when applied to a vectorized E eigenfield, produces
|
Returns an operator which, when applied to a vectorized E eigenfield, produces
|
||||||
the vectorized H eigenfield.
|
the vectorized H eigenfield slice.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
||||||
|
|
@ -584,19 +642,19 @@ def e2h(
|
||||||
"""
|
"""
|
||||||
op = curl_e(wavenumber, dxes) / (-1j * omega)
|
op = curl_e(wavenumber, dxes) / (-1j * omega)
|
||||||
if mu is not None:
|
if mu is not None:
|
||||||
op = sparse.diags(1 / mu) @ op
|
op = sparse.diags_array(1 / mu) @ op
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
||||||
def h2e(
|
def h2e(
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t
|
epsilon: vfdslice,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Returns an operator which, when applied to a vectorized H eigenfield, produces
|
Returns an operator which, when applied to a vectorized H eigenfield, produces
|
||||||
the vectorized E eigenfield.
|
the vectorized E eigenfield slice.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
||||||
|
|
@ -607,13 +665,13 @@ def h2e(
|
||||||
Returns:
|
Returns:
|
||||||
Sparse matrix representation of the operator.
|
Sparse matrix representation of the operator.
|
||||||
"""
|
"""
|
||||||
op = sparse.diags(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes)
|
op = sparse.diags_array(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes)
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
||||||
def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
|
def curl_e(wavenumber: complex, dxes: dx_lists2_t) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Discretized curl operator for use with the waveguide E field.
|
Discretized curl operator for use with the waveguide E field slice.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
||||||
|
|
@ -622,18 +680,18 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
|
||||||
Returns:
|
Returns:
|
||||||
Sparse matrix representation of the operator.
|
Sparse matrix representation of the operator.
|
||||||
"""
|
"""
|
||||||
n = 1
|
nn = 1
|
||||||
for d in dxes[0]:
|
for dd in dxes[0]:
|
||||||
n *= len(d)
|
nn *= len(dd)
|
||||||
|
|
||||||
Bz = -1j * wavenumber * sparse.eye(n)
|
Bz = -1j * wavenumber * sparse.eye_array(nn)
|
||||||
Dfx, Dfy = deriv_forward(dxes[0])
|
Dfx, Dfy = deriv_forward(dxes[0])
|
||||||
return cross([Dfx, Dfy, Bz])
|
return cross([Dfx, Dfy, Bz])
|
||||||
|
|
||||||
|
|
||||||
def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
|
def curl_h(wavenumber: complex, dxes: dx_lists2_t) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Discretized curl operator for use with the waveguide H field.
|
Discretized curl operator for use with the waveguide H field slice.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
|
||||||
|
|
@ -642,22 +700,22 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
|
||||||
Returns:
|
Returns:
|
||||||
Sparse matrix representation of the operator.
|
Sparse matrix representation of the operator.
|
||||||
"""
|
"""
|
||||||
n = 1
|
nn = 1
|
||||||
for d in dxes[1]:
|
for dd in dxes[1]:
|
||||||
n *= len(d)
|
nn *= len(dd)
|
||||||
|
|
||||||
Bz = -1j * wavenumber * sparse.eye(n)
|
Bz = -1j * wavenumber * sparse.eye_array(nn)
|
||||||
Dbx, Dby = deriv_back(dxes[1])
|
Dbx, Dby = deriv_back(dxes[1])
|
||||||
return cross([Dbx, Dby, Bz])
|
return cross([Dbx, Dby, Bz])
|
||||||
|
|
||||||
|
|
||||||
def h_err(
|
def h_err(
|
||||||
h: vcfdfield_t,
|
h: vcfdslice,
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None
|
mu: vfdslice | None = None
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates the relative error in the H field
|
Calculates the relative error in the H field
|
||||||
|
|
@ -676,7 +734,7 @@ def h_err(
|
||||||
ce = curl_e(wavenumber, dxes)
|
ce = curl_e(wavenumber, dxes)
|
||||||
ch = curl_h(wavenumber, dxes)
|
ch = curl_h(wavenumber, dxes)
|
||||||
|
|
||||||
eps_inv = sparse.diags(1 / epsilon)
|
eps_inv = sparse.diags_array(1 / epsilon)
|
||||||
|
|
||||||
if mu is None:
|
if mu is None:
|
||||||
op = ce @ eps_inv @ ch @ h - omega ** 2 * h
|
op = ce @ eps_inv @ ch @ h - omega ** 2 * h
|
||||||
|
|
@ -687,12 +745,12 @@ def h_err(
|
||||||
|
|
||||||
|
|
||||||
def e_err(
|
def e_err(
|
||||||
e: vcfdfield_t,
|
e: vcfdslice,
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdslice | None = None,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates the relative error in the E field
|
Calculates the relative error in the E field
|
||||||
|
|
@ -714,21 +772,21 @@ def e_err(
|
||||||
if mu is None:
|
if mu is None:
|
||||||
op = ch @ ce @ e - omega ** 2 * (epsilon * e)
|
op = ch @ ce @ e - omega ** 2 * (epsilon * e)
|
||||||
else:
|
else:
|
||||||
mu_inv = sparse.diags(1 / mu)
|
mu_inv = sparse.diags_array(1 / mu)
|
||||||
op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e)
|
op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e)
|
||||||
|
|
||||||
return float(norm(op) / norm(e))
|
return float(norm(op) / norm(e))
|
||||||
|
|
||||||
|
|
||||||
def sensitivity(
|
def sensitivity(
|
||||||
e_norm: vcfdfield_t,
|
e_norm: vcfdslice,
|
||||||
h_norm: vcfdfield_t,
|
h_norm: vcfdslice,
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdslice | None = None,
|
||||||
) -> vcfdfield_t:
|
) -> vcfdslice_t:
|
||||||
r"""
|
r"""
|
||||||
Given a waveguide structure (`dxes`, `epsilon`, `mu`) and mode fields
|
Given a waveguide structure (`dxes`, `epsilon`, `mu`) and mode fields
|
||||||
(`e_norm`, `h_norm`, `wavenumber`, `omega`), calculates the sensitivity of the wavenumber
|
(`e_norm`, `h_norm`, `wavenumber`, `omega`), calculates the sensitivity of the wavenumber
|
||||||
|
|
@ -802,11 +860,11 @@ def sensitivity(
|
||||||
Dbx, Dby = deriv_back(dxes[1])
|
Dbx, Dby = deriv_back(dxes[1])
|
||||||
|
|
||||||
eps_x, eps_y, eps_z = numpy.split(epsilon, 3)
|
eps_x, eps_y, eps_z = numpy.split(epsilon, 3)
|
||||||
eps_xy = sparse.diags(numpy.hstack((eps_x, eps_y)))
|
eps_xy = sparse.diags_array(numpy.hstack((eps_x, eps_y)))
|
||||||
eps_z_inv = sparse.diags(1 / eps_z)
|
eps_z_inv = sparse.diags_array(1 / eps_z)
|
||||||
|
|
||||||
mu_x, mu_y, _mu_z = numpy.split(mu, 3)
|
mu_x, mu_y, _mu_z = numpy.split(mu, 3)
|
||||||
mu_yx = sparse.diags(numpy.hstack((mu_y, mu_x)))
|
mu_yx = sparse.diags_array(numpy.hstack((mu_y, mu_x)))
|
||||||
|
|
||||||
da_exxhyy = vec(dxes[1][0][:, None] * dxes[0][1][None, :])
|
da_exxhyy = vec(dxes[1][0][:, None] * dxes[0][1][None, :])
|
||||||
da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None])
|
da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None])
|
||||||
|
|
@ -820,15 +878,15 @@ def sensitivity(
|
||||||
norm = hv_yx_conj @ ev_xy
|
norm = hv_yx_conj @ ev_xy
|
||||||
|
|
||||||
sens_tot = numpy.concatenate([sens_xy1 + sens_xy2, sens_z]) / (2 * wavenumber * norm)
|
sens_tot = numpy.concatenate([sens_xy1 + sens_xy2, sens_z]) / (2 * wavenumber * norm)
|
||||||
return sens_tot
|
return vcfdslice_t(sens_tot)
|
||||||
|
|
||||||
|
|
||||||
def solve_modes(
|
def solve_modes(
|
||||||
mode_numbers: list[int],
|
mode_numbers: Sequence[int],
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
mu: vfdfield_t | None = None,
|
mu: vfdslice | None = None,
|
||||||
mode_margin: int = 2,
|
mode_margin: int = 2,
|
||||||
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
|
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -845,32 +903,38 @@ def solve_modes(
|
||||||
ability to find the correct mode. Default 2.
|
ability to find the correct mode. Default 2.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
e_xys: list of vfdfield_t specifying fields
|
e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
|
||||||
wavenumbers: list of wavenumbers
|
wavenumbers: list of wavenumbers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
'''
|
#
|
||||||
Solve for the largest-magnitude eigenvalue of the real operator
|
# Solve for the largest-magnitude eigenvalue of the real operator
|
||||||
'''
|
#
|
||||||
dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
|
dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
|
||||||
mu_real = None if mu is None else numpy.real(mu)
|
mu_real = None if mu is None else numpy.real(mu)
|
||||||
A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real)
|
A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real)
|
||||||
|
|
||||||
eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
|
eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
|
||||||
e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)]
|
keep_inds = -(numpy.array(mode_numbers) + 1)
|
||||||
|
e_xys = eigvecs[:, keep_inds].T
|
||||||
|
eigvals = eigvals[keep_inds]
|
||||||
|
|
||||||
'''
|
#
|
||||||
Now solve for the eigenvector of the full operator, using the real operator's
|
# Now solve for the eigenvector of the full operator, using the real operator's
|
||||||
eigenvector as an initial guess for Rayleigh quotient iteration.
|
# eigenvector as an initial guess for Rayleigh quotient iteration.
|
||||||
'''
|
#
|
||||||
A = operator_e(omega, dxes, epsilon, mu)
|
A = operator_e(omega, dxes, epsilon, mu)
|
||||||
for nn in range(len(mode_numbers)):
|
for nn in range(len(mode_numbers)):
|
||||||
eigvals[nn], e_xys[:, nn] = rayleigh_quotient_iteration(A, e_xys[:, nn])
|
eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
|
||||||
|
|
||||||
# Calculate the wave-vector (force the real part to be positive)
|
# Calculate the wave-vector (force the real part to be positive)
|
||||||
wavenumbers = numpy.sqrt(eigvals)
|
wavenumbers = numpy.sqrt(eigvals)
|
||||||
wavenumbers *= numpy.sign(numpy.real(wavenumbers))
|
wavenumbers *= numpy.sign(numpy.real(wavenumbers))
|
||||||
|
|
||||||
|
order = wavenumbers.argsort()[::-1]
|
||||||
|
e_xys = e_xys[order]
|
||||||
|
wavenumbers = wavenumbers[order]
|
||||||
|
|
||||||
return e_xys, wavenumbers
|
return e_xys, wavenumbers
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -878,7 +942,7 @@ def solve_mode(
|
||||||
mode_number: int,
|
mode_number: int,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[vcfdfield_t, complex]:
|
) -> tuple[vcfdfield2_t, complex]:
|
||||||
"""
|
"""
|
||||||
Wrapper around `solve_modes()` that solves for a single mode.
|
Wrapper around `solve_modes()` that solves for a single mode.
|
||||||
|
|
||||||
|
|
@ -892,4 +956,66 @@ def solve_mode(
|
||||||
"""
|
"""
|
||||||
kwargs['mode_numbers'] = [mode_number]
|
kwargs['mode_numbers'] = [mode_number]
|
||||||
e_xys, wavenumbers = solve_modes(*args, **kwargs)
|
e_xys, wavenumbers = solve_modes(*args, **kwargs)
|
||||||
return e_xys[:, 0], wavenumbers[0]
|
return vcfdfield2_t(e_xys[0]), wavenumbers[0]
|
||||||
|
|
||||||
|
|
||||||
|
def inner_product(
|
||||||
|
e1: vcfdfield2,
|
||||||
|
h2: vcfdfield2,
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
prop_phase: float = 0,
|
||||||
|
conj_h: bool = False,
|
||||||
|
trapezoid: bool = False,
|
||||||
|
) -> complex:
|
||||||
|
r"""
|
||||||
|
Compute the discrete waveguide overlap / Poynting inner product.
|
||||||
|
|
||||||
|
This is the 2D transverse integral corresponding to the time-averaged
|
||||||
|
longitudinal Poynting flux,
|
||||||
|
|
||||||
|
$$
|
||||||
|
\frac{1}{2}\int (E_x H_y - E_y H_x) \, dx \, dy
|
||||||
|
$$
|
||||||
|
|
||||||
|
with the Yee-grid staggering and optional propagation-phase adjustment used
|
||||||
|
by the waveguide helpers in this module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e1: Vectorized electric field, typically from `exy2e(...)` or
|
||||||
|
`normalized_fields_e(...)`.
|
||||||
|
h2: Vectorized magnetic field, typically from `hxy2h(...)`,
|
||||||
|
`exy2h(...)`, or one of the normalization helpers.
|
||||||
|
dxes: Two-dimensional Yee-grid spacing lists `[dx_e, dx_h]`.
|
||||||
|
prop_phase: Phase advance over one propagation cell. This is used to
|
||||||
|
shift the H field into the same longitudinal reference plane as the
|
||||||
|
E field.
|
||||||
|
conj_h: Whether to conjugate `h2` before forming the overlap. Use
|
||||||
|
`True` for the usual time-averaged power normalization.
|
||||||
|
trapezoid: Whether to use trapezoidal quadrature instead of the default
|
||||||
|
rectangular Yee-cell sum.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complex overlap / longitudinal power integral.
|
||||||
|
"""
|
||||||
|
|
||||||
|
shape = [s.size for s in dxes[0]]
|
||||||
|
|
||||||
|
# H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
|
||||||
|
phase = numpy.exp(-1j * -prop_phase / 2)
|
||||||
|
|
||||||
|
E1 = unvec(e1, shape)
|
||||||
|
H2 = unvec(h2, shape) * phase
|
||||||
|
|
||||||
|
if conj_h:
|
||||||
|
H2 = numpy.conj(H2)
|
||||||
|
|
||||||
|
# Find time-averaged Sz and normalize to it
|
||||||
|
dxes_real = [[numpy.real(dxyz) for dxyz in dxeh] for dxeh in dxes]
|
||||||
|
if trapezoid:
|
||||||
|
Sz_a = numpy.trapezoid(numpy.trapezoid(E1[0] * H2[1], numpy.cumsum(dxes_real[0][1])), numpy.cumsum(dxes_real[1][0]))
|
||||||
|
Sz_b = numpy.trapezoid(numpy.trapezoid(E1[1] * H2[0], numpy.cumsum(dxes_real[0][0])), numpy.cumsum(dxes_real[1][1]))
|
||||||
|
else:
|
||||||
|
Sz_a = E1[0] * H2[1] * dxes_real[1][0][:, None] * dxes_real[0][1][None, :]
|
||||||
|
Sz_b = E1[1] * H2[0] * dxes_real[0][0][:, None] * dxes_real[1][1][None, :]
|
||||||
|
Sz = 0.5 * (Sz_a.sum() - Sz_b.sum())
|
||||||
|
return Sz
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,40 @@ Tools for working with waveguide modes in 3D domains.
|
||||||
|
|
||||||
This module relies heavily on `waveguide_2d` and mostly just transforms
|
This module relies heavily on `waveguide_2d` and mostly just transforms
|
||||||
its parameters into 2D equivalents and expands the results back into 3D.
|
its parameters into 2D equivalents and expands the results back into 3D.
|
||||||
|
|
||||||
|
The intended workflow is:
|
||||||
|
|
||||||
|
1. Select a single-cell slice normal to the propagation axis.
|
||||||
|
2. Solve the corresponding 2D mode problem with `solve_mode(...)`.
|
||||||
|
3. Turn that mode into a one-sided source with `compute_source(...)`.
|
||||||
|
4. Build an overlap window with `compute_overlap_e(...)` for port readout.
|
||||||
|
|
||||||
|
`polarity` is part of the public convention throughout this module:
|
||||||
|
|
||||||
|
- `+1` means forward propagation toward increasing index along `axis`
|
||||||
|
- `-1` means backward propagation toward decreasing index along `axis`
|
||||||
|
|
||||||
|
That same convention controls which side of the selected slice is used for the
|
||||||
|
overlap window and how the expanded field is phased.
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Any
|
from typing import Any, TypedDict, cast
|
||||||
|
import warnings
|
||||||
|
from collections.abc import Sequence
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
from numpy import complexfloating
|
||||||
|
|
||||||
from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, cfdfield_t
|
from ..fdmath import vec, unvec, dx_lists_t, cfdfield_t, fdfield, cfdfield
|
||||||
from . import operators, waveguide_2d
|
from . import operators, waveguide_2d
|
||||||
|
|
||||||
|
|
||||||
|
class Waveguide3DMode(TypedDict):
|
||||||
|
wavenumber: complex
|
||||||
|
wavenumber_2d: complex
|
||||||
|
H: NDArray[complexfloating]
|
||||||
|
E: NDArray[complexfloating]
|
||||||
|
|
||||||
|
|
||||||
def solve_mode(
|
def solve_mode(
|
||||||
mode_number: int,
|
mode_number: int,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
|
|
@ -19,10 +44,10 @@ def solve_mode(
|
||||||
axis: int,
|
axis: int,
|
||||||
polarity: int,
|
polarity: int,
|
||||||
slices: Sequence[slice],
|
slices: Sequence[slice],
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
) -> dict[str, complex | NDArray[numpy.float_]]:
|
) -> Waveguide3DMode:
|
||||||
"""
|
r"""
|
||||||
Given a 3D grid, selects a slice from the grid and attempts to
|
Given a 3D grid, selects a slice from the grid and attempts to
|
||||||
solve for an eigenmode propagating through that slice.
|
solve for an eigenmode propagating through that slice.
|
||||||
|
|
||||||
|
|
@ -33,27 +58,31 @@ def solve_mode(
|
||||||
axis: Propagation axis (0=x, 1=y, 2=z)
|
axis: Propagation axis (0=x, 1=y, 2=z)
|
||||||
polarity: Propagation direction (+1 for +ve, -1 for -ve)
|
polarity: Propagation direction (+1 for +ve, -1 for -ve)
|
||||||
slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use
|
slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use
|
||||||
as the waveguide cross-section. `slices[axis]` should select only one item.
|
as the waveguide cross-section. `slices[axis]` must select exactly one item.
|
||||||
epsilon: Dielectric constant
|
epsilon: Dielectric constant
|
||||||
mu: Magnetic permeability (default 1 everywhere)
|
mu: Magnetic permeability (default 1 everywhere)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
```
|
Dictionary containing:
|
||||||
{
|
|
||||||
'E': list[NDArray[numpy.float_]],
|
- `E`: full-grid electric field for the solved mode
|
||||||
'H': list[NDArray[numpy.float_]],
|
- `H`: full-grid magnetic field for the solved mode
|
||||||
'wavenumber': complex,
|
- `wavenumber`: propagation constant corrected for the discretized
|
||||||
}
|
propagation axis
|
||||||
```
|
- `wavenumber_2d`: propagation constant of the reduced 2D eigenproblem
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The returned fields are normalized through the `waveguide_2d`
|
||||||
|
normalization convention before being expanded back to 3D.
|
||||||
"""
|
"""
|
||||||
if mu is None:
|
if mu is None:
|
||||||
mu = numpy.ones_like(epsilon)
|
mu = numpy.ones_like(epsilon)
|
||||||
|
|
||||||
slices = tuple(slices)
|
slices = tuple(slices)
|
||||||
|
|
||||||
'''
|
#
|
||||||
Solve the 2D problem in the specified plane
|
# Solve the 2D problem in the specified plane
|
||||||
'''
|
#
|
||||||
# Define rotation to set z as propagation direction
|
# Define rotation to set z as propagation direction
|
||||||
order = numpy.roll(range(3), 2 - axis)
|
order = numpy.roll(range(3), 2 - axis)
|
||||||
reverse_order = numpy.roll(range(3), axis - 2)
|
reverse_order = numpy.roll(range(3), axis - 2)
|
||||||
|
|
@ -71,9 +100,10 @@ def solve_mode(
|
||||||
}
|
}
|
||||||
e_xy, wavenumber_2d = waveguide_2d.solve_mode(mode_number, **args_2d)
|
e_xy, wavenumber_2d = waveguide_2d.solve_mode(mode_number, **args_2d)
|
||||||
|
|
||||||
'''
|
#
|
||||||
Apply corrections and expand to 3D
|
# Apply corrections and expand to 3D
|
||||||
'''
|
#
|
||||||
|
|
||||||
# Correct wavenumber to account for numerical dispersion.
|
# Correct wavenumber to account for numerical dispersion.
|
||||||
wavenumber = 2 / dx_prop * numpy.arcsin(wavenumber_2d * dx_prop / 2)
|
wavenumber = 2 / dx_prop * numpy.arcsin(wavenumber_2d * dx_prop / 2)
|
||||||
|
|
||||||
|
|
@ -92,11 +122,12 @@ def solve_mode(
|
||||||
# Expand E, H to full epsilon space we were given
|
# Expand E, H to full epsilon space we were given
|
||||||
E = numpy.zeros_like(epsilon, dtype=complex)
|
E = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
H = numpy.zeros_like(epsilon, dtype=complex)
|
H = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
for a, o in enumerate(reverse_order):
|
for aa, oo in enumerate(reverse_order):
|
||||||
E[(a, *slices)] = e[o][:, :, None].transpose(reverse_order)
|
iii = cast('tuple[slice | int]', (aa, *slices))
|
||||||
H[(a, *slices)] = h[o][:, :, None].transpose(reverse_order)
|
E[iii] = e[oo][:, :, None].transpose(reverse_order)
|
||||||
|
H[iii] = h[oo][:, :, None].transpose(reverse_order)
|
||||||
|
|
||||||
results = {
|
results: Waveguide3DMode = {
|
||||||
'wavenumber': wavenumber,
|
'wavenumber': wavenumber,
|
||||||
'wavenumber_2d': wavenumber_2d,
|
'wavenumber_2d': wavenumber_2d,
|
||||||
'H': H,
|
'H': H,
|
||||||
|
|
@ -106,15 +137,15 @@ def solve_mode(
|
||||||
|
|
||||||
|
|
||||||
def compute_source(
|
def compute_source(
|
||||||
E: cfdfield_t,
|
E: cfdfield,
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
omega: complex,
|
omega: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
axis: int,
|
axis: int,
|
||||||
polarity: int,
|
polarity: int,
|
||||||
slices: Sequence[slice],
|
slices: Sequence[slice],
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
) -> cfdfield_t:
|
) -> cfdfield_t:
|
||||||
"""
|
"""
|
||||||
Given an eigenmode obtained by `solve_mode`, returns the current source distribution
|
Given an eigenmode obtained by `solve_mode`, returns the current source distribution
|
||||||
|
|
@ -132,7 +163,14 @@ def compute_source(
|
||||||
mu: Magnetic permeability (default 1 everywhere)
|
mu: Magnetic permeability (default 1 everywhere)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
J distribution for the unidirectional source
|
`J` distribution for a one-sided electric-current source.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The source is built from the expanded mode field and a boundary-source
|
||||||
|
operator. The resulting current is intended to be injected with the
|
||||||
|
same sign convention used elsewhere in the package:
|
||||||
|
|
||||||
|
`E -= dt * J / epsilon`
|
||||||
"""
|
"""
|
||||||
E_expanded = expand_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis,
|
E_expanded = expand_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis,
|
||||||
polarity=polarity, slices=slices)
|
polarity=polarity, slices=slices)
|
||||||
|
|
@ -148,66 +186,113 @@ def compute_source(
|
||||||
|
|
||||||
masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu))
|
masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu))
|
||||||
J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:])
|
J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:])
|
||||||
return J
|
return cfdfield_t(J)
|
||||||
|
|
||||||
|
|
||||||
def compute_overlap_e(
|
def compute_overlap_e(
|
||||||
E: cfdfield_t,
|
E: cfdfield,
|
||||||
wavenumber: complex,
|
|
||||||
dxes: dx_lists_t,
|
|
||||||
axis: int,
|
|
||||||
polarity: int,
|
|
||||||
slices: Sequence[slice],
|
|
||||||
) -> cfdfield_t: # TODO DOCS
|
|
||||||
"""
|
|
||||||
Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the
|
|
||||||
mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn)
|
|
||||||
[assumes reflection symmetry].
|
|
||||||
|
|
||||||
TODO: add reference
|
|
||||||
|
|
||||||
Args:
|
|
||||||
E: E-field of the mode
|
|
||||||
H: H-field of the mode (advanced by half of a Yee cell from E)
|
|
||||||
wavenumber: Wavenumber of the mode
|
|
||||||
omega: Angular frequency of the simulation
|
|
||||||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
|
||||||
axis: Propagation axis (0=x, 1=y, 2=z)
|
|
||||||
polarity: Propagation direction (+1 for +ve, -1 for -ve)
|
|
||||||
slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use
|
|
||||||
as the waveguide cross-section. slices[axis] should select only one item.
|
|
||||||
mu: Magnetic permeability (default 1 everywhere)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
overlap_e such that `numpy.sum(overlap_e * other_e.conj())` computes the overlap integral
|
|
||||||
"""
|
|
||||||
slices = tuple(slices)
|
|
||||||
|
|
||||||
Ee = expand_e(E=E, wavenumber=wavenumber, dxes=dxes,
|
|
||||||
axis=axis, polarity=polarity, slices=slices)
|
|
||||||
|
|
||||||
start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity))
|
|
||||||
|
|
||||||
slices2_l = list(slices)
|
|
||||||
slices2_l[axis] = slice(start, stop)
|
|
||||||
slices2 = (slice(None), *slices2_l)
|
|
||||||
|
|
||||||
Etgt = numpy.zeros_like(Ee)
|
|
||||||
Etgt[slices2] = Ee[slices2]
|
|
||||||
|
|
||||||
Etgt /= (Etgt.conj() * Etgt).sum()
|
|
||||||
return Etgt
|
|
||||||
|
|
||||||
|
|
||||||
def expand_e(
|
|
||||||
E: cfdfield_t,
|
|
||||||
wavenumber: complex,
|
wavenumber: complex,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
axis: int,
|
axis: int,
|
||||||
polarity: int,
|
polarity: int,
|
||||||
slices: Sequence[slice],
|
slices: Sequence[slice],
|
||||||
) -> cfdfield_t:
|
) -> cfdfield_t:
|
||||||
|
r"""
|
||||||
|
Build an overlap field for projecting another 3D electric field onto a mode.
|
||||||
|
|
||||||
|
The returned field is intended for the discrete overlap expression
|
||||||
|
|
||||||
|
$$
|
||||||
|
\sum \mathrm{overlap\_e} \; E_\mathrm{other}^*
|
||||||
|
$$
|
||||||
|
|
||||||
|
where the sum is over the full Yee-grid field storage.
|
||||||
|
|
||||||
|
The construction uses a two-cell window immediately upstream of the selected
|
||||||
|
slice:
|
||||||
|
|
||||||
|
- for `polarity=+1`, the two cells just before `slices[axis].start`
|
||||||
|
- for `polarity=-1`, the two cells just after `slices[axis].stop`
|
||||||
|
|
||||||
|
The window is clipped to the simulation domain if necessary. A clipped but
|
||||||
|
non-empty window raises `RuntimeWarning`; an empty window raises
|
||||||
|
`ValueError`.
|
||||||
|
|
||||||
|
The derivation below assumes reflection symmetry and the standard waveguide
|
||||||
|
overlap relation involving
|
||||||
|
|
||||||
|
$$
|
||||||
|
\int ((E \times H_\mathrm{mode}) + (E_\mathrm{mode} \times H)) \cdot dn.
|
||||||
|
$$
|
||||||
|
|
||||||
|
E x H_mode + E_mode x H
|
||||||
|
-> Ex Hmy - EyHmx + Emx Hy - Emy Hx (Z-prop)
|
||||||
|
Ex we/B Emx + Ex i/B dy Hmz - Ey (-we/B Emy) - Ey i/B dx Hmz
|
||||||
|
we/B (Ex Emx + Ey Emy) + i/B (Ex dy Hmz - Ey dx Hmz)
|
||||||
|
we/B (Ex Emx + Ey Emy) + i/B (Ex dy (dx Emy - dy Emx) - Ey dx (dx Emy - dy Emx))
|
||||||
|
we/B (Ex Emx + Ey Emy) + i/B (Ex dy dx Emy - Ex dy dy Emx - Ey dx dx Emy - Ey dx dy Emx)
|
||||||
|
|
||||||
|
Ex j/wu (-jB Emx - dx Emz) - Ey j/wu (dy Emz + jB Emy)
|
||||||
|
B/wu (Ex Emx + Ey Emy) - j/wu (Ex dx Emz + Ey dy Emz)
|
||||||
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
E: E-field of the mode
|
||||||
|
wavenumber: Wavenumber of the mode
|
||||||
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
||||||
|
axis: Propagation axis (0=x, 1=y, 2=z)
|
||||||
|
polarity: Propagation direction (+1 for +ve, -1 for -ve)
|
||||||
|
slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use
|
||||||
|
as the waveguide cross-section. slices[axis] should select only one item.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`overlap_e` normalized so that `numpy.sum(overlap_e * E.conj()) == 1`
|
||||||
|
over the retained overlap window.
|
||||||
"""
|
"""
|
||||||
|
slices = tuple(slices)
|
||||||
|
|
||||||
|
Ee = expand_e(E=E, wavenumber=wavenumber, dxes=dxes,
|
||||||
|
axis=axis, polarity=polarity, slices=slices)
|
||||||
|
|
||||||
|
axis_size = E.shape[axis + 1]
|
||||||
|
if polarity > 0:
|
||||||
|
start = slices[axis].start - 2
|
||||||
|
stop = slices[axis].start
|
||||||
|
else:
|
||||||
|
start = slices[axis].stop
|
||||||
|
stop = slices[axis].stop + 2
|
||||||
|
|
||||||
|
clipped_start = max(0, start)
|
||||||
|
clipped_stop = min(axis_size, stop)
|
||||||
|
if clipped_start >= clipped_stop:
|
||||||
|
raise ValueError('Requested overlap window lies outside the domain')
|
||||||
|
if clipped_start != start or clipped_stop != stop:
|
||||||
|
warnings.warn('Requested overlap window was clipped to fit within the domain', RuntimeWarning, stacklevel=2)
|
||||||
|
|
||||||
|
slices2_l = list(slices)
|
||||||
|
slices2_l[axis] = slice(clipped_start, clipped_stop)
|
||||||
|
slices2 = (slice(None), *slices2_l)
|
||||||
|
|
||||||
|
Etgt = numpy.zeros_like(Ee)
|
||||||
|
Etgt[slices2] = Ee[slices2]
|
||||||
|
|
||||||
|
# Note: We normalize so that (Etgt @ E.conj()) == 1, so (Etgt @ Etgt.conj) != 1
|
||||||
|
norm = (Etgt.conj() * Etgt).sum()
|
||||||
|
if norm == 0:
|
||||||
|
raise ValueError('Requested overlap window contains no overlap field support')
|
||||||
|
Etgt = Etgt / norm
|
||||||
|
return cfdfield_t(Etgt)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_e(
|
||||||
|
E: cfdfield,
|
||||||
|
wavenumber: complex,
|
||||||
|
dxes: dx_lists_t,
|
||||||
|
axis: int,
|
||||||
|
polarity: int,
|
||||||
|
slices: Sequence[slice],
|
||||||
|
) -> cfdfield_t:
|
||||||
|
r"""
|
||||||
Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D
|
Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D
|
||||||
slice where the mode was calculated to the entire domain (along the propagation
|
slice where the mode was calculated to the entire domain (along the propagation
|
||||||
axis). This assumes the epsilon cross-section remains constant throughout the
|
axis). This assumes the epsilon cross-section remains constant throughout the
|
||||||
|
|
@ -225,6 +310,16 @@ def expand_e(
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`E`, with the original field expanded along the specified `axis`.
|
`E`, with the original field expanded along the specified `axis`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This helper assumes that the waveguide cross-section remains constant
|
||||||
|
along the propagation axis and applies the phase factor
|
||||||
|
|
||||||
|
$$
|
||||||
|
e^{-i \, \mathrm{polarity} \, wavenumber \, \Delta z}
|
||||||
|
$$
|
||||||
|
|
||||||
|
to each copied slice.
|
||||||
"""
|
"""
|
||||||
slices = tuple(slices)
|
slices = tuple(slices)
|
||||||
|
|
||||||
|
|
@ -245,4 +340,4 @@ def expand_e(
|
||||||
slices_in = (slice(None), *slices)
|
slices_in = (slice(None), *slices)
|
||||||
|
|
||||||
E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in]
|
E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in]
|
||||||
return E_expanded
|
return cfdfield_t(E_expanded)
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,181 @@
|
||||||
"""
|
r"""
|
||||||
Operators and helper functions for cylindrical waveguides with unchanging cross-section.
|
Operators and helper functions for cylindrical waveguides with unchanging cross-section.
|
||||||
|
|
||||||
WORK IN PROGRESS, CURRENTLY BROKEN
|
Waveguide operator is derived according to 10.1364/OL.33.001848.
|
||||||
|
|
||||||
As the z-dependence is known, all the functions in this file assume a 2D grid
|
As in `waveguide_2d`, the propagation dependence is separated from the
|
||||||
(i.e. `dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`).
|
transverse solve. Here the propagation coordinate is the bend angle `\theta`,
|
||||||
|
and the fields are assumed to have the form
|
||||||
|
|
||||||
|
$$
|
||||||
|
\vec{E}(r, y, \theta), \vec{H}(r, y, \theta) \propto e^{-\imath m \theta},
|
||||||
|
$$
|
||||||
|
|
||||||
|
where `m` is the angular wavenumber returned by `solve_mode(s)`. It is often
|
||||||
|
convenient to introduce the corresponding linear wavenumber
|
||||||
|
|
||||||
|
$$
|
||||||
|
\beta = \frac{m}{r_{\min}},
|
||||||
|
$$
|
||||||
|
|
||||||
|
so that the cylindrical problem resembles the straight-waveguide problem with
|
||||||
|
additional metric factors.
|
||||||
|
|
||||||
|
Those metric factors live on the staggered radial Yee grids. If the left edge of
|
||||||
|
the computational window is at `r = r_{\min}`, define the electric-grid and
|
||||||
|
magnetic-grid radial sample locations by
|
||||||
|
|
||||||
|
$$
|
||||||
|
\begin{aligned}
|
||||||
|
r_a(n) &= r_{\min} + \sum_{j \le n} \Delta r_{e, j}, \\
|
||||||
|
r_b\!\left(n + \tfrac{1}{2}\right) &= r_{\min} + \tfrac{1}{2}\Delta r_{e, n}
|
||||||
|
+ \sum_{j < n} \Delta r_{h, j},
|
||||||
|
\end{aligned}
|
||||||
|
$$
|
||||||
|
|
||||||
|
and from them the diagonal metric matrices
|
||||||
|
|
||||||
|
$$
|
||||||
|
\begin{aligned}
|
||||||
|
T_a &= \operatorname{diag}(r_a / r_{\min}), \\
|
||||||
|
T_b &= \operatorname{diag}(r_b / r_{\min}).
|
||||||
|
\end{aligned}
|
||||||
|
$$
|
||||||
|
|
||||||
|
With the same forward/backward derivative notation used in `waveguide_2d`, the
|
||||||
|
coordinate-transformed discrete curl equations used here are
|
||||||
|
|
||||||
|
$$
|
||||||
|
\begin{aligned}
|
||||||
|
-\imath \omega \mu_{rr} H_r &= \tilde{\partial}_y E_z + \imath \beta T_a^{-1} E_y, \\
|
||||||
|
-\imath \omega \mu_{yy} H_y &= -\imath \beta T_b^{-1} E_r
|
||||||
|
- T_b^{-1} \tilde{\partial}_r (T_a E_z), \\
|
||||||
|
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_r E_y - \tilde{\partial}_y E_r, \\
|
||||||
|
\imath \beta H_y &= -\imath \omega T_b \epsilon_{rr} E_r - T_b \hat{\partial}_y H_z, \\
|
||||||
|
\imath \beta H_r &= \imath \omega T_a \epsilon_{yy} E_y
|
||||||
|
- T_b T_a^{-1} \hat{\partial}_r (T_b H_z), \\
|
||||||
|
\imath \omega E_z &= T_a \epsilon_{zz}^{-1}
|
||||||
|
\left(\hat{\partial}_r H_y - \hat{\partial}_y H_r\right).
|
||||||
|
\end{aligned}
|
||||||
|
$$
|
||||||
|
|
||||||
|
The first three equations are the cylindrical analogue of the straight-guide
|
||||||
|
relations for `H_r`, `H_y`, and `H_z`. The next two are the metric-weighted
|
||||||
|
versions of the straight-guide identities for `\imath \beta H_y` and
|
||||||
|
`\imath \beta H_r`, and the last equation plays the same role as the
|
||||||
|
longitudinal `E_z` reconstruction in `waveguide_2d`.
|
||||||
|
|
||||||
|
Following the same elimination steps as in `waveguide_2d`, apply
|
||||||
|
`\imath \beta \tilde{\partial}_r` and `\imath \beta \tilde{\partial}_y` to the
|
||||||
|
equation for `E_z`, substitute for `\imath \beta H_r` and `\imath \beta H_y`,
|
||||||
|
and then eliminate `H_z` with
|
||||||
|
|
||||||
|
$$
|
||||||
|
H_z = \frac{1}{-\imath \omega \mu_{zz}}
|
||||||
|
\left(\tilde{\partial}_r E_y - \tilde{\partial}_y E_r\right).
|
||||||
|
$$
|
||||||
|
|
||||||
|
This yields the transverse electric eigenproblem implemented by
|
||||||
|
`cylindrical_operator(...)`:
|
||||||
|
|
||||||
|
$$
|
||||||
|
\beta^2
|
||||||
|
\begin{bmatrix} E_r \\ E_y \end{bmatrix}
|
||||||
|
=
|
||||||
|
\left(
|
||||||
|
\omega^2
|
||||||
|
\begin{bmatrix}
|
||||||
|
T_b^2 \mu_{yy} \epsilon_{xx} & 0 \\
|
||||||
|
0 & T_a^2 \mu_{xx} \epsilon_{yy}
|
||||||
|
\end{bmatrix}
|
||||||
|
+
|
||||||
|
\begin{bmatrix}
|
||||||
|
-T_b \mu_{yy} \hat{\partial}_y \\
|
||||||
|
T_a \mu_{xx} \hat{\partial}_x
|
||||||
|
\end{bmatrix}
|
||||||
|
T_b \mu_{zz}^{-1}
|
||||||
|
\begin{bmatrix}
|
||||||
|
-\tilde{\partial}_y & \tilde{\partial}_x
|
||||||
|
\end{bmatrix}
|
||||||
|
+
|
||||||
|
\begin{bmatrix}
|
||||||
|
\tilde{\partial}_x \\
|
||||||
|
\tilde{\partial}_y
|
||||||
|
\end{bmatrix}
|
||||||
|
T_a \epsilon_{zz}^{-1}
|
||||||
|
\begin{bmatrix}
|
||||||
|
\hat{\partial}_x T_b \epsilon_{xx} &
|
||||||
|
\hat{\partial}_y T_a \epsilon_{yy}
|
||||||
|
\end{bmatrix}
|
||||||
|
\right)
|
||||||
|
\begin{bmatrix} E_r \\ E_y \end{bmatrix}.
|
||||||
|
$$
|
||||||
|
|
||||||
|
Since `\beta = m / r_{\min}`, the solver implemented in this file returns the
|
||||||
|
angular wavenumber `m`, while the operator itself is most naturally written in
|
||||||
|
terms of the linear quantity `\beta`. The helpers below reconstruct the full
|
||||||
|
field components from the solved transverse eigenvector and then normalize the
|
||||||
|
mode to unit forward power with the same discrete longitudinal Poynting inner
|
||||||
|
product used by `waveguide_2d`.
|
||||||
|
|
||||||
|
As in the straight-waveguide case, all functions here assume a 2D grid:
|
||||||
|
|
||||||
|
`dxes = [[[dr_e_0, dr_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`.
|
||||||
"""
|
"""
|
||||||
# TODO update module docs
|
from typing import Any, cast
|
||||||
|
from collections.abc import Sequence
|
||||||
|
import logging
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import scipy.sparse as sparse # type: ignore
|
from numpy.typing import NDArray, ArrayLike
|
||||||
|
from scipy import sparse
|
||||||
|
|
||||||
from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t, cfdfield_t
|
from ..fdmath import vec, unvec, dx_lists2_t, vcfdslice_t, vfdslice, vcfdslice, vcfdfield2
|
||||||
from ..fdmath.operators import deriv_forward, deriv_back
|
from ..fdmath.operators import deriv_forward, deriv_back
|
||||||
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
|
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
|
||||||
|
from . import waveguide_2d
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def cylindrical_operator(
|
def cylindrical_operator(
|
||||||
omega: complex,
|
omega: float,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
r0: float,
|
rmin: float,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
r"""
|
||||||
Cylindrical coordinate waveguide operator of the form
|
Cylindrical coordinate waveguide operator of the form
|
||||||
|
|
||||||
(NOTE: See 10.1364/OL.33.001848)
|
$$
|
||||||
TODO: consider 10.1364/OE.20.021583
|
(\omega^2 \begin{bmatrix} T_b T_b \mu_{yy} \epsilon_{xx} & 0 \\
|
||||||
|
0 & T_a T_a \mu_{xx} \epsilon_{yy} \end{bmatrix} +
|
||||||
TODO
|
\begin{bmatrix} -T_b \mu_{yy} \hat{\partial}_y \\
|
||||||
|
T_a \mu_{xx} \hat{\partial}_x \end{bmatrix} T_b \mu_{zz}^{-1}
|
||||||
|
\begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
|
||||||
|
\begin{bmatrix} \tilde{\partial}_x \\
|
||||||
|
\tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1}
|
||||||
|
\begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix})
|
||||||
|
\begin{bmatrix} E_r \\
|
||||||
|
E_y \end{bmatrix}
|
||||||
|
$$
|
||||||
|
|
||||||
for use with a field vector of the form `[E_r, E_y]`.
|
for use with a field vector of the form `[E_r, E_y]`.
|
||||||
|
|
||||||
This operator can be used to form an eigenvalue problem of the form
|
This operator can be used to form an eigenvalue problem of the form
|
||||||
A @ [E_r, E_y] = wavenumber**2 * [E_r, E_y]
|
A @ [E_r, E_y] = beta**2 * [E_r, E_y]
|
||||||
|
|
||||||
which can then be solved for the eigenmodes of the system
|
which can then be solved for the eigenmodes of the system
|
||||||
(an `exp(-i * wavenumber * theta)` theta-dependence is assumed for the fields).
|
(an `exp(-i * angular_wavenumber * theta)` theta-dependence is assumed for
|
||||||
|
the fields, with `beta = angular_wavenumber / rmin`).
|
||||||
|
|
||||||
|
(NOTE: See module docs and 10.1364/OL.33.001848)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
omega: The angular frequency of the system
|
omega: The angular frequency of the system
|
||||||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
||||||
epsilon: Vectorized dielectric constant grid
|
epsilon: Vectorized dielectric constant grid
|
||||||
r0: Radius of curvature for the simulation. This should be the minimum value of
|
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
|
||||||
r within the simulation domain.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Sparse matrix representation of the operator
|
Sparse matrix representation of the operator
|
||||||
|
|
@ -52,97 +184,409 @@ def cylindrical_operator(
|
||||||
Dfx, Dfy = deriv_forward(dxes[0])
|
Dfx, Dfy = deriv_forward(dxes[0])
|
||||||
Dbx, Dby = deriv_back(dxes[1])
|
Dbx, Dby = deriv_back(dxes[1])
|
||||||
|
|
||||||
rx = r0 + numpy.cumsum(dxes[0][0])
|
Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
|
||||||
ry = r0 + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0])
|
|
||||||
tx = rx / r0
|
|
||||||
ty = ry / r0
|
|
||||||
|
|
||||||
Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1)))
|
|
||||||
Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1)))
|
|
||||||
|
|
||||||
eps_parts = numpy.split(epsilon, 3)
|
eps_parts = numpy.split(epsilon, 3)
|
||||||
eps_x = sparse.diags(eps_parts[0])
|
eps_x = sparse.diags_array(eps_parts[0])
|
||||||
eps_y = sparse.diags(eps_parts[1])
|
eps_y = sparse.diags_array(eps_parts[1])
|
||||||
eps_z_inv = sparse.diags(1 / eps_parts[2])
|
eps_z_inv = sparse.diags_array(1 / eps_parts[2])
|
||||||
|
|
||||||
pa = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dbx, Dby))
|
|
||||||
pb = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dby, Dbx))
|
|
||||||
a0 = Ty @ eps_x + omega**-2 * Dby @ Ty @ Dfy
|
|
||||||
a1 = Tx @ eps_y + omega**-2 * Dbx @ Ty @ Dfx
|
|
||||||
b0 = Dbx @ Ty @ Dfy
|
|
||||||
b1 = Dby @ Ty @ Dfx
|
|
||||||
|
|
||||||
diag = sparse.block_diag
|
|
||||||
|
|
||||||
omega2 = omega * omega
|
omega2 = omega * omega
|
||||||
|
diag = sparse.block_diag
|
||||||
|
|
||||||
op = (
|
sq0 = omega2 * diag((Tb @ Tb @ eps_x,
|
||||||
(omega2 * diag((Tx, Ty)) + pa) @ diag((a0, a1))
|
Ta @ Ta @ eps_y))
|
||||||
- (sparse.bmat(((None, Ty), (Tx, None))) + pb / omega2) @ diag((b0, b1))
|
lin0 = sparse.vstack((-Tb @ Dby, Ta @ Dbx)) @ Tb @ sparse.hstack((-Dfy, Dfx))
|
||||||
)
|
lin1 = sparse.vstack((Dfx, Dfy)) @ Ta @ eps_z_inv @ sparse.hstack((Dbx @ Tb @ eps_x,
|
||||||
|
Dby @ Ta @ eps_y))
|
||||||
|
op = sq0 + lin0 + lin1
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
||||||
def solve_mode(
|
def solve_modes(
|
||||||
mode_number: int,
|
mode_numbers: Sequence[int],
|
||||||
omega: complex,
|
omega: float,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists2_t,
|
||||||
epsilon: vfdfield_t,
|
epsilon: vfdslice,
|
||||||
r0: float,
|
rmin: float,
|
||||||
) -> dict[str, complex | cfdfield_t]:
|
mode_margin: int = 2,
|
||||||
|
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
|
||||||
"""
|
"""
|
||||||
TODO: fixup
|
|
||||||
Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
|
Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
|
||||||
of the bent waveguide with the specified mode number.
|
of the bent waveguide with the specified mode number.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mode_number: Number of the mode, 0-indexed
|
mode_numbers: Mode numbers to solve, 0-indexed.
|
||||||
omega: Angular frequency of the simulation
|
omega: Angular frequency of the simulation
|
||||||
dxes: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types.
|
dxes: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types.
|
||||||
The first coordinate is assumed to be r, the second is y.
|
The first coordinate is assumed to be r, the second is y.
|
||||||
epsilon: Dielectric constant
|
epsilon: Dielectric constant
|
||||||
r0: Radius of curvature for the simulation. This should be the minimum value of
|
rmin: Radius of curvature for the simulation. This should be the minimum value of
|
||||||
r within the simulation domain.
|
r within the simulation domain.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
```
|
e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
|
||||||
{
|
angular_wavenumbers: list of wavenumbers in 1/rad units.
|
||||||
'E': list[NDArray[numpy.complex_]],
|
|
||||||
'H': list[NDArray[numpy.complex_]],
|
|
||||||
'wavenumber': complex,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
'''
|
#
|
||||||
Solve for the largest-magnitude eigenvalue of the real operator
|
# Solve for the largest-magnitude eigenvalue of the real operator
|
||||||
'''
|
#
|
||||||
dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
|
dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
|
||||||
|
|
||||||
A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0)
|
A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), rmin=rmin)
|
||||||
eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3)
|
eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
|
||||||
e_xy = eigvecs[:, -(mode_number + 1)]
|
keep_inds = -(numpy.array(mode_numbers) + 1)
|
||||||
|
e_xys = eigvecs[:, keep_inds].T
|
||||||
|
eigvals = eigvals[keep_inds]
|
||||||
|
|
||||||
'''
|
#
|
||||||
Now solve for the eigenvector of the full operator, using the real operator's
|
# Now solve for the eigenvector of the full operator, using the real operator's
|
||||||
eigenvector as an initial guess for Rayleigh quotient iteration.
|
# eigenvector as an initial guess for Rayleigh quotient iteration.
|
||||||
'''
|
#
|
||||||
A = cylindrical_operator(omega, dxes, epsilon, r0)
|
A = cylindrical_operator(omega, dxes, epsilon, rmin=rmin)
|
||||||
eigval, e_xy = rayleigh_quotient_iteration(A, e_xy)
|
for nn in range(len(mode_numbers)):
|
||||||
|
eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
|
||||||
|
|
||||||
# Calculate the wave-vector (force the real part to be positive)
|
# Calculate the wave-vector (force the real part to be positive)
|
||||||
wavenumber = numpy.sqrt(eigval)
|
wavenumbers = numpy.sqrt(eigvals)
|
||||||
wavenumber *= numpy.sign(numpy.real(wavenumber))
|
wavenumbers *= numpy.sign(numpy.real(wavenumbers))
|
||||||
|
|
||||||
# TODO: Perform correction on wavenumber to account for numerical dispersion.
|
# Wavenumbers assume the mode is at rmin, which is unlikely
|
||||||
|
# Instead, return the wavenumber in inverse radians
|
||||||
|
angular_wavenumbers = wavenumbers * cast('complex', rmin)
|
||||||
|
|
||||||
shape = [d.size for d in dxes[0]]
|
order = angular_wavenumbers.argsort()[::-1]
|
||||||
e_xy = numpy.hstack((e_xy, numpy.zeros(shape[0] * shape[1])))
|
e_xys = e_xys[order]
|
||||||
fields = {
|
angular_wavenumbers = angular_wavenumbers[order]
|
||||||
'wavenumber': wavenumber,
|
|
||||||
'E': unvec(e_xy, shape),
|
|
||||||
# 'E': unvec(e, shape),
|
|
||||||
# 'H': unvec(h, shape),
|
|
||||||
}
|
|
||||||
|
|
||||||
return fields
|
return e_xys, angular_wavenumbers
|
||||||
|
|
||||||
|
|
||||||
|
def solve_mode(
|
||||||
|
mode_number: int,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> tuple[vcfdfield2, complex]:
|
||||||
|
"""
|
||||||
|
Wrapper around `solve_modes()` that solves for a single mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode_number: 0-indexed mode number to solve for
|
||||||
|
*args: passed to `solve_modes()`
|
||||||
|
**kwargs: passed to `solve_modes()`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(e_xy, angular_wavenumber)
|
||||||
|
"""
|
||||||
|
kwargs['mode_numbers'] = [mode_number]
|
||||||
|
e_xys, angular_wavenumbers = solve_modes(*args, **kwargs)
|
||||||
|
return e_xys[0], angular_wavenumbers[0]
|
||||||
|
|
||||||
|
|
||||||
|
def linear_wavenumbers(
|
||||||
|
e_xys: Sequence[vcfdfield2] | NDArray[numpy.complex128],
|
||||||
|
angular_wavenumbers: ArrayLike,
|
||||||
|
epsilon: vfdslice,
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
rmin: float,
|
||||||
|
) -> NDArray[numpy.complex128]:
|
||||||
|
"""
|
||||||
|
Calculate linear wavenumbers (1/distance) based on angular wavenumbers (1/rad)
|
||||||
|
and the mode's energy distribution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e_xys: Vectorized mode fields with shape (num_modes, 2 * x *y)
|
||||||
|
angular_wavenumbers: Wavenumbers assuming fields have theta-dependence of
|
||||||
|
`exp(-i * angular_wavenumber * theta)`. They should satisfy
|
||||||
|
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
|
||||||
|
epsilon: Vectorized dielectric constant grid with shape (3, x, y)
|
||||||
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
||||||
|
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
NDArray containing the calculated linear (1/distance) wavenumbers
|
||||||
|
"""
|
||||||
|
angular_wavenumbers = numpy.asarray(angular_wavenumbers)
|
||||||
|
mode_radii = numpy.empty_like(angular_wavenumbers, dtype=float)
|
||||||
|
|
||||||
|
shape2d = (len(dxes[0][0]), len(dxes[0][1]))
|
||||||
|
epsilon2d = unvec(epsilon, shape2d)[:2]
|
||||||
|
grid_radii = rmin + numpy.cumsum(dxes[0][0])
|
||||||
|
for ii in range(angular_wavenumbers.size):
|
||||||
|
efield = unvec(e_xys[ii], shape2d, 2)
|
||||||
|
energy = numpy.real((efield * efield.conj()) * epsilon2d)
|
||||||
|
energy_vs_x = energy.sum(axis=(0, 2))
|
||||||
|
mode_radii[ii] = (grid_radii * energy_vs_x).sum() / energy_vs_x.sum()
|
||||||
|
|
||||||
|
logger.info(f'{mode_radii=}')
|
||||||
|
lin_wavenumbers = angular_wavenumbers / mode_radii
|
||||||
|
return lin_wavenumbers
|
||||||
|
|
||||||
|
|
||||||
|
def exy2h(
|
||||||
|
angular_wavenumber: complex,
|
||||||
|
omega: float,
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
rmin: float,
|
||||||
|
epsilon: vfdslice,
|
||||||
|
mu: vfdslice | None = None
|
||||||
|
) -> sparse.sparray:
|
||||||
|
"""
|
||||||
|
Operator which transforms the vector `e_xy` containing the vectorized E_r and E_y fields,
|
||||||
|
into a vectorized H containing all three H components
|
||||||
|
|
||||||
|
Args:
|
||||||
|
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
|
||||||
|
`exp(-i * angular_wavenumber * theta)`. It should satisfy
|
||||||
|
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
|
||||||
|
omega: The angular frequency of the system
|
||||||
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
||||||
|
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
|
||||||
|
epsilon: Vectorized dielectric constant grid
|
||||||
|
mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sparse matrix representing the operator.
|
||||||
|
"""
|
||||||
|
e2hop = e2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, mu=mu)
|
||||||
|
return e2hop @ exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon)
|
||||||
|
|
||||||
|
|
||||||
|
def exy2e(
|
||||||
|
angular_wavenumber: complex,
|
||||||
|
omega: float,
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
rmin: float,
|
||||||
|
epsilon: vfdslice,
|
||||||
|
) -> sparse.sparray:
|
||||||
|
"""
|
||||||
|
Operator which transforms the vector `e_xy` containing the vectorized E_r and E_y fields,
|
||||||
|
into a vectorized E containing all three E components
|
||||||
|
|
||||||
|
Unlike the straight waveguide case, the H_z components do not cancel and must be calculated
|
||||||
|
from E_r and E_y in order to then calculate E_z.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
|
||||||
|
`exp(-i * angular_wavenumber * theta)`. It should satisfy
|
||||||
|
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
|
||||||
|
omega: The angular frequency of the system
|
||||||
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
||||||
|
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
|
||||||
|
epsilon: Vectorized dielectric constant grid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sparse matrix representing the operator.
|
||||||
|
"""
|
||||||
|
Dfx, Dfy = deriv_forward(dxes[0])
|
||||||
|
Dbx, Dby = deriv_back(dxes[1])
|
||||||
|
wavenumber = angular_wavenumber / rmin
|
||||||
|
|
||||||
|
Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
|
||||||
|
Tai = sparse.diags_array(1 / Ta.diagonal())
|
||||||
|
#Tbi = sparse.diags_array(1 / Tb.diagonal())
|
||||||
|
|
||||||
|
epsilon_parts = numpy.split(epsilon, 3)
|
||||||
|
epsilon_x, epsilon_y = (sparse.diags_array(epsi) for epsi in epsilon_parts[:2])
|
||||||
|
epsilon_z_inv = sparse.diags_array(1 / epsilon_parts[2])
|
||||||
|
|
||||||
|
n_pts = dxes[0][0].size * dxes[0][1].size
|
||||||
|
zeros = sparse.coo_array((n_pts, n_pts))
|
||||||
|
|
||||||
|
mu_z = numpy.ones(n_pts)
|
||||||
|
mu_z_inv = sparse.diags_array(1 / mu_z)
|
||||||
|
exy2hz = 1 / (-1j * omega) * mu_z_inv @ sparse.hstack((Dfy, -Dfx))
|
||||||
|
hxy2ez = 1 / (1j * omega) * epsilon_z_inv @ sparse.hstack((Dby, -Dbx))
|
||||||
|
|
||||||
|
exy2hy = Tb / (1j * wavenumber) @ (-1j * omega * sparse.hstack((epsilon_x, zeros)) - Dby @ exy2hz)
|
||||||
|
exy2hx = Tb / (1j * wavenumber) @ ( 1j * omega * sparse.hstack((zeros, epsilon_y)) - Tai @ Dbx @ Tb @ exy2hz)
|
||||||
|
|
||||||
|
exy2ez = hxy2ez @ sparse.vstack((exy2hx, exy2hy))
|
||||||
|
|
||||||
|
op = sparse.vstack((sparse.eye_array(2 * n_pts),
|
||||||
|
exy2ez))
|
||||||
|
return op
|
||||||
|
|
||||||
|
|
||||||
|
def e2h(
|
||||||
|
angular_wavenumber: complex,
|
||||||
|
omega: float,
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
rmin: float,
|
||||||
|
mu: vfdslice | None = None
|
||||||
|
) -> sparse.sparray:
|
||||||
|
r"""
|
||||||
|
Returns an operator which, when applied to a vectorized E eigenfield, produces
|
||||||
|
the vectorized H eigenfield.
|
||||||
|
|
||||||
|
This operator is created directly from the initial coordinate-transformed equations:
|
||||||
|
$$
|
||||||
|
\begin{aligned}
|
||||||
|
-\imath \omega \mu_{rr} H_r &= \tilde{\partial}_y E_z + \imath \beta T_a^{-1} E_y, \\
|
||||||
|
-\imath \omega \mu_{yy} H_y &= -\imath \beta T_b^{-1} E_r
|
||||||
|
- T_b^{-1} \tilde{\partial}_r (T_a E_z), \\
|
||||||
|
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_r E_y - \tilde{\partial}_y E_r,
|
||||||
|
\end{aligned}
|
||||||
|
$$
|
||||||
|
|
||||||
|
Args:
|
||||||
|
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
|
||||||
|
`exp(-i * angular_wavenumber * theta)`. It should satisfy
|
||||||
|
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
|
||||||
|
omega: The angular frequency of the system
|
||||||
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
||||||
|
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
|
||||||
|
mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sparse matrix representation of the operator.
|
||||||
|
"""
|
||||||
|
Dfx, Dfy = deriv_forward(dxes[0])
|
||||||
|
Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
|
||||||
|
Tai = sparse.diags_array(1 / Ta.diagonal())
|
||||||
|
Tbi = sparse.diags_array(1 / Tb.diagonal())
|
||||||
|
|
||||||
|
jB = 1j * angular_wavenumber / rmin
|
||||||
|
op = sparse.block_array([[ None, -jB * Tai, -Dfy],
|
||||||
|
[jB * Tbi, None, Tbi @ Dfx @ Ta],
|
||||||
|
[ Dfy, -Dfx, None]]) / (-1j * omega)
|
||||||
|
if mu is not None:
|
||||||
|
op = sparse.diags_array(1 / mu) @ op
|
||||||
|
return op
|
||||||
|
|
||||||
|
|
||||||
|
def dxes2T(
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
rmin: float,
|
||||||
|
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||||
|
r"""
|
||||||
|
Construct the cylindrical metric matrices $T_a$ and $T_b$.
|
||||||
|
|
||||||
|
`T_a` is sampled on the E-grid radial locations, while `T_b` is sampled on
|
||||||
|
the staggered H-grid radial locations. These are the diagonal matrices that
|
||||||
|
convert the straight-waveguide algebra into its cylindrical counterpart.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
||||||
|
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sparse diagonal matrices `(T_a, T_b)`.
|
||||||
|
"""
|
||||||
|
ra = rmin + numpy.cumsum(dxes[0][0]) # Radius at Ey points
|
||||||
|
rb = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0]) # Radius at Ex points
|
||||||
|
ta = ra / rmin
|
||||||
|
tb = rb / rmin
|
||||||
|
|
||||||
|
Ta = sparse.diags_array(vec(ta[:, None].repeat(dxes[0][1].size, axis=1)))
|
||||||
|
Tb = sparse.diags_array(vec(tb[:, None].repeat(dxes[1][1].size, axis=1)))
|
||||||
|
return Ta, Tb
|
||||||
|
|
||||||
|
|
||||||
|
def normalized_fields_e(
|
||||||
|
e_xy: vcfdfield2,
|
||||||
|
angular_wavenumber: complex,
|
||||||
|
omega: float,
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
rmin: float,
|
||||||
|
epsilon: vfdslice,
|
||||||
|
mu: vfdslice | None = None,
|
||||||
|
prop_phase: float = 0,
|
||||||
|
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||||
|
r"""
|
||||||
|
Given a vector `e_xy` containing the vectorized E_r and E_y fields,
|
||||||
|
returns normalized, vectorized E and H fields for the system.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e_xy: Vector containing E_r and E_y fields
|
||||||
|
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
|
||||||
|
`exp(-i * angular_wavenumber * theta)`. It should satisfy
|
||||||
|
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
|
||||||
|
omega: The angular frequency of the system
|
||||||
|
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
||||||
|
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
|
||||||
|
epsilon: Vectorized dielectric constant grid
|
||||||
|
mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||||
|
prop_phase: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction.
|
||||||
|
Default 0 (continuous propagation direction, i.e. dz->0).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`(e, h)`, where each field is vectorized, normalized,
|
||||||
|
and contains all three vector components.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The normalization step is delegated to `_normalized_fields(...)`, which
|
||||||
|
enforces unit forward power under the discrete inner product
|
||||||
|
|
||||||
|
$$
|
||||||
|
\frac{1}{2}\int (E_r H_y^* - E_y H_r^*) \, dr \, dy.
|
||||||
|
$$
|
||||||
|
|
||||||
|
The angular wavenumber `m` is first converted into the full three-component
|
||||||
|
fields, then the overall complex phase and sign are fixed so the result is
|
||||||
|
reproducible for symmetric modes.
|
||||||
|
"""
|
||||||
|
e = exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon) @ e_xy
|
||||||
|
h = exy2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, mu=mu) @ e_xy
|
||||||
|
e_norm, h_norm = _normalized_fields(
|
||||||
|
e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase,
|
||||||
|
)
|
||||||
|
return e_norm, h_norm
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_fields(
|
||||||
|
e: vcfdslice,
|
||||||
|
h: vcfdslice,
|
||||||
|
dxes: dx_lists2_t,
|
||||||
|
epsilon: vfdslice,
|
||||||
|
prop_phase: float = 0,
|
||||||
|
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||||
|
r"""
|
||||||
|
Normalize a cylindrical waveguide mode to unit forward power.
|
||||||
|
|
||||||
|
The cylindrical helpers reuse the straight-waveguide inner product after the
|
||||||
|
field reconstruction step. The extra metric factors have already been folded
|
||||||
|
into the reconstructed `e`/`h` fields through `dxes2T(...)` and the
|
||||||
|
cylindrical `exy2e(...)` / `exy2h(...)` operators, so the same discrete
|
||||||
|
longitudinal Poynting integral can be used here.
|
||||||
|
|
||||||
|
The normalization procedure is:
|
||||||
|
|
||||||
|
1. Flip the magnetic field sign so the reconstructed `(e, h)` pair follows
|
||||||
|
the same forward-power convention as `waveguide_2d`.
|
||||||
|
2. Compute the time-averaged forward power with
|
||||||
|
`waveguide_2d.inner_product(..., conj_h=True)`.
|
||||||
|
3. Scale by `1 / sqrt(S_z)` so the resulting mode has unit forward power.
|
||||||
|
4. Remove the arbitrary complex phase and apply a quadrant-sum sign heuristic
|
||||||
|
so symmetric modes choose a stable representative.
|
||||||
|
|
||||||
|
`prop_phase` has the same meaning as in `waveguide_2d`: it compensates for
|
||||||
|
the half-cell longitudinal staggering between the E and H fields when the
|
||||||
|
propagation direction is itself discretized.
|
||||||
|
"""
|
||||||
|
h *= -1
|
||||||
|
shape = [s.size for s in dxes[0]]
|
||||||
|
|
||||||
|
# Find time-averaged Sz and normalize to it
|
||||||
|
# H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
|
||||||
|
Sz_tavg = waveguide_2d.inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real # Note, using linear poynting vector
|
||||||
|
assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
|
||||||
|
|
||||||
|
energy = numpy.real(epsilon * e.conj() * e)
|
||||||
|
|
||||||
|
norm_amplitude = 1 / numpy.sqrt(Sz_tavg)
|
||||||
|
norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric
|
||||||
|
|
||||||
|
# Try to break symmetry to assign a consistent sign [experimental]
|
||||||
|
E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape)
|
||||||
|
sign = numpy.sign(E_weighted[:,
|
||||||
|
:max(shape[0] // 2, 1),
|
||||||
|
:max(shape[1] // 2, 1)].real.sum())
|
||||||
|
assert sign != 0
|
||||||
|
|
||||||
|
norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle)
|
||||||
|
|
||||||
|
e *= norm_factor
|
||||||
|
h *= norm_factor
|
||||||
|
return vcfdslice_t(e), vcfdslice_t(h)
|
||||||
|
|
|
||||||
|
|
@ -741,8 +741,46 @@ the true values can be multiplied back in after the simulation is complete if no
|
||||||
normalized results are needed.
|
normalized results are needed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .types import fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t, dx_lists_t, dx_lists_mut
|
from .types import (
|
||||||
from .types import fdfield_updater_t, cfdfield_updater_t
|
fdfield_t as fdfield_t,
|
||||||
from .vectorization import vec, unvec
|
vfdfield_t as vfdfield_t,
|
||||||
from . import operators, functional, types, vectorization
|
cfdfield_t as cfdfield_t,
|
||||||
|
vcfdfield_t as vcfdfield_t,
|
||||||
|
fdfield2_t as fdfield2_t,
|
||||||
|
vfdfield2_t as vfdfield2_t,
|
||||||
|
cfdfield2_t as cfdfield2_t,
|
||||||
|
vcfdfield2_t as vcfdfield2_t,
|
||||||
|
fdfield as fdfield,
|
||||||
|
vfdfield as vfdfield,
|
||||||
|
cfdfield as cfdfield,
|
||||||
|
vcfdfield as vcfdfield,
|
||||||
|
fdfield2 as fdfield2,
|
||||||
|
vfdfield2 as vfdfield2,
|
||||||
|
cfdfield2 as cfdfield2,
|
||||||
|
vcfdfield2 as vcfdfield2,
|
||||||
|
fdslice_t as fdslice_t,
|
||||||
|
vfdslice_t as vfdslice_t,
|
||||||
|
cfdslice_t as cfdslice_t,
|
||||||
|
vcfdslice_t as vcfdslice_t,
|
||||||
|
fdslice as fdslice,
|
||||||
|
vfdslice as vfdslice,
|
||||||
|
cfdslice as cfdslice,
|
||||||
|
vcfdslice as vcfdslice,
|
||||||
|
dx_lists_t as dx_lists_t,
|
||||||
|
dx_lists2_t as dx_lists2_t,
|
||||||
|
dx_lists_mut as dx_lists_mut,
|
||||||
|
dx_lists2_mut as dx_lists2_mut,
|
||||||
|
fdfield_updater_t as fdfield_updater_t,
|
||||||
|
cfdfield_updater_t as cfdfield_updater_t,
|
||||||
|
)
|
||||||
|
from .vectorization import (
|
||||||
|
vec as vec,
|
||||||
|
unvec as unvec,
|
||||||
|
)
|
||||||
|
from . import (
|
||||||
|
operators as operators,
|
||||||
|
functional as functional,
|
||||||
|
types as types,
|
||||||
|
vectorization as vectorization,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,18 @@ Math functions for finite difference simulations
|
||||||
|
|
||||||
Basic discrete calculus etc.
|
Basic discrete calculus etc.
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Callable
|
from typing import TypeVar
|
||||||
|
from collections.abc import Sequence, Callable
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
from numpy import floating, complexfloating
|
||||||
|
|
||||||
from .types import fdfield_t, fdfield_updater_t
|
from .types import fdfield, fdfield_updater_t
|
||||||
|
|
||||||
|
|
||||||
def deriv_forward(
|
def deriv_forward(
|
||||||
dx_e: Sequence[NDArray[numpy.float_]] | None = None,
|
dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
|
||||||
) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
|
) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
|
||||||
"""
|
"""
|
||||||
Utility operators for taking discretized derivatives (backward variant).
|
Utility operators for taking discretized derivatives (backward variant).
|
||||||
|
|
@ -36,7 +38,7 @@ def deriv_forward(
|
||||||
|
|
||||||
|
|
||||||
def deriv_back(
|
def deriv_back(
|
||||||
dx_h: Sequence[NDArray[numpy.float_]] | None = None,
|
dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
|
||||||
) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
|
) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
|
||||||
"""
|
"""
|
||||||
Utility operators for taking discretized derivatives (forward variant).
|
Utility operators for taking discretized derivatives (forward variant).
|
||||||
|
|
@ -59,9 +61,12 @@ def deriv_back(
|
||||||
return derivs
|
return derivs
|
||||||
|
|
||||||
|
|
||||||
|
TT = TypeVar('TT', bound='NDArray[floating | complexfloating]')
|
||||||
|
|
||||||
|
|
||||||
def curl_forward(
|
def curl_forward(
|
||||||
dx_e: Sequence[NDArray[numpy.float_]] | None = None,
|
dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
|
||||||
) -> fdfield_updater_t:
|
) -> Callable[[TT], TT]:
|
||||||
r"""
|
r"""
|
||||||
Curl operator for use with the E field.
|
Curl operator for use with the E field.
|
||||||
|
|
||||||
|
|
@ -75,7 +80,7 @@ def curl_forward(
|
||||||
"""
|
"""
|
||||||
Dx, Dy, Dz = deriv_forward(dx_e)
|
Dx, Dy, Dz = deriv_forward(dx_e)
|
||||||
|
|
||||||
def ce_fun(e: fdfield_t) -> fdfield_t:
|
def ce_fun(e: TT) -> TT:
|
||||||
output = numpy.empty_like(e)
|
output = numpy.empty_like(e)
|
||||||
output[0] = Dy(e[2])
|
output[0] = Dy(e[2])
|
||||||
output[1] = Dz(e[0])
|
output[1] = Dz(e[0])
|
||||||
|
|
@ -89,8 +94,8 @@ def curl_forward(
|
||||||
|
|
||||||
|
|
||||||
def curl_back(
|
def curl_back(
|
||||||
dx_h: Sequence[NDArray[numpy.float_]] | None = None,
|
dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
|
||||||
) -> fdfield_updater_t:
|
) -> Callable[[TT], TT]:
|
||||||
r"""
|
r"""
|
||||||
Create a function which takes the backward curl of a field.
|
Create a function which takes the backward curl of a field.
|
||||||
|
|
||||||
|
|
@ -104,7 +109,7 @@ def curl_back(
|
||||||
"""
|
"""
|
||||||
Dx, Dy, Dz = deriv_back(dx_h)
|
Dx, Dy, Dz = deriv_back(dx_h)
|
||||||
|
|
||||||
def ch_fun(h: fdfield_t) -> fdfield_t:
|
def ch_fun(h: TT) -> TT:
|
||||||
output = numpy.empty_like(h)
|
output = numpy.empty_like(h)
|
||||||
output[0] = Dy(h[2])
|
output[0] = Dy(h[2])
|
||||||
output[1] = Dz(h[0])
|
output[1] = Dz(h[0])
|
||||||
|
|
@ -118,11 +123,11 @@ def curl_back(
|
||||||
|
|
||||||
|
|
||||||
def curl_forward_parts(
|
def curl_forward_parts(
|
||||||
dx_e: Sequence[NDArray[numpy.float_]] | None = None,
|
dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
Dx, Dy, Dz = deriv_forward(dx_e)
|
Dx, Dy, Dz = deriv_forward(dx_e)
|
||||||
|
|
||||||
def mkparts_fwd(e: fdfield_t) -> tuple[tuple[fdfield_t, fdfield_t], ...]:
|
def mkparts_fwd(e: fdfield) -> tuple[tuple[fdfield, fdfield], ...]:
|
||||||
return ((-Dz(e[1]), Dy(e[2])),
|
return ((-Dz(e[1]), Dy(e[2])),
|
||||||
( Dz(e[0]), -Dx(e[2])),
|
( Dz(e[0]), -Dx(e[2])),
|
||||||
(-Dy(e[0]), Dx(e[1])))
|
(-Dy(e[0]), Dx(e[1])))
|
||||||
|
|
@ -131,11 +136,11 @@ def curl_forward_parts(
|
||||||
|
|
||||||
|
|
||||||
def curl_back_parts(
|
def curl_back_parts(
|
||||||
dx_h: Sequence[NDArray[numpy.float_]] | None = None,
|
dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
Dx, Dy, Dz = deriv_back(dx_h)
|
Dx, Dy, Dz = deriv_back(dx_h)
|
||||||
|
|
||||||
def mkparts_back(h: fdfield_t) -> tuple[tuple[fdfield_t, fdfield_t], ...]:
|
def mkparts_back(h: fdfield) -> tuple[tuple[fdfield, fdfield], ...]:
|
||||||
return ((-Dz(h[1]), Dy(h[2])),
|
return ((-Dz(h[1]), Dy(h[2])),
|
||||||
( Dz(h[0]), -Dx(h[2])),
|
( Dz(h[0]), -Dx(h[2])),
|
||||||
(-Dy(h[0]), Dx(h[1])))
|
(-Dy(h[0]), Dx(h[1])))
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,20 @@ Matrix operators for finite difference simulations
|
||||||
|
|
||||||
Basic discrete calculus etc.
|
Basic discrete calculus etc.
|
||||||
"""
|
"""
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
import scipy.sparse as sparse # type: ignore
|
from numpy import floating, complexfloating
|
||||||
|
from scipy import sparse
|
||||||
|
|
||||||
from .types import vfdfield_t
|
from .types import vfdfield
|
||||||
|
|
||||||
|
|
||||||
def shift_circ(
|
def shift_circ(
|
||||||
axis: int,
|
axis: int,
|
||||||
shape: Sequence[int],
|
shape: Sequence[int],
|
||||||
shift_distance: int = 1,
|
shift_distance: int = 1,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Utility operator for performing a circular shift along a specified axis by a
|
Utility operator for performing a circular shift along a specified axis by a
|
||||||
specified number of elements.
|
specified number of elements.
|
||||||
|
|
@ -33,8 +34,8 @@ def shift_circ(
|
||||||
if axis not in range(len(shape)):
|
if axis not in range(len(shape)):
|
||||||
raise Exception(f'Invalid direction: {axis}, shape is {shape}')
|
raise Exception(f'Invalid direction: {axis}, shape is {shape}')
|
||||||
|
|
||||||
shifts = [abs(shift_distance) if a == axis else 0 for a in range(3)]
|
shifts = [abs(shift_distance) if a == axis else 0 for a in range(len(shape))]
|
||||||
shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)]
|
shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts, strict=True)]
|
||||||
ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
|
ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
|
||||||
|
|
||||||
n = numpy.prod(shape)
|
n = numpy.prod(shape)
|
||||||
|
|
@ -43,7 +44,7 @@ def shift_circ(
|
||||||
|
|
||||||
vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C')))
|
vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C')))
|
||||||
|
|
||||||
d = sparse.csr_matrix(vij, shape=(n, n))
|
d = sparse.csr_array(vij, shape=(n, n))
|
||||||
|
|
||||||
if shift_distance < 0:
|
if shift_distance < 0:
|
||||||
d = d.T
|
d = d.T
|
||||||
|
|
@ -55,7 +56,7 @@ def shift_with_mirror(
|
||||||
axis: int,
|
axis: int,
|
||||||
shape: Sequence[int],
|
shape: Sequence[int],
|
||||||
shift_distance: int = 1,
|
shift_distance: int = 1,
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Utility operator for performing an n-element shift along a specified axis, with mirror
|
Utility operator for performing an n-element shift along a specified axis, with mirror
|
||||||
boundary conditions applied to the cells beyond the receding edge.
|
boundary conditions applied to the cells beyond the receding edge.
|
||||||
|
|
@ -81,8 +82,8 @@ def shift_with_mirror(
|
||||||
v = numpy.where(v < 0, - 1 - v, v)
|
v = numpy.where(v < 0, - 1 - v, v)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
shifts = [shift_distance if a == axis else 0 for a in range(3)]
|
shifts = [shift_distance if a == axis else 0 for a in range(len(shape))]
|
||||||
shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts)]
|
shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts, strict=True)]
|
||||||
ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
|
ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
|
||||||
|
|
||||||
n = numpy.prod(shape)
|
n = numpy.prod(shape)
|
||||||
|
|
@ -91,13 +92,13 @@ def shift_with_mirror(
|
||||||
|
|
||||||
vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C')))
|
vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C')))
|
||||||
|
|
||||||
d = sparse.csr_matrix(vij, shape=(n, n))
|
d = sparse.csr_array(vij, shape=(n, n))
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def deriv_forward(
|
def deriv_forward(
|
||||||
dx_e: Sequence[NDArray[numpy.float_]],
|
dx_e: Sequence[NDArray[floating | complexfloating]],
|
||||||
) -> list[sparse.spmatrix]:
|
) -> list[sparse.sparray]:
|
||||||
"""
|
"""
|
||||||
Utility operators for taking discretized derivatives (forward variant).
|
Utility operators for taking discretized derivatives (forward variant).
|
||||||
|
|
||||||
|
|
@ -113,18 +114,18 @@ def deriv_forward(
|
||||||
|
|
||||||
dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij')
|
dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij')
|
||||||
|
|
||||||
def deriv(axis: int) -> sparse.spmatrix:
|
def deriv(axis: int) -> sparse.sparray:
|
||||||
return shift_circ(axis, shape, 1) - sparse.eye(n)
|
return shift_circ(axis, shape, 1) - sparse.eye_array(n)
|
||||||
|
|
||||||
Ds = [sparse.diags(+1 / dx.ravel(order='C')) @ deriv(a)
|
Ds = [sparse.diags_array(+1 / dx.ravel(order='C')) @ deriv(a)
|
||||||
for a, dx in enumerate(dx_e_expanded)]
|
for a, dx in enumerate(dx_e_expanded)]
|
||||||
|
|
||||||
return Ds
|
return Ds
|
||||||
|
|
||||||
|
|
||||||
def deriv_back(
|
def deriv_back(
|
||||||
dx_h: Sequence[NDArray[numpy.float_]],
|
dx_h: Sequence[NDArray[floating | complexfloating]],
|
||||||
) -> list[sparse.spmatrix]:
|
) -> list[sparse.sparray]:
|
||||||
"""
|
"""
|
||||||
Utility operators for taking discretized derivatives (backward variant).
|
Utility operators for taking discretized derivatives (backward variant).
|
||||||
|
|
||||||
|
|
@ -140,18 +141,18 @@ def deriv_back(
|
||||||
|
|
||||||
dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij')
|
dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij')
|
||||||
|
|
||||||
def deriv(axis: int) -> sparse.spmatrix:
|
def deriv(axis: int) -> sparse.sparray:
|
||||||
return shift_circ(axis, shape, -1) - sparse.eye(n)
|
return shift_circ(axis, shape, -1) - sparse.eye_array(n)
|
||||||
|
|
||||||
Ds = [sparse.diags(-1 / dx.ravel(order='C')) @ deriv(a)
|
Ds = [sparse.diags_array(-1 / dx.ravel(order='C')) @ deriv(a)
|
||||||
for a, dx in enumerate(dx_h_expanded)]
|
for a, dx in enumerate(dx_h_expanded)]
|
||||||
|
|
||||||
return Ds
|
return Ds
|
||||||
|
|
||||||
|
|
||||||
def cross(
|
def cross(
|
||||||
B: Sequence[sparse.spmatrix],
|
B: Sequence[sparse.sparray],
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Cross product operator
|
Cross product operator
|
||||||
|
|
||||||
|
|
@ -163,13 +164,14 @@ def cross(
|
||||||
Sparse matrix corresponding to (B x), where x is the cross product.
|
Sparse matrix corresponding to (B x), where x is the cross product.
|
||||||
"""
|
"""
|
||||||
n = B[0].shape[0]
|
n = B[0].shape[0]
|
||||||
zero = sparse.csr_matrix((n, n))
|
zero = sparse.csr_array((n, n))
|
||||||
return sparse.bmat([[zero, -B[2], B[1]],
|
return sparse.block_array([
|
||||||
|
[zero, -B[2], B[1]],
|
||||||
[B[2], zero, -B[0]],
|
[B[2], zero, -B[0]],
|
||||||
[-B[1], B[0], zero]])
|
[-B[1], B[0], zero]])
|
||||||
|
|
||||||
|
|
||||||
def vec_cross(b: vfdfield_t) -> sparse.spmatrix:
|
def vec_cross(b: vfdfield) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Vector cross product operator
|
Vector cross product operator
|
||||||
|
|
||||||
|
|
@ -181,11 +183,11 @@ def vec_cross(b: vfdfield_t) -> sparse.spmatrix:
|
||||||
Sparse matrix corresponding to (b x), where x is the cross product.
|
Sparse matrix corresponding to (b x), where x is the cross product.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
B = [sparse.diags(c) for c in numpy.split(b, 3)]
|
B = [sparse.diags_array(c) for c in numpy.split(b, 3)]
|
||||||
return cross(B)
|
return cross(B)
|
||||||
|
|
||||||
|
|
||||||
def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
|
def avg_forward(axis: int, shape: Sequence[int]) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Forward average operator `(x4 = (x4 + x5) / 2)`
|
Forward average operator `(x4 = (x4 + x5) / 2)`
|
||||||
|
|
||||||
|
|
@ -200,10 +202,10 @@ def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
|
||||||
raise Exception(f'Invalid shape: {shape}')
|
raise Exception(f'Invalid shape: {shape}')
|
||||||
|
|
||||||
n = numpy.prod(shape)
|
n = numpy.prod(shape)
|
||||||
return 0.5 * (sparse.eye(n) + shift_circ(axis, shape))
|
return 0.5 * (sparse.eye_array(n) + shift_circ(axis, shape))
|
||||||
|
|
||||||
|
|
||||||
def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
|
def avg_back(axis: int, shape: Sequence[int]) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Backward average operator `(x4 = (x4 + x3) / 2)`
|
Backward average operator `(x4 = (x4 + x3) / 2)`
|
||||||
|
|
||||||
|
|
@ -218,8 +220,8 @@ def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
|
||||||
|
|
||||||
|
|
||||||
def curl_forward(
|
def curl_forward(
|
||||||
dx_e: Sequence[NDArray[numpy.float_]],
|
dx_e: Sequence[NDArray[floating | complexfloating]],
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Curl operator for use with the E field.
|
Curl operator for use with the E field.
|
||||||
|
|
||||||
|
|
@ -234,8 +236,8 @@ def curl_forward(
|
||||||
|
|
||||||
|
|
||||||
def curl_back(
|
def curl_back(
|
||||||
dx_h: Sequence[NDArray[numpy.float_]],
|
dx_h: Sequence[NDArray[floating | complexfloating]],
|
||||||
) -> sparse.spmatrix:
|
) -> sparse.sparray:
|
||||||
"""
|
"""
|
||||||
Curl operator for use with the H field.
|
Curl operator for use with the H field.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,65 @@
|
||||||
"""
|
"""
|
||||||
Types shared across multiple submodules
|
Types shared across multiple submodules
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Callable, MutableSequence
|
from typing import NewType
|
||||||
import numpy
|
from collections.abc import Sequence, Callable, MutableSequence
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
from numpy import floating, complexfloating
|
||||||
|
|
||||||
|
|
||||||
# Field types
|
# Field types
|
||||||
fdfield_t = NDArray[numpy.float_]
|
fdfield_t = NewType('fdfield_t', NDArray[floating])
|
||||||
|
type fdfield = fdfield_t | NDArray[floating]
|
||||||
"""Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
|
"""Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
|
||||||
|
|
||||||
vfdfield_t = NDArray[numpy.float_]
|
vfdfield_t = NewType('vfdfield_t', NDArray[floating])
|
||||||
|
type vfdfield = vfdfield_t | NDArray[floating]
|
||||||
"""Linearized vector field (single vector of length 3*X*Y*Z)"""
|
"""Linearized vector field (single vector of length 3*X*Y*Z)"""
|
||||||
|
|
||||||
cfdfield_t = NDArray[numpy.complex_]
|
cfdfield_t = NewType('cfdfield_t', NDArray[complexfloating])
|
||||||
|
type cfdfield = cfdfield_t | NDArray[complexfloating]
|
||||||
"""Complex vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
|
"""Complex vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
|
||||||
|
|
||||||
vcfdfield_t = NDArray[numpy.complex_]
|
vcfdfield_t = NewType('vcfdfield_t', NDArray[complexfloating])
|
||||||
|
type vcfdfield = vcfdfield_t | NDArray[complexfloating]
|
||||||
"""Linearized complex vector field (single vector of length 3*X*Y*Z)"""
|
"""Linearized complex vector field (single vector of length 3*X*Y*Z)"""
|
||||||
|
|
||||||
|
|
||||||
dx_lists_t = Sequence[Sequence[NDArray[numpy.float_]]]
|
fdslice_t = NewType('fdslice_t', NDArray[floating])
|
||||||
|
type fdslice = fdslice_t | NDArray[floating]
|
||||||
|
"""Vector field slice with shape (3, X, Y) (e.g. `[E_x, E_y, E_z]` at a single Z position)"""
|
||||||
|
|
||||||
|
vfdslice_t = NewType('vfdslice_t', NDArray[floating])
|
||||||
|
type vfdslice = vfdslice_t | NDArray[floating]
|
||||||
|
"""Linearized vector field slice (single vector of length 3*X*Y)"""
|
||||||
|
|
||||||
|
cfdslice_t = NewType('cfdslice_t', NDArray[complexfloating])
|
||||||
|
type cfdslice = cfdslice_t | NDArray[complexfloating]
|
||||||
|
"""Complex vector field slice with shape (3, X, Y) (e.g. `[E_x, E_y, E_z]` at a single Z position)"""
|
||||||
|
|
||||||
|
vcfdslice_t = NewType('vcfdslice_t', NDArray[complexfloating])
|
||||||
|
type vcfdslice = vcfdslice_t | NDArray[complexfloating]
|
||||||
|
"""Linearized complex vector field slice (single vector of length 3*X*Y)"""
|
||||||
|
|
||||||
|
|
||||||
|
fdfield2_t = NewType('fdfield2_t', NDArray[floating])
|
||||||
|
type fdfield2 = fdfield2_t | NDArray[floating]
|
||||||
|
"""2D Vector field with shape (2, X, Y) (e.g. `[E_x, E_y]`)"""
|
||||||
|
|
||||||
|
vfdfield2_t = NewType('vfdfield2_t', NDArray[floating])
|
||||||
|
type vfdfield2 = vfdfield2_t | NDArray[floating]
|
||||||
|
"""2D Linearized vector field (single vector of length 2*X*Y)"""
|
||||||
|
|
||||||
|
cfdfield2_t = NewType('cfdfield2_t', NDArray[complexfloating])
|
||||||
|
type cfdfield2 = cfdfield2_t | NDArray[complexfloating]
|
||||||
|
"""2D Complex vector field with shape (2, X, Y) (e.g. `[E_x, E_y]`)"""
|
||||||
|
|
||||||
|
vcfdfield2_t = NewType('vcfdfield2_t', NDArray[complexfloating])
|
||||||
|
type vcfdfield2 = vcfdfield2_t | NDArray[complexfloating]
|
||||||
|
"""2D Linearized complex vector field (single vector of length 2*X*Y)"""
|
||||||
|
|
||||||
|
|
||||||
|
dx_lists_t = Sequence[Sequence[NDArray[floating | complexfloating]]]
|
||||||
"""
|
"""
|
||||||
'dxes' datastructure which contains grid cell width information in the following format:
|
'dxes' datastructure which contains grid cell width information in the following format:
|
||||||
|
|
||||||
|
|
@ -31,12 +70,26 @@ dx_lists_t = Sequence[Sequence[NDArray[numpy.float_]]]
|
||||||
and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc.
|
and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dx_lists_mut = MutableSequence[MutableSequence[NDArray[numpy.float_]]]
|
dx_lists2_t = Sequence[Sequence[NDArray[floating | complexfloating]]]
|
||||||
|
"""
|
||||||
|
2D 'dxes' datastructure which contains grid cell width information in the following format:
|
||||||
|
|
||||||
|
[[[dx_e[0], dx_e[1], ...], [dy_e[0], ...]],
|
||||||
|
[[dx_h[0], dx_h[1], ...], [dy_h[0], ...]]]
|
||||||
|
|
||||||
|
where `dx_e[0]` is the x-width of the `x=0` cells, as used when calculating dE/dx,
|
||||||
|
and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dx_lists_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloating]]]
|
||||||
"""Mutable version of `dx_lists_t`"""
|
"""Mutable version of `dx_lists_t`"""
|
||||||
|
|
||||||
|
dx_lists2_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloating]]]
|
||||||
|
"""Mutable version of `dx_lists2_t`"""
|
||||||
|
|
||||||
fdfield_updater_t = Callable[..., fdfield_t]
|
|
||||||
"""Convenience type for functions which take and return an fdfield_t"""
|
|
||||||
|
|
||||||
cfdfield_updater_t = Callable[..., cfdfield_t]
|
fdfield_updater_t = Callable[..., fdfield]
|
||||||
"""Convenience type for functions which take and return an cfdfield_t"""
|
"""Convenience type for functions which take and return a real `fdfield`"""
|
||||||
|
|
||||||
|
cfdfield_updater_t = Callable[..., cfdfield]
|
||||||
|
"""Convenience type for functions which take and return a complex `cfdfield`"""
|
||||||
|
|
|
||||||
|
|
@ -4,37 +4,60 @@ and a 1D array representation of that field `[f_x0, f_x1, f_x2,... f_y0,... f_z0
|
||||||
Vectorized versions of the field use row-major (ie., C-style) ordering.
|
Vectorized versions of the field use row-major (ie., C-style) ordering.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import overload, Sequence
|
from typing import overload
|
||||||
|
from collections.abc import Sequence
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
from .types import fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t
|
from .types import (
|
||||||
|
fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t,
|
||||||
|
fdslice_t, vfdslice_t, cfdslice_t, vcfdslice_t,
|
||||||
|
fdfield2_t, vfdfield2_t, cfdfield2_t, vcfdfield2_t,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def vec(f: None) -> None:
|
def vec(f: None) -> None:
|
||||||
pass
|
pass # pragma: no cover
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def vec(f: fdfield_t) -> vfdfield_t:
|
def vec(f: fdfield_t) -> vfdfield_t:
|
||||||
pass
|
pass # pragma: no cover
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def vec(f: cfdfield_t) -> vcfdfield_t:
|
def vec(f: cfdfield_t) -> vcfdfield_t:
|
||||||
pass
|
pass # pragma: no cover
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def vec(f: ArrayLike) -> vfdfield_t | vcfdfield_t:
|
def vec(f: fdfield2_t) -> vfdfield2_t:
|
||||||
pass
|
pass # pragma: no cover
|
||||||
|
|
||||||
def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_t | None:
|
@overload
|
||||||
|
def vec(f: cfdfield2_t) -> vcfdfield2_t:
|
||||||
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def vec(f: fdslice_t) -> vfdslice_t:
|
||||||
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def vec(f: cfdslice_t) -> vcfdslice_t:
|
||||||
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def vec(f: ArrayLike) -> NDArray:
|
||||||
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
def vec(
|
||||||
|
f: fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | ArrayLike | None,
|
||||||
|
) -> vfdfield_t | vcfdfield_t | vfdfield2_t | vcfdfield2_t | vfdslice_t | vcfdslice_t | NDArray | None:
|
||||||
"""
|
"""
|
||||||
Create a 1D ndarray from a 3D vector field which spans a 1-3D region.
|
Create a 1D ndarray from a vector field which spans a 1-3D region.
|
||||||
|
|
||||||
Returns `None` if called with `f=None`.
|
Returns `None` if called with `f=None`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
f: A vector field, `[f_x, f_y, f_z]` where each `f_` component is a 1- to
|
f: A vector field, e.g. `[f_x, f_y, f_z]` where each `f_` component is a 1- to
|
||||||
3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`.
|
3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -42,37 +65,61 @@ def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_
|
||||||
"""
|
"""
|
||||||
if f is None:
|
if f is None:
|
||||||
return None
|
return None
|
||||||
return numpy.ravel(f, order='C')
|
return numpy.ravel(f, order='C') # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def unvec(v: None, shape: Sequence[int]) -> None:
|
def unvec(v: None, shape: Sequence[int], nvdim: int = 3) -> None:
|
||||||
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int = 3) -> fdfield_t:
|
||||||
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield_t:
|
||||||
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def unvec(v: vfdfield2_t, shape: Sequence[int], nvdim: int = 3) -> fdfield2_t:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def unvec(v: vfdfield_t, shape: Sequence[int]) -> fdfield_t:
|
def unvec(v: vcfdfield2_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield2_t:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def unvec(v: vcfdfield_t, shape: Sequence[int]) -> cfdfield_t:
|
def unvec(v: vfdslice_t, shape: Sequence[int], nvdim: int = 3) -> fdslice_t:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def unvec(v: vfdfield_t | vcfdfield_t | None, shape: Sequence[int]) -> fdfield_t | cfdfield_t | None:
|
@overload
|
||||||
|
def unvec(v: vcfdslice_t, shape: Sequence[int], nvdim: int = 3) -> cfdslice_t:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def unvec(v: ArrayLike, shape: Sequence[int], nvdim: int = 3) -> NDArray:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def unvec(
|
||||||
|
v: vfdfield_t | vcfdfield_t | vfdfield2_t | vcfdfield2_t | vfdslice_t | vcfdslice_t | ArrayLike | None,
|
||||||
|
shape: Sequence[int],
|
||||||
|
nvdim: int = 3,
|
||||||
|
) -> fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | NDArray | None:
|
||||||
"""
|
"""
|
||||||
Perform the inverse of vec(): take a 1D ndarray and output a 3D field
|
Perform the inverse of vec(): take a 1D ndarray and output an `nvdim`-component field
|
||||||
of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional
|
of form e.g. `[f_x, f_y, f_z]` (`nvdim=3`) where each of `f_*` is a len(shape)-dimensional
|
||||||
ndarray.
|
ndarray.
|
||||||
|
|
||||||
Returns `None` if called with `v=None`.
|
Returns `None` if called with `v=None`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
v: 1D ndarray representing a 3D vector field of shape shape (or None)
|
v: 1D ndarray representing a vector field of shape shape (or None)
|
||||||
shape: shape of the vector field
|
shape: shape of the vector field
|
||||||
|
nvdim: Number of components in each vector
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`)
|
`[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`)
|
||||||
"""
|
"""
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
return v.reshape((3, *shape), order='C')
|
return v.reshape((nvdim, *shape), order='C') # type: ignore
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,50 @@ It is often useful to excite the simulation with an arbitrary broadband pulse an
|
||||||
extract the frequency-domain response by performing an on-the-fly Fourier transform
|
extract the frequency-domain response by performing an on-the-fly Fourier transform
|
||||||
of the time-domain fields.
|
of the time-domain fields.
|
||||||
|
|
||||||
|
`accumulate_phasor` in `meanas.fdtd.phasor` performs the phase accumulation for one
|
||||||
|
or more target frequencies, while leaving source normalization and simulation-loop
|
||||||
|
policy to the caller. `temporal_phasor(...)` and `temporal_phasor_scale(...)`
|
||||||
|
apply the same Fourier sum to a scalar waveform, which is useful when a pulsed
|
||||||
|
source envelope must be normalized before being applied to a point source or
|
||||||
|
mode source. `real_injection_scale(...)` is the corresponding helper for the
|
||||||
|
common real-valued injection pattern `numpy.real(scale * waveform)`. Convenience
|
||||||
|
wrappers `accumulate_phasor_e`, `accumulate_phasor_h`, and `accumulate_phasor_j`
|
||||||
|
apply the usual Yee time offsets. `reconstruct_real(...)` and the corresponding
|
||||||
|
`reconstruct_real_e/h/j(...)` wrappers perform the inverse operation, converting
|
||||||
|
phasors back into real-valued field snapshots at explicit Yee-aligned times.
|
||||||
|
For scalar `omega`, the reconstruction helpers accept either a plain field
|
||||||
|
phasor or the batched `(1, *sample_shape)` form used by the accumulator helpers.
|
||||||
|
The helpers accumulate
|
||||||
|
|
||||||
|
$$ \Delta_t \sum_l w_l e^{-i \omega t_l} f_l $$
|
||||||
|
|
||||||
|
with caller-provided sample times and weights. In this codebase the matching
|
||||||
|
electric-current convention is typically `E -= dt * J / epsilon` in FDTD and
|
||||||
|
`-i \omega J` on the right-hand side of the FDFD wave equation.
|
||||||
|
|
||||||
|
For FDTD/FDFD equivalence, this phasor path is the primary comparison workflow.
|
||||||
|
It isolates the guided `+\omega` response that the frequency-domain solver
|
||||||
|
targets directly, regardless of whether the underlying FDTD run used real- or
|
||||||
|
complex-valued fields.
|
||||||
|
|
||||||
|
For exact pulsed FDTD/FDFD equivalence it is often simplest to keep the
|
||||||
|
injected source, fields, and CPML auxiliary state complex-valued. The
|
||||||
|
`real_injection_scale(...)` helper is instead for the more ordinary one-run
|
||||||
|
real-valued source path, where the intended positive-frequency waveform is
|
||||||
|
injected through `numpy.real(scale * waveform)` and any remaining negative-
|
||||||
|
frequency leakage is controlled by the pulse bandwidth and run length.
|
||||||
|
|
||||||
|
`reconstruct_real(...)` is for a different question: given a phasor, what late
|
||||||
|
real-valued field snapshot should it produce? That raw-snapshot comparison is
|
||||||
|
stricter and noisier because a monitor plane generally contains both the guided
|
||||||
|
field and the remaining orthogonal content,
|
||||||
|
|
||||||
|
$$ E_{\text{monitor}} = E_{\text{guided}} + E_{\text{residual}} . $$
|
||||||
|
|
||||||
|
Phasor/modal comparisons mostly validate the guided `+\omega` term. Raw
|
||||||
|
real-field comparisons expose both terms at once, so they should be treated as
|
||||||
|
secondary diagnostics rather than the main solver-equivalence benchmark.
|
||||||
|
|
||||||
The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse
|
The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse
|
||||||
shape. It can be written
|
shape. It can be written
|
||||||
|
|
||||||
|
|
@ -156,11 +200,75 @@ t=0 (assuming the source is off for t<0 this gives $\sim 10^{-3}$ error at t=0).
|
||||||
|
|
||||||
Boundary conditions
|
Boundary conditions
|
||||||
===================
|
===================
|
||||||
# TODO notes about boundaries / PMLs
|
|
||||||
|
`meanas.fdtd` exposes two boundary-related building blocks:
|
||||||
|
|
||||||
|
- `conducting_boundary(...)` for simple perfect-electric-conductor style field
|
||||||
|
clamping at one face of the domain.
|
||||||
|
- `cpml_params(...)` and `updates_with_cpml(...)` for convolutional perfectly
|
||||||
|
matched layers (CPMLs) attached to one or more faces of the Yee grid.
|
||||||
|
|
||||||
|
`updates_with_cpml(...)` accepts a three-by-two table of CPML parameter blocks:
|
||||||
|
|
||||||
|
```
|
||||||
|
cpml_params[axis][polarity_index]
|
||||||
|
```
|
||||||
|
|
||||||
|
where `axis` is `0`, `1`, or `2` and `polarity_index` corresponds to `(-1, +1)`.
|
||||||
|
Passing `None` for one entry disables CPML on that face while leaving the other
|
||||||
|
directions unchanged. This is how mixed boundary setups such as "absorbing in x,
|
||||||
|
periodic in y/z" are expressed.
|
||||||
|
|
||||||
|
When comparing an FDTD run against an FDFD solve, use the same stretched
|
||||||
|
coordinate system in both places:
|
||||||
|
|
||||||
|
1. Build the FDTD update with the desired CPML parameters.
|
||||||
|
2. Stretch the FDFD `dxes` with the matching SCPML transform.
|
||||||
|
3. Compare the extracted phasor against the FDFD field or residual on those
|
||||||
|
stretched `dxes`.
|
||||||
|
|
||||||
|
The electric-current sign convention used throughout the examples and tests is
|
||||||
|
|
||||||
|
$$
|
||||||
|
E \leftarrow E - \Delta_t J / \epsilon
|
||||||
|
$$
|
||||||
|
|
||||||
|
which matches the FDFD right-hand side
|
||||||
|
|
||||||
|
$$
|
||||||
|
-i \omega J.
|
||||||
|
$$
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .base import maxwell_e, maxwell_h
|
from .base import (
|
||||||
from .pml import cpml_params, updates_with_cpml
|
maxwell_e as maxwell_e,
|
||||||
from .energy import (poynting, poynting_divergence, energy_hstep, energy_estep,
|
maxwell_h as maxwell_h,
|
||||||
delta_energy_h2e, delta_energy_j)
|
)
|
||||||
from .boundaries import conducting_boundary
|
from .pml import (
|
||||||
|
cpml_params as cpml_params,
|
||||||
|
updates_with_cpml as updates_with_cpml,
|
||||||
|
)
|
||||||
|
from .energy import (
|
||||||
|
poynting as poynting,
|
||||||
|
poynting_divergence as poynting_divergence,
|
||||||
|
energy_hstep as energy_hstep,
|
||||||
|
energy_estep as energy_estep,
|
||||||
|
delta_energy_h2e as delta_energy_h2e,
|
||||||
|
delta_energy_j as delta_energy_j,
|
||||||
|
)
|
||||||
|
from .boundaries import (
|
||||||
|
conducting_boundary as conducting_boundary,
|
||||||
|
)
|
||||||
|
from .phasor import (
|
||||||
|
accumulate_phasor as accumulate_phasor,
|
||||||
|
accumulate_phasor_e as accumulate_phasor_e,
|
||||||
|
accumulate_phasor_h as accumulate_phasor_h,
|
||||||
|
accumulate_phasor_j as accumulate_phasor_j,
|
||||||
|
real_injection_scale as real_injection_scale,
|
||||||
|
reconstruct_real as reconstruct_real,
|
||||||
|
reconstruct_real_e as reconstruct_real_e,
|
||||||
|
reconstruct_real_h as reconstruct_real_h,
|
||||||
|
reconstruct_real_j as reconstruct_real_j,
|
||||||
|
temporal_phasor as temporal_phasor,
|
||||||
|
temporal_phasor_scale as temporal_phasor_scale,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ Basic FDTD field updates
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t
|
from ..fdmath import dx_lists_t, fdfield, fdfield_updater_t
|
||||||
from ..fdmath.functional import curl_forward, curl_back
|
from ..fdmath.functional import curl_forward, curl_back
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ def maxwell_e(
|
||||||
else:
|
else:
|
||||||
curl_h_fun = curl_back()
|
curl_h_fun = curl_back()
|
||||||
|
|
||||||
def me_fun(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t | float) -> fdfield_t:
|
def me_fun(e: fdfield, h: fdfield, epsilon: fdfield | float) -> fdfield:
|
||||||
"""
|
"""
|
||||||
Update the E-field.
|
Update the E-field.
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ def maxwell_h(
|
||||||
else:
|
else:
|
||||||
curl_e_fun = curl_forward()
|
curl_e_fun = curl_forward()
|
||||||
|
|
||||||
def mh_fun(e: fdfield_t, h: fdfield_t, mu: fdfield_t | float | None = None) -> fdfield_t:
|
def mh_fun(e: fdfield, h: fdfield, mu: fdfield | float | None = None) -> fdfield:
|
||||||
"""
|
"""
|
||||||
Update the H-field.
|
Update the H-field.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Boundary conditions
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ..fdmath import fdfield_t, fdfield_updater_t
|
from ..fdmath import fdfield, fdfield_updater_t
|
||||||
|
|
||||||
|
|
||||||
def conducting_boundary(
|
def conducting_boundary(
|
||||||
|
|
@ -15,7 +15,7 @@ def conducting_boundary(
|
||||||
) -> tuple[fdfield_updater_t, fdfield_updater_t]:
|
) -> tuple[fdfield_updater_t, fdfield_updater_t]:
|
||||||
dirs = [0, 1, 2]
|
dirs = [0, 1, 2]
|
||||||
if direction not in dirs:
|
if direction not in dirs:
|
||||||
raise Exception(f'Invalid direction: {direction}')
|
raise ValueError(f'Invalid direction: {direction}')
|
||||||
dirs.remove(direction)
|
dirs.remove(direction)
|
||||||
u, v = dirs
|
u, v = dirs
|
||||||
|
|
||||||
|
|
@ -28,17 +28,19 @@ def conducting_boundary(
|
||||||
shifted1_slice = [slice(None)] * 3
|
shifted1_slice = [slice(None)] * 3
|
||||||
boundary_slice[direction] = 0
|
boundary_slice[direction] = 0
|
||||||
shifted1_slice[direction] = 1
|
shifted1_slice[direction] = 1
|
||||||
|
boundary = tuple(boundary_slice)
|
||||||
|
shifted1 = tuple(shifted1_slice)
|
||||||
|
|
||||||
def en(e: fdfield_t) -> fdfield_t:
|
def en(e: fdfield) -> fdfield:
|
||||||
e[direction][boundary_slice] = 0
|
e[direction][boundary] = 0
|
||||||
e[u][boundary_slice] = e[u][shifted1_slice]
|
e[u][boundary] = e[u][shifted1]
|
||||||
e[v][boundary_slice] = e[v][shifted1_slice]
|
e[v][boundary] = e[v][shifted1]
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def hn(h: fdfield_t) -> fdfield_t:
|
def hn(h: fdfield) -> fdfield:
|
||||||
h[direction][boundary_slice] = h[direction][shifted1_slice]
|
h[direction][boundary] = h[direction][shifted1]
|
||||||
h[u][boundary_slice] = 0
|
h[u][boundary] = 0
|
||||||
h[v][boundary_slice] = 0
|
h[v][boundary] = 0
|
||||||
return h
|
return h
|
||||||
|
|
||||||
return en, hn
|
return en, hn
|
||||||
|
|
@ -50,22 +52,25 @@ def conducting_boundary(
|
||||||
boundary_slice[direction] = -1
|
boundary_slice[direction] = -1
|
||||||
shifted1_slice[direction] = -2
|
shifted1_slice[direction] = -2
|
||||||
shifted2_slice[direction] = -3
|
shifted2_slice[direction] = -3
|
||||||
|
boundary = tuple(boundary_slice)
|
||||||
|
shifted1 = tuple(shifted1_slice)
|
||||||
|
shifted2 = tuple(shifted2_slice)
|
||||||
|
|
||||||
def ep(e: fdfield_t) -> fdfield_t:
|
def ep(e: fdfield) -> fdfield:
|
||||||
e[direction][boundary_slice] = -e[direction][shifted2_slice]
|
e[direction][boundary] = -e[direction][shifted2]
|
||||||
e[direction][shifted1_slice] = 0
|
e[direction][shifted1] = 0
|
||||||
e[u][boundary_slice] = e[u][shifted1_slice]
|
e[u][boundary] = e[u][shifted1]
|
||||||
e[v][boundary_slice] = e[v][shifted1_slice]
|
e[v][boundary] = e[v][shifted1]
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def hp(h: fdfield_t) -> fdfield_t:
|
def hp(h: fdfield) -> fdfield:
|
||||||
h[direction][boundary_slice] = h[direction][shifted1_slice]
|
h[direction][boundary] = h[direction][shifted1]
|
||||||
h[u][boundary_slice] = -h[u][shifted2_slice]
|
h[u][boundary] = -h[u][shifted2]
|
||||||
h[u][shifted1_slice] = 0
|
h[u][shifted1] = 0
|
||||||
h[v][boundary_slice] = -h[v][shifted2_slice]
|
h[v][boundary] = -h[v][shifted2]
|
||||||
h[v][shifted1_slice] = 0
|
h[v][shifted1] = 0
|
||||||
return h
|
return h
|
||||||
|
|
||||||
return ep, hp
|
return ep, hp
|
||||||
|
|
||||||
raise Exception(f'Bad polarity: {polarity}')
|
raise ValueError(f'Bad polarity: {polarity}')
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,29 @@
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
from ..fdmath import dx_lists_t, fdfield_t
|
from ..fdmath import dx_lists_t, fdfield_t, fdfield
|
||||||
from ..fdmath.functional import deriv_back
|
from ..fdmath.functional import deriv_back
|
||||||
|
|
||||||
|
|
||||||
# TODO documentation
|
"""
|
||||||
|
Energy- and flux-accounting helpers for Yee-grid FDTD fields.
|
||||||
|
|
||||||
|
These functions complement the derivation in `meanas.fdtd`:
|
||||||
|
|
||||||
|
- `poynting(...)` and `poynting_divergence(...)` evaluate the discrete flux terms
|
||||||
|
from the exact time-domain Poynting identity.
|
||||||
|
- `energy_hstep(...)` / `energy_estep(...)` evaluate the two staggered energy
|
||||||
|
expressions.
|
||||||
|
- `delta_energy_*` helpers evaluate the corresponding energy changes between
|
||||||
|
adjacent half-steps.
|
||||||
|
|
||||||
|
The helpers are intended for diagnostics, regression tests, and consistency
|
||||||
|
checks between source work, field energy, and flux through cell faces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def poynting(
|
def poynting(
|
||||||
e: fdfield_t,
|
e: fdfield,
|
||||||
h: fdfield_t,
|
h: fdfield,
|
||||||
dxes: dx_lists_t | None = None,
|
dxes: dx_lists_t | None = None,
|
||||||
) -> fdfield_t:
|
) -> fdfield_t:
|
||||||
r"""
|
r"""
|
||||||
|
|
@ -43,6 +57,7 @@ def poynting(
|
||||||
(see `meanas.tests.test_fdtd.test_poynting_planes`)
|
(see `meanas.tests.test_fdtd.test_poynting_planes`)
|
||||||
|
|
||||||
The full relationship is
|
The full relationship is
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{aligned}
|
\begin{aligned}
|
||||||
(U_{l+\frac{1}{2}} - U_l) / \Delta_t
|
(U_{l+\frac{1}{2}} - U_l) / \Delta_t
|
||||||
|
|
@ -84,14 +99,14 @@ def poynting(
|
||||||
s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy
|
s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy
|
||||||
s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz
|
s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz
|
||||||
s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx
|
s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx
|
||||||
return s
|
return fdfield_t(s)
|
||||||
|
|
||||||
|
|
||||||
def poynting_divergence(
|
def poynting_divergence(
|
||||||
s: fdfield_t | None = None,
|
s: fdfield | None = None,
|
||||||
*,
|
*,
|
||||||
e: fdfield_t | None = None,
|
e: fdfield | None = None,
|
||||||
h: fdfield_t | None = None,
|
h: fdfield | None = None,
|
||||||
dxes: dx_lists_t | None = None,
|
dxes: dx_lists_t | None = None,
|
||||||
) -> fdfield_t:
|
) -> fdfield_t:
|
||||||
"""
|
"""
|
||||||
|
|
@ -122,15 +137,15 @@ def poynting_divergence(
|
||||||
|
|
||||||
Dx, Dy, Dz = deriv_back()
|
Dx, Dy, Dz = deriv_back()
|
||||||
ds = Dx(s[0]) + Dy(s[1]) + Dz(s[2])
|
ds = Dx(s[0]) + Dy(s[1]) + Dz(s[2])
|
||||||
return ds
|
return fdfield_t(ds)
|
||||||
|
|
||||||
|
|
||||||
def energy_hstep(
|
def energy_hstep(
|
||||||
e0: fdfield_t,
|
e0: fdfield,
|
||||||
h1: fdfield_t,
|
h1: fdfield,
|
||||||
e2: fdfield_t,
|
e2: fdfield,
|
||||||
epsilon: fdfield_t | None = None,
|
epsilon: fdfield | None = None,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
dxes: dx_lists_t | None = None,
|
dxes: dx_lists_t | None = None,
|
||||||
) -> fdfield_t:
|
) -> fdfield_t:
|
||||||
"""
|
"""
|
||||||
|
|
@ -150,15 +165,15 @@ def energy_hstep(
|
||||||
Energy, at the time of the H-field `h1`.
|
Energy, at the time of the H-field `h1`.
|
||||||
"""
|
"""
|
||||||
u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes)
|
u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes)
|
||||||
return u
|
return fdfield_t(u)
|
||||||
|
|
||||||
|
|
||||||
def energy_estep(
|
def energy_estep(
|
||||||
h0: fdfield_t,
|
h0: fdfield,
|
||||||
e1: fdfield_t,
|
e1: fdfield,
|
||||||
h2: fdfield_t,
|
h2: fdfield,
|
||||||
epsilon: fdfield_t | None = None,
|
epsilon: fdfield | None = None,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
dxes: dx_lists_t | None = None,
|
dxes: dx_lists_t | None = None,
|
||||||
) -> fdfield_t:
|
) -> fdfield_t:
|
||||||
"""
|
"""
|
||||||
|
|
@ -178,17 +193,17 @@ def energy_estep(
|
||||||
Energy, at the time of the E-field `e1`.
|
Energy, at the time of the E-field `e1`.
|
||||||
"""
|
"""
|
||||||
u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes)
|
u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes)
|
||||||
return u
|
return fdfield_t(u)
|
||||||
|
|
||||||
|
|
||||||
def delta_energy_h2e(
|
def delta_energy_h2e(
|
||||||
dt: float,
|
dt: float,
|
||||||
e0: fdfield_t,
|
e0: fdfield,
|
||||||
h1: fdfield_t,
|
h1: fdfield,
|
||||||
e2: fdfield_t,
|
e2: fdfield,
|
||||||
h3: fdfield_t,
|
h3: fdfield,
|
||||||
epsilon: fdfield_t | None = None,
|
epsilon: fdfield | None = None,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
dxes: dx_lists_t | None = None,
|
dxes: dx_lists_t | None = None,
|
||||||
) -> fdfield_t:
|
) -> fdfield_t:
|
||||||
"""
|
"""
|
||||||
|
|
@ -211,17 +226,17 @@ def delta_energy_h2e(
|
||||||
de = e2 * (e2 - e0) / dt
|
de = e2 * (e2 - e0) / dt
|
||||||
dh = h1 * (h3 - h1) / dt
|
dh = h1 * (h3 - h1) / dt
|
||||||
du = dxmul(de, dh, epsilon, mu, dxes)
|
du = dxmul(de, dh, epsilon, mu, dxes)
|
||||||
return du
|
return fdfield_t(du)
|
||||||
|
|
||||||
|
|
||||||
def delta_energy_e2h(
|
def delta_energy_e2h(
|
||||||
dt: float,
|
dt: float,
|
||||||
h0: fdfield_t,
|
h0: fdfield,
|
||||||
e1: fdfield_t,
|
e1: fdfield,
|
||||||
h2: fdfield_t,
|
h2: fdfield,
|
||||||
e3: fdfield_t,
|
e3: fdfield,
|
||||||
epsilon: fdfield_t | None = None,
|
epsilon: fdfield | None = None,
|
||||||
mu: fdfield_t | None = None,
|
mu: fdfield | None = None,
|
||||||
dxes: dx_lists_t | None = None,
|
dxes: dx_lists_t | None = None,
|
||||||
) -> fdfield_t:
|
) -> fdfield_t:
|
||||||
"""
|
"""
|
||||||
|
|
@ -244,21 +259,31 @@ def delta_energy_e2h(
|
||||||
de = e1 * (e3 - e1) / dt
|
de = e1 * (e3 - e1) / dt
|
||||||
dh = h2 * (h2 - h0) / dt
|
dh = h2 * (h2 - h0) / dt
|
||||||
du = dxmul(de, dh, epsilon, mu, dxes)
|
du = dxmul(de, dh, epsilon, mu, dxes)
|
||||||
return du
|
return fdfield_t(du)
|
||||||
|
|
||||||
|
|
||||||
def delta_energy_j(
|
def delta_energy_j(
|
||||||
j0: fdfield_t,
|
j0: fdfield,
|
||||||
e1: fdfield_t,
|
e1: fdfield,
|
||||||
dxes: dx_lists_t | None = None,
|
dxes: dx_lists_t | None = None,
|
||||||
) -> fdfield_t:
|
) -> fdfield_t:
|
||||||
"""
|
r"""
|
||||||
Calculate
|
Calculate the electric-current work term $J \cdot E$ on the Yee grid.
|
||||||
|
|
||||||
Note that each value of $J$ contributes to the energy twice (i.e. once per field update)
|
This is the source contribution that appears beside the flux divergence in
|
||||||
despite only causing the value of $E$ to change once (same for $M$ and $H$).
|
the discrete Poynting identities documented in `meanas.fdtd`.
|
||||||
|
|
||||||
|
Note that each value of `J` contributes twice in a full Yee cycle (once per
|
||||||
|
half-step energy balance) even though it directly changes `E` only once.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
j0: Electric-current density sampled at the same half-step as the
|
||||||
|
current work term.
|
||||||
|
e1: Electric field sampled at the matching integer timestep.
|
||||||
|
dxes: Grid description; defaults to unit spacing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Per-cell source-work contribution with the scalar field shape.
|
||||||
"""
|
"""
|
||||||
if dxes is None:
|
if dxes is None:
|
||||||
dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2))
|
dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2))
|
||||||
|
|
@ -267,16 +292,30 @@ def delta_energy_j(
|
||||||
* dxes[0][0][:, None, None]
|
* dxes[0][0][:, None, None]
|
||||||
* dxes[0][1][None, :, None]
|
* dxes[0][1][None, :, None]
|
||||||
* dxes[0][2][None, None, :])
|
* dxes[0][2][None, None, :])
|
||||||
return du
|
return fdfield_t(du)
|
||||||
|
|
||||||
|
|
||||||
def dxmul(
|
def dxmul(
|
||||||
ee: fdfield_t,
|
ee: fdfield,
|
||||||
hh: fdfield_t,
|
hh: fdfield,
|
||||||
epsilon: fdfield_t | float | None = None,
|
epsilon: fdfield | float | None = None,
|
||||||
mu: fdfield_t | float | None = None,
|
mu: fdfield | float | None = None,
|
||||||
dxes: dx_lists_t | None = None,
|
dxes: dx_lists_t | None = None,
|
||||||
) -> fdfield_t:
|
) -> fdfield_t:
|
||||||
|
"""
|
||||||
|
Multiply E- and H-like field products by material weights and cell volumes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ee: Three-component electric-field product, such as `e0 * e2`.
|
||||||
|
hh: Three-component magnetic-field product, such as `h1 * h1`.
|
||||||
|
epsilon: Electric material weight; defaults to `1`.
|
||||||
|
mu: Magnetic material weight; defaults to `1`.
|
||||||
|
dxes: Grid description; defaults to unit spacing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Scalar field containing the weighted electric plus magnetic contribution
|
||||||
|
for each Yee cell.
|
||||||
|
"""
|
||||||
if epsilon is None:
|
if epsilon is None:
|
||||||
epsilon = 1
|
epsilon = 1
|
||||||
if mu is None:
|
if mu is None:
|
||||||
|
|
@ -292,4 +331,4 @@ def dxmul(
|
||||||
* dxes[1][0][:, None, None]
|
* dxes[1][0][:, None, None]
|
||||||
* dxes[1][1][None, :, None]
|
* dxes[1][1][None, :, None]
|
||||||
* dxes[1][2][None, None, :])
|
* dxes[1][2][None, None, :])
|
||||||
return result
|
return fdfield_t(result)
|
||||||
|
|
|
||||||
180
meanas/fdtd/misc.py
Normal file
180
meanas/fdtd/misc.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
from collections.abc import Callable
|
||||||
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import NDArray, ArrayLike
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
type pulse_scalar_t = float | NDArray[numpy.floating]
|
||||||
|
pulse_fn_t = Callable[[ArrayLike], tuple[pulse_scalar_t, pulse_scalar_t, pulse_scalar_t]]
|
||||||
|
|
||||||
|
|
||||||
|
def _scalar_or_array(values: NDArray[numpy.floating]) -> pulse_scalar_t:
|
||||||
|
if values.ndim == 0:
|
||||||
|
return float(values)
|
||||||
|
return cast('NDArray[numpy.floating]', values)
|
||||||
|
|
||||||
|
|
||||||
|
def gaussian_packet(
|
||||||
|
wl: float,
|
||||||
|
dwl: float,
|
||||||
|
dt: float,
|
||||||
|
turn_on: float = 1e-10,
|
||||||
|
one_sided: bool = False,
|
||||||
|
) -> tuple[pulse_fn_t, float]:
|
||||||
|
"""
|
||||||
|
Gaussian pulse (or gaussian ramp) for FDTD excitation
|
||||||
|
|
||||||
|
exp(-a*t*t) ==> exp(-omega * omega / (4 * a)) [fourier, ignoring leading const.]
|
||||||
|
|
||||||
|
FWHM_time is 2 * sqrt(2 * log(2)) * sqrt(2 / a)
|
||||||
|
FWHM_omega is 2 * sqrt(2 * log(2)) * sqrt(2 * a) = 4 * sqrt(log(2) * a)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wl: wavelength
|
||||||
|
dwl: Gaussian's FWHM in wavelength space
|
||||||
|
dt: Timestep
|
||||||
|
turn_on: Max allowable amplitude at t=0
|
||||||
|
one_sided: If `True`, source amplitude never decreases after reaching max
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Source function: src(timestep) -> (envelope[tt], cos[... * tt], sin[... * tt])
|
||||||
|
Delay: number of initial timesteps for which envelope[tt] will be 0
|
||||||
|
"""
|
||||||
|
logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO
|
||||||
|
# dt * dw = 4 * ln(2)
|
||||||
|
|
||||||
|
omega = 2 * pi / wl
|
||||||
|
freq = 1 / wl
|
||||||
|
fwhm_omega = dwl * omega * omega / (2 * pi) # dwl -> d_omega (approx)
|
||||||
|
alpha = (fwhm_omega * fwhm_omega) * numpy.log(2) / 8
|
||||||
|
delay = numpy.sqrt(-numpy.log(turn_on) / alpha)
|
||||||
|
delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase
|
||||||
|
logger.info(f'src_time {2 * delay / dt}')
|
||||||
|
|
||||||
|
def source_phasor(ii: ArrayLike) -> tuple[pulse_scalar_t, pulse_scalar_t, pulse_scalar_t]:
|
||||||
|
ii_array = numpy.asarray(ii, dtype=float)
|
||||||
|
t0 = ii_array * dt - delay
|
||||||
|
envelope = numpy.sqrt(numpy.sqrt(2 * alpha / pi)) * numpy.exp(-alpha * t0 * t0)
|
||||||
|
|
||||||
|
if one_sided:
|
||||||
|
envelope = numpy.where(t0 > 0, 1.0, envelope)
|
||||||
|
|
||||||
|
cc = numpy.cos(omega * t0)
|
||||||
|
ss = numpy.sin(omega * t0)
|
||||||
|
|
||||||
|
return _scalar_or_array(envelope), _scalar_or_array(cc), _scalar_or_array(ss)
|
||||||
|
|
||||||
|
# nrm = numpy.exp(-omega * omega / alpha) / 2
|
||||||
|
|
||||||
|
return source_phasor, delay
|
||||||
|
|
||||||
|
|
||||||
|
def ricker_pulse(
|
||||||
|
wl: float,
|
||||||
|
dt: float,
|
||||||
|
turn_on: float = 1e-10,
|
||||||
|
) -> tuple[pulse_fn_t, float]:
|
||||||
|
"""
|
||||||
|
Ricker wavelet (second derivative of a gaussian pulse)
|
||||||
|
|
||||||
|
t0 = ii * dt - delay
|
||||||
|
R = w_peak * t0 / 2
|
||||||
|
f(t) = (1 - 2 * (pi * f_peak * t0) ** 2) * exp(-(pi * f_peak * t0)**2
|
||||||
|
= (1 - (w_peak * t0)**2 / 2 exp(-(w_peak * t0 / 2) **2)
|
||||||
|
= (1 - 2 * R * R) * exp(-R * R)
|
||||||
|
|
||||||
|
# NOTE: don't use cosine/sine for J, just for phasor readout
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wl: wavelength
|
||||||
|
dt: Timestep
|
||||||
|
turn_on: Max allowable amplitude at t=0
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Source function: src(timestep) -> (envelope[tt], cos[... * tt], sin[... * tt])
|
||||||
|
Delay: number of initial timesteps for which envelope[tt] will be 0
|
||||||
|
"""
|
||||||
|
logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO
|
||||||
|
omega = 2 * pi / wl
|
||||||
|
freq = 1 / wl
|
||||||
|
|
||||||
|
from scipy.optimize import root_scalar
|
||||||
|
delay_results = root_scalar(
|
||||||
|
lambda tt: (1 - omega * omega * tt * tt / 2) * numpy.exp(-omega * omega * tt * tt / 4) - turn_on,
|
||||||
|
x0=0,
|
||||||
|
x1=-2 / omega,
|
||||||
|
)
|
||||||
|
|
||||||
|
delay = delay_results.root
|
||||||
|
delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase
|
||||||
|
|
||||||
|
def source_phasor(ii: ArrayLike) -> tuple[pulse_scalar_t, pulse_scalar_t, pulse_scalar_t]:
|
||||||
|
ii_array = numpy.asarray(ii, dtype=float)
|
||||||
|
t0 = ii_array * dt - delay
|
||||||
|
rr = omega * t0 / 2
|
||||||
|
ff = (1 - 2 * rr * rr) * numpy.exp(-rr * rr)
|
||||||
|
|
||||||
|
cc = numpy.cos(omega * t0)
|
||||||
|
ss = numpy.sin(omega * t0)
|
||||||
|
|
||||||
|
return _scalar_or_array(ff), _scalar_or_array(cc), _scalar_or_array(ss)
|
||||||
|
|
||||||
|
return source_phasor, delay
|
||||||
|
|
||||||
|
|
||||||
|
def gaussian_beam(
|
||||||
|
xyz: list[NDArray],
|
||||||
|
center: ArrayLike,
|
||||||
|
waist_radius: float,
|
||||||
|
wl: float,
|
||||||
|
tilt: float = 0,
|
||||||
|
) -> NDArray[numpy.complex128]:
|
||||||
|
"""
|
||||||
|
Gaussian beam
|
||||||
|
(solution to paraxial Helmholtz equation)
|
||||||
|
|
||||||
|
Default (no tilt) corresponds to a beam propagating in the -z direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xyz: List of [[x0, x1, ...], [y0, ...], [z0, ...]] positions specifying grid
|
||||||
|
locations at which the field will be sampled.
|
||||||
|
center: [x, y, z] location of beam waist
|
||||||
|
waist_radius: Beam radius at the waist
|
||||||
|
wl: Wavelength
|
||||||
|
tilt: Rotation around y axis. Default (0) has beam propagating in -z direction.
|
||||||
|
"""
|
||||||
|
logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO
|
||||||
|
w0 = waist_radius
|
||||||
|
grids = numpy.asarray(numpy.meshgrid(*xyz, indexing='ij'))
|
||||||
|
grids -= numpy.asarray(center)[:, None, None, None]
|
||||||
|
|
||||||
|
rot = numpy.array([
|
||||||
|
[ numpy.cos(tilt), 0, numpy.sin(tilt)],
|
||||||
|
[ 0, 1, 0],
|
||||||
|
[-numpy.sin(tilt), 0, numpy.cos(tilt)],
|
||||||
|
])
|
||||||
|
|
||||||
|
xx, yy, zz = numpy.einsum('ij,jxyz->ixyz', rot, grids)
|
||||||
|
r2 = xx * xx + yy * yy
|
||||||
|
z2 = zz * zz
|
||||||
|
|
||||||
|
zr = pi * w0 * w0 / wl
|
||||||
|
zr2 = zr * zr
|
||||||
|
wz2 = w0 * w0 * (1 + z2 / zr2)
|
||||||
|
wz = numpy.sqrt(wz2) # == fwhm(z) / sqrt(2 * ln(2))
|
||||||
|
|
||||||
|
kk = 2 * pi / wl
|
||||||
|
inv_Rz = numpy.divide(zz, z2 + zr2, out=numpy.zeros_like(zz), where=(z2 + zr2) != 0)
|
||||||
|
gouy = numpy.arctan(zz / zr)
|
||||||
|
|
||||||
|
gaussian = w0 / wz * numpy.exp(-r2 / wz2) * numpy.exp(1j * (kk * zz + kk * r2 * inv_Rz / 2 - gouy))
|
||||||
|
|
||||||
|
row = gaussian[:, :, gaussian.shape[2] // 2]
|
||||||
|
norm = numpy.linalg.norm(row)
|
||||||
|
return gaussian / norm
|
||||||
319
meanas/fdtd/phasor.py
Normal file
319
meanas/fdtd/phasor.py
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
"""
|
||||||
|
Helpers for extracting single- or multi-frequency phasors from FDTD samples.
|
||||||
|
|
||||||
|
These helpers are intentionally low-level: callers own the accumulator arrays and
|
||||||
|
the sampling policy. The accumulated quantity is
|
||||||
|
|
||||||
|
dt * sum(weight * exp(-1j * omega * t_step) * sample_step)
|
||||||
|
|
||||||
|
where `t_step = (step + offset_steps) * dt`.
|
||||||
|
|
||||||
|
The usual Yee offsets are:
|
||||||
|
|
||||||
|
- `accumulate_phasor_e(..., step=l)` for `E_l`
|
||||||
|
- `accumulate_phasor_h(..., step=l)` for `H_{l + 1/2}`
|
||||||
|
- `accumulate_phasor_j(..., step=l)` for `J_{l + 1/2}`
|
||||||
|
|
||||||
|
`temporal_phasor(...)` and `temporal_phasor_scale(...)` apply the same Fourier
|
||||||
|
sum to a 1D scalar waveform. They are useful for normalizing a pulsed source
|
||||||
|
before that scalar waveform is applied to a point source or spatial mode source.
|
||||||
|
`real_injection_scale(...)` is a companion helper for the common real-valued
|
||||||
|
injection pattern `numpy.real(scale * waveform)`, where `waveform` is the
|
||||||
|
analytic positive-frequency signal and the injected real current should still
|
||||||
|
produce a desired phasor response.
|
||||||
|
`reconstruct_real(...)` and its `E/H/J` wrappers perform the inverse operation:
|
||||||
|
they turn one or more phasors back into real-valued field snapshots at explicit
|
||||||
|
Yee-aligned sample times. For a scalar target frequency they accept either a
|
||||||
|
plain field phasor or the batched `(1, *sample_shape)` form used elsewhere in
|
||||||
|
this module.
|
||||||
|
|
||||||
|
These helpers do not choose warmup/accumulation windows or pulse-envelope
|
||||||
|
normalization. They also do not impose a current sign convention. In this
|
||||||
|
codebase, electric-current injection normally appears as `E -= dt * J / epsilon`,
|
||||||
|
which matches the FDFD right-hand side `-1j * omega * J`.
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_omegas(
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
omega_array = numpy.atleast_1d(numpy.asarray(omegas, dtype=complex))
|
||||||
|
if omega_array.ndim != 1 or omega_array.size == 0:
|
||||||
|
raise ValueError('omegas must be a scalar or non-empty 1D sequence')
|
||||||
|
return omega_array
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_weight(
|
||||||
|
weight: ArrayLike,
|
||||||
|
omega_shape: tuple[int, ...],
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
weight_array = numpy.asarray(weight, dtype=complex)
|
||||||
|
if weight_array.ndim == 0:
|
||||||
|
return numpy.full(omega_shape, weight_array, dtype=complex)
|
||||||
|
if weight_array.shape == omega_shape:
|
||||||
|
return weight_array
|
||||||
|
raise ValueError(f'weight must be scalar or have shape {omega_shape}, got {weight_array.shape}')
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_temporal_samples(
|
||||||
|
samples: ArrayLike,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
sample_array = numpy.asarray(samples, dtype=complex)
|
||||||
|
if sample_array.ndim != 1 or sample_array.size == 0:
|
||||||
|
raise ValueError('samples must be a non-empty 1D sequence')
|
||||||
|
return sample_array
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_reconstruction_inputs(
|
||||||
|
phasors: ArrayLike,
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
) -> tuple[NDArray[numpy.complexfloating], NDArray[numpy.complexfloating], bool]:
|
||||||
|
if dt <= 0:
|
||||||
|
raise ValueError('dt must be positive')
|
||||||
|
|
||||||
|
omega_array = _normalize_omegas(omegas)
|
||||||
|
phasor_array = numpy.asarray(phasors, dtype=complex)
|
||||||
|
expected_leading = omega_array.size
|
||||||
|
if phasor_array.ndim == 0:
|
||||||
|
raise ValueError(
|
||||||
|
f'phasors must have shape ({expected_leading}, *sample_shape) or sample_shape for scalar omega, got {phasor_array.shape}',
|
||||||
|
)
|
||||||
|
added_axis = False
|
||||||
|
if expected_leading == 1 and (phasor_array.ndim == 1 or phasor_array.shape[0] != expected_leading):
|
||||||
|
phasor_array = phasor_array[numpy.newaxis, ...]
|
||||||
|
added_axis = True
|
||||||
|
elif phasor_array.shape[0] != expected_leading:
|
||||||
|
raise ValueError(
|
||||||
|
f'phasors must have shape ({expected_leading}, *sample_shape) or sample_shape for scalar omega, got {phasor_array.shape}',
|
||||||
|
)
|
||||||
|
return omega_array, phasor_array, added_axis
|
||||||
|
|
||||||
|
|
||||||
|
def accumulate_phasor(
|
||||||
|
accumulator: NDArray[numpy.complexfloating],
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
sample: ArrayLike,
|
||||||
|
step: int,
|
||||||
|
*,
|
||||||
|
offset_steps: float = 0.0,
|
||||||
|
weight: ArrayLike = 1.0,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
"""
|
||||||
|
Add one time-domain sample into a phasor accumulator.
|
||||||
|
|
||||||
|
The added quantity is
|
||||||
|
|
||||||
|
dt * weight * exp(-1j * omega * t_step) * sample
|
||||||
|
|
||||||
|
where `t_step = (step + offset_steps) * dt`.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This helper already multiplies by `dt`. If the caller's normalization
|
||||||
|
factor was derived from a discrete sum that already includes `dt`, pass
|
||||||
|
`weight / dt` here.
|
||||||
|
"""
|
||||||
|
if dt <= 0:
|
||||||
|
raise ValueError('dt must be positive')
|
||||||
|
|
||||||
|
omega_array = _normalize_omegas(omegas)
|
||||||
|
sample_array = numpy.asarray(sample)
|
||||||
|
expected_shape = (omega_array.size, *sample_array.shape)
|
||||||
|
if accumulator.shape != expected_shape:
|
||||||
|
raise ValueError(f'accumulator must have shape {expected_shape}, got {accumulator.shape}')
|
||||||
|
|
||||||
|
weight_array = _normalize_weight(weight, omega_array.shape)
|
||||||
|
time = (step + offset_steps) * dt
|
||||||
|
phase = numpy.exp(-1j * omega_array * time)
|
||||||
|
scaled = dt * (weight_array * phase).reshape((-1,) + (1,) * sample_array.ndim)
|
||||||
|
accumulator += scaled * sample_array
|
||||||
|
return accumulator
|
||||||
|
|
||||||
|
|
||||||
|
def temporal_phasor(
|
||||||
|
samples: ArrayLike,
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
*,
|
||||||
|
start_step: int = 0,
|
||||||
|
offset_steps: float = 0.0,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
"""
|
||||||
|
Fourier-project a 1D temporal waveform onto one or more angular frequencies.
|
||||||
|
|
||||||
|
The returned quantity is
|
||||||
|
|
||||||
|
dt * sum(exp(-1j * omega * t_step) * samples[step_index])
|
||||||
|
|
||||||
|
where `t_step = (start_step + step_index + offset_steps) * dt`.
|
||||||
|
"""
|
||||||
|
if dt <= 0:
|
||||||
|
raise ValueError('dt must be positive')
|
||||||
|
|
||||||
|
omega_array = _normalize_omegas(omegas)
|
||||||
|
sample_array = _normalize_temporal_samples(samples)
|
||||||
|
steps = start_step + numpy.arange(sample_array.size, dtype=float) + offset_steps
|
||||||
|
phase = numpy.exp(-1j * omega_array[:, None] * (steps[None, :] * dt))
|
||||||
|
return dt * (phase @ sample_array)
|
||||||
|
|
||||||
|
|
||||||
|
def temporal_phasor_scale(
|
||||||
|
samples: ArrayLike,
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
*,
|
||||||
|
start_step: int = 0,
|
||||||
|
offset_steps: float = 0.0,
|
||||||
|
target: ArrayLike = 1.0,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
"""
|
||||||
|
Return the scalar multiplier that gives a desired temporal phasor response.
|
||||||
|
|
||||||
|
The returned scale satisfies
|
||||||
|
|
||||||
|
temporal_phasor(scale * samples, omegas, dt, ...) == target
|
||||||
|
|
||||||
|
for each target frequency. The result keeps a leading frequency axis even
|
||||||
|
when `omegas` is scalar.
|
||||||
|
"""
|
||||||
|
response = temporal_phasor(samples, omegas, dt, start_step=start_step, offset_steps=offset_steps)
|
||||||
|
target_array = _normalize_weight(target, response.shape)
|
||||||
|
if numpy.any(numpy.abs(response) <= numpy.finfo(float).eps):
|
||||||
|
raise ValueError('cannot normalize a waveform with zero temporal phasor response')
|
||||||
|
return target_array / response
|
||||||
|
|
||||||
|
|
||||||
|
def real_injection_scale(
|
||||||
|
samples: ArrayLike,
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
*,
|
||||||
|
start_step: int = 0,
|
||||||
|
offset_steps: float = 0.0,
|
||||||
|
target: ArrayLike = 1.0,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
"""
|
||||||
|
Return the scale for a real-valued injection built from an analytic waveform.
|
||||||
|
|
||||||
|
If the time-domain source is applied as
|
||||||
|
|
||||||
|
numpy.real(scale * samples[step])
|
||||||
|
|
||||||
|
then the desired positive-frequency phasor is obtained by compensating for
|
||||||
|
the 1/2 factor between the real-valued source and its analytic component:
|
||||||
|
|
||||||
|
scale = 2 * target / temporal_phasor(samples, ...)
|
||||||
|
|
||||||
|
This helper normalizes only the intended positive-frequency component. Any
|
||||||
|
residual negative-frequency leakage is controlled by the waveform design and
|
||||||
|
the accumulation window.
|
||||||
|
"""
|
||||||
|
response = temporal_phasor(samples, omegas, dt, start_step=start_step, offset_steps=offset_steps)
|
||||||
|
target_array = _normalize_weight(target, response.shape)
|
||||||
|
if numpy.any(numpy.abs(response) <= numpy.finfo(float).eps):
|
||||||
|
raise ValueError('cannot normalize a waveform with zero temporal phasor response')
|
||||||
|
return 2 * target_array / response
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct_real(
|
||||||
|
phasors: ArrayLike,
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
step: int,
|
||||||
|
*,
|
||||||
|
offset_steps: float = 0.0,
|
||||||
|
) -> NDArray[numpy.floating]:
|
||||||
|
"""
|
||||||
|
Reconstruct a real-valued field snapshot from one or more phasors.
|
||||||
|
|
||||||
|
The returned quantity is
|
||||||
|
|
||||||
|
real(phasor * exp(1j * omega * t_step))
|
||||||
|
|
||||||
|
where `t_step = (step + offset_steps) * dt`.
|
||||||
|
|
||||||
|
For multi-frequency inputs, the leading frequency axis is preserved. For a
|
||||||
|
scalar `omega`, callers may pass either `(1, *sample_shape)` or
|
||||||
|
`sample_shape`; the return shape matches that choice.
|
||||||
|
"""
|
||||||
|
omega_array, phasor_array, added_axis = _validate_reconstruction_inputs(phasors, omegas, dt)
|
||||||
|
time = (step + offset_steps) * dt
|
||||||
|
phase = numpy.exp(1j * omega_array * time).reshape((-1,) + (1,) * (phasor_array.ndim - 1))
|
||||||
|
reconstructed = numpy.real(phasor_array * phase)
|
||||||
|
if added_axis:
|
||||||
|
return reconstructed[0]
|
||||||
|
return reconstructed
|
||||||
|
|
||||||
|
|
||||||
|
def accumulate_phasor_e(
|
||||||
|
accumulator: NDArray[numpy.complexfloating],
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
sample: ArrayLike,
|
||||||
|
step: int,
|
||||||
|
*,
|
||||||
|
weight: ArrayLike = 1.0,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
"""Accumulate an E-field sample taken at integer timestep `step`."""
|
||||||
|
return accumulate_phasor(accumulator, omegas, dt, sample, step, offset_steps=0.0, weight=weight)
|
||||||
|
|
||||||
|
|
||||||
|
def accumulate_phasor_h(
|
||||||
|
accumulator: NDArray[numpy.complexfloating],
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
sample: ArrayLike,
|
||||||
|
step: int,
|
||||||
|
*,
|
||||||
|
weight: ArrayLike = 1.0,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
"""Accumulate an H-field sample corresponding to `H_{step + 1/2}`."""
|
||||||
|
return accumulate_phasor(accumulator, omegas, dt, sample, step, offset_steps=0.5, weight=weight)
|
||||||
|
|
||||||
|
|
||||||
|
def accumulate_phasor_j(
|
||||||
|
accumulator: NDArray[numpy.complexfloating],
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
sample: ArrayLike,
|
||||||
|
step: int,
|
||||||
|
*,
|
||||||
|
weight: ArrayLike = 1.0,
|
||||||
|
) -> NDArray[numpy.complexfloating]:
|
||||||
|
"""Accumulate a current sample corresponding to `J_{step + 1/2}`."""
|
||||||
|
return accumulate_phasor(accumulator, omegas, dt, sample, step, offset_steps=0.5, weight=weight)
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct_real_e(
|
||||||
|
phasors: ArrayLike,
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
step: int,
|
||||||
|
) -> NDArray[numpy.floating]:
|
||||||
|
"""Reconstruct a real E-field snapshot taken at integer timestep `step`."""
|
||||||
|
return reconstruct_real(phasors, omegas, dt, step, offset_steps=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct_real_h(
|
||||||
|
phasors: ArrayLike,
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
step: int,
|
||||||
|
) -> NDArray[numpy.floating]:
|
||||||
|
"""Reconstruct a real H-field snapshot corresponding to `H_{step + 1/2}`."""
|
||||||
|
return reconstruct_real(phasors, omegas, dt, step, offset_steps=0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct_real_j(
|
||||||
|
phasors: ArrayLike,
|
||||||
|
omegas: float | complex | Sequence[float | complex] | NDArray,
|
||||||
|
dt: float,
|
||||||
|
step: int,
|
||||||
|
) -> NDArray[numpy.floating]:
|
||||||
|
"""Reconstruct a real current snapshot corresponding to `J_{step + 1/2}`."""
|
||||||
|
return reconstruct_real(phasors, omegas, dt, step, offset_steps=0.5)
|
||||||
|
|
@ -1,18 +1,29 @@
|
||||||
"""
|
"""
|
||||||
PML implementations
|
Convolutional perfectly matched layer (CPML) support for FDTD updates.
|
||||||
|
|
||||||
#TODO discussion of PMLs
|
The helpers in this module construct per-face CPML parameters and then wrap the
|
||||||
#TODO cpml documentation
|
standard Yee updates with the additional auxiliary `psi` fields needed by the
|
||||||
|
CPML recurrence.
|
||||||
|
|
||||||
|
The intended call pattern is:
|
||||||
|
|
||||||
|
1. Build a `cpml_params[axis][polarity_index]` table with `cpml_params(...)`.
|
||||||
|
2. Pass that table into `updates_with_cpml(...)` together with `dt`, `dxes`, and
|
||||||
|
`epsilon`.
|
||||||
|
3. Advance the returned `update_E` / `update_H` closures in the simulation loop.
|
||||||
|
|
||||||
|
Each face can be enabled or disabled independently by replacing one table entry
|
||||||
|
with `None`.
|
||||||
"""
|
"""
|
||||||
# TODO retest pmls!
|
# TODO retest pmls!
|
||||||
|
|
||||||
from typing import Callable, Sequence, Any
|
from typing import Any
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray, DTypeLike
|
from numpy.typing import NDArray, DTypeLike
|
||||||
|
|
||||||
from ..fdmath import fdfield_t, dx_lists_t
|
from ..fdmath import fdfield, dx_lists_t
|
||||||
from ..fdmath.functional import deriv_forward, deriv_back
|
from ..fdmath.functional import deriv_forward, deriv_back
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,18 +42,41 @@ def cpml_params(
|
||||||
ma: float = 1,
|
ma: float = 1,
|
||||||
cfs_alpha: float = 0,
|
cfs_alpha: float = 0,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Construct the parameter block for one CPML face.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
axis: Which Cartesian axis the CPML is normal to (`0`, `1`, or `2`).
|
||||||
|
polarity: Which face along that axis (`-1` for the low-index face,
|
||||||
|
`+1` for the high-index face).
|
||||||
|
dt: Timestep used by the Yee update.
|
||||||
|
thickness: Number of Yee cells occupied by the CPML region.
|
||||||
|
ln_R_per_layer: Logarithmic attenuation target per layer.
|
||||||
|
epsilon_eff: Effective permittivity used when choosing the CPML scaling.
|
||||||
|
mu_eff: Effective permeability used when choosing the CPML scaling.
|
||||||
|
m: Polynomial grading exponent for `sigma` and `kappa`.
|
||||||
|
ma: Polynomial grading exponent for the complex-frequency shift `alpha`.
|
||||||
|
cfs_alpha: Maximum complex-frequency shift parameter.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with:
|
||||||
|
|
||||||
|
- `param_e`: `(p0, p1, p2)` arrays for the E update
|
||||||
|
- `param_h`: `(p0, p1, p2)` arrays for the H update
|
||||||
|
- `region`: slice tuple selecting the CPML cells on that face
|
||||||
|
"""
|
||||||
|
|
||||||
if axis not in range(3):
|
if axis not in range(3):
|
||||||
raise Exception(f'Invalid axis: {axis}')
|
raise ValueError(f'Invalid axis: {axis}')
|
||||||
|
|
||||||
if polarity not in (-1, 1):
|
if polarity not in (-1, 1):
|
||||||
raise Exception(f'Invalid polarity: {polarity}')
|
raise ValueError(f'Invalid polarity: {polarity}')
|
||||||
|
|
||||||
if thickness <= 2:
|
if thickness <= 2:
|
||||||
raise Exception('It would be wise to have a pml with 4+ cells of thickness')
|
raise ValueError('It would be wise to have a pml with 4+ cells of thickness')
|
||||||
|
|
||||||
if epsilon_eff <= 0:
|
if epsilon_eff <= 0:
|
||||||
raise Exception('epsilon_eff must be positive')
|
raise ValueError('epsilon_eff must be positive')
|
||||||
|
|
||||||
sigma_max = -ln_R_per_layer / 2 * (m + 1)
|
sigma_max = -ln_R_per_layer / 2 * (m + 1)
|
||||||
kappa_max = numpy.sqrt(epsilon_eff * mu_eff)
|
kappa_max = numpy.sqrt(epsilon_eff * mu_eff)
|
||||||
|
|
@ -56,8 +90,6 @@ def cpml_params(
|
||||||
xh -= 0.5
|
xh -= 0.5
|
||||||
xe = xe[::-1]
|
xe = xe[::-1]
|
||||||
xh = xh[::-1]
|
xh = xh[::-1]
|
||||||
else:
|
|
||||||
raise Exception('Bad polarity!')
|
|
||||||
|
|
||||||
expand_slice_l: list[Any] = [None, None, None]
|
expand_slice_l: list[Any] = [None, None, None]
|
||||||
expand_slice_l[axis] = slice(None)
|
expand_slice_l[axis] = slice(None)
|
||||||
|
|
@ -81,8 +113,6 @@ def cpml_params(
|
||||||
region_list[axis] = slice(None, thickness)
|
region_list[axis] = slice(None, thickness)
|
||||||
elif polarity > 0:
|
elif polarity > 0:
|
||||||
region_list[axis] = slice(-thickness, None)
|
region_list[axis] = slice(-thickness, None)
|
||||||
else:
|
|
||||||
raise Exception('Bad polarity!')
|
|
||||||
region = tuple(region_list)
|
region = tuple(region_list)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -96,11 +126,31 @@ def updates_with_cpml(
|
||||||
cpml_params: Sequence[Sequence[dict[str, Any] | None]],
|
cpml_params: Sequence[Sequence[dict[str, Any] | None]],
|
||||||
dt: float,
|
dt: float,
|
||||||
dxes: dx_lists_t,
|
dxes: dx_lists_t,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
*,
|
*,
|
||||||
dtype: DTypeLike = numpy.float32,
|
dtype: DTypeLike = numpy.float32,
|
||||||
) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None],
|
) -> tuple[Callable[..., None], Callable[..., None]]:
|
||||||
Callable[[fdfield_t, fdfield_t, fdfield_t], None]]:
|
"""
|
||||||
|
Build Yee-step update closures augmented with CPML terms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cpml_params: Three-by-two sequence indexed as `[axis][polarity_index]`.
|
||||||
|
Entries are the dictionaries returned by `cpml_params(...)`; use
|
||||||
|
`None` to disable CPML on one face.
|
||||||
|
dt: Timestep.
|
||||||
|
dxes: Yee-grid spacing lists `[dx_e, dx_h]`.
|
||||||
|
epsilon: Electric material distribution used by the E update.
|
||||||
|
dtype: Storage dtype for the auxiliary CPML state arrays.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`(update_E, update_H)` closures with the same call shape as the basic
|
||||||
|
Yee updates:
|
||||||
|
|
||||||
|
- `update_E(e, h, epsilon)`
|
||||||
|
- `update_H(e, h, mu)`
|
||||||
|
|
||||||
|
The closures retain the CPML auxiliary state internally.
|
||||||
|
"""
|
||||||
|
|
||||||
Dfx, Dfy, Dfz = deriv_forward(dxes[1])
|
Dfx, Dfy, Dfz = deriv_forward(dxes[1])
|
||||||
Dbx, Dby, Dbz = deriv_back(dxes[1])
|
Dbx, Dby, Dbz = deriv_back(dxes[1])
|
||||||
|
|
@ -111,7 +161,7 @@ def updates_with_cpml(
|
||||||
params_H: list[list[tuple[Any, Any, Any, Any]]] = deepcopy(params_E)
|
params_H: list[list[tuple[Any, Any, Any, Any]]] = deepcopy(params_E)
|
||||||
|
|
||||||
for axis in range(3):
|
for axis in range(3):
|
||||||
for pp, polarity in enumerate((-1, 1)):
|
for pp, _polarity in enumerate((-1, 1)):
|
||||||
cpml_param = cpml_params[axis][pp]
|
cpml_param = cpml_params[axis][pp]
|
||||||
if cpml_param is None:
|
if cpml_param is None:
|
||||||
psi_E[axis][pp] = (None, None)
|
psi_E[axis][pp] = (None, None)
|
||||||
|
|
@ -136,9 +186,9 @@ def updates_with_cpml(
|
||||||
pH = numpy.empty_like(epsilon, dtype=dtype)
|
pH = numpy.empty_like(epsilon, dtype=dtype)
|
||||||
|
|
||||||
def update_E(
|
def update_E(
|
||||||
e: fdfield_t,
|
e: fdfield,
|
||||||
h: fdfield_t,
|
h: fdfield,
|
||||||
epsilon: fdfield_t,
|
epsilon: fdfield,
|
||||||
) -> None:
|
) -> None:
|
||||||
dyHx = Dby(h[0])
|
dyHx = Dby(h[0])
|
||||||
dzHx = Dbz(h[0])
|
dzHx = Dbz(h[0])
|
||||||
|
|
@ -182,9 +232,9 @@ def updates_with_cpml(
|
||||||
e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2])
|
e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2])
|
||||||
|
|
||||||
def update_H(
|
def update_H(
|
||||||
e: fdfield_t,
|
e: fdfield,
|
||||||
h: fdfield_t,
|
h: fdfield,
|
||||||
mu: fdfield_t = numpy.ones(3),
|
mu: fdfield | tuple[int, int, int] = (1, 1, 1),
|
||||||
) -> None:
|
) -> None:
|
||||||
dyEx = Dfy(e[0])
|
dyEx = Dfy(e[0])
|
||||||
dzEx = Dfz(e[0])
|
dzEx = Dfz(e[0])
|
||||||
|
|
|
||||||
37
meanas/test/_bloch_case.py
Normal file
37
meanas/test/_bloch_case.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
SHAPE = (2, 2, 2)
|
||||||
|
G_MATRIX = numpy.eye(3)
|
||||||
|
K0_GENERAL = numpy.array([0.1, 0.2, 0.3], dtype=float)
|
||||||
|
K0_AXIAL = numpy.array([0.0, 0.0, 0.25], dtype=float)
|
||||||
|
K0_X = numpy.array([0.1, 0.0, 0.0], dtype=float)
|
||||||
|
EPSILON = numpy.ones((3, *SHAPE), dtype=float)
|
||||||
|
MU = numpy.stack([
|
||||||
|
numpy.linspace(2.0, 2.7, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
numpy.linspace(2.1, 2.8, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
numpy.linspace(2.2, 2.9, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
])
|
||||||
|
H_SIZE = 2 * numpy.prod(SHAPE)
|
||||||
|
H_MN = (numpy.arange(H_SIZE) + 0.25j).astype(complex)
|
||||||
|
ZERO_H_MN = numpy.zeros_like(H_MN)
|
||||||
|
Y0 = (numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE))[None, :]
|
||||||
|
Y0_TWO_MODE = numpy.vstack(
|
||||||
|
[
|
||||||
|
numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE),
|
||||||
|
numpy.linspace(2.0, 3.5, H_SIZE) - 0.5j * numpy.arange(H_SIZE, dtype=float),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_overlap_fixture() -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]:
|
||||||
|
e_in = numpy.zeros((3, *SHAPE), dtype=complex)
|
||||||
|
h_in = numpy.zeros_like(e_in)
|
||||||
|
e_out = numpy.zeros_like(e_in)
|
||||||
|
h_out = numpy.zeros_like(e_in)
|
||||||
|
|
||||||
|
e_in[1] = 1.0
|
||||||
|
h_in[2] = 2.0
|
||||||
|
e_out[1] = 3.0
|
||||||
|
h_out[2] = 4.0
|
||||||
|
return e_in, h_in, e_out, h_out
|
||||||
49
meanas/test/_fdfd_case.py
Normal file
49
meanas/test/_fdfd_case.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from ..fdmath import vec, unvec
|
||||||
|
|
||||||
|
|
||||||
|
OMEGA = 1 / 1500
|
||||||
|
SHAPE = (2, 3, 2)
|
||||||
|
DXES = [
|
||||||
|
[numpy.array([1.0, 1.5]), numpy.array([0.75, 1.25, 1.5]), numpy.array([1.2, 0.8])],
|
||||||
|
[numpy.array([0.9, 1.4]), numpy.array([0.8, 1.1, 1.4]), numpy.array([1.0, 0.7])],
|
||||||
|
]
|
||||||
|
|
||||||
|
EPSILON = numpy.stack([
|
||||||
|
numpy.linspace(1.0, 2.2, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
numpy.linspace(1.1, 2.3, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
numpy.linspace(1.2, 2.4, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
])
|
||||||
|
MU = numpy.stack([
|
||||||
|
numpy.linspace(2.0, 3.2, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
numpy.linspace(2.1, 3.3, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
numpy.linspace(2.2, 3.4, numpy.prod(SHAPE)).reshape(SHAPE),
|
||||||
|
])
|
||||||
|
|
||||||
|
E_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.5j).astype(complex)
|
||||||
|
H_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) * 0.25 - 0.75j).astype(complex)
|
||||||
|
|
||||||
|
PEC = numpy.zeros((3, *SHAPE), dtype=float)
|
||||||
|
PEC[1, 0, 1, 0] = 1.0
|
||||||
|
PMC = numpy.zeros((3, *SHAPE), dtype=float)
|
||||||
|
PMC[2, 1, 2, 1] = 1.0
|
||||||
|
|
||||||
|
TF_REGION = numpy.zeros((3, *SHAPE), dtype=float)
|
||||||
|
TF_REGION[:, 0, 1, 0] = 1.0
|
||||||
|
|
||||||
|
BOUNDARY_SHAPE = (3, 4, 3)
|
||||||
|
BOUNDARY_DXES = [
|
||||||
|
[numpy.array([1.0, 1.5, 0.8]), numpy.array([0.75, 1.25, 1.5, 0.9]), numpy.array([1.2, 0.8, 1.1])],
|
||||||
|
[numpy.array([0.9, 1.4, 1.0]), numpy.array([0.8, 1.1, 1.4, 1.0]), numpy.array([1.0, 0.7, 1.3])],
|
||||||
|
]
|
||||||
|
BOUNDARY_EPSILON = numpy.stack([
|
||||||
|
numpy.linspace(1.0, 2.2, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE),
|
||||||
|
numpy.linspace(1.1, 2.3, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE),
|
||||||
|
numpy.linspace(1.2, 2.4, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE),
|
||||||
|
])
|
||||||
|
BOUNDARY_FIELD = (numpy.arange(3 * numpy.prod(BOUNDARY_SHAPE)).reshape((3, *BOUNDARY_SHAPE)) + 0.5j).astype(complex)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_fdfd_matrix(op, field: numpy.ndarray, shape: tuple[int, ...] = SHAPE) -> numpy.ndarray:
|
||||||
|
return unvec(op @ vec(field), shape)
|
||||||
56
meanas/test/_solver_cases.py
Normal file
56
meanas/test/_solver_cases.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import dataclasses
|
||||||
|
import numpy
|
||||||
|
from scipy import sparse
|
||||||
|
import scipy.sparse.linalg as spalg
|
||||||
|
|
||||||
|
from ._test_builders import unit_dxes
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class DiagonalEigenCase:
|
||||||
|
operator: sparse.csr_matrix
|
||||||
|
linear_operator: spalg.LinearOperator
|
||||||
|
guess_default: numpy.ndarray
|
||||||
|
guess_sparse: numpy.ndarray
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class SolverPlumbingCase:
|
||||||
|
omega: float
|
||||||
|
a0: sparse.csr_matrix
|
||||||
|
pl: sparse.csr_matrix
|
||||||
|
pr: sparse.csr_matrix
|
||||||
|
j: numpy.ndarray
|
||||||
|
guess: numpy.ndarray
|
||||||
|
solver_result: numpy.ndarray
|
||||||
|
dxes: tuple[tuple[numpy.ndarray, ...], tuple[numpy.ndarray, ...]]
|
||||||
|
epsilon: numpy.ndarray
|
||||||
|
|
||||||
|
|
||||||
|
def diagonal_eigen_case() -> DiagonalEigenCase:
|
||||||
|
operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr()
|
||||||
|
linear_operator = spalg.LinearOperator(
|
||||||
|
shape=operator.shape,
|
||||||
|
dtype=complex,
|
||||||
|
matvec=lambda vv: operator @ vv,
|
||||||
|
)
|
||||||
|
return DiagonalEigenCase(
|
||||||
|
operator=operator,
|
||||||
|
linear_operator=linear_operator,
|
||||||
|
guess_default=numpy.array([0.0, 1.0, 1e-6, 0.0], dtype=complex),
|
||||||
|
guess_sparse=numpy.array([1.0, 0.1, 0.0, 0.0], dtype=complex),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def solver_plumbing_case() -> SolverPlumbingCase:
|
||||||
|
return SolverPlumbingCase(
|
||||||
|
omega=2.0,
|
||||||
|
a0=sparse.csr_matrix(numpy.array([[1.0 + 2.0j, 2.0], [3.0 - 1.0j, 4.0]])),
|
||||||
|
pl=sparse.csr_matrix(numpy.array([[2.0, 0.0], [0.0, 3.0j]])),
|
||||||
|
pr=sparse.csr_matrix(numpy.array([[0.5, 0.0], [0.0, -2.0j]])),
|
||||||
|
j=numpy.array([1.0 + 0.5j, -2.0]),
|
||||||
|
guess=numpy.array([0.25 - 0.75j, 1.5 + 0.5j]),
|
||||||
|
solver_result=numpy.array([3.0 - 1.0j, -4.0 + 2.0j]),
|
||||||
|
dxes=unit_dxes((1, 1, 1)),
|
||||||
|
epsilon=numpy.ones(2),
|
||||||
|
)
|
||||||
22
meanas/test/_test_builders.py
Normal file
22
meanas/test/_test_builders.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
def real_ramp(shape: tuple[int, ...], *, scale: float = 1.0, offset: float = 0.0) -> numpy.ndarray:
|
||||||
|
return numpy.arange(numpy.prod(shape), dtype=float).reshape(shape, order='C') * scale + offset
|
||||||
|
|
||||||
|
|
||||||
|
def complex_ramp(
|
||||||
|
shape: tuple[int, ...],
|
||||||
|
*,
|
||||||
|
scale: float = 1.0,
|
||||||
|
offset: float = 0.0,
|
||||||
|
imag_scale: float = 0.0,
|
||||||
|
imag_offset: float = 0.0,
|
||||||
|
) -> numpy.ndarray:
|
||||||
|
real = real_ramp(shape, scale=scale, offset=offset)
|
||||||
|
imag = real_ramp(shape, scale=imag_scale, offset=imag_offset)
|
||||||
|
return (real + 1j * imag).astype(complex)
|
||||||
|
|
||||||
|
|
||||||
|
def unit_dxes(shape: tuple[int, ...]) -> tuple[tuple[numpy.ndarray, ...], tuple[numpy.ndarray, ...]]:
|
||||||
|
return tuple(tuple(numpy.ones(length) for length in shape) for _ in range(2)) # type: ignore[return-value]
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
Test fixtures
|
Test fixtures
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from typing import Iterable, Any
|
# ruff: noqa: ARG001
|
||||||
|
from typing import Any
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from .utils import PRNG
|
from .utils import make_prng
|
||||||
|
|
||||||
|
|
||||||
FixtureRequest = Any
|
FixtureRequest = Any
|
||||||
|
|
@ -20,18 +21,18 @@ FixtureRequest = Any
|
||||||
(5, 5, 5),
|
(5, 5, 5),
|
||||||
# (7, 7, 7),
|
# (7, 7, 7),
|
||||||
])
|
])
|
||||||
def shape(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
|
def shape(request: FixtureRequest) -> tuple[int, ...]:
|
||||||
yield (3, *request.param)
|
return (3, *request.param)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module', params=[1.0, 1.5])
|
@pytest.fixture(scope='module', params=[1.0, 1.5])
|
||||||
def epsilon_bg(request: FixtureRequest) -> Iterable[float]:
|
def epsilon_bg(request: FixtureRequest) -> float:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module', params=[1.0, 2.5])
|
@pytest.fixture(scope='module', params=[1.0, 2.5])
|
||||||
def epsilon_fg(request: FixtureRequest) -> Iterable[float]:
|
def epsilon_fg(request: FixtureRequest) -> float:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module', params=['center', '000', 'random'])
|
@pytest.fixture(scope='module', params=['center', '000', 'random'])
|
||||||
|
|
@ -40,7 +41,8 @@ def epsilon(
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
epsilon_bg: float,
|
epsilon_bg: float,
|
||||||
epsilon_fg: float,
|
epsilon_fg: float,
|
||||||
) -> Iterable[NDArray[numpy.float64]]:
|
) -> NDArray[numpy.float64]:
|
||||||
|
prng = make_prng()
|
||||||
is3d = (numpy.array(shape) == 1).sum() == 0
|
is3d = (numpy.array(shape) == 1).sum() == 0
|
||||||
if is3d:
|
if is3d:
|
||||||
if request.param == '000':
|
if request.param == '000':
|
||||||
|
|
@ -56,21 +58,23 @@ def epsilon(
|
||||||
elif request.param == '000':
|
elif request.param == '000':
|
||||||
epsilon[:, 0, 0, 0] = epsilon_fg
|
epsilon[:, 0, 0, 0] = epsilon_fg
|
||||||
elif request.param == 'random':
|
elif request.param == 'random':
|
||||||
epsilon[:] = PRNG.uniform(low=min(epsilon_bg, epsilon_fg),
|
epsilon[:] = prng.uniform(
|
||||||
|
low=min(epsilon_bg, epsilon_fg),
|
||||||
high=max(epsilon_bg, epsilon_fg),
|
high=max(epsilon_bg, epsilon_fg),
|
||||||
size=shape)
|
size=shape,
|
||||||
|
)
|
||||||
|
|
||||||
yield epsilon
|
return epsilon
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module', params=[1.0]) # 1.5
|
@pytest.fixture(scope='module', params=[1.0]) # 1.5
|
||||||
def j_mag(request: FixtureRequest) -> Iterable[float]:
|
def j_mag(request: FixtureRequest) -> float:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module', params=[1.0, 1.5])
|
@pytest.fixture(scope='module', params=[1.0, 1.5])
|
||||||
def dx(request: FixtureRequest) -> Iterable[float]:
|
def dx(request: FixtureRequest) -> float:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module', params=['uniform', 'centerbig'])
|
@pytest.fixture(scope='module', params=['uniform', 'centerbig'])
|
||||||
|
|
@ -78,7 +82,8 @@ def dxes(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
dx: float,
|
dx: float,
|
||||||
) -> Iterable[list[list[NDArray[numpy.float64]]]]:
|
) -> list[list[NDArray[numpy.float64]]]:
|
||||||
|
prng = make_prng()
|
||||||
if request.param == 'uniform':
|
if request.param == 'uniform':
|
||||||
dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
|
dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
|
||||||
elif request.param == 'centerbig':
|
elif request.param == 'centerbig':
|
||||||
|
|
@ -87,8 +92,7 @@ def dxes(
|
||||||
for ax in (0, 1, 2):
|
for ax in (0, 1, 2):
|
||||||
dxes[eh][ax][dxes[eh][ax].size // 2] *= 1.1
|
dxes[eh][ax][dxes[eh][ax].size // 2] *= 1.1
|
||||||
elif request.param == 'random':
|
elif request.param == 'random':
|
||||||
dxe = [PRNG.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]]
|
dxe = [prng.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]]
|
||||||
dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe]
|
dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe]
|
||||||
dxes = [dxe, dxh]
|
dxes = [dxe, dxh]
|
||||||
yield dxes
|
return dxes
|
||||||
|
|
||||||
|
|
|
||||||
73
meanas/test/test_bloch_foundations.py
Normal file
73
meanas/test/test_bloch_foundations.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import numpy
|
||||||
|
from numpy.linalg import norm
|
||||||
|
|
||||||
|
from ..fdfd import bloch
|
||||||
|
from ._bloch_case import EPSILON, G_MATRIX, H_MN, K0_AXIAL, K0_GENERAL, MU, SHAPE, ZERO_H_MN
|
||||||
|
from .utils import assert_close
|
||||||
|
|
||||||
|
def test_generate_kmn_general_case_returns_orthonormal_basis() -> None:
|
||||||
|
k_mag, m_vecs, n_vecs = bloch.generate_kmn(K0_GENERAL, G_MATRIX, SHAPE)
|
||||||
|
|
||||||
|
assert k_mag.shape == SHAPE + (1,)
|
||||||
|
assert m_vecs.shape == SHAPE + (3,)
|
||||||
|
assert n_vecs.shape == SHAPE + (3,)
|
||||||
|
assert numpy.isfinite(k_mag).all()
|
||||||
|
assert numpy.isfinite(m_vecs).all()
|
||||||
|
assert numpy.isfinite(n_vecs).all()
|
||||||
|
|
||||||
|
assert_close(norm(m_vecs.reshape(-1, 3), axis=1), 1.0)
|
||||||
|
assert_close(norm(n_vecs.reshape(-1, 3), axis=1), 1.0)
|
||||||
|
assert_close(numpy.sum(m_vecs * n_vecs, axis=3), 0.0, atol=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_kmn_z_aligned_uses_default_transverse_basis() -> None:
|
||||||
|
k_mag, m_vecs, n_vecs = bloch.generate_kmn(K0_AXIAL, G_MATRIX, (1, 1, 1))
|
||||||
|
|
||||||
|
assert numpy.isfinite(k_mag).all()
|
||||||
|
assert_close(m_vecs[0, 0, 0], [0.0, 1.0, 0.0])
|
||||||
|
assert_close(numpy.sum(m_vecs * n_vecs, axis=3), 0.0, atol=1e-12)
|
||||||
|
assert_close(norm(n_vecs.reshape(-1, 3), axis=1), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_maxwell_operator_returns_finite_column_vector_without_mu() -> None:
|
||||||
|
operator = bloch.maxwell_operator(K0_GENERAL, G_MATRIX, EPSILON)
|
||||||
|
|
||||||
|
result = operator(H_MN.copy())
|
||||||
|
zero_result = operator(ZERO_H_MN.copy())
|
||||||
|
|
||||||
|
assert result.shape == (2 * numpy.prod(SHAPE), 1)
|
||||||
|
assert numpy.isfinite(result).all()
|
||||||
|
assert_close(zero_result, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_maxwell_operator_returns_finite_column_vector_with_mu() -> None:
|
||||||
|
operator = bloch.maxwell_operator(K0_GENERAL, G_MATRIX, EPSILON, MU)
|
||||||
|
|
||||||
|
result = operator(H_MN.copy())
|
||||||
|
zero_result = operator(ZERO_H_MN.copy())
|
||||||
|
|
||||||
|
assert result.shape == (2 * numpy.prod(SHAPE), 1)
|
||||||
|
assert numpy.isfinite(result).all()
|
||||||
|
assert_close(zero_result, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inverse_maxwell_operator_returns_finite_column_vector_for_both_mu_branches() -> None:
|
||||||
|
for mu in (None, MU):
|
||||||
|
operator = bloch.inverse_maxwell_operator_approx(K0_GENERAL, G_MATRIX, EPSILON, mu)
|
||||||
|
|
||||||
|
result = operator(H_MN.copy())
|
||||||
|
zero_result = operator(ZERO_H_MN.copy())
|
||||||
|
|
||||||
|
assert result.shape == (2 * numpy.prod(SHAPE), 1)
|
||||||
|
assert numpy.isfinite(result).all()
|
||||||
|
assert_close(zero_result, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bloch_field_converters_return_finite_fields() -> None:
|
||||||
|
e_field = bloch.hmn_2_exyz(K0_GENERAL, G_MATRIX, EPSILON)(H_MN.copy())
|
||||||
|
h_field = bloch.hmn_2_hxyz(K0_GENERAL, G_MATRIX, EPSILON)(H_MN.copy())
|
||||||
|
|
||||||
|
assert e_field.shape == (3, *SHAPE)
|
||||||
|
assert h_field.shape == (3, *SHAPE)
|
||||||
|
assert numpy.isfinite(e_field).all()
|
||||||
|
assert numpy.isfinite(h_field).all()
|
||||||
315
meanas/test/test_bloch_interactions.py
Normal file
315
meanas/test/test_bloch_interactions.py
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
import numpy
|
||||||
|
import pytest
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from ..fdfd import bloch
|
||||||
|
from ._bloch_case import EPSILON, G_MATRIX, H_SIZE, K0_X, Y0, Y0_TWO_MODE, build_overlap_fixture
|
||||||
|
from .utils import assert_close
|
||||||
|
|
||||||
|
|
||||||
|
def test_rtrace_atb_matches_real_frobenius_inner_product() -> None:
|
||||||
|
a_mat = numpy.array([[1.0 + 2.0j, 3.0 - 1.0j], [2.0j, 4.0]], dtype=complex)
|
||||||
|
b_mat = numpy.array([[5.0 - 1.0j, 1.0 + 1.0j], [2.0, 3.0j]], dtype=complex)
|
||||||
|
expected = numpy.real(numpy.sum(a_mat.conj() * b_mat))
|
||||||
|
|
||||||
|
assert bloch._rtrace_AtB(a_mat, b_mat) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_symmetrize_returns_hermitian_average() -> None:
|
||||||
|
matrix = numpy.array([[1.0 + 2.0j, 3.0 - 1.0j], [2.0j, 4.0]], dtype=complex)
|
||||||
|
result = bloch._symmetrize(matrix)
|
||||||
|
|
||||||
|
assert_close(result, 0.5 * (matrix + matrix.conj().T))
|
||||||
|
assert_close(result, result.conj().T)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inner_product_is_nonmutating_and_obeys_sign_symmetry() -> None:
|
||||||
|
e_in, h_in, e_out, h_out = build_overlap_fixture()
|
||||||
|
originals = (e_in.copy(), h_in.copy(), e_out.copy(), h_out.copy())
|
||||||
|
|
||||||
|
pp = bloch.inner_product(e_out, h_out, e_in, h_in)
|
||||||
|
pn = bloch.inner_product(e_out, h_out, e_in, -h_in)
|
||||||
|
np_term = bloch.inner_product(e_out, -h_out, e_in, h_in)
|
||||||
|
nn = bloch.inner_product(e_out, -h_out, e_in, -h_in)
|
||||||
|
|
||||||
|
assert_close(pp, 0.8164965809277263 + 0.0j)
|
||||||
|
assert_close(pp, -nn, atol=1e-12, rtol=1e-12)
|
||||||
|
assert_close(pn, -np_term, atol=1e-12, rtol=1e-12)
|
||||||
|
assert numpy.array_equal(e_in, originals[0])
|
||||||
|
assert numpy.array_equal(h_in, originals[1])
|
||||||
|
assert numpy.array_equal(e_out, originals[2])
|
||||||
|
assert numpy.array_equal(h_out, originals[3])
|
||||||
|
|
||||||
|
|
||||||
|
def test_trq_returns_expected_transmission_and_reflection() -> None:
|
||||||
|
e_in, h_in, e_out, h_out = build_overlap_fixture()
|
||||||
|
|
||||||
|
transmission, reflection = bloch.trq(e_in, h_in, e_out, h_out)
|
||||||
|
|
||||||
|
assert_close(transmission, 0.9797958971132713 + 0.0j, atol=1e-12, rtol=1e-12)
|
||||||
|
assert_close(reflection, 0.2 + 0.0j, atol=1e-12, rtol=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_returns_finite_modes_with_small_residual() -> None:
|
||||||
|
callback_count = 0
|
||||||
|
|
||||||
|
def callback() -> None:
|
||||||
|
nonlocal callback_count
|
||||||
|
callback_count += 1
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0_X,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=50,
|
||||||
|
y0=Y0,
|
||||||
|
callback=callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
operator = bloch.maxwell_operator(K0_X, G_MATRIX, EPSILON)
|
||||||
|
eigvec = eigvecs[0] / numpy.linalg.norm(eigvecs[0])
|
||||||
|
residual = numpy.linalg.norm(operator(eigvec).reshape(-1) - eigvals[0] * eigvec) / numpy.linalg.norm(eigvec)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
assert residual < 1e-5
|
||||||
|
assert callback_count > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_without_initial_guess_returns_finite_modes() -> None:
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0_X,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=20,
|
||||||
|
y0=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
operator = bloch.maxwell_operator(K0_X, G_MATRIX, EPSILON)
|
||||||
|
eigvec = eigvecs[0] / numpy.linalg.norm(eigvecs[0])
|
||||||
|
residual = numpy.linalg.norm(operator(eigvec).reshape(-1) - eigvals[0] * eigvec) / numpy.linalg.norm(eigvec)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
assert residual < 1e-5
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_recovers_from_singular_initial_subspace(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
class FakeRng:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.calls = 0
|
||||||
|
|
||||||
|
def random(self, shape: tuple[int, ...]) -> numpy.ndarray:
|
||||||
|
self.calls += 1
|
||||||
|
return numpy.arange(numpy.prod(shape), dtype=float).reshape(shape) + self.calls
|
||||||
|
|
||||||
|
fake_rng = FakeRng()
|
||||||
|
singular_y0 = numpy.vstack([Y0_TWO_MODE[0], Y0_TWO_MODE[0]])
|
||||||
|
monkeypatch.setattr(bloch.numpy.random, 'default_rng', lambda: fake_rng)
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
2,
|
||||||
|
K0_X,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=20,
|
||||||
|
y0=singular_y0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert fake_rng.calls == 2
|
||||||
|
assert eigvals.shape == (2,)
|
||||||
|
assert eigvecs.shape == (2, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_reconditions_large_trace_initial_subspace(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
original_inv = bloch.numpy.linalg.inv
|
||||||
|
original_sqrtm = bloch.scipy.linalg.sqrtm
|
||||||
|
sqrtm_calls = 0
|
||||||
|
inv_calls = 0
|
||||||
|
|
||||||
|
def inv_with_large_first_trace(matrix: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
nonlocal inv_calls
|
||||||
|
inv_calls += 1
|
||||||
|
if inv_calls == 1:
|
||||||
|
return numpy.eye(matrix.shape[0], dtype=complex) * 1e9
|
||||||
|
return original_inv(matrix)
|
||||||
|
|
||||||
|
def sqrtm_wrapper(matrix: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
nonlocal sqrtm_calls
|
||||||
|
sqrtm_calls += 1
|
||||||
|
return original_sqrtm(matrix)
|
||||||
|
|
||||||
|
monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_with_large_first_trace)
|
||||||
|
monkeypatch.setattr(bloch.scipy.linalg, 'sqrtm', sqrtm_wrapper)
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
2,
|
||||||
|
K0_X,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=20,
|
||||||
|
y0=Y0_TWO_MODE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sqrtm_calls >= 2
|
||||||
|
assert eigvals.shape == (2,)
|
||||||
|
assert eigvecs.shape == (2, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_qi_memoization_reuses_cached_theta(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace:
|
||||||
|
theta = 0.3
|
||||||
|
first = func(theta)
|
||||||
|
second = func(theta)
|
||||||
|
assert_allclose(second, first)
|
||||||
|
return SimpleNamespace(fun=second, x=theta)
|
||||||
|
|
||||||
|
monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar)
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0_X,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=1,
|
||||||
|
y0=Y0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('theta', [numpy.pi / 2 - 1e-8, 1e-8])
|
||||||
|
def test_eigsolve_qi_taylor_expansions_return_finite_modes(monkeypatch: pytest.MonkeyPatch, theta: float) -> None:
|
||||||
|
original_inv = bloch.numpy.linalg.inv
|
||||||
|
inv_calls = 0
|
||||||
|
|
||||||
|
def inv_raise_once_for_q(matrix: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
nonlocal inv_calls
|
||||||
|
inv_calls += 1
|
||||||
|
if inv_calls == 3:
|
||||||
|
raise numpy.linalg.LinAlgError('forced singular Q')
|
||||||
|
return original_inv(matrix)
|
||||||
|
|
||||||
|
def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace:
|
||||||
|
value = func(theta)
|
||||||
|
return SimpleNamespace(fun=value, x=theta)
|
||||||
|
|
||||||
|
monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_raise_once_for_q)
|
||||||
|
monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar)
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0_X,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=1,
|
||||||
|
y0=Y0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_qi_inexplicable_singularity_raises(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
original_inv = bloch.numpy.linalg.inv
|
||||||
|
inv_calls = 0
|
||||||
|
|
||||||
|
def inv_raise_once_for_q(matrix: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
nonlocal inv_calls
|
||||||
|
inv_calls += 1
|
||||||
|
if inv_calls == 3:
|
||||||
|
raise numpy.linalg.LinAlgError('forced singular Q')
|
||||||
|
return original_inv(matrix)
|
||||||
|
|
||||||
|
def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace:
|
||||||
|
func(numpy.pi / 4)
|
||||||
|
raise AssertionError('unreachable after trace_func exception')
|
||||||
|
|
||||||
|
monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_raise_once_for_q)
|
||||||
|
monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='Inexplicable singularity in trace_func'):
|
||||||
|
bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0_X,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=1,
|
||||||
|
y0=Y0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_k_returns_vector_frequency_and_callbacks() -> None:
|
||||||
|
target_eigvals, _target_eigvecs = bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0_X,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=50,
|
||||||
|
y0=Y0,
|
||||||
|
)
|
||||||
|
target_frequency = float(numpy.sqrt(abs(numpy.real(target_eigvals[0]))))
|
||||||
|
|
||||||
|
solve_calls = 0
|
||||||
|
iter_calls = 0
|
||||||
|
|
||||||
|
def solve_callback(k_mag: float, eigvals: numpy.ndarray, eigvecs: numpy.ndarray, frequency: float) -> None:
|
||||||
|
nonlocal solve_calls
|
||||||
|
solve_calls += 1
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert isinstance(k_mag, float)
|
||||||
|
assert isinstance(frequency, float)
|
||||||
|
|
||||||
|
def iter_callback() -> None:
|
||||||
|
nonlocal iter_calls
|
||||||
|
iter_calls += 1
|
||||||
|
|
||||||
|
found_k, found_frequency, eigvals, eigvecs = bloch.find_k(
|
||||||
|
target_frequency,
|
||||||
|
1e-4,
|
||||||
|
[1, 0, 0],
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
band=0,
|
||||||
|
k_bounds=(0.05, 0.15),
|
||||||
|
v0=Y0,
|
||||||
|
solve_callback=solve_callback,
|
||||||
|
iter_callback=iter_callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert found_k.shape == (3,)
|
||||||
|
assert numpy.isfinite(found_k).all()
|
||||||
|
assert_close(numpy.cross(found_k, [1.0, 0.0, 0.0]), 0.0, atol=1e-12, rtol=1e-12)
|
||||||
|
assert_close(found_k, K0_X, atol=1e-4, rtol=1e-4)
|
||||||
|
assert abs(found_frequency - target_frequency) <= 1e-4
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
assert solve_calls > 0
|
||||||
|
assert iter_calls > 0
|
||||||
101
meanas/test/test_eigensolvers_numerics.py
Normal file
101
meanas/test/test_eigensolvers_numerics.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import numpy
|
||||||
|
from numpy.linalg import norm
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ._solver_cases import diagonal_eigen_case
|
||||||
|
from .utils import assert_close
|
||||||
|
from ..eigensolvers import power_iteration, rayleigh_quotient_iteration, signed_eigensolve
|
||||||
|
|
||||||
|
|
||||||
|
def test_rayleigh_quotient_iteration_with_linear_operator() -> None:
|
||||||
|
case = diagonal_eigen_case()
|
||||||
|
|
||||||
|
def dense_solver(
|
||||||
|
shifted_operator,
|
||||||
|
rhs: numpy.ndarray,
|
||||||
|
) -> numpy.ndarray:
|
||||||
|
basis = numpy.eye(case.operator.shape[0], dtype=complex)
|
||||||
|
columns = [shifted_operator.matvec(basis[:, ii]) for ii in range(case.operator.shape[0])]
|
||||||
|
dense_matrix = numpy.column_stack(columns)
|
||||||
|
return numpy.linalg.lstsq(dense_matrix, rhs, rcond=None)[0]
|
||||||
|
|
||||||
|
eigval, eigvec = rayleigh_quotient_iteration(
|
||||||
|
case.linear_operator,
|
||||||
|
case.guess_default,
|
||||||
|
iterations=8,
|
||||||
|
solver=dense_solver,
|
||||||
|
)
|
||||||
|
|
||||||
|
residual = norm(case.operator @ eigvec - eigval * eigvec)
|
||||||
|
assert abs(eigval - 3.0) < 1e-12
|
||||||
|
assert residual < 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
def test_signed_eigensolve_negative_returns_largest_negative_mode() -> None:
|
||||||
|
case = diagonal_eigen_case()
|
||||||
|
|
||||||
|
eigvals, eigvecs = signed_eigensolve(case.operator, how_many=1, negative=True)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (4, 1)
|
||||||
|
assert abs(eigvals[0] + 2.0) < 1e-12
|
||||||
|
assert abs(eigvecs[3, 0]) > 0.99
|
||||||
|
|
||||||
|
|
||||||
|
def test_rayleigh_quotient_iteration_uses_default_linear_operator_solver() -> None:
|
||||||
|
case = diagonal_eigen_case()
|
||||||
|
|
||||||
|
eigval, eigvec = rayleigh_quotient_iteration(
|
||||||
|
case.linear_operator,
|
||||||
|
case.guess_default,
|
||||||
|
iterations=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
residual = norm(case.operator @ eigvec - eigval * eigvec)
|
||||||
|
assert abs(eigval - 3.0) < 1e-12
|
||||||
|
assert residual < 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
def test_signed_eigensolve_linear_operator_fallback_returns_dominant_positive_mode() -> None:
|
||||||
|
case = diagonal_eigen_case()
|
||||||
|
|
||||||
|
eigvals, eigvecs = signed_eigensolve(case.linear_operator, how_many=1)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (4, 1)
|
||||||
|
assert_close(eigvals[0], 5.0, atol=1e-12, rtol=1e-12)
|
||||||
|
assert abs(eigvecs[0, 0]) > 0.99
|
||||||
|
|
||||||
|
|
||||||
|
def test_power_iteration_finds_dominant_mode() -> None:
|
||||||
|
case = diagonal_eigen_case()
|
||||||
|
eigval, eigvec = power_iteration(case.operator, guess_vector=numpy.ones(4, dtype=complex), iterations=20)
|
||||||
|
|
||||||
|
assert eigval == pytest.approx(5.0, rel=1e-6)
|
||||||
|
assert abs(eigvec[0]) > abs(eigvec[1])
|
||||||
|
|
||||||
|
|
||||||
|
def test_rayleigh_quotient_iteration_refines_known_sparse_mode() -> None:
|
||||||
|
case = diagonal_eigen_case()
|
||||||
|
|
||||||
|
def solver(matrix, rhs: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
return numpy.linalg.lstsq(matrix.toarray(), rhs, rcond=None)[0]
|
||||||
|
|
||||||
|
eigval, eigvec = rayleigh_quotient_iteration(
|
||||||
|
case.operator,
|
||||||
|
case.guess_sparse,
|
||||||
|
iterations=8,
|
||||||
|
solver=solver,
|
||||||
|
)
|
||||||
|
|
||||||
|
residual = numpy.linalg.norm(case.operator @ eigvec - eigval * eigvec)
|
||||||
|
assert eigval == pytest.approx(3.0, rel=1e-6)
|
||||||
|
assert residual < 1e-8
|
||||||
|
|
||||||
|
|
||||||
|
def test_signed_eigensolve_returns_largest_positive_modes() -> None:
|
||||||
|
case = diagonal_eigen_case()
|
||||||
|
eigvals, eigvecs = signed_eigensolve(case.operator, how_many=2)
|
||||||
|
|
||||||
|
assert_close(eigvals, [3.0, 5.0], atol=1e-6)
|
||||||
|
assert eigvecs.shape == (4, 2)
|
||||||
491
meanas/test/test_eme_numerics.py
Normal file
491
meanas/test/test_eme_numerics.py
Normal file
|
|
@ -0,0 +1,491 @@
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import pytest
|
||||||
|
from scipy import sparse
|
||||||
|
|
||||||
|
from ..fdmath import vec
|
||||||
|
from ..fdfd import eme, waveguide_2d, waveguide_cyl
|
||||||
|
from ._test_builders import complex_ramp, unit_dxes
|
||||||
|
from .utils import assert_close
|
||||||
|
|
||||||
|
|
||||||
|
SHAPE = (3, 2, 2)
|
||||||
|
DXES = unit_dxes((2, 2))
|
||||||
|
WAVENUMBERS_L = numpy.array([1.0, 0.8])
|
||||||
|
WAVENUMBERS_R = numpy.array([0.9, 0.7])
|
||||||
|
OMEGA = 1 / 1500
|
||||||
|
REAL_DXES = unit_dxes((5, 5))
|
||||||
|
|
||||||
|
|
||||||
|
def _mode(scale: float) -> tuple[numpy.ndarray, numpy.ndarray]:
|
||||||
|
e_field = complex_ramp(SHAPE, offset=1.0 + scale)
|
||||||
|
h_field = complex_ramp(SHAPE, scale=0.2, offset=2.0, imag_offset=0.05 * scale)
|
||||||
|
return vec(e_field), vec(h_field)
|
||||||
|
|
||||||
|
|
||||||
|
def _mode_sets() -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], list[tuple[numpy.ndarray, numpy.ndarray]]]:
|
||||||
|
left_modes = [_mode(0.0), _mode(0.7)]
|
||||||
|
right_modes = [_mode(1.4), _mode(2.1)]
|
||||||
|
return left_modes, right_modes
|
||||||
|
|
||||||
|
|
||||||
|
def _gain_only_tr(*args, **kwargs) -> tuple[numpy.ndarray, numpy.ndarray]:
|
||||||
|
return numpy.array([[2.0, 0.0], [0.0, 0.5]]), numpy.zeros((2, 2))
|
||||||
|
|
||||||
|
|
||||||
|
def _gain_and_reflection_tr(*args, **kwargs) -> tuple[numpy.ndarray, numpy.ndarray]:
|
||||||
|
return numpy.array([[2.0, 0.0], [0.0, 0.5]]), numpy.array([[0.0, 1.0], [2.0, 0.0]])
|
||||||
|
|
||||||
|
|
||||||
|
def _nonsymmetric_tr(left_marker: object):
|
||||||
|
def fake_get_tr(_eh_left, wavenumbers_left, _eh_right, _wavenumbers_right, **kwargs):
|
||||||
|
if wavenumbers_left is left_marker:
|
||||||
|
return (
|
||||||
|
numpy.array([[1.0, 2.0], [0.5, 1.0]]),
|
||||||
|
numpy.array([[0.0, 1.0], [2.0, 0.0]]),
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
numpy.array([[1.0, -1.0], [0.0, 1.0]]),
|
||||||
|
numpy.array([[0.0, 0.5], [1.5, 0.0]]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return fake_get_tr
|
||||||
|
|
||||||
|
|
||||||
|
def _dummy_modes() -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], numpy.ndarray]:
|
||||||
|
return [_mode(0.0), _mode(0.7)], numpy.array([1.0, 0.5])
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tr_returns_finite_bounded_transfer_matrices() -> None:
|
||||||
|
left_modes, right_modes = _mode_sets()
|
||||||
|
|
||||||
|
transmission, reflection = eme.get_tr(
|
||||||
|
left_modes,
|
||||||
|
WAVENUMBERS_L,
|
||||||
|
right_modes,
|
||||||
|
WAVENUMBERS_R,
|
||||||
|
dxes=DXES,
|
||||||
|
)
|
||||||
|
|
||||||
|
singular_values = numpy.linalg.svd(transmission, compute_uv=False)
|
||||||
|
|
||||||
|
assert transmission.shape == (2, 2)
|
||||||
|
assert reflection.shape == (2, 2)
|
||||||
|
assert numpy.isfinite(transmission).all()
|
||||||
|
assert numpy.isfinite(reflection).all()
|
||||||
|
assert (singular_values <= 1.0 + 1e-12).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_abcd_matches_explicit_block_formula() -> None:
|
||||||
|
left_modes, right_modes = _mode_sets()
|
||||||
|
t12, r12 = eme.get_tr(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES)
|
||||||
|
t21, r21 = eme.get_tr(right_modes, WAVENUMBERS_R, left_modes, WAVENUMBERS_L, dxes=DXES)
|
||||||
|
t21_inv = numpy.linalg.pinv(t21)
|
||||||
|
|
||||||
|
expected = numpy.block([
|
||||||
|
[t12 - r21 @ t21_inv @ r12, r21 @ t21_inv],
|
||||||
|
[-t21_inv @ r12, t21_inv],
|
||||||
|
])
|
||||||
|
abcd = eme.get_abcd(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES)
|
||||||
|
|
||||||
|
assert sparse.issparse(abcd)
|
||||||
|
assert abcd.shape == (4, 4)
|
||||||
|
assert_close(abcd.toarray(), expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_s_plain_matches_block_assembly_from_get_tr() -> None:
|
||||||
|
left_modes, right_modes = _mode_sets()
|
||||||
|
t12, r12 = eme.get_tr(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES)
|
||||||
|
t21, r21 = eme.get_tr(right_modes, WAVENUMBERS_R, left_modes, WAVENUMBERS_L, dxes=DXES)
|
||||||
|
expected = numpy.block([[r12, t12], [t21, r21]])
|
||||||
|
|
||||||
|
ss = eme.get_s(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES)
|
||||||
|
|
||||||
|
assert ss.shape == (4, 4)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert_close(ss, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_s_force_nogain_caps_singular_values(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(eme, 'get_tr', _gain_only_tr)
|
||||||
|
modes, wavenumbers = _dummy_modes()
|
||||||
|
|
||||||
|
plain_s = eme.get_s(modes, wavenumbers, modes, wavenumbers)
|
||||||
|
clipped_s = eme.get_s(modes, wavenumbers, modes, wavenumbers, force_nogain=True)
|
||||||
|
|
||||||
|
plain_singular_values = numpy.linalg.svd(plain_s, compute_uv=False)
|
||||||
|
clipped_singular_values = numpy.linalg.svd(clipped_s, compute_uv=False)
|
||||||
|
|
||||||
|
assert plain_singular_values.max() > 1.0
|
||||||
|
assert (clipped_singular_values <= 1.0 + 1e-12).all()
|
||||||
|
assert numpy.isfinite(clipped_s).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch) -> None:
|
||||||
|
left = numpy.array([1.0, 0.5])
|
||||||
|
right = numpy.array([0.9, 0.4])
|
||||||
|
modes, _wavenumbers = _dummy_modes()
|
||||||
|
|
||||||
|
monkeypatch.setattr(eme, 'get_tr', _nonsymmetric_tr(left))
|
||||||
|
ss = eme.get_s(modes, left, modes, right, force_reciprocal=True)
|
||||||
|
|
||||||
|
assert_close(ss, ss.T)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_s_force_nogain_and_reciprocal_returns_finite_output(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(eme, 'get_tr', _gain_and_reflection_tr)
|
||||||
|
modes, wavenumbers = _dummy_modes()
|
||||||
|
ss = eme.get_s(modes, wavenumbers, modes, wavenumbers, force_nogain=True, force_reciprocal=True)
|
||||||
|
|
||||||
|
assert ss.shape == (4, 4)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert_close(ss, ss.T)
|
||||||
|
assert (numpy.linalg.svd(ss, compute_uv=False) <= 1.0 + 1e-12).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tr_rejects_length_mismatches() -> None:
|
||||||
|
left_modes, right_modes = _mode_sets()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='same length'):
|
||||||
|
eme.get_tr(left_modes[:1], WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tr_rejects_malformed_mode_tuples() -> None:
|
||||||
|
bad_modes = cast(list[tuple[numpy.ndarray, numpy.ndarray]], [(numpy.ones(4, dtype=complex),)])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='2-tuple'):
|
||||||
|
eme.get_tr(bad_modes, [1.0], bad_modes, [1.0], dxes=DXES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tr_rejects_incompatible_field_shapes() -> None:
|
||||||
|
left_modes = [(numpy.ones(4, dtype=complex), numpy.ones(4, dtype=complex))]
|
||||||
|
right_modes = [(numpy.ones(6, dtype=complex), numpy.ones(6, dtype=complex))]
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='same E/H shapes'):
|
||||||
|
eme.get_tr(left_modes, [1.0], right_modes, [1.0], dxes=DXES)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_real_epsilon() -> numpy.ndarray:
|
||||||
|
epsilon = numpy.ones((3, 5, 5), dtype=float)
|
||||||
|
epsilon[:, 2, 1] = 2.0
|
||||||
|
return vec(epsilon)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_straight_mode() -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex, numpy.ndarray]:
|
||||||
|
epsilon = _build_real_epsilon()
|
||||||
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
||||||
|
0,
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=REAL_DXES,
|
||||||
|
epsilon=epsilon,
|
||||||
|
)
|
||||||
|
e_field, h_field = waveguide_2d.normalized_fields_e(
|
||||||
|
e_xy,
|
||||||
|
wavenumber=wavenumber,
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=REAL_DXES,
|
||||||
|
epsilon=epsilon,
|
||||||
|
)
|
||||||
|
return (e_field, h_field), wavenumber, epsilon
|
||||||
|
|
||||||
|
|
||||||
|
def _build_bend_mode() -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex]:
|
||||||
|
epsilon = vec(numpy.ones((3, 5, 5), dtype=float))
|
||||||
|
rmin = 10.0
|
||||||
|
e_xy, angular_wavenumber = waveguide_cyl.solve_mode(
|
||||||
|
0,
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=REAL_DXES,
|
||||||
|
epsilon=epsilon,
|
||||||
|
rmin=rmin,
|
||||||
|
)
|
||||||
|
linear_wavenumber = waveguide_cyl.linear_wavenumbers(
|
||||||
|
[e_xy],
|
||||||
|
[angular_wavenumber],
|
||||||
|
epsilon=epsilon,
|
||||||
|
dxes=REAL_DXES,
|
||||||
|
rmin=rmin,
|
||||||
|
)[0]
|
||||||
|
e_field, h_field = waveguide_cyl.normalized_fields_e(
|
||||||
|
e_xy,
|
||||||
|
angular_wavenumber=angular_wavenumber,
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=REAL_DXES,
|
||||||
|
epsilon=epsilon,
|
||||||
|
rmin=rmin,
|
||||||
|
)
|
||||||
|
return (e_field, h_field), linear_wavenumber
|
||||||
|
|
||||||
|
|
||||||
|
def _build_uniform_mode(index: float) -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex]:
|
||||||
|
area = 25.0
|
||||||
|
e_field = numpy.zeros((3, 5, 5), dtype=complex)
|
||||||
|
h_field = numpy.zeros((3, 5, 5), dtype=complex)
|
||||||
|
e_field[0] = numpy.sqrt(2.0 / (index * area))
|
||||||
|
h_field[1] = numpy.sqrt(2.0 * index / area)
|
||||||
|
return (vec(e_field), vec(h_field)), complex(index * OMEGA)
|
||||||
|
|
||||||
|
|
||||||
|
def _interface_s(n_left: float, n_right: float) -> numpy.ndarray:
|
||||||
|
left_mode, left_beta = _build_uniform_mode(n_left)
|
||||||
|
right_mode, right_beta = _build_uniform_mode(n_right)
|
||||||
|
return eme.get_s([left_mode], [left_beta], [right_mode], [right_beta], dxes=REAL_DXES)
|
||||||
|
|
||||||
|
|
||||||
|
def _interface_abcd(n_left: float, n_right: float) -> numpy.ndarray:
|
||||||
|
left_mode, left_beta = _build_uniform_mode(n_left)
|
||||||
|
right_mode, right_beta = _build_uniform_mode(n_right)
|
||||||
|
return eme.get_abcd([left_mode], [left_beta], [right_mode], [right_beta], dxes=REAL_DXES).toarray()
|
||||||
|
|
||||||
|
|
||||||
|
def _expected_interface_s(n_left: float, n_right: float) -> numpy.ndarray:
|
||||||
|
reflection = (n_left - n_right) / (n_left + n_right)
|
||||||
|
transmission = 2 * numpy.sqrt(n_left * n_right) / (n_left + n_right)
|
||||||
|
return numpy.array(
|
||||||
|
[
|
||||||
|
[reflection, transmission],
|
||||||
|
[transmission, -reflection],
|
||||||
|
],
|
||||||
|
dtype=complex,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _propagation_abcd(beta: complex, length: float) -> numpy.ndarray:
|
||||||
|
phase = numpy.exp(-1j * beta * length)
|
||||||
|
return numpy.array(
|
||||||
|
[
|
||||||
|
[phase, 0.0],
|
||||||
|
[0.0, phase ** -1],
|
||||||
|
],
|
||||||
|
dtype=complex,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _abcd_to_s(abcd: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
aa = abcd[0, 0]
|
||||||
|
bb = abcd[0, 1]
|
||||||
|
cc = abcd[1, 0]
|
||||||
|
dd = abcd[1, 1]
|
||||||
|
t21 = 1.0 / dd
|
||||||
|
r21 = bb / dd
|
||||||
|
r12 = -cc / dd
|
||||||
|
t12 = aa - bb * cc / dd
|
||||||
|
return numpy.array(
|
||||||
|
[
|
||||||
|
[r12, t12],
|
||||||
|
[t21, r21],
|
||||||
|
],
|
||||||
|
dtype=complex,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _expected_bragg_reflector_s(n_low: float, n_high: float, periods: int) -> numpy.ndarray:
|
||||||
|
ratio = n_high / n_low
|
||||||
|
reflection = (1 - ratio ** (2 * periods)) / (1 + ratio ** (2 * periods))
|
||||||
|
transmission = ((-1) ** periods) * 2 * ratio ** periods / (1 + ratio ** (2 * periods))
|
||||||
|
return numpy.array(
|
||||||
|
[
|
||||||
|
[reflection, transmission],
|
||||||
|
[transmission, -reflection],
|
||||||
|
],
|
||||||
|
dtype=complex,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_s_is_near_identity_for_identical_solved_straight_modes() -> None:
|
||||||
|
mode, wavenumber, _epsilon = _build_straight_mode()
|
||||||
|
|
||||||
|
ss = eme.get_s([mode], [wavenumber], [mode], [wavenumber], dxes=REAL_DXES)
|
||||||
|
|
||||||
|
assert ss.shape == (2, 2)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert abs(ss[0, 0]) < 1e-6
|
||||||
|
assert abs(ss[1, 1]) < 1e-6
|
||||||
|
assert abs(abs(ss[0, 1]) - 1.0) < 1e-6
|
||||||
|
assert abs(abs(ss[1, 0]) - 1.0) < 1e-6
|
||||||
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_s_returns_finite_passive_output_for_small_straight_to_bend_fixture() -> None:
|
||||||
|
straight_mode, straight_wavenumber, _epsilon = _build_straight_mode()
|
||||||
|
bend_mode, bend_wavenumber = _build_bend_mode()
|
||||||
|
|
||||||
|
ss = eme.get_s([straight_mode], [straight_wavenumber], [bend_mode], [bend_wavenumber], dxes=REAL_DXES)
|
||||||
|
|
||||||
|
assert ss.shape == (2, 2)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_s_matches_analytic_fresnel_interface_for_uniform_one_mode_ports() -> None:
|
||||||
|
"""
|
||||||
|
For power-normalized one-mode ports at normal incidence, the interface matrix is
|
||||||
|
|
||||||
|
r12 = (n_left - n_right) / (n_left + n_right)
|
||||||
|
r21 = -r12
|
||||||
|
t12 = t21 = 2 * sqrt(n_left * n_right) / (n_left + n_right)
|
||||||
|
|
||||||
|
so
|
||||||
|
|
||||||
|
S = [[r12, t12], [t21, r21]].
|
||||||
|
"""
|
||||||
|
ss = _interface_s(1.0, 2.0)
|
||||||
|
expected = _expected_interface_s(1.0, 2.0)
|
||||||
|
|
||||||
|
assert ss.shape == (2, 2)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert_close(ss, expected, atol=1e-6, rtol=1e-6)
|
||||||
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarter_wave_matching_layer_is_nearly_reflectionless_at_design_frequency() -> None:
|
||||||
|
"""
|
||||||
|
A single quarter-wave matching layer with
|
||||||
|
|
||||||
|
n1 = sqrt(n0 * n2), beta1 * L = pi / 2
|
||||||
|
|
||||||
|
cancels the two interface reflections at the design wavelength, so the
|
||||||
|
normal-incidence stack should satisfy `r = 0` and `|t| = 1`.
|
||||||
|
"""
|
||||||
|
n0 = 1.0
|
||||||
|
n1 = numpy.sqrt(2.0)
|
||||||
|
n2 = 2.0
|
||||||
|
interface_01 = _interface_abcd(n0, n1)
|
||||||
|
interface_12 = _interface_abcd(n1, n2)
|
||||||
|
_mode_1, beta_1 = _build_uniform_mode(float(n1))
|
||||||
|
quarter_wave_length = numpy.pi / (2 * numpy.real(beta_1))
|
||||||
|
|
||||||
|
stack_abcd = interface_01 @ _propagation_abcd(beta_1, quarter_wave_length) @ interface_12
|
||||||
|
ss = _abcd_to_s(stack_abcd)
|
||||||
|
|
||||||
|
assert ss.shape == (2, 2)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert abs(ss[0, 0]) < 1e-12
|
||||||
|
assert abs(ss[1, 1]) < 1e-12
|
||||||
|
assert abs(abs(ss[0, 1]) - 1.0) < 1e-12
|
||||||
|
assert abs(abs(ss[1, 0]) - 1.0) < 1e-12
|
||||||
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarter_wave_ar_layer_reduces_reflection_relative_to_abrupt_interface() -> None:
|
||||||
|
"""
|
||||||
|
Compare the abrupt interface `n0 -> n2` against the quarter-wave matching-layer
|
||||||
|
stack `n0 -> sqrt(n0 n2) -> n2` at the same design wavelength.
|
||||||
|
|
||||||
|
For the canonical `n0 = 1`, `n2 = 2` case, the abrupt interface has
|
||||||
|
|
||||||
|
|r_abrupt| = |(n0 - n2) / (n0 + n2)| = 1 / 3,
|
||||||
|
|
||||||
|
while the quarter-wave matching layer should cancel the interface reflections
|
||||||
|
so that `|r_ar|` is essentially zero and `|t_ar|` is correspondingly larger.
|
||||||
|
"""
|
||||||
|
n0 = 1.0
|
||||||
|
n2 = 2.0
|
||||||
|
abrupt = _interface_s(n0, n2)
|
||||||
|
|
||||||
|
n1 = numpy.sqrt(n0 * n2)
|
||||||
|
interface_01 = _interface_abcd(n0, n1)
|
||||||
|
interface_12 = _interface_abcd(n1, n2)
|
||||||
|
_mode_1, beta_1 = _build_uniform_mode(float(n1))
|
||||||
|
quarter_wave_length = numpy.pi / (2 * numpy.real(beta_1))
|
||||||
|
ar_stack = _abcd_to_s(interface_01 @ _propagation_abcd(beta_1, quarter_wave_length) @ interface_12)
|
||||||
|
|
||||||
|
abrupt_reflection = abs(abrupt[0, 0])
|
||||||
|
abrupt_transmission = abs(abrupt[1, 0])
|
||||||
|
ar_reflection = abs(ar_stack[0, 0])
|
||||||
|
ar_transmission = abs(ar_stack[1, 0])
|
||||||
|
|
||||||
|
assert numpy.linalg.svd(abrupt, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
|
assert numpy.linalg.svd(ar_stack, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
|
assert ar_reflection < abrupt_reflection
|
||||||
|
assert ar_transmission > abrupt_transmission
|
||||||
|
assert ar_reflection < 1e-12
|
||||||
|
assert abs(abrupt_reflection - (1.0 / 3.0)) < 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
def test_half_wave_uniform_slab_restores_unit_transmission_between_matched_media() -> None:
|
||||||
|
"""
|
||||||
|
For matched exterior media `n0 = n2`, a half-wave slab with
|
||||||
|
|
||||||
|
beta1 * L = pi
|
||||||
|
|
||||||
|
contributes only a global phase, so the stack returns to `r = 0` and
|
||||||
|
`|t| = 1` at the design wavelength.
|
||||||
|
"""
|
||||||
|
n0 = 1.0
|
||||||
|
n1 = 2.0
|
||||||
|
interface_01 = _interface_abcd(n0, n1)
|
||||||
|
interface_10 = _interface_abcd(n1, n0)
|
||||||
|
_mode_1, beta_1 = _build_uniform_mode(n1)
|
||||||
|
half_wave_length = numpy.pi / numpy.real(beta_1)
|
||||||
|
|
||||||
|
stack_abcd = interface_01 @ _propagation_abcd(beta_1, half_wave_length) @ interface_10
|
||||||
|
ss = _abcd_to_s(stack_abcd)
|
||||||
|
|
||||||
|
assert ss.shape == (2, 2)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert abs(ss[0, 0]) < 1e-12
|
||||||
|
assert abs(ss[1, 1]) < 1e-12
|
||||||
|
assert abs(abs(ss[0, 1]) - 1.0) < 1e-12
|
||||||
|
assert abs(abs(ss[1, 0]) - 1.0) < 1e-12
|
||||||
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
|
|
||||||
|
|
||||||
|
def test_strong_uniform_index_mismatch_behaves_like_near_termination() -> None:
|
||||||
|
"""
|
||||||
|
In the large-index-ratio limit, the same Fresnel formulas approach a hard-wall
|
||||||
|
reflector:
|
||||||
|
|
||||||
|
|r| -> 1, |t| -> 0 as n_right / n_left -> infinity.
|
||||||
|
"""
|
||||||
|
ss = _interface_s(1.0, 100.0)
|
||||||
|
expected = _expected_interface_s(1.0, 100.0)
|
||||||
|
|
||||||
|
assert ss.shape == (2, 2)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert_close(ss, expected, atol=1e-6, rtol=1e-6)
|
||||||
|
assert abs(ss[0, 0]) > 0.9
|
||||||
|
assert abs(ss[1, 0]) < 0.25
|
||||||
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarter_wave_bragg_reflector_matches_closed_form_stopband_response() -> None:
|
||||||
|
"""
|
||||||
|
For `N` quarter-wave high/low periods at the Bragg wavelength with identical
|
||||||
|
low-index incident and exit media (`n0 = ns = n_low`),
|
||||||
|
|
||||||
|
M_pair = diag(-(n_low / n_high), -(n_high / n_low))
|
||||||
|
M_stack = M_pair ** N
|
||||||
|
|
||||||
|
which yields the closed-form scattering amplitudes
|
||||||
|
|
||||||
|
r = (1 - (n_high / n_low) ** (2N)) / (1 + (n_high / n_low) ** (2N))
|
||||||
|
t = 2 * (n_high / n_low) ** N / (1 + (n_high / n_low) ** (2N)).
|
||||||
|
|
||||||
|
The reflector should therefore sit deep in the stopband with `|r|` near 1 and
|
||||||
|
`|t|` correspondingly small.
|
||||||
|
"""
|
||||||
|
n_low = 1.0
|
||||||
|
n_high = 2.0
|
||||||
|
periods = 5
|
||||||
|
interface_lh = _interface_abcd(n_low, n_high)
|
||||||
|
interface_hl = _interface_abcd(n_high, n_low)
|
||||||
|
_mode_h, beta_h = _build_uniform_mode(n_high)
|
||||||
|
_mode_l, beta_l = _build_uniform_mode(n_low)
|
||||||
|
quarter_wave_high = numpy.pi / (2 * numpy.real(beta_h))
|
||||||
|
quarter_wave_low = numpy.pi / (2 * numpy.real(beta_l))
|
||||||
|
|
||||||
|
stack_abcd = numpy.eye(2, dtype=complex)
|
||||||
|
for _ in range(periods):
|
||||||
|
stack_abcd = stack_abcd @ interface_lh @ _propagation_abcd(beta_h, quarter_wave_high)
|
||||||
|
stack_abcd = stack_abcd @ interface_hl @ _propagation_abcd(beta_l, quarter_wave_low)
|
||||||
|
ss = _abcd_to_s(stack_abcd)
|
||||||
|
expected = _expected_bragg_reflector_s(n_low, n_high, periods)
|
||||||
|
|
||||||
|
assert ss.shape == (2, 2)
|
||||||
|
assert numpy.isfinite(ss).all()
|
||||||
|
assert_close(ss, expected, atol=1e-12, rtol=1e-12)
|
||||||
|
assert abs(ss[0, 0]) > 0.99
|
||||||
|
assert abs(ss[1, 0]) < 0.1
|
||||||
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
||||||
47
meanas/test/test_examples_smoke.py
Normal file
47
meanas/test/test_examples_smoke.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.complete
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def _run_example(example_name: str, tmp_path: Path) -> subprocess.CompletedProcess[str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['MPLBACKEND'] = 'Agg'
|
||||||
|
env['MPLCONFIGDIR'] = str(tmp_path / f'mpl-{example_name}')
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, str(REPO_ROOT / 'examples' / example_name)],
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
env=env,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_eme_example_smoke_runs(tmp_path: Path) -> None:
|
||||||
|
pytest.importorskip('matplotlib')
|
||||||
|
|
||||||
|
result = _run_example('eme.py', tmp_path)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
assert 'left effective indices:' in result.stdout
|
||||||
|
assert 'fundamental left-to-right transmission' in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_eme_bend_example_smoke_runs(tmp_path: Path) -> None:
|
||||||
|
pytest.importorskip('matplotlib')
|
||||||
|
pytest.importorskip('skrf')
|
||||||
|
|
||||||
|
result = _run_example('eme_bend.py', tmp_path)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
assert 'straight effective indices:' in result.stdout
|
||||||
|
assert 'cascaded bend through power' in result.stdout
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Iterable
|
# ruff: noqa: ARG001
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -6,7 +6,7 @@ from numpy.typing import NDArray
|
||||||
#from numpy.testing import assert_allclose, assert_array_equal
|
#from numpy.testing import assert_allclose, assert_array_equal
|
||||||
|
|
||||||
from .. import fdfd
|
from .. import fdfd
|
||||||
from ..fdmath import vec, unvec
|
from ..fdmath import vec, unvec, vcfdfield, vfdfield, dx_lists_t
|
||||||
from .utils import assert_close # , assert_fields_close
|
from .utils import assert_close # , assert_fields_close
|
||||||
from .conftest import FixtureRequest
|
from .conftest import FixtureRequest
|
||||||
|
|
||||||
|
|
@ -61,24 +61,24 @@ def test_poynting_planes(sim: 'FDResult') -> None:
|
||||||
# Also see conftest.py
|
# Also see conftest.py
|
||||||
|
|
||||||
@pytest.fixture(params=[1 / 1500])
|
@pytest.fixture(params=[1 / 1500])
|
||||||
def omega(request: FixtureRequest) -> Iterable[float]:
|
def omega(request: FixtureRequest) -> float:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[None])
|
@pytest.fixture(params=[None])
|
||||||
def pec(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
|
def pec(request: FixtureRequest) -> NDArray[numpy.float64] | None:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[None])
|
@pytest.fixture(params=[None])
|
||||||
def pmc(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
|
def pmc(request: FixtureRequest) -> NDArray[numpy.float64] | None:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
#@pytest.fixture(scope='module',
|
#@pytest.fixture(scope='module',
|
||||||
# params=[(25, 5, 5)])
|
# params=[(25, 5, 5)])
|
||||||
#def shape(request):
|
#def shape(request: FixtureRequest):
|
||||||
# yield (3, *request.param)
|
# return (3, *request.param)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=['diag']) # 'center'
|
@pytest.fixture(params=['diag']) # 'center'
|
||||||
|
|
@ -86,7 +86,7 @@ def j_distribution(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
j_mag: float,
|
j_mag: float,
|
||||||
) -> Iterable[NDArray[numpy.float64]]:
|
) -> NDArray[numpy.float64]:
|
||||||
j = numpy.zeros(shape, dtype=complex)
|
j = numpy.zeros(shape, dtype=complex)
|
||||||
center_mask = numpy.zeros(shape, dtype=bool)
|
center_mask = numpy.zeros(shape, dtype=bool)
|
||||||
center_mask[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True
|
center_mask[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True
|
||||||
|
|
@ -96,22 +96,22 @@ def j_distribution(
|
||||||
elif request.param == 'diag':
|
elif request.param == 'diag':
|
||||||
j[numpy.roll(center_mask, [1, 1, 1], axis=(1, 2, 3))] = (1 + 1j) * j_mag
|
j[numpy.roll(center_mask, [1, 1, 1], axis=(1, 2, 3))] = (1 + 1j) * j_mag
|
||||||
j[numpy.roll(center_mask, [-1, -1, -1], axis=(1, 2, 3))] = (1 - 1j) * j_mag
|
j[numpy.roll(center_mask, [-1, -1, -1], axis=(1, 2, 3))] = (1 - 1j) * j_mag
|
||||||
yield j
|
return j
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass()
|
@dataclasses.dataclass()
|
||||||
class FDResult:
|
class FDResult:
|
||||||
shape: tuple[int, ...]
|
shape: tuple[int, ...]
|
||||||
dxes: list[list[NDArray[numpy.float64]]]
|
dxes: dx_lists_t
|
||||||
epsilon: NDArray[numpy.float64]
|
epsilon: vfdfield
|
||||||
omega: complex
|
omega: complex
|
||||||
j: NDArray[numpy.complex128]
|
j: vcfdfield
|
||||||
e: NDArray[numpy.complex128]
|
e: vcfdfield
|
||||||
pmc: NDArray[numpy.float64] | None
|
pmc: vfdfield | None
|
||||||
pec: NDArray[numpy.float64] | None
|
pec: vfdfield | None
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture
|
||||||
def sim(
|
def sim(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
|
|
@ -141,11 +141,11 @@ def sim(
|
||||||
j_vec = vec(j_distribution)
|
j_vec = vec(j_distribution)
|
||||||
eps_vec = vec(epsilon)
|
eps_vec = vec(epsilon)
|
||||||
e_vec = fdfd.solvers.generic(
|
e_vec = fdfd.solvers.generic(
|
||||||
J=j_vec,
|
J = j_vec,
|
||||||
omega=omega,
|
omega = omega,
|
||||||
dxes=dxes,
|
dxes = dxes,
|
||||||
epsilon=eps_vec,
|
epsilon = eps_vec,
|
||||||
matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11},
|
matrix_solver_opts = dict(atol=1e-15, rtol=1e-11),
|
||||||
)
|
)
|
||||||
e = unvec(e_vec, shape[1:])
|
e = unvec(e_vec, shape[1:])
|
||||||
|
|
||||||
|
|
|
||||||
213
meanas/test/test_fdfd_algebra_helpers.py
Normal file
213
meanas/test/test_fdfd_algebra_helpers.py
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from ..fdmath import vec, unvec
|
||||||
|
from ..fdmath import functional as fd_functional
|
||||||
|
from ..fdfd import operators, scpml
|
||||||
|
from ._fdfd_case import (
|
||||||
|
BOUNDARY_DXES,
|
||||||
|
BOUNDARY_EPSILON,
|
||||||
|
BOUNDARY_FIELD,
|
||||||
|
BOUNDARY_SHAPE,
|
||||||
|
DXES,
|
||||||
|
EPSILON,
|
||||||
|
E_FIELD,
|
||||||
|
MU,
|
||||||
|
H_FIELD,
|
||||||
|
OMEGA,
|
||||||
|
PEC,
|
||||||
|
PMC,
|
||||||
|
SHAPE,
|
||||||
|
apply_fdfd_matrix,
|
||||||
|
)
|
||||||
|
from .utils import assert_close, assert_fields_close
|
||||||
|
|
||||||
|
|
||||||
|
def _dense_e_full(mu: numpy.ndarray | None) -> numpy.ndarray:
|
||||||
|
ce = fd_functional.curl_forward(DXES[0])
|
||||||
|
ch = fd_functional.curl_back(DXES[1])
|
||||||
|
pe = numpy.where(PEC, 0.0, 1.0)
|
||||||
|
pm = numpy.where(PMC, 0.0, 1.0)
|
||||||
|
|
||||||
|
masked_e = pe * E_FIELD
|
||||||
|
curl_term = ce(masked_e)
|
||||||
|
if mu is not None:
|
||||||
|
curl_term = curl_term / mu
|
||||||
|
curl_term = pm * curl_term
|
||||||
|
curl_term = ch(curl_term)
|
||||||
|
return pe * (curl_term - OMEGA**2 * EPSILON * masked_e)
|
||||||
|
|
||||||
|
|
||||||
|
def _dense_h_full(mu: numpy.ndarray | None) -> numpy.ndarray:
|
||||||
|
ce = fd_functional.curl_forward(DXES[0])
|
||||||
|
ch = fd_functional.curl_back(DXES[1])
|
||||||
|
pe = numpy.where(PEC, 0.0, 1.0)
|
||||||
|
pm = numpy.where(PMC, 0.0, 1.0)
|
||||||
|
magnetic = numpy.ones_like(EPSILON) if mu is None else mu
|
||||||
|
|
||||||
|
masked_h = pm * H_FIELD
|
||||||
|
curl_term = ch(masked_h)
|
||||||
|
curl_term = pe * (curl_term / EPSILON)
|
||||||
|
curl_term = ce(curl_term)
|
||||||
|
return pm * (curl_term - OMEGA**2 * magnetic * masked_h)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_distance(u: numpy.ndarray, size: int, thickness: int) -> numpy.ndarray:
|
||||||
|
return ((thickness - u).clip(0) + (u - (size - thickness)).clip(0)) / thickness
|
||||||
|
|
||||||
|
|
||||||
|
def test_h_full_matches_dense_reference_with_and_without_mu() -> None:
|
||||||
|
for mu in (None, MU):
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.h_full(OMEGA, DXES, vec(EPSILON), None if mu is None else vec(mu), vec(PEC), vec(PMC)),
|
||||||
|
H_FIELD,
|
||||||
|
)
|
||||||
|
dense_result = _dense_h_full(mu)
|
||||||
|
assert_fields_close(matrix_result, dense_result, atol=1e-10, rtol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e_full_matches_dense_reference_with_masks() -> None:
|
||||||
|
for mu in (None, MU):
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.e_full(OMEGA, DXES, vec(EPSILON), None if mu is None else vec(mu), vec(PEC), vec(PMC)),
|
||||||
|
E_FIELD,
|
||||||
|
)
|
||||||
|
dense_result = _dense_e_full(mu)
|
||||||
|
assert_fields_close(matrix_result, dense_result, atol=1e-10, rtol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_h_full_without_masks_matches_dense_reference() -> None:
|
||||||
|
ce = fd_functional.curl_forward(DXES[0])
|
||||||
|
ch = fd_functional.curl_back(DXES[1])
|
||||||
|
dense_result = ce(ch(H_FIELD) / EPSILON) - OMEGA**2 * MU * H_FIELD
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.h_full(OMEGA, DXES, vec(EPSILON), vec(MU)),
|
||||||
|
H_FIELD,
|
||||||
|
)
|
||||||
|
assert_fields_close(matrix_result, dense_result, atol=1e-10, rtol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_eh_full_matches_manual_block_operator_with_masks() -> None:
|
||||||
|
pe = numpy.where(PEC, 0.0, 1.0)
|
||||||
|
pm = numpy.where(PMC, 0.0, 1.0)
|
||||||
|
ce = fd_functional.curl_forward(DXES[0])
|
||||||
|
ch = fd_functional.curl_back(DXES[1])
|
||||||
|
|
||||||
|
matrix_result = operators.eh_full(OMEGA, DXES, vec(EPSILON), vec(MU), vec(PEC), vec(PMC)) @ numpy.concatenate(
|
||||||
|
[vec(E_FIELD), vec(H_FIELD)],
|
||||||
|
)
|
||||||
|
matrix_e, matrix_h = (unvec(part, SHAPE) for part in numpy.split(matrix_result, 2))
|
||||||
|
|
||||||
|
dense_e = pe * ch(pm * H_FIELD) - pe * (1j * OMEGA * EPSILON * (pe * E_FIELD))
|
||||||
|
dense_h = pm * ce(pe * E_FIELD) + pm * (1j * OMEGA * MU * (pm * H_FIELD))
|
||||||
|
|
||||||
|
assert_fields_close(matrix_e, dense_e, atol=1e-10, rtol=1e-10)
|
||||||
|
assert_fields_close(matrix_h, dense_h, atol=1e-10, rtol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e2h_pmc_mask_matches_masked_unmasked_result() -> None:
|
||||||
|
pmc_complement = numpy.where(PMC, 0.0, 1.0)
|
||||||
|
unmasked = apply_fdfd_matrix(operators.e2h(OMEGA, DXES, vec(MU)), E_FIELD)
|
||||||
|
masked = apply_fdfd_matrix(operators.e2h(OMEGA, DXES, vec(MU), vec(PMC)), E_FIELD)
|
||||||
|
|
||||||
|
assert_fields_close(masked, pmc_complement * unmasked, atol=1e-10, rtol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_poynting_h_cross_matches_negative_e_cross_relation() -> None:
|
||||||
|
h_cross_e = apply_fdfd_matrix(operators.poynting_h_cross(vec(H_FIELD), DXES), E_FIELD)
|
||||||
|
e_cross_h = apply_fdfd_matrix(operators.poynting_e_cross(vec(E_FIELD), DXES), H_FIELD)
|
||||||
|
|
||||||
|
assert_fields_close(h_cross_e, -e_cross_h, atol=1e-10, rtol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e_boundary_source_interior_mask_is_independent_of_periodic_edges() -> None:
|
||||||
|
mask = numpy.zeros((3, *BOUNDARY_SHAPE), dtype=float)
|
||||||
|
mask[:, 1, 1, 1] = 1.0
|
||||||
|
|
||||||
|
periodic = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=True)
|
||||||
|
mirrored = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=False)
|
||||||
|
|
||||||
|
assert_close(periodic.toarray(), mirrored.toarray())
|
||||||
|
|
||||||
|
|
||||||
|
def test_e_boundary_source_periodic_edges_add_opposite_face_response() -> None:
|
||||||
|
mask = numpy.zeros((3, *BOUNDARY_SHAPE), dtype=float)
|
||||||
|
mask[:, 0, 1, 1] = 1.0
|
||||||
|
|
||||||
|
periodic = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=True)
|
||||||
|
mirrored = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=False)
|
||||||
|
diff = unvec((periodic - mirrored) @ vec(BOUNDARY_FIELD), BOUNDARY_SHAPE)
|
||||||
|
|
||||||
|
assert numpy.isfinite(diff).all()
|
||||||
|
assert_close(diff[:, 1:-1, :, :], 0.0)
|
||||||
|
assert numpy.linalg.norm(diff[:, -1, :, :]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_s_function_matches_closed_form_polynomial() -> None:
|
||||||
|
ln_r = -12.0
|
||||||
|
order = 3.0
|
||||||
|
distances = numpy.array([0.0, 0.25, 0.5, 1.0])
|
||||||
|
s_function = scpml.prepare_s_function(ln_R=ln_r, m=order)
|
||||||
|
expected = (order + 1) * ln_r / 2 * distances**order
|
||||||
|
|
||||||
|
assert_close(s_function(distances), expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_uniform_grid_scpml_matches_expected_stretch_profile() -> None:
|
||||||
|
s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0)
|
||||||
|
dxes = scpml.uniform_grid_scpml((6, 4, 3), (2, 0, 1), omega=2.0, epsilon_effective=4.0, s_function=s_function)
|
||||||
|
correction = numpy.sqrt(4.0) * 2.0
|
||||||
|
|
||||||
|
for axis, size, thickness in ((0, 6, 2), (2, 3, 1)):
|
||||||
|
grid = numpy.arange(size, dtype=float)
|
||||||
|
expected_a = 1 + 1j * s_function(_normalized_distance(grid, size, thickness)) / correction
|
||||||
|
expected_b = 1 + 1j * s_function(_normalized_distance(grid + 0.5, size, thickness)) / correction
|
||||||
|
assert_close(dxes[0][axis], expected_a)
|
||||||
|
assert_close(dxes[1][axis], expected_b)
|
||||||
|
|
||||||
|
assert_close(dxes[0][1], 1.0)
|
||||||
|
assert_close(dxes[1][1], 1.0)
|
||||||
|
assert numpy.isfinite(dxes[0][0]).all()
|
||||||
|
assert numpy.isfinite(dxes[1][0]).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_uniform_grid_scpml_default_s_function_matches_explicit_default() -> None:
|
||||||
|
implicit = scpml.uniform_grid_scpml((6, 4, 3), (2, 0, 1), omega=2.0)
|
||||||
|
explicit = scpml.uniform_grid_scpml((6, 4, 3), (2, 0, 1), omega=2.0, s_function=scpml.prepare_s_function())
|
||||||
|
|
||||||
|
for implicit_group, explicit_group in zip(implicit, explicit, strict=True):
|
||||||
|
for implicit_axis, explicit_axis in zip(implicit_group, explicit_group, strict=True):
|
||||||
|
assert_close(implicit_axis, explicit_axis)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stretch_with_scpml_only_modifies_requested_front_edge() -> None:
|
||||||
|
s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0)
|
||||||
|
base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)]
|
||||||
|
stretched = scpml.stretch_with_scpml(base, axis=0, polarity=1, omega=2.0, epsilon_effective=4.0, thickness=2, s_function=s_function)
|
||||||
|
|
||||||
|
assert_close(stretched[0][0][2:], 1.0)
|
||||||
|
assert_close(stretched[1][0][2:], 1.0)
|
||||||
|
assert_close(stretched[0][0][-2:], 1.0)
|
||||||
|
assert_close(stretched[1][0][-2:], 1.0)
|
||||||
|
assert numpy.linalg.norm(stretched[0][0][:2] - 1.0) > 0
|
||||||
|
assert numpy.linalg.norm(stretched[1][0][:2] - 1.0) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_stretch_with_scpml_only_modifies_requested_back_edge() -> None:
|
||||||
|
s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0)
|
||||||
|
base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)]
|
||||||
|
stretched = scpml.stretch_with_scpml(base, axis=0, polarity=-1, omega=2.0, epsilon_effective=4.0, thickness=2, s_function=s_function)
|
||||||
|
|
||||||
|
assert_close(stretched[0][0][:4], 1.0)
|
||||||
|
assert_close(stretched[1][0][:4], 1.0)
|
||||||
|
assert numpy.linalg.norm(stretched[0][0][-2:] - 1.0) > 0
|
||||||
|
assert numpy.linalg.norm(stretched[1][0][-2:] - 1.0) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_stretch_with_scpml_thickness_zero_is_noop() -> None:
|
||||||
|
s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0)
|
||||||
|
base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)]
|
||||||
|
stretched = scpml.stretch_with_scpml(base, axis=0, polarity=-1, omega=2.0, epsilon_effective=4.0, thickness=0, s_function=s_function)
|
||||||
|
|
||||||
|
for grid_group in stretched:
|
||||||
|
for axis_grid in grid_group:
|
||||||
|
assert_close(axis_grid, 1.0)
|
||||||
100
meanas/test/test_fdfd_farfield.py
Normal file
100
meanas/test/test_fdfd_farfield.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import numpy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..fdfd import farfield
|
||||||
|
|
||||||
|
|
||||||
|
NEAR_SHAPE = (2, 3)
|
||||||
|
E_NEAR = [numpy.zeros(NEAR_SHAPE, dtype=complex), numpy.zeros(NEAR_SHAPE, dtype=complex)]
|
||||||
|
H_NEAR = [numpy.zeros(NEAR_SHAPE, dtype=complex), numpy.zeros(NEAR_SHAPE, dtype=complex)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_near_to_farfield_rejects_wrong_length_inputs() -> None:
|
||||||
|
with pytest.raises(Exception, match='E_near must be a length-2 list'):
|
||||||
|
farfield.near_to_farfield(E_NEAR[:1], H_NEAR, dx=0.2, dy=0.3)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='H_near must be a length-2 list'):
|
||||||
|
farfield.near_to_farfield(E_NEAR, H_NEAR[:1], dx=0.2, dy=0.3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_near_to_farfield_rejects_mismatched_shapes() -> None:
|
||||||
|
bad_h_near = [H_NEAR[0], numpy.zeros((2, 4), dtype=complex)]
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='All fields must be the same shape'):
|
||||||
|
farfield.near_to_farfield(E_NEAR, bad_h_near, dx=0.2, dy=0.3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_near_to_farfield_uses_default_and_scalar_padding_shapes() -> None:
|
||||||
|
default_result = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3)
|
||||||
|
scalar_result = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8)
|
||||||
|
|
||||||
|
assert default_result['E'][0].shape == (2, 4)
|
||||||
|
assert default_result['H'][0].shape == (2, 4)
|
||||||
|
assert scalar_result['E'][0].shape == (8, 8)
|
||||||
|
assert scalar_result['H'][0].shape == (8, 8)
|
||||||
|
|
||||||
|
|
||||||
|
def test_far_to_nearfield_rejects_wrong_length_inputs() -> None:
|
||||||
|
ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='E_far must be a length-2 list'):
|
||||||
|
farfield.far_to_nearfield(ff['E'][:1], ff['H'], ff['dkx'], ff['dky'])
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='H_far must be a length-2 list'):
|
||||||
|
farfield.far_to_nearfield(ff['E'], ff['H'][:1], ff['dkx'], ff['dky'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_far_to_nearfield_rejects_mismatched_shapes() -> None:
|
||||||
|
ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8)
|
||||||
|
bad_h_far = [ff['H'][0], numpy.zeros((8, 4), dtype=complex)]
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='All fields must be the same shape'):
|
||||||
|
farfield.far_to_nearfield(ff['E'], bad_h_far, ff['dkx'], ff['dky'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_far_to_nearfield_uses_default_and_scalar_padding_shapes() -> None:
|
||||||
|
ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8)
|
||||||
|
default_result = farfield.far_to_nearfield(
|
||||||
|
[field.copy() for field in ff['E']],
|
||||||
|
[field.copy() for field in ff['H']],
|
||||||
|
ff['dkx'],
|
||||||
|
ff['dky'],
|
||||||
|
)
|
||||||
|
scalar_result = farfield.far_to_nearfield(
|
||||||
|
[field.copy() for field in ff['E']],
|
||||||
|
[field.copy() for field in ff['H']],
|
||||||
|
ff['dkx'],
|
||||||
|
ff['dky'],
|
||||||
|
padded_size=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert default_result['E'][0].shape == (8, 8)
|
||||||
|
assert default_result['H'][0].shape == (8, 8)
|
||||||
|
assert scalar_result['E'][0].shape == (4, 4)
|
||||||
|
assert scalar_result['H'][0].shape == (4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_farfield_roundtrip_supports_rectangular_arrays() -> None:
|
||||||
|
e_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)]
|
||||||
|
h_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)]
|
||||||
|
e_near[0][1, 3] = 1.0 + 0.25j
|
||||||
|
h_near[1][2, 5] = -0.5j
|
||||||
|
|
||||||
|
ff = farfield.near_to_farfield(e_near, h_near, dx=0.2, dy=0.3, padded_size=(4, 8))
|
||||||
|
restored = farfield.far_to_nearfield(
|
||||||
|
[field.copy() for field in ff['E']],
|
||||||
|
[field.copy() for field in ff['H']],
|
||||||
|
ff['dkx'],
|
||||||
|
ff['dky'],
|
||||||
|
padded_size=(4, 8),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(ff['dkx'], float)
|
||||||
|
assert isinstance(ff['dky'], float)
|
||||||
|
assert ff['E'][0].shape == (4, 8)
|
||||||
|
assert restored['E'][0].shape == (4, 8)
|
||||||
|
assert restored['H'][0].shape == (4, 8)
|
||||||
|
assert restored['dx'] == pytest.approx(0.2)
|
||||||
|
assert restored['dy'] == pytest.approx(0.3)
|
||||||
|
assert numpy.isfinite(restored['E'][0]).all()
|
||||||
|
assert numpy.isfinite(restored['H'][0]).all()
|
||||||
122
meanas/test/test_fdfd_functional.py
Normal file
122
meanas/test/test_fdfd_functional.py
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from ..fdmath import unvec, vec
|
||||||
|
from ..fdfd import functional, operators
|
||||||
|
from ._fdfd_case import DXES, EPSILON, E_FIELD, H_FIELD, MU, OMEGA, SHAPE, TF_REGION, apply_fdfd_matrix
|
||||||
|
from .utils import assert_fields_close
|
||||||
|
|
||||||
|
|
||||||
|
ATOL = 1e-9
|
||||||
|
RTOL = 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def assert_fields_match(actual: numpy.ndarray, expected: numpy.ndarray) -> None:
|
||||||
|
assert_fields_close(actual, expected, atol=ATOL, rtol=RTOL)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e_full_matches_sparse_operator_without_mu() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.e_full(OMEGA, DXES, vec(EPSILON)),
|
||||||
|
E_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.e_full(OMEGA, DXES, EPSILON)(E_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e_full_matches_sparse_operator_with_mu() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.e_full(OMEGA, DXES, vec(EPSILON), vec(MU)),
|
||||||
|
E_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.e_full(OMEGA, DXES, EPSILON, MU)(E_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_eh_full_matches_sparse_operator_with_mu() -> None:
|
||||||
|
matrix_result = operators.eh_full(OMEGA, DXES, vec(EPSILON), vec(MU)) @ numpy.concatenate([vec(E_FIELD), vec(H_FIELD)])
|
||||||
|
matrix_e, matrix_h = (unvec(part, SHAPE) for part in numpy.split(matrix_result, 2))
|
||||||
|
functional_e, functional_h = functional.eh_full(OMEGA, DXES, EPSILON, MU)(E_FIELD, H_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_e, matrix_e)
|
||||||
|
assert_fields_match(functional_h, matrix_h)
|
||||||
|
|
||||||
|
|
||||||
|
def test_eh_full_matches_sparse_operator_without_mu() -> None:
|
||||||
|
matrix_result = operators.eh_full(OMEGA, DXES, vec(EPSILON)) @ numpy.concatenate([vec(E_FIELD), vec(H_FIELD)])
|
||||||
|
matrix_e, matrix_h = (unvec(part, SHAPE) for part in numpy.split(matrix_result, 2))
|
||||||
|
functional_e, functional_h = functional.eh_full(OMEGA, DXES, EPSILON)(E_FIELD, H_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_e, matrix_e)
|
||||||
|
assert_fields_match(functional_h, matrix_h)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e2h_matches_sparse_operator_with_mu() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.e2h(OMEGA, DXES, vec(MU)),
|
||||||
|
E_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.e2h(OMEGA, DXES, MU)(E_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e2h_matches_sparse_operator_without_mu() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.e2h(OMEGA, DXES),
|
||||||
|
E_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.e2h(OMEGA, DXES)(E_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_m2j_matches_sparse_operator_without_mu() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.m2j(OMEGA, DXES),
|
||||||
|
H_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.m2j(OMEGA, DXES)(H_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_m2j_matches_sparse_operator_with_mu() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.m2j(OMEGA, DXES, vec(MU)),
|
||||||
|
H_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.m2j(OMEGA, DXES, MU)(H_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e_tfsf_source_matches_sparse_operator_without_mu() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.e_tfsf_source(vec(TF_REGION), OMEGA, DXES, vec(EPSILON)),
|
||||||
|
E_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.e_tfsf_source(TF_REGION, OMEGA, DXES, EPSILON)(E_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e_tfsf_source_matches_sparse_operator_with_mu() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.e_tfsf_source(vec(TF_REGION), OMEGA, DXES, vec(EPSILON), vec(MU)),
|
||||||
|
E_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.e_tfsf_source(TF_REGION, OMEGA, DXES, EPSILON, MU)(E_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_poynting_e_cross_h_matches_sparse_operator() -> None:
|
||||||
|
matrix_result = apply_fdfd_matrix(
|
||||||
|
operators.poynting_e_cross(vec(E_FIELD), DXES),
|
||||||
|
H_FIELD,
|
||||||
|
)
|
||||||
|
functional_result = functional.poynting_e_cross_h(DXES)(E_FIELD, H_FIELD)
|
||||||
|
|
||||||
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
from typing import Iterable
|
# ruff: noqa: ARG001
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
from numpy.testing import assert_allclose
|
from numpy.testing import assert_allclose
|
||||||
|
|
||||||
from .. import fdfd
|
from .. import fdfd
|
||||||
from ..fdmath import vec, unvec, dx_lists_mut
|
from ..fdmath import vec, unvec, dx_lists_mut, vfdfield, cfdfield_t
|
||||||
#from .utils import assert_close, assert_fields_close
|
#from .utils import assert_close, assert_fields_close
|
||||||
from .test_fdfd import FDResult
|
from .test_fdfd import FDResult
|
||||||
from .conftest import FixtureRequest
|
from .conftest import FixtureRequest
|
||||||
|
|
@ -44,49 +44,51 @@ def test_pml(sim: FDResult, src_polarity: int) -> None:
|
||||||
# Also see conftest.py
|
# Also see conftest.py
|
||||||
|
|
||||||
@pytest.fixture(params=[1 / 1500])
|
@pytest.fixture(params=[1 / 1500])
|
||||||
def omega(request: FixtureRequest) -> Iterable[float]:
|
def omega(request: FixtureRequest) -> float:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[None])
|
@pytest.fixture(params=[None])
|
||||||
def pec(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
|
def pec(request: FixtureRequest) -> NDArray[numpy.float64] | None:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[None])
|
@pytest.fixture(params=[None])
|
||||||
def pmc(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
|
def pmc(request: FixtureRequest) -> NDArray[numpy.float64] | None:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[(30, 1, 1),
|
@pytest.fixture(params=[(30, 1, 1),
|
||||||
(1, 30, 1),
|
(1, 30, 1),
|
||||||
(1, 1, 30)])
|
(1, 1, 30)])
|
||||||
def shape(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
|
def shape(request: FixtureRequest) -> tuple[int, int, int]:
|
||||||
yield (3, *request.param)
|
return (3, *request.param)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[+1, -1])
|
@pytest.fixture(params=[+1, -1])
|
||||||
def src_polarity(request: FixtureRequest) -> Iterable[int]:
|
def src_polarity(request: FixtureRequest) -> int:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture
|
||||||
def j_distribution(
|
def j_distribution(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
epsilon: NDArray[numpy.float64],
|
epsilon: vfdfield,
|
||||||
dxes: dx_lists_mut,
|
dxes: dx_lists_mut,
|
||||||
omega: float,
|
omega: float,
|
||||||
src_polarity: int,
|
src_polarity: int,
|
||||||
) -> Iterable[NDArray[numpy.complex128]]:
|
) -> cfdfield_t:
|
||||||
j = numpy.zeros(shape, dtype=complex)
|
j = numpy.zeros(shape, dtype=complex)
|
||||||
|
|
||||||
dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis
|
dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis
|
||||||
other_dims = [0, 1, 2]
|
other_dims = [0, 1, 2]
|
||||||
other_dims.remove(dim)
|
other_dims.remove(dim)
|
||||||
|
|
||||||
dx_prop = (dxes[0][dim][shape[dim + 1] // 2]
|
dx_prop = (
|
||||||
+ dxes[1][dim][shape[dim + 1] // 2]) / 2 # noqa: E128 # TODO is this right for nonuniform dxes?
|
dxes[0][dim][shape[dim + 1] // 2]
|
||||||
|
+ dxes[1][dim][shape[dim + 1] // 2]
|
||||||
|
) / 2 # TODO is this right for nonuniform dxes?
|
||||||
|
|
||||||
# Mask only contains components orthogonal to propagation direction
|
# Mask only contains components orthogonal to propagation direction
|
||||||
center_mask = numpy.zeros(shape, dtype=bool)
|
center_mask = numpy.zeros(shape, dtype=bool)
|
||||||
|
|
@ -106,18 +108,18 @@ def j_distribution(
|
||||||
|
|
||||||
j = fdfd.waveguide_3d.compute_source(E=e, wavenumber=wavenumber_corrected, omega=omega, dxes=dxes,
|
j = fdfd.waveguide_3d.compute_source(E=e, wavenumber=wavenumber_corrected, omega=omega, dxes=dxes,
|
||||||
axis=dim, polarity=src_polarity, slices=slices, epsilon=epsilon)
|
axis=dim, polarity=src_polarity, slices=slices, epsilon=epsilon)
|
||||||
yield j
|
return j
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture
|
||||||
def epsilon(
|
def epsilon(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
epsilon_bg: float,
|
epsilon_bg: float,
|
||||||
epsilon_fg: float,
|
epsilon_fg: float,
|
||||||
) -> Iterable[NDArray[numpy.float64]]:
|
) -> NDArray[numpy.float64]:
|
||||||
epsilon = numpy.full(shape, epsilon_fg, dtype=float)
|
epsilon = numpy.full(shape, epsilon_fg, dtype=float)
|
||||||
yield epsilon
|
return epsilon
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=['uniform'])
|
@pytest.fixture(params=['uniform'])
|
||||||
|
|
@ -127,7 +129,7 @@ def dxes(
|
||||||
dx: float,
|
dx: float,
|
||||||
omega: float,
|
omega: float,
|
||||||
epsilon_fg: float,
|
epsilon_fg: float,
|
||||||
) -> Iterable[list[list[NDArray[numpy.float64]]]]:
|
) -> list[list[NDArray[numpy.float64]]]:
|
||||||
if request.param == 'uniform':
|
if request.param == 'uniform':
|
||||||
dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
|
dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
|
||||||
dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis
|
dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis
|
||||||
|
|
@ -141,10 +143,10 @@ def dxes(
|
||||||
epsilon_effective=epsilon_fg,
|
epsilon_effective=epsilon_fg,
|
||||||
thickness=10,
|
thickness=10,
|
||||||
)
|
)
|
||||||
yield dxes
|
return dxes
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture
|
||||||
def sim(
|
def sim(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
|
|
@ -162,7 +164,7 @@ def sim(
|
||||||
omega=omega,
|
omega=omega,
|
||||||
dxes=dxes,
|
dxes=dxes,
|
||||||
epsilon=eps_vec,
|
epsilon=eps_vec,
|
||||||
matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11},
|
matrix_solver_opts={'atol': 1e-15, 'rtol': 1e-11},
|
||||||
)
|
)
|
||||||
e = unvec(e_vec, shape[1:])
|
e = unvec(e_vec, shape[1:])
|
||||||
|
|
||||||
|
|
|
||||||
128
meanas/test/test_fdfd_solvers.py
Normal file
128
meanas/test/test_fdfd_solvers.py
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from ..fdfd import solvers
|
||||||
|
from ._solver_cases import solver_plumbing_case
|
||||||
|
from .utils import assert_close
|
||||||
|
|
||||||
|
|
||||||
|
def test_scipy_qmr_wraps_user_callback_without_recursion(monkeypatch) -> None:
|
||||||
|
seen: list[tuple[float, ...]] = []
|
||||||
|
|
||||||
|
def fake_qmr(a, b: numpy.ndarray, **kwargs):
|
||||||
|
kwargs['callback'](numpy.array([1.0, 2.0]))
|
||||||
|
return numpy.array([3.0, 4.0]), 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(solvers.scipy.sparse.linalg, 'qmr', fake_qmr)
|
||||||
|
result = solvers._scipy_qmr(
|
||||||
|
solver_plumbing_case().a0,
|
||||||
|
numpy.array([1.0, 0.0]),
|
||||||
|
callback=lambda xk: seen.append(tuple(xk)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_close(result, [3.0, 4.0])
|
||||||
|
assert seen == [(1.0, 2.0)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_scipy_qmr_installs_logging_callback_when_missing(monkeypatch) -> None:
|
||||||
|
callback_seen: list[numpy.ndarray] = []
|
||||||
|
|
||||||
|
def fake_qmr(a, b: numpy.ndarray, **kwargs):
|
||||||
|
callback = kwargs['callback']
|
||||||
|
callback(numpy.array([5.0, 6.0]))
|
||||||
|
callback_seen.append(b.copy())
|
||||||
|
return numpy.array([7.0, 8.0]), 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(solvers.scipy.sparse.linalg, 'qmr', fake_qmr)
|
||||||
|
result = solvers._scipy_qmr(solver_plumbing_case().a0, numpy.array([1.0, 0.0]))
|
||||||
|
|
||||||
|
assert_close(result, [7.0, 8.0])
|
||||||
|
assert len(callback_seen) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_generic_forward_preconditions_system_and_guess(monkeypatch) -> None:
|
||||||
|
case = solver_plumbing_case()
|
||||||
|
captured: dict[str, numpy.ndarray | float | object] = {}
|
||||||
|
|
||||||
|
monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: case.a0)
|
||||||
|
monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr))
|
||||||
|
|
||||||
|
def fake_solver(a, b: numpy.ndarray, **kwargs):
|
||||||
|
captured['a'] = a
|
||||||
|
captured['b'] = b
|
||||||
|
captured['x0'] = kwargs['x0']
|
||||||
|
captured['atol'] = kwargs['atol']
|
||||||
|
return case.solver_result
|
||||||
|
|
||||||
|
result = solvers.generic(
|
||||||
|
omega=case.omega,
|
||||||
|
dxes=case.dxes,
|
||||||
|
J=case.j,
|
||||||
|
epsilon=case.epsilon,
|
||||||
|
matrix_solver=fake_solver,
|
||||||
|
matrix_solver_opts={'atol': 1e-12},
|
||||||
|
E_guess=case.guess,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_close(cast(object, captured['a']).toarray(), (case.pl @ case.a0 @ case.pr).toarray()) # type: ignore[attr-defined]
|
||||||
|
assert_close(cast(numpy.ndarray, captured['b']), case.pl @ (-1j * case.omega * case.j))
|
||||||
|
assert_close(cast(numpy.ndarray, captured['x0']), case.pl @ case.guess)
|
||||||
|
assert captured['atol'] == 1e-12
|
||||||
|
assert_close(result, case.pr @ case.solver_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generic_adjoint_preconditions_system_and_guess(monkeypatch) -> None:
|
||||||
|
case = solver_plumbing_case()
|
||||||
|
captured: dict[str, numpy.ndarray | float | object] = {}
|
||||||
|
|
||||||
|
monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: case.a0)
|
||||||
|
monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr))
|
||||||
|
|
||||||
|
def fake_solver(a, b: numpy.ndarray, **kwargs):
|
||||||
|
captured['a'] = a
|
||||||
|
captured['b'] = b
|
||||||
|
captured['x0'] = kwargs['x0']
|
||||||
|
captured['rtol'] = kwargs['rtol']
|
||||||
|
return case.solver_result
|
||||||
|
|
||||||
|
result = solvers.generic(
|
||||||
|
omega=case.omega,
|
||||||
|
dxes=case.dxes,
|
||||||
|
J=case.j,
|
||||||
|
epsilon=case.epsilon,
|
||||||
|
matrix_solver=fake_solver,
|
||||||
|
matrix_solver_opts={'rtol': 1e-9},
|
||||||
|
E_guess=case.guess,
|
||||||
|
adjoint=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_matrix = (case.pl @ case.a0 @ case.pr).T.conjugate()
|
||||||
|
assert_close(cast(object, captured['a']).toarray(), expected_matrix.toarray()) # type: ignore[attr-defined]
|
||||||
|
assert_close(cast(numpy.ndarray, captured['b']), case.pr.T.conjugate() @ (-1j * case.omega * case.j))
|
||||||
|
assert_close(cast(numpy.ndarray, captured['x0']), case.pr.T.conjugate() @ case.guess)
|
||||||
|
assert captured['rtol'] == 1e-9
|
||||||
|
assert_close(result, case.pl.T.conjugate() @ case.solver_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generic_without_guess_does_not_inject_x0(monkeypatch) -> None:
|
||||||
|
case = solver_plumbing_case()
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: case.a0)
|
||||||
|
monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr))
|
||||||
|
|
||||||
|
def fake_solver(a, b: numpy.ndarray, **kwargs):
|
||||||
|
captured['kwargs'] = kwargs
|
||||||
|
return numpy.array([1.0, -1.0])
|
||||||
|
|
||||||
|
result = solvers.generic(
|
||||||
|
omega=1.0,
|
||||||
|
dxes=case.dxes,
|
||||||
|
J=numpy.array([2.0, 3.0]),
|
||||||
|
epsilon=case.epsilon,
|
||||||
|
matrix_solver=fake_solver,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'x0' not in cast(dict[str, object], captured['kwargs'])
|
||||||
|
assert_close(result, case.pr @ numpy.array([1.0, -1.0]))
|
||||||
60
meanas/test/test_fdmath_functional.py
Normal file
60
meanas/test/test_fdmath_functional.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from ..fdmath import functional as fd_functional
|
||||||
|
from ..fdmath import operators as fd_operators
|
||||||
|
from ..fdmath import vec, unvec
|
||||||
|
from .utils import assert_close, assert_fields_close
|
||||||
|
|
||||||
|
|
||||||
|
SHAPE = (2, 3, 2)
|
||||||
|
DX_E = [numpy.array([1.0, 1.5]), numpy.array([0.75, 1.25, 1.5]), numpy.array([1.2, 0.8])]
|
||||||
|
DX_H = [numpy.array([0.9, 1.4]), numpy.array([0.8, 1.1, 1.4]), numpy.array([1.0, 0.7])]
|
||||||
|
|
||||||
|
SCALAR_FIELD = (
|
||||||
|
numpy.arange(numpy.prod(SHAPE)).reshape(SHAPE)
|
||||||
|
+ 0.1j * numpy.arange(numpy.prod(SHAPE)).reshape(SHAPE)
|
||||||
|
).astype(complex)
|
||||||
|
VECTOR_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.25j).astype(complex)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deriv_forward_without_dx_matches_numpy_roll() -> None:
|
||||||
|
for axis, deriv in enumerate(fd_functional.deriv_forward()):
|
||||||
|
expected = numpy.roll(SCALAR_FIELD, -1, axis=axis) - SCALAR_FIELD
|
||||||
|
assert_close(deriv(SCALAR_FIELD), expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deriv_back_without_dx_matches_numpy_roll() -> None:
|
||||||
|
for axis, deriv in enumerate(fd_functional.deriv_back()):
|
||||||
|
expected = SCALAR_FIELD - numpy.roll(SCALAR_FIELD, 1, axis=axis)
|
||||||
|
assert_close(deriv(SCALAR_FIELD), expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_curl_parts_sum_to_full_curl() -> None:
|
||||||
|
curl_forward = fd_functional.curl_forward(DX_E)(VECTOR_FIELD)
|
||||||
|
curl_back = fd_functional.curl_back(DX_H)(VECTOR_FIELD)
|
||||||
|
forward_parts = fd_functional.curl_forward_parts(DX_E)(VECTOR_FIELD)
|
||||||
|
back_parts = fd_functional.curl_back_parts(DX_H)(VECTOR_FIELD)
|
||||||
|
|
||||||
|
for axis in range(3):
|
||||||
|
assert_close(forward_parts[axis][0] + forward_parts[axis][1], curl_forward[axis])
|
||||||
|
assert_close(back_parts[axis][0] + back_parts[axis][1], curl_back[axis])
|
||||||
|
|
||||||
|
|
||||||
|
def test_derivatives_match_sparse_operators_on_nonuniform_grid() -> None:
|
||||||
|
for axis, deriv in enumerate(fd_functional.deriv_forward(DX_E)):
|
||||||
|
matrix_result = (fd_operators.deriv_forward(DX_E)[axis] @ SCALAR_FIELD.ravel(order='C')).reshape(SHAPE, order='C')
|
||||||
|
assert_close(deriv(SCALAR_FIELD), matrix_result, atol=1e-12, rtol=1e-12)
|
||||||
|
|
||||||
|
for axis, deriv in enumerate(fd_functional.deriv_back(DX_H)):
|
||||||
|
matrix_result = (fd_operators.deriv_back(DX_H)[axis] @ SCALAR_FIELD.ravel(order='C')).reshape(SHAPE, order='C')
|
||||||
|
assert_close(deriv(SCALAR_FIELD), matrix_result, atol=1e-12, rtol=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_curls_match_sparse_operators_on_nonuniform_grid() -> None:
|
||||||
|
curl_forward = fd_functional.curl_forward(DX_E)(VECTOR_FIELD)
|
||||||
|
curl_back = fd_functional.curl_back(DX_H)(VECTOR_FIELD)
|
||||||
|
matrix_forward = unvec(fd_operators.curl_forward(DX_E) @ vec(VECTOR_FIELD), SHAPE)
|
||||||
|
matrix_back = unvec(fd_operators.curl_back(DX_H) @ vec(VECTOR_FIELD), SHAPE)
|
||||||
|
|
||||||
|
assert_fields_close(curl_forward, matrix_forward, atol=1e-12, rtol=1e-12)
|
||||||
|
assert_fields_close(curl_back, matrix_back, atol=1e-12, rtol=1e-12)
|
||||||
90
meanas/test/test_fdmath_operators.py
Normal file
90
meanas/test/test_fdmath_operators.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import numpy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..fdmath import operators, unvec, vec
|
||||||
|
from ._test_builders import real_ramp
|
||||||
|
from .utils import assert_close
|
||||||
|
|
||||||
|
|
||||||
|
SHAPE = (2, 3, 2)
|
||||||
|
SCALAR_FIELD = real_ramp(SHAPE)
|
||||||
|
VECTOR_LEFT = real_ramp((3, *SHAPE), offset=0.5)
|
||||||
|
VECTOR_RIGHT = real_ramp((3, *SHAPE), scale=1 / 3, offset=2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_scalar_matrix(op: operators.sparse.spmatrix) -> numpy.ndarray:
|
||||||
|
return (op @ SCALAR_FIELD.ravel(order='C')).reshape(SHAPE, order='C')
|
||||||
|
|
||||||
|
|
||||||
|
def _mirrored_indices(size: int, shift_distance: int) -> numpy.ndarray:
|
||||||
|
indices = numpy.arange(size) + shift_distance
|
||||||
|
indices = numpy.where(indices >= size, 2 * size - indices - 1, indices)
|
||||||
|
indices = numpy.where(indices < 0, -1 - indices, indices)
|
||||||
|
return indices
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('axis', 'shift_distance'), [(0, 1), (1, -1), (2, 1)])
|
||||||
|
def test_shift_circ_matches_numpy_roll(axis: int, shift_distance: int) -> None:
|
||||||
|
matrix_result = _apply_scalar_matrix(operators.shift_circ(axis, SHAPE, shift_distance))
|
||||||
|
expected = numpy.roll(SCALAR_FIELD, -shift_distance, axis=axis)
|
||||||
|
assert_close(matrix_result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('axis', 'shift_distance'), [(0, 1), (1, -1), (2, 1)])
|
||||||
|
def test_shift_with_mirror_matches_explicit_mirrored_indices(axis: int, shift_distance: int) -> None:
|
||||||
|
matrix_result = _apply_scalar_matrix(operators.shift_with_mirror(axis, SHAPE, shift_distance))
|
||||||
|
indices = [numpy.arange(length) for length in SHAPE]
|
||||||
|
indices[axis] = _mirrored_indices(SHAPE[axis], shift_distance)
|
||||||
|
expected = SCALAR_FIELD[numpy.ix_(*indices)]
|
||||||
|
assert_close(matrix_result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('args', 'message'),
|
||||||
|
[
|
||||||
|
((0, (2,), 1), 'Invalid shape'),
|
||||||
|
((3, SHAPE, 1), 'Invalid direction'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_shift_circ_rejects_invalid_arguments(args: tuple[int, tuple[int, ...], int], message: str) -> None:
|
||||||
|
with pytest.raises(Exception, match=message):
|
||||||
|
operators.shift_circ(*args)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('args', 'message'),
|
||||||
|
[
|
||||||
|
((0, (2,), 1), 'Invalid shape'),
|
||||||
|
((3, SHAPE, 1), 'Invalid direction'),
|
||||||
|
((0, SHAPE, SHAPE[0]), 'too large'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_shift_with_mirror_rejects_invalid_arguments(args: tuple[int, tuple[int, ...], int], message: str) -> None:
|
||||||
|
with pytest.raises(Exception, match=message):
|
||||||
|
operators.shift_with_mirror(*args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vec_cross_matches_pointwise_cross_product() -> None:
|
||||||
|
matrix_result = unvec(operators.vec_cross(vec(VECTOR_LEFT)) @ vec(VECTOR_RIGHT), SHAPE)
|
||||||
|
expected = numpy.empty_like(VECTOR_LEFT)
|
||||||
|
expected[0] = VECTOR_LEFT[1] * VECTOR_RIGHT[2] - VECTOR_LEFT[2] * VECTOR_RIGHT[1]
|
||||||
|
expected[1] = VECTOR_LEFT[2] * VECTOR_RIGHT[0] - VECTOR_LEFT[0] * VECTOR_RIGHT[2]
|
||||||
|
expected[2] = VECTOR_LEFT[0] * VECTOR_RIGHT[1] - VECTOR_LEFT[1] * VECTOR_RIGHT[0]
|
||||||
|
assert_close(matrix_result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_avg_forward_matches_half_sum_with_forward_neighbor() -> None:
|
||||||
|
matrix_result = _apply_scalar_matrix(operators.avg_forward(1, SHAPE))
|
||||||
|
expected = 0.5 * (SCALAR_FIELD + numpy.roll(SCALAR_FIELD, -1, axis=1))
|
||||||
|
assert_close(matrix_result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_avg_back_matches_half_sum_with_backward_neighbor() -> None:
|
||||||
|
matrix_result = _apply_scalar_matrix(operators.avg_back(1, SHAPE))
|
||||||
|
expected = 0.5 * (SCALAR_FIELD + numpy.roll(SCALAR_FIELD, 1, axis=1))
|
||||||
|
assert_close(matrix_result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_avg_forward_rejects_invalid_shape() -> None:
|
||||||
|
with pytest.raises(Exception, match='Invalid shape'):
|
||||||
|
operators.avg_forward(0, (2,))
|
||||||
46
meanas/test/test_fdmath_vectorization.py
Normal file
46
meanas/test/test_fdmath_vectorization.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from ..fdmath import unvec, vec
|
||||||
|
from ._test_builders import complex_ramp, real_ramp
|
||||||
|
from .utils import assert_close
|
||||||
|
|
||||||
|
|
||||||
|
SHAPE = (2, 3, 2)
|
||||||
|
FIELD = real_ramp((3, *SHAPE))
|
||||||
|
COMPLEX_FIELD = complex_ramp((3, *SHAPE), imag_scale=0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vec_and_unvec_return_none_for_none_input() -> None:
|
||||||
|
assert vec(None) is None
|
||||||
|
assert unvec(None, SHAPE) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_field_round_trip_preserves_shape_and_values() -> None:
|
||||||
|
vector = vec(FIELD)
|
||||||
|
assert vector is not None
|
||||||
|
restored = unvec(vector, SHAPE)
|
||||||
|
assert restored is not None
|
||||||
|
assert restored.shape == (3, *SHAPE)
|
||||||
|
assert_close(restored, FIELD)
|
||||||
|
|
||||||
|
|
||||||
|
def test_complex_field_round_trip_preserves_shape_and_values() -> None:
|
||||||
|
vector = vec(COMPLEX_FIELD)
|
||||||
|
assert vector is not None
|
||||||
|
restored = unvec(vector, SHAPE)
|
||||||
|
assert restored is not None
|
||||||
|
assert restored.shape == (3, *SHAPE)
|
||||||
|
assert_close(restored, COMPLEX_FIELD)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unvec_with_two_components_round_trips_vector() -> None:
|
||||||
|
vector = numpy.arange(2 * numpy.prod(SHAPE), dtype=float)
|
||||||
|
field = unvec(vector, SHAPE, nvdim=2)
|
||||||
|
assert field is not None
|
||||||
|
assert field.shape == (2, *SHAPE)
|
||||||
|
assert_close(vec(field), vector)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vec_accepts_arraylike_input() -> None:
|
||||||
|
arraylike = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
|
||||||
|
assert_close(vec(arraylike), numpy.ravel(arraylike, order='C'))
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from typing import Iterable, Any
|
# ruff: noqa: ARG001
|
||||||
|
from typing import Any
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -6,7 +7,7 @@ from numpy.typing import NDArray
|
||||||
#from numpy.testing import assert_allclose, assert_array_equal
|
#from numpy.testing import assert_allclose, assert_array_equal
|
||||||
|
|
||||||
from .. import fdtd
|
from .. import fdtd
|
||||||
from .utils import assert_close, assert_fields_close, PRNG
|
from .utils import assert_close, assert_fields_close, make_prng
|
||||||
from .conftest import FixtureRequest
|
from .conftest import FixtureRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -150,8 +151,8 @@ def test_poynting_planes(sim: 'TDResult') -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[0.3])
|
@pytest.fixture(params=[0.3])
|
||||||
def dt(request: FixtureRequest) -> Iterable[float]:
|
def dt(request: FixtureRequest) -> float:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass()
|
@dataclasses.dataclass()
|
||||||
|
|
@ -168,8 +169,8 @@ class TDResult:
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[(0, 4, 8)]) # (0,)
|
@pytest.fixture(params=[(0, 4, 8)]) # (0,)
|
||||||
def j_steps(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
|
def j_steps(request: FixtureRequest) -> tuple[int, ...]:
|
||||||
yield request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=['center', 'random'])
|
@pytest.fixture(params=['center', 'random'])
|
||||||
|
|
@ -177,18 +178,19 @@ def j_distribution(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
j_mag: float,
|
j_mag: float,
|
||||||
) -> Iterable[NDArray[numpy.float64]]:
|
) -> NDArray[numpy.float64]:
|
||||||
|
prng = make_prng()
|
||||||
j = numpy.zeros(shape)
|
j = numpy.zeros(shape)
|
||||||
if request.param == 'center':
|
if request.param == 'center':
|
||||||
j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag
|
j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag
|
||||||
elif request.param == '000':
|
elif request.param == '000':
|
||||||
j[:, 0, 0, 0] = j_mag
|
j[:, 0, 0, 0] = j_mag
|
||||||
elif request.param == 'random':
|
elif request.param == 'random':
|
||||||
j[:] = PRNG.uniform(low=-j_mag, high=j_mag, size=shape)
|
j[:] = prng.uniform(low=-j_mag, high=j_mag, size=shape)
|
||||||
yield j
|
return j
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture
|
||||||
def sim(
|
def sim(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
shape: tuple[int, ...],
|
shape: tuple[int, ...],
|
||||||
|
|
@ -199,8 +201,7 @@ def sim(
|
||||||
j_steps: tuple[int, ...],
|
j_steps: tuple[int, ...],
|
||||||
) -> TDResult:
|
) -> TDResult:
|
||||||
is3d = (numpy.array(shape) == 1).sum() == 0
|
is3d = (numpy.array(shape) == 1).sum() == 0
|
||||||
if is3d:
|
if is3d and dt != 0.3:
|
||||||
if dt != 0.3:
|
|
||||||
pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)')
|
pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)')
|
||||||
|
|
||||||
sim = TDResult(
|
sim = TDResult(
|
||||||
|
|
|
||||||
43
meanas/test/test_fdtd_base.py
Normal file
43
meanas/test/test_fdtd_base.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from ..fdmath import functional as fd_functional
|
||||||
|
from ..fdtd import base
|
||||||
|
from ._test_builders import real_ramp
|
||||||
|
from .utils import assert_close
|
||||||
|
|
||||||
|
|
||||||
|
DT = 0.25
|
||||||
|
SHAPE = (3, 2, 2, 2)
|
||||||
|
E_FIELD = real_ramp(SHAPE, scale=1 / 5)
|
||||||
|
H_FIELD = real_ramp(SHAPE, scale=1 / 7, offset=1 / 7)
|
||||||
|
EPSILON = 1.5 + E_FIELD / 10.0
|
||||||
|
MU_FIELD = 2.0 + H_FIELD / 8.0
|
||||||
|
MU_SCALAR = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_maxwell_e_without_dxes_matches_unit_spacing_update() -> None:
|
||||||
|
updater = base.maxwell_e(dt=DT)
|
||||||
|
expected = E_FIELD + DT * fd_functional.curl_back()(H_FIELD) / EPSILON
|
||||||
|
|
||||||
|
updated = updater(E_FIELD.copy(), H_FIELD.copy(), EPSILON)
|
||||||
|
|
||||||
|
assert_close(updated, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_maxwell_h_without_dxes_and_without_mu_matches_unit_spacing_update() -> None:
|
||||||
|
updater = base.maxwell_h(dt=DT)
|
||||||
|
expected = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD)
|
||||||
|
|
||||||
|
updated = updater(E_FIELD.copy(), H_FIELD.copy())
|
||||||
|
|
||||||
|
assert_close(updated, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_maxwell_h_without_dxes_accepts_scalar_and_field_mu() -> None:
|
||||||
|
updater = base.maxwell_h(dt=DT)
|
||||||
|
|
||||||
|
updated_scalar = updater(E_FIELD.copy(), H_FIELD.copy(), MU_SCALAR)
|
||||||
|
expected_scalar = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) / MU_SCALAR
|
||||||
|
assert_close(updated_scalar, expected_scalar)
|
||||||
|
|
||||||
|
updated_field = updater(E_FIELD.copy(), H_FIELD.copy(), MU_FIELD)
|
||||||
|
expected_field = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) / MU_FIELD
|
||||||
|
assert_close(updated_field, expected_field)
|
||||||
62
meanas/test/test_fdtd_boundaries.py
Normal file
62
meanas/test/test_fdtd_boundaries.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import numpy
|
||||||
|
import pytest
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
|
||||||
|
from ..fdtd.boundaries import conducting_boundary
|
||||||
|
|
||||||
|
|
||||||
|
def _axis_index(axis: int, index: int) -> tuple[slice | int, ...]:
|
||||||
|
coords: list[slice | int] = [slice(None), slice(None), slice(None)]
|
||||||
|
coords[axis] = index
|
||||||
|
return tuple(coords)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('direction', [0, 1, 2])
|
||||||
|
@pytest.mark.parametrize('polarity', [-1, 1])
|
||||||
|
def test_conducting_boundary_updates_expected_faces(direction: int, polarity: int) -> None:
|
||||||
|
e = numpy.arange(3 * 4 * 4 * 4, dtype=float).reshape(3, 4, 4, 4)
|
||||||
|
h = e.copy()
|
||||||
|
e0 = e.copy()
|
||||||
|
h0 = h.copy()
|
||||||
|
|
||||||
|
update_e, update_h = conducting_boundary(direction, polarity)
|
||||||
|
update_e(e)
|
||||||
|
update_h(h)
|
||||||
|
|
||||||
|
dirs = [0, 1, 2]
|
||||||
|
dirs.remove(direction)
|
||||||
|
u, v = dirs
|
||||||
|
|
||||||
|
if polarity < 0:
|
||||||
|
boundary = _axis_index(direction, 0)
|
||||||
|
shifted1 = _axis_index(direction, 1)
|
||||||
|
|
||||||
|
assert_allclose(e[direction][boundary], 0)
|
||||||
|
assert_allclose(e[u][boundary], e0[u][shifted1])
|
||||||
|
assert_allclose(e[v][boundary], e0[v][shifted1])
|
||||||
|
assert_allclose(h[direction][boundary], h0[direction][shifted1])
|
||||||
|
assert_allclose(h[u][boundary], 0)
|
||||||
|
assert_allclose(h[v][boundary], 0)
|
||||||
|
else:
|
||||||
|
boundary = _axis_index(direction, -1)
|
||||||
|
shifted1 = _axis_index(direction, -2)
|
||||||
|
shifted2 = _axis_index(direction, -3)
|
||||||
|
|
||||||
|
assert_allclose(e[direction][boundary], -e0[direction][shifted2])
|
||||||
|
assert_allclose(e[direction][shifted1], 0)
|
||||||
|
assert_allclose(e[u][boundary], e0[u][shifted1])
|
||||||
|
assert_allclose(e[v][boundary], e0[v][shifted1])
|
||||||
|
assert_allclose(h[direction][boundary], h0[direction][shifted1])
|
||||||
|
assert_allclose(h[u][boundary], -h0[u][shifted2])
|
||||||
|
assert_allclose(h[u][shifted1], 0)
|
||||||
|
assert_allclose(h[v][boundary], -h0[v][shifted2])
|
||||||
|
assert_allclose(h[v][shifted1], 0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('direction', 'polarity'),
|
||||||
|
[(-1, 1), (3, 1), (0, 0)],
|
||||||
|
)
|
||||||
|
def test_conducting_boundary_rejects_invalid_arguments(direction: int, polarity: int) -> None:
|
||||||
|
with pytest.raises(ValueError, match='Invalid direction|Bad polarity'):
|
||||||
|
conducting_boundary(direction, polarity)
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue