From 112d88cac1c11d1b4b9f91fc9e7a5025f78e1a85 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Wed, 28 Oct 2026 19:03:29 -0700 Subject: [PATCH] [farfield] align Fourier projection with outgoing wave convention --- meanas/fdfd/farfield.py | 20 ++++++++++---------- meanas/test/test_fdfd_farfield.py | 23 ++++++++++++++++++++++- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 2fece74..8769289 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -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, diff --git a/meanas/test/test_fdfd_farfield.py b/meanas/test/test_fdfd_farfield.py index 2d6c0d3..a8ed907 100644 --- a/meanas/test/test_fdfd_farfield.py +++ b/meanas/test/test_fdfd_farfield.py @@ -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)