pysie2d 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pysie2d-0.2.0/PKG-INFO +150 -0
- pysie2d-0.2.0/README.md +138 -0
- pysie2d-0.2.0/pyproject.toml +67 -0
- pysie2d-0.2.0/src/pysie2d/__init__.py +40 -0
- pysie2d-0.2.0/src/pysie2d/fields.py +166 -0
- pysie2d-0.2.0/src/pysie2d/geometry.py +496 -0
- pysie2d-0.2.0/src/pysie2d/green.py +122 -0
- pysie2d-0.2.0/src/pysie2d/kernels.py +286 -0
- pysie2d-0.2.0/src/pysie2d/material.py +48 -0
- pysie2d-0.2.0/src/pysie2d/py.typed +0 -0
- pysie2d-0.2.0/src/pysie2d/reference/__init__.py +1 -0
- pysie2d-0.2.0/src/pysie2d/reference/mie.py +226 -0
- pysie2d-0.2.0/src/pysie2d/solver.py +241 -0
- pysie2d-0.2.0/src/pysie2d/sources.py +133 -0
pysie2d-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pysie2d
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 2-D boundary-integral (Müller BIE) solver for electromagnetic scattering from a cylinder in a homogeneous background.
|
|
5
|
+
Author: Claudio Silvestre Castro
|
|
6
|
+
Author-email: Claudio Silvestre Castro <claudio.silvestre.castro@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: numpy>=1.26
|
|
9
|
+
Requires-Dist: scipy>=1.11
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# pysie2d
|
|
14
|
+
|
|
15
|
+
A 2-D surface-integral-equation solver for time-harmonic
|
|
16
|
+
electromagnetic scattering from a single smooth cylinder — circular or
|
|
17
|
+
Gielis-superformula cross-section, embedded in a homogeneous background. Validated
|
|
18
|
+
against analytic Mie theory, with a typed public API and CI.
|
|
19
|
+
|
|
20
|
+
## Scope and non-goals
|
|
21
|
+
|
|
22
|
+
This package is distilled from a larger private research code; it deliberately
|
|
23
|
+
covers only the **homogeneous-background, single-particle core** — the part
|
|
24
|
+
that can be validated end-to-end against a closed-form reference. Potential extensions
|
|
25
|
+
in the mid/long-term include slab waveguide backgrounds, multiple-particle simulations,
|
|
26
|
+
and quasinormal-mode searches based on the surface-integral matrix operator.
|
|
27
|
+
|
|
28
|
+
## Figures
|
|
29
|
+
|
|
30
|
+
Relative error of the scattering efficiency `Q_sca` versus the number of
|
|
31
|
+
boundary points, converging toward analytic Mie theory (both polarisations):
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
Near field of a Gielis `m = 6` star under plane-wave illumination (scattered
|
|
36
|
+
field outside the boundary, internal field inside):
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
Relative local density of states (Purcell map) around the same Gielis `m = 6`
|
|
41
|
+
star, at one of its `qsca` resonances: a line-dipole emitter placed in a red
|
|
42
|
+
lobe decays faster than in free space (`1 + 4·Im S > 1`), while blue regions
|
|
43
|
+
suppress it. The six-fold pattern mirrors the particle's symmetry. The drive
|
|
44
|
+
*and* the decay rate of an embedded emitter both come from this map — it is the
|
|
45
|
+
entry point of quantum-dynamics calculations downstream:
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
Regenerate them with:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run python examples/convergence_study.py
|
|
53
|
+
uv run python examples/nearfield_map.py
|
|
54
|
+
uv run python examples/purcell_map.py
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Formulation (summary)
|
|
58
|
+
|
|
59
|
+
- The cylinder is invariant along its axis, so Maxwell reduces to a scalar
|
|
60
|
+
Helmholtz problem for one field component (`E_y` for TE, `H_y` for TM).
|
|
61
|
+
- The self-consistent field solution is given **everywhere** in terms of the surface field and its normal derivative;
|
|
62
|
+
matching across the interface gives a Fredholm integral equation of the second kind.
|
|
63
|
+
- Discretising the boundary with `nn` quadrature points yields a dense
|
|
64
|
+
`2nn × 2nn` complex system `M(λ)·ei = rhs`, solved directly.
|
|
65
|
+
- The logarithmic Green-function singularity is handled analytically in the
|
|
66
|
+
diagonal terms; complex wavenumbers are supported throughout.
|
|
67
|
+
- Lengths are in nm, the time convention is `exp(-iωt)`, and outgoing waves are
|
|
68
|
+
`H_n^{(1)}`.
|
|
69
|
+
|
|
70
|
+
Full details and every sign/layout convention are in
|
|
71
|
+
[docs/conventions.md](docs/conventions.md). The analytic reference is Bohren &
|
|
72
|
+
Huffman, *Absorption and Scattering of Light by Small Particles*, ch. 8; the
|
|
73
|
+
surface-integral formulation follows [Valencia et al's formulation](https://doi.org/10.1364/JOSAB.20.002150).
|
|
74
|
+
|
|
75
|
+
## Validation
|
|
76
|
+
|
|
77
|
+
The physics test suite compares the solver against analytic Mie theory for a
|
|
78
|
+
circular cylinder: scattering / extinction / absorption efficiencies, the
|
|
79
|
+
optical theorem on a lossy particle, energy conservation on a lossless one, the
|
|
80
|
+
convergence rate, and the 2-D `1/√(kr)` far-field decay. At `nn = 300` the
|
|
81
|
+
efficiencies agree with Mie to a few parts in `10³`; the error decreases with
|
|
82
|
+
`nn` until it reaches the fixed angular-quadrature floor of the far-field
|
|
83
|
+
integrator. See `tests/` for the exact tolerances and the reasoning behind them.
|
|
84
|
+
|
|
85
|
+
The line-dipole / self-Green machinery (v0.2) is validated the same way:
|
|
86
|
+
reciprocity of the scattered field (to `10⁻⁶`), the free-space limit
|
|
87
|
+
(`LDOS → 1` far from the particle), LDOS positivity, and — the strong anchor —
|
|
88
|
+
the self-Green function of a circular cylinder against its closed-form
|
|
89
|
+
Graf-addition-theorem sum on both `Re S` and `Im S`. That near-field anchor
|
|
90
|
+
converges at first order in `nn`, so it is run at `nn = 1000` to reach `1 %`;
|
|
91
|
+
the resolved scattered-field sign convention is recorded in
|
|
92
|
+
[docs/conventions.md](docs/conventions.md).
|
|
93
|
+
|
|
94
|
+
## Install / run / test
|
|
95
|
+
|
|
96
|
+
Requires Python 3.12 and [uv](https://docs.astral.sh/uv/).
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv sync # create the environment
|
|
100
|
+
uv run pytest # run the validation suite
|
|
101
|
+
uv run ruff format --check . # formatting
|
|
102
|
+
uv run ruff check . # lint
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Minimal use:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pysie2d import BIESolver, Geometry, Material
|
|
109
|
+
|
|
110
|
+
geom = Geometry.gielis(rad=200, n_pts=300, m=0) # circular cylinder, nm
|
|
111
|
+
mat = Material(n_core=1.5, n_clad=1.0, pol=2) # TE
|
|
112
|
+
result = BIESolver(geom, mat).scatter(wavelength=600.0)
|
|
113
|
+
|
|
114
|
+
print(result.efficiencies()) # {'qsca', 'qext', 'qabs'}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Line-dipole emitter and Purcell effect:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from pysie2d import BIESolver, Geometry, Material, relative_ldos
|
|
121
|
+
|
|
122
|
+
geom = Geometry.gielis(rad=200, n_pts=300, m=6, n1=6, n2=12, n3=12) # Gielis star
|
|
123
|
+
solver = BIESolver(geom, Material(n_core=2.0))
|
|
124
|
+
print(relative_ldos(solver, wavelength=540.0, x_s=430.0, z_s=0.0)) # LDOS vs free space
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Performance
|
|
128
|
+
|
|
129
|
+
The system is a dense `2nn × 2nn` complex matrix; at `nn = 300` (a `600 × 600`
|
|
130
|
+
solve) a single wavelength takes below one second in a modern computer, so wavelength
|
|
131
|
+
sweeps are cheap serial `for` loops — no parallelism required.
|
|
132
|
+
|
|
133
|
+
For a Purcell map, every grid point is a different source position, hence a
|
|
134
|
+
different right-hand side — but the matrix `M(λ)` is the same for all of them.
|
|
135
|
+
`relative_ldos_map` therefore factorises `M` **once** with
|
|
136
|
+
`scipy.linalg.lu_factor` and reuses it across all sources (`lu_solve`), turning
|
|
137
|
+
what would be an hour-long sweep into a few seconds.
|
|
138
|
+
|
|
139
|
+
## Roadmap
|
|
140
|
+
|
|
141
|
+
- **v0.1.0** — core scattering: plane-wave excitation, near/far fields,
|
|
142
|
+
cross-section efficiencies, Mie validation, convergence study, CI.
|
|
143
|
+
- **v0.2.0** — line-dipole (point-source) excitation and the self-Green
|
|
144
|
+
function → relative LDOS / Purcell maps. _(this release)_
|
|
145
|
+
- **v0.3.0** — quasi-normal-mode extraction via Beyn's contour method,
|
|
146
|
+
validated against analytic Mie resonances.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT — see [LICENSE](LICENSE).
|
pysie2d-0.2.0/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# pysie2d
|
|
2
|
+
|
|
3
|
+
A 2-D surface-integral-equation solver for time-harmonic
|
|
4
|
+
electromagnetic scattering from a single smooth cylinder — circular or
|
|
5
|
+
Gielis-superformula cross-section, embedded in a homogeneous background. Validated
|
|
6
|
+
against analytic Mie theory, with a typed public API and CI.
|
|
7
|
+
|
|
8
|
+
## Scope and non-goals
|
|
9
|
+
|
|
10
|
+
This package is distilled from a larger private research code; it deliberately
|
|
11
|
+
covers only the **homogeneous-background, single-particle core** — the part
|
|
12
|
+
that can be validated end-to-end against a closed-form reference. Potential extensions
|
|
13
|
+
in the mid/long-term include slab waveguide backgrounds, multiple-particle simulations,
|
|
14
|
+
and quasinormal-mode searches based on the surface-integral matrix operator.
|
|
15
|
+
|
|
16
|
+
## Figures
|
|
17
|
+
|
|
18
|
+
Relative error of the scattering efficiency `Q_sca` versus the number of
|
|
19
|
+
boundary points, converging toward analytic Mie theory (both polarisations):
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
Near field of a Gielis `m = 6` star under plane-wave illumination (scattered
|
|
24
|
+
field outside the boundary, internal field inside):
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
Relative local density of states (Purcell map) around the same Gielis `m = 6`
|
|
29
|
+
star, at one of its `qsca` resonances: a line-dipole emitter placed in a red
|
|
30
|
+
lobe decays faster than in free space (`1 + 4·Im S > 1`), while blue regions
|
|
31
|
+
suppress it. The six-fold pattern mirrors the particle's symmetry. The drive
|
|
32
|
+
*and* the decay rate of an embedded emitter both come from this map — it is the
|
|
33
|
+
entry point of quantum-dynamics calculations downstream:
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
Regenerate them with:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv run python examples/convergence_study.py
|
|
41
|
+
uv run python examples/nearfield_map.py
|
|
42
|
+
uv run python examples/purcell_map.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Formulation (summary)
|
|
46
|
+
|
|
47
|
+
- The cylinder is invariant along its axis, so Maxwell reduces to a scalar
|
|
48
|
+
Helmholtz problem for one field component (`E_y` for TE, `H_y` for TM).
|
|
49
|
+
- The self-consistent field solution is given **everywhere** in terms of the surface field and its normal derivative;
|
|
50
|
+
matching across the interface gives a Fredholm integral equation of the second kind.
|
|
51
|
+
- Discretising the boundary with `nn` quadrature points yields a dense
|
|
52
|
+
`2nn × 2nn` complex system `M(λ)·ei = rhs`, solved directly.
|
|
53
|
+
- The logarithmic Green-function singularity is handled analytically in the
|
|
54
|
+
diagonal terms; complex wavenumbers are supported throughout.
|
|
55
|
+
- Lengths are in nm, the time convention is `exp(-iωt)`, and outgoing waves are
|
|
56
|
+
`H_n^{(1)}`.
|
|
57
|
+
|
|
58
|
+
Full details and every sign/layout convention are in
|
|
59
|
+
[docs/conventions.md](docs/conventions.md). The analytic reference is Bohren &
|
|
60
|
+
Huffman, *Absorption and Scattering of Light by Small Particles*, ch. 8; the
|
|
61
|
+
surface-integral formulation follows [Valencia et al's formulation](https://doi.org/10.1364/JOSAB.20.002150).
|
|
62
|
+
|
|
63
|
+
## Validation
|
|
64
|
+
|
|
65
|
+
The physics test suite compares the solver against analytic Mie theory for a
|
|
66
|
+
circular cylinder: scattering / extinction / absorption efficiencies, the
|
|
67
|
+
optical theorem on a lossy particle, energy conservation on a lossless one, the
|
|
68
|
+
convergence rate, and the 2-D `1/√(kr)` far-field decay. At `nn = 300` the
|
|
69
|
+
efficiencies agree with Mie to a few parts in `10³`; the error decreases with
|
|
70
|
+
`nn` until it reaches the fixed angular-quadrature floor of the far-field
|
|
71
|
+
integrator. See `tests/` for the exact tolerances and the reasoning behind them.
|
|
72
|
+
|
|
73
|
+
The line-dipole / self-Green machinery (v0.2) is validated the same way:
|
|
74
|
+
reciprocity of the scattered field (to `10⁻⁶`), the free-space limit
|
|
75
|
+
(`LDOS → 1` far from the particle), LDOS positivity, and — the strong anchor —
|
|
76
|
+
the self-Green function of a circular cylinder against its closed-form
|
|
77
|
+
Graf-addition-theorem sum on both `Re S` and `Im S`. That near-field anchor
|
|
78
|
+
converges at first order in `nn`, so it is run at `nn = 1000` to reach `1 %`;
|
|
79
|
+
the resolved scattered-field sign convention is recorded in
|
|
80
|
+
[docs/conventions.md](docs/conventions.md).
|
|
81
|
+
|
|
82
|
+
## Install / run / test
|
|
83
|
+
|
|
84
|
+
Requires Python 3.12 and [uv](https://docs.astral.sh/uv/).
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv sync # create the environment
|
|
88
|
+
uv run pytest # run the validation suite
|
|
89
|
+
uv run ruff format --check . # formatting
|
|
90
|
+
uv run ruff check . # lint
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Minimal use:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from pysie2d import BIESolver, Geometry, Material
|
|
97
|
+
|
|
98
|
+
geom = Geometry.gielis(rad=200, n_pts=300, m=0) # circular cylinder, nm
|
|
99
|
+
mat = Material(n_core=1.5, n_clad=1.0, pol=2) # TE
|
|
100
|
+
result = BIESolver(geom, mat).scatter(wavelength=600.0)
|
|
101
|
+
|
|
102
|
+
print(result.efficiencies()) # {'qsca', 'qext', 'qabs'}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Line-dipole emitter and Purcell effect:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pysie2d import BIESolver, Geometry, Material, relative_ldos
|
|
109
|
+
|
|
110
|
+
geom = Geometry.gielis(rad=200, n_pts=300, m=6, n1=6, n2=12, n3=12) # Gielis star
|
|
111
|
+
solver = BIESolver(geom, Material(n_core=2.0))
|
|
112
|
+
print(relative_ldos(solver, wavelength=540.0, x_s=430.0, z_s=0.0)) # LDOS vs free space
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Performance
|
|
116
|
+
|
|
117
|
+
The system is a dense `2nn × 2nn` complex matrix; at `nn = 300` (a `600 × 600`
|
|
118
|
+
solve) a single wavelength takes below one second in a modern computer, so wavelength
|
|
119
|
+
sweeps are cheap serial `for` loops — no parallelism required.
|
|
120
|
+
|
|
121
|
+
For a Purcell map, every grid point is a different source position, hence a
|
|
122
|
+
different right-hand side — but the matrix `M(λ)` is the same for all of them.
|
|
123
|
+
`relative_ldos_map` therefore factorises `M` **once** with
|
|
124
|
+
`scipy.linalg.lu_factor` and reuses it across all sources (`lu_solve`), turning
|
|
125
|
+
what would be an hour-long sweep into a few seconds.
|
|
126
|
+
|
|
127
|
+
## Roadmap
|
|
128
|
+
|
|
129
|
+
- **v0.1.0** — core scattering: plane-wave excitation, near/far fields,
|
|
130
|
+
cross-section efficiencies, Mie validation, convergence study, CI.
|
|
131
|
+
- **v0.2.0** — line-dipole (point-source) excitation and the self-Green
|
|
132
|
+
function → relative LDOS / Purcell maps. _(this release)_
|
|
133
|
+
- **v0.3.0** — quasi-normal-mode extraction via Beyn's contour method,
|
|
134
|
+
validated against analytic Mie resonances.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pysie2d"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "2-D boundary-integral (Müller BIE) solver for electromagnetic scattering from a cylinder in a homogeneous background."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Claudio Silvestre Castro", email = "claudio.silvestre.castro@gmail.com" },
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = ["numpy>=1.26", "scipy>=1.11"]
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["uv_build>=0.10.12,<0.11.0"]
|
|
15
|
+
build-backend = "uv_build"
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = ["pytest>=8", "ruff>=0.8", "matplotlib>=3.9"]
|
|
19
|
+
|
|
20
|
+
[tool.ruff]
|
|
21
|
+
line-length = 88
|
|
22
|
+
src = ["src", "tests"]
|
|
23
|
+
|
|
24
|
+
[tool.ruff.lint]
|
|
25
|
+
select = ["E", "F", "D", "N", "B", "I", "C4", "A"]
|
|
26
|
+
ignore = [
|
|
27
|
+
"D203", "D204", "D213", "D215", "D400", "D401",
|
|
28
|
+
"D404", "D406", "D407", "D408", "D409", "D413",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint.pydocstyle]
|
|
32
|
+
convention = "google"
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint.per-file-ignores]
|
|
35
|
+
"tests/*" = ["D"] # test names are the documentation
|
|
36
|
+
# Keep the Bessel/Hankel math notation (J_n, H_n) that maps directly onto the
|
|
37
|
+
# cited Bohren & Huffman equations; snake_case would obscure the reference.
|
|
38
|
+
"src/pysie2d/reference/mie.py" = ["N806"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
|
|
43
|
+
[tool.semantic_release]
|
|
44
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
45
|
+
commit_parser = "conventional"
|
|
46
|
+
commit_parser_options = { ignore_merge_commits = true }
|
|
47
|
+
major_on_zero = false
|
|
48
|
+
build_command = "uv build"
|
|
49
|
+
|
|
50
|
+
[tool.semantic_release.branches.main]
|
|
51
|
+
match = "main"
|
|
52
|
+
prerelease = false
|
|
53
|
+
|
|
54
|
+
[tool.semantic_release.changelog]
|
|
55
|
+
changelog_file = "CHANGELOG.md"
|
|
56
|
+
exclude_commit_patterns = [
|
|
57
|
+
'''chore(?:\([^)]*?\))?: .+''',
|
|
58
|
+
'''ci(?:\([^)]*?\))?: .+''',
|
|
59
|
+
'''test(?:\([^)]*?\))?: .+''',
|
|
60
|
+
'''Merged? .*''',
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.semantic_release.remote]
|
|
64
|
+
type = "github"
|
|
65
|
+
|
|
66
|
+
[tool.semantic_release.publish]
|
|
67
|
+
upload_to_vcs_release = true
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""pysie2d — 2-D boundary-integral scattering solver (homogeneous background).
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
Geometry: Gielis-superformula boundary parameterisation.
|
|
5
|
+
Material: optical properties of the scatterer.
|
|
6
|
+
BIESolver: solver façade; call ``scatter``/``scatter_dipole`` to obtain a
|
|
7
|
+
``ScatterResult``.
|
|
8
|
+
ScatterResult: carries the solution and exposes far/near-field analysis.
|
|
9
|
+
assemble_matrix: the vectorised BIE system-matrix assembly.
|
|
10
|
+
plane_wave_rhs, line_dipole_rhs: excitation right-hand sides.
|
|
11
|
+
eval_field, far_field: field-evaluation primitives.
|
|
12
|
+
self_green, relative_ldos, relative_ldos_map: self-Green function and
|
|
13
|
+
LDOS / Purcell-effect analysis (line-dipole excitation).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .fields import eval_field, far_field
|
|
17
|
+
from .geometry import Geometry
|
|
18
|
+
from .green import relative_ldos, relative_ldos_map, self_green
|
|
19
|
+
from .kernels import assemble_matrix, assemble_matrix_reference
|
|
20
|
+
from .material import Material
|
|
21
|
+
from .solver import BIESolver, ScatterResult
|
|
22
|
+
from .sources import line_dipole_rhs, plane_wave_rhs
|
|
23
|
+
|
|
24
|
+
__version__ = "0.2.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BIESolver",
|
|
28
|
+
"Geometry",
|
|
29
|
+
"Material",
|
|
30
|
+
"ScatterResult",
|
|
31
|
+
"assemble_matrix",
|
|
32
|
+
"assemble_matrix_reference",
|
|
33
|
+
"eval_field",
|
|
34
|
+
"far_field",
|
|
35
|
+
"line_dipole_rhs",
|
|
36
|
+
"plane_wave_rhs",
|
|
37
|
+
"relative_ldos",
|
|
38
|
+
"relative_ldos_map",
|
|
39
|
+
"self_green",
|
|
40
|
+
]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Field evaluation: far-field amplitude and arbitrary-point near fields."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .kernels import hank0, hank1
|
|
6
|
+
|
|
7
|
+
PI = np.pi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
# Far-field amplitude (subroutine efi)
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def far_field(
|
|
16
|
+
nn: int,
|
|
17
|
+
nff: int,
|
|
18
|
+
lambd: float,
|
|
19
|
+
f: np.ndarray,
|
|
20
|
+
g: np.ndarray,
|
|
21
|
+
df: np.ndarray,
|
|
22
|
+
dg: np.ndarray,
|
|
23
|
+
delt: float | np.ndarray,
|
|
24
|
+
ei: np.ndarray,
|
|
25
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
26
|
+
"""Compute the 2-D far-field scattering amplitude.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
nn: Number of boundary points.
|
|
30
|
+
nff: Number of far-field angles.
|
|
31
|
+
lambd: Wavelength (nm).
|
|
32
|
+
f: (nn,) boundary x coordinates (nm).
|
|
33
|
+
g: (nn,) boundary z coordinates (nm).
|
|
34
|
+
df: (nn,) first derivative of f w.r.t. θ.
|
|
35
|
+
dg: (nn,) first derivative of g w.r.t. θ.
|
|
36
|
+
delt: Quadrature θ-step (scalar or per-point array).
|
|
37
|
+
ei: complex (2nn,) BIE solution vector.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
amp: complex (nff,) far-field amplitude.
|
|
41
|
+
angles: float (nff,) observation angles (rad), from −π to π.
|
|
42
|
+
"""
|
|
43
|
+
wnum = 2.0 * PI / lambd
|
|
44
|
+
angles = -PI + np.arange(nff) * 2.0 * PI / (nff - 1.0)
|
|
45
|
+
se = np.sin(angles)
|
|
46
|
+
co = np.cos(angles)
|
|
47
|
+
|
|
48
|
+
# shape (nn, nff)
|
|
49
|
+
arg = -1j * wnum * (f[:, None] * se + g[:, None] * co)
|
|
50
|
+
phi_j = ei[:nn, None]
|
|
51
|
+
chi_j = ei[nn:, None]
|
|
52
|
+
puto = 1j * wnum * (dg[:, None] * se - df[:, None] * co) * phi_j - chi_j
|
|
53
|
+
# delt may be a scalar or a per-point (nn,) array
|
|
54
|
+
delt_col = np.reshape(delt, (-1, 1)) if np.ndim(delt) > 0 else delt
|
|
55
|
+
amp = np.sum(np.exp(arg) * puto * delt_col, axis=0)
|
|
56
|
+
|
|
57
|
+
return amp, angles
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Inside/outside test (subroutine eicero)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_outside(
|
|
66
|
+
x1: float,
|
|
67
|
+
x3: float,
|
|
68
|
+
f: np.ndarray,
|
|
69
|
+
g: np.ndarray,
|
|
70
|
+
df: np.ndarray,
|
|
71
|
+
dg: np.ndarray,
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""Return True if (x1, x3) is outside the particle.
|
|
74
|
+
|
|
75
|
+
Uses the sign of the dot product between the displacement vector and the
|
|
76
|
+
outward normal at the nearest boundary point. This nearest-neighbour
|
|
77
|
+
heuristic is reliable for convex and mildly star-shaped boundaries, but
|
|
78
|
+
unreliable for extreme concave superformula shapes.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
x1: Observation x-coordinate (nm).
|
|
82
|
+
x3: Observation z-coordinate (nm).
|
|
83
|
+
f: (nn,) boundary x coordinates (nm).
|
|
84
|
+
g: (nn,) boundary z coordinates (nm).
|
|
85
|
+
df: (nn,) first derivative of f w.r.t. θ.
|
|
86
|
+
dg: (nn,) first derivative of g w.r.t. θ.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if the point lies outside the particle.
|
|
90
|
+
"""
|
|
91
|
+
imin = np.argmin((x1 - f) ** 2 + (x3 - g) ** 2)
|
|
92
|
+
norm = -(x1 - f[imin]) * dg[imin] + (x3 - g[imin]) * df[imin]
|
|
93
|
+
return norm > 0.0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Scattered field at arbitrary points
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def eval_field(
|
|
102
|
+
ei: np.ndarray,
|
|
103
|
+
nn: int,
|
|
104
|
+
f: np.ndarray,
|
|
105
|
+
df: np.ndarray,
|
|
106
|
+
g: np.ndarray,
|
|
107
|
+
dg: np.ndarray,
|
|
108
|
+
delt: float | np.ndarray,
|
|
109
|
+
wnum: complex,
|
|
110
|
+
x_pts: np.ndarray,
|
|
111
|
+
z_pts: np.ndarray,
|
|
112
|
+
ri: complex | None = None,
|
|
113
|
+
) -> np.ndarray:
|
|
114
|
+
"""Evaluate the field at arbitrary (x, z) points.
|
|
115
|
+
|
|
116
|
+
Uses the BIE representation formula (Huygens principle) for exterior points.
|
|
117
|
+
For interior points, the same formula is used with the particle wavenumber
|
|
118
|
+
wn1 = ri * wnum when ri is provided; otherwise interior points return 0+0j.
|
|
119
|
+
|
|
120
|
+
Never evaluate on or very near the boundary: the representation-formula
|
|
121
|
+
integrand is near-singular there and degrades within roughly 2–3 boundary-
|
|
122
|
+
point spacings (~2π·rad/nn) of the surface. Keep observation points at
|
|
123
|
+
least ~5 spacings away.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
ei: complex (2nn,) BIE solution vector (φ = ei[:nn], χ = ei[nn:]).
|
|
127
|
+
nn: Number of boundary points.
|
|
128
|
+
f: (nn,) boundary x coordinates (nm).
|
|
129
|
+
df: (nn,) first derivative of f w.r.t. θ.
|
|
130
|
+
g: (nn,) boundary z coordinates (nm).
|
|
131
|
+
dg: (nn,) first derivative of g w.r.t. θ.
|
|
132
|
+
delt: Quadrature θ-step.
|
|
133
|
+
wnum: Background wavenumber 2π/λ.
|
|
134
|
+
x_pts: (M,) observation x-coordinates (nm).
|
|
135
|
+
z_pts: (M,) observation z-coordinates (nm).
|
|
136
|
+
ri: Particle refractive index (relative to background). When provided,
|
|
137
|
+
interior points are evaluated using wn1 = ri * wnum. When None
|
|
138
|
+
(default), interior points return 0+0j.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
field: complex (M,) field E_y at each observation point (exterior
|
|
142
|
+
scattered field outside, interior field inside when ri is given).
|
|
143
|
+
"""
|
|
144
|
+
x_pts = np.asarray(x_pts)
|
|
145
|
+
z_pts = np.asarray(z_pts)
|
|
146
|
+
n_pts = len(x_pts)
|
|
147
|
+
field = np.zeros(n_pts, dtype=complex)
|
|
148
|
+
|
|
149
|
+
wn1 = ri * wnum if ri is not None else None
|
|
150
|
+
|
|
151
|
+
for j in range(n_pts):
|
|
152
|
+
outside = _is_outside(x_pts[j], z_pts[j], f, g, df, dg)
|
|
153
|
+
if not outside and wn1 is None:
|
|
154
|
+
continue
|
|
155
|
+
xmf = x_pts[j] - f
|
|
156
|
+
zmg = z_pts[j] - g
|
|
157
|
+
dist = np.sqrt(xmf**2 + zmg**2)
|
|
158
|
+
arg2 = -dg * xmf + df * zmg
|
|
159
|
+
k = wnum if outside else wn1
|
|
160
|
+
arg1 = k * dist
|
|
161
|
+
sum_h = np.sum(
|
|
162
|
+
(k**2 * arg2 * hank1(arg1) / arg1 * ei[:nn] - hank0(arg1) * ei[nn:]) * delt
|
|
163
|
+
)
|
|
164
|
+
field[j] = (1j / 4.0) * sum_h
|
|
165
|
+
|
|
166
|
+
return field
|