Skip to content

Commit c25310d

Browse files
committed
first commit
1 parent b15bd4b commit c25310d

1 file changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env python3
2+
"""Generate scattnlay near-field reference arrays for notebook validation.
3+
4+
This script uses the Python bindings from scattnlay (`fieldnlay`, `scattnlay`)
5+
for two homogeneous-sphere cases and saves `.npy` outputs under `docs/data/`:
6+
7+
- nonabs: m_scatt = 1.5 + 0.0j
8+
- abs: m_scatt = 1.5 + 0.1j (mapped to physical 1.5 - 0.1j in miepython)
9+
10+
Saved files per case:
11+
- scattnlay_<case>_X.npy
12+
- scattnlay_<case>_Z.npy
13+
- scattnlay_<case>_E.npy (shape: 3 x nx x nz)
14+
- scattnlay_<case>_H.npy (shape: 3 x nx x nz)
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import contextlib
20+
import importlib.metadata
21+
import json
22+
import os
23+
from pathlib import Path
24+
25+
import numpy as np
26+
from scattnlay import fieldnlay, scattnlay
27+
28+
29+
SCATTNLAY_COMMIT = "211fc8d2168deff500b67042cea952fc74c84b64"
30+
31+
32+
@contextlib.contextmanager
33+
def _suppress_native_output() -> None:
34+
"""Suppress verbose stdout/stderr emitted by the C++ extension."""
35+
with open(os.devnull, "w", encoding="utf-8") as devnull:
36+
saved_stdout = os.dup(1)
37+
saved_stderr = os.dup(2)
38+
try:
39+
os.dup2(devnull.fileno(), 1)
40+
os.dup2(devnull.fileno(), 2)
41+
yield
42+
finally:
43+
os.dup2(saved_stdout, 1)
44+
os.dup2(saved_stderr, 2)
45+
os.close(saved_stdout)
46+
os.close(saved_stderr)
47+
48+
49+
def _reshape_field_components(raw: np.ndarray, npts: int) -> np.ndarray:
50+
"""Return field components as (3, npts, npts)."""
51+
if raw.shape == (npts * npts, 3):
52+
return raw.T.reshape(3, npts, npts, order="C")
53+
if raw.shape == (3, npts * npts):
54+
return raw.reshape(3, npts, npts, order="C")
55+
raise RuntimeError(f"Unexpected field array shape from scattnlay: {raw.shape}")
56+
57+
58+
def run_fieldnlay_case(
59+
d_sphere: float,
60+
n_env: float,
61+
lambda0: float,
62+
m_scatt: complex,
63+
extent: float,
64+
npts: int,
65+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, dict[str, float | int]]:
66+
"""Run fieldnlay for one case and return X,Z,E,H on physical coordinates."""
67+
x_size_parameter = np.pi * d_sphere * n_env / lambda0
68+
k_env = 2.0 * np.pi * n_env / lambda0
69+
70+
# scattnlay field coordinates are dimensionless and consistent with x = k*r.
71+
lo_kr = -extent * k_env
72+
hi_kr = extent * k_env
73+
74+
x_scan = np.linspace(lo_kr, hi_kr, npts)
75+
z_scan = np.linspace(lo_kr, hi_kr, npts)
76+
Xkr, Zkr = np.meshgrid(x_scan, z_scan, indexing="ij")
77+
Ykr = np.zeros_like(Xkr)
78+
79+
xp = Xkr.ravel(order="C")
80+
yp = Ykr.ravel(order="C")
81+
zp = Zkr.ravel(order="C")
82+
83+
x_layers = np.array([x_size_parameter], dtype=float)
84+
m_layers = np.array([m_scatt], dtype=complex)
85+
86+
with _suppress_native_output():
87+
terms, e_raw, h_raw = fieldnlay(x_layers, m_layers, xp, yp, zp)
88+
(
89+
terms_scatt,
90+
qext,
91+
qsca,
92+
qabs,
93+
qbk,
94+
qpr,
95+
asymmetry,
96+
albedo,
97+
_s1,
98+
_s2,
99+
) = scattnlay(x_layers, m_layers)
100+
101+
E = _reshape_field_components(np.asarray(e_raw), npts)
102+
H = _reshape_field_components(np.asarray(h_raw), npts)
103+
104+
X = Xkr / k_env
105+
Z = Zkr / k_env
106+
107+
scattering = {
108+
"terms_fieldnlay": int(terms),
109+
"terms_scattnlay": int(terms_scatt),
110+
"Qext": float(qext),
111+
"Qsca": float(qsca),
112+
"Qabs": float(qabs),
113+
"Qbk": float(qbk),
114+
"Qpr": float(qpr),
115+
"g": float(asymmetry),
116+
"albedo": float(albedo),
117+
}
118+
return X, Z, E, H, scattering
119+
120+
121+
def main() -> None:
122+
data_dir = Path(__file__).resolve().parent
123+
data_dir.mkdir(parents=True, exist_ok=True)
124+
125+
d_sphere = 2.0
126+
n_env = 1.0
127+
lambda0 = 1.0
128+
extent = 3.0
129+
npts = 121
130+
131+
cases = {
132+
"nonabs": {
133+
"m_scatt": 1.5 + 0.0j,
134+
"n_sphere_physical": "1.5+0.0j",
135+
"n_sphere_miepython": "1.5+0.0j",
136+
},
137+
"abs": {
138+
"m_scatt": 1.5 + 0.1j,
139+
"n_sphere_physical": "1.5-0.1j",
140+
"n_sphere_miepython": "1.5-0.1j",
141+
},
142+
}
143+
144+
metadata = {
145+
"scattnlay_python_package": importlib.metadata.version("scattnlay"),
146+
"d_sphere": d_sphere,
147+
"n_env": n_env,
148+
"lambda0": lambda0,
149+
"extent": extent,
150+
"npts": npts,
151+
"cases": {},
152+
"scattnlay_commit": SCATTNLAY_COMMIT,
153+
}
154+
155+
for case, cfg in cases.items():
156+
X, Z, E, H, scattering = run_fieldnlay_case(
157+
d_sphere=d_sphere,
158+
n_env=n_env,
159+
lambda0=lambda0,
160+
m_scatt=cfg["m_scatt"],
161+
extent=extent,
162+
npts=npts,
163+
)
164+
165+
fx = f"scattnlay_{case}_X.npy"
166+
fz = f"scattnlay_{case}_Z.npy"
167+
fe = f"scattnlay_{case}_E.npy"
168+
fh = f"scattnlay_{case}_H.npy"
169+
170+
np.save(data_dir / fx, X)
171+
np.save(data_dir / fz, Z)
172+
np.save(data_dir / fe, E)
173+
np.save(data_dir / fh, H)
174+
175+
e_abs = np.sqrt(np.sum(np.abs(E) ** 2, axis=0))
176+
h_abs = np.sqrt(np.sum(np.abs(H) ** 2, axis=0))
177+
metadata["cases"][case] = {
178+
"scattering": scattering,
179+
"n_sphere_scatt": [cfg["m_scatt"].real, cfg["m_scatt"].imag],
180+
"n_sphere_physical": cfg["n_sphere_physical"],
181+
"n_sphere_miepython": cfg["n_sphere_miepython"],
182+
"max_abs_E": float(np.max(e_abs)),
183+
"max_abs_H_raw": float(np.max(h_abs)),
184+
"files": {"X": fx, "Z": fz, "E": fe, "H": fh},
185+
}
186+
187+
print(
188+
f"[{case}] wrote {fx}, {fz}, {fe}, {fh}; "
189+
f"max|E|={np.max(e_abs):.6g}, max|H|={np.max(h_abs):.6g}"
190+
)
191+
192+
meta_path = data_dir / "scattnlay_reference_metadata.json"
193+
meta_path.write_text(json.dumps(metadata, indent=2))
194+
print(f"Wrote {meta_path}")
195+
196+
197+
if __name__ == "__main__":
198+
main()

0 commit comments

Comments
 (0)