[farfield] align Fourier projection with outgoing wave convention

This commit is contained in:
Forgejo Actions 2026-10-28 19:03:29 -07:00
commit 112d88cac1
2 changed files with 32 additions and 11 deletions

View file

@ -56,6 +56,8 @@ def near_to_farfield(
The input fields should be complex phasors. The spatial origin is the
sample at ``(nx // 2, ny // 2)``. Padding/cropping preserves that origin.
Fourier integrals include the cell area ``dx * dy``.
Phasors use ``exp(+i omega t)``; an outgoing plane wave has spatial
dependence ``exp(-i k dot r)`` and is projected with a positive Fourier phase.
Args:
E_near: List of 2 ndarrays containing the 2D phasor field slices for the transverse
@ -71,9 +73,9 @@ def near_to_farfield(
Returns:
Dict with keys
- `E`: Normalized E-field farfield; multiply by
- `E`: Normalized polar and azimuthal E-field components; multiply by
(i k exp(-i k r) / (4 pi r)) to get the actual field value.
- `H`: Normalized H-field farfield; multiply by
- `H`: Normalized polar and azimuthal H-field components; multiply by
(i k exp(-i k r) / (4 pi r)) to get the actual field value.
- `kx`, `ky`: Wavevector values corresponding to the x- and y- axes in E_far and H_far,
normalized to wavelength (dimensionless).
@ -95,8 +97,8 @@ def near_to_farfield(
padded_shape = _padded_shape(s, padded_size)
En_fft = [fftshift(fft2(ifftshift(_resize_centered(Eni, padded_shape)))) * dx * dy for Eni in E_near]
Hn_fft = [fftshift(fft2(ifftshift(_resize_centered(Hni, padded_shape)))) * dx * dy for Hni in H_near]
En_fft = [fftshift(ifft2(ifftshift(_resize_centered(Eni, padded_shape)), norm='forward')) * dx * dy for Eni in E_near]
Hn_fft = [fftshift(ifft2(ifftshift(_resize_centered(Hni, padded_shape)), norm='forward')) * dx * dy for Hni in H_near]
# Propagation vectors kx, ky
k = 2 * pi
@ -165,12 +167,10 @@ def far_to_nearfield(
inverse Fourier quadrature restores the cell-area normalization.
Args:
E_far: List of 2 ndarrays containing the 2D phasor field slices for the transverse
E fields (e.g. [Ex, Ey] for calculating the nearfield toward the z-direction).
E_far: List of 2 ndarrays containing polar and azimuthal E components.
Fields should be normalized so that
E_far = E_far_actual / (i k exp(-i k r) / (4 pi r))
H_far: List of 2 ndarrays containing the 2D phasor field slices for the transverse
H fields (e.g. [Hx, hy] for calculating the nearfield toward the z-direction).
H_far: List of 2 ndarrays containing polar and azimuthal H components.
Fields should be normalized so that
H_far = H_far_actual / (i k exp(-i k r) / (4 pi r))
dkx: kx discretization, in units of wavelength.
@ -262,8 +262,8 @@ def far_to_nearfield(
dx = 2 * pi / (padded_shape[0] * dkx)
dy = 2 * pi / (padded_shape[1] * dky)
E_near = [fftshift(ifft2(ifftshift(_resize_centered(Ei, padded_shape)))) / (dx * dy) for Ei in En_fft]
H_near = [fftshift(ifft2(ifftshift(_resize_centered(Hi, padded_shape)))) / (dx * dy) for Hi in Hn_fft]
E_near = [fftshift(fft2(ifftshift(_resize_centered(Ei, padded_shape)), norm='forward')) / (dx * dy) for Ei in En_fft]
H_near = [fftshift(fft2(ifftshift(_resize_centered(Hi, padded_shape)), norm='forward')) / (dx * dy) for Hi in Hn_fft]
outputs = {
'E': E_near,

View file

@ -135,7 +135,7 @@ def test_inverse_resizing_matches_plane_wave_quadrature(shape) -> None:
assert result['dx'] == pytest.approx(dx)
assert result['dy'] == pytest.approx(dy)
x = (numpy.arange(shape[0]) - shape[0] // 2) * dx
expected = numpy.broadcast_to(amplitude * dkx * dky / (2 * numpy.pi) ** 2 * numpy.exp(1j * dkx * x[:, None]), shape)
expected = numpy.broadcast_to(amplitude * dkx * dky / (2 * numpy.pi) ** 2 * numpy.exp(-1j * dkx * x[:, None]), shape)
numpy.testing.assert_allclose(result['E'][0], expected, atol=1e-14)
numpy.testing.assert_allclose(result['H'][1], expected / numpy.sqrt(1 - (dkx / (2 * numpy.pi)) ** 2), atol=1e-14)
@ -150,3 +150,24 @@ def test_inverse_accepts_readonly_spectra_without_modifying_them() -> None:
assert numpy.isfinite(result['E']).all()
for field, original in zip(fields, originals, strict=True):
numpy.testing.assert_array_equal(field, original)
@pytest.mark.parametrize('direction', [-1, 1])
def test_farfield_direction_and_roundtrip_match_outgoing_phasors(direction) -> None:
shape = (16, 8)
dx = dy = 0.25
k = 2 * numpy.pi
kx = direction * 2 * numpy.pi / (shape[0] * dx)
kz = numpy.sqrt(k * k - kx * kx)
x = (numpy.arange(shape[0]) - shape[0] // 2) * dx
phase = numpy.broadcast_to(numpy.exp(-1j * kx * x[:, None]), shape)
zeros = numpy.zeros_like(phase)
electric, magnetic = [kz / k * phase, zeros], [zeros, phase]
result = farfield.near_to_farfield(electric, magnetic, dx, dy, padded_size=shape)
intensity = numpy.abs(result['E'][0]) ** 2 + numpy.abs(result['E'][1]) ** 2
peak = numpy.unravel_index(numpy.argmax(intensity), shape)
assert result['kx'][peak] == pytest.approx(kx)
assert result['phi'][peak] == pytest.approx(numpy.arccos(kz / k))
restored = farfield.far_to_nearfield(result['E'], result['H'], result['dkx'], result['dky'], padded_size=shape)
numpy.testing.assert_allclose(restored['E'], electric, atol=1e-13)
numpy.testing.assert_allclose(restored['H'], magnetic, atol=1e-13)