Far field, directivity and RCS
Record a closed Huygens surface, project the near field to the far field, and compute radiation patterns, directivity, gain and bistatic radar cross section.
To get a radiation pattern or a radar cross section, record the tangential fields on a closed box around the radiator or scatterer, then project them to any distance with NearFieldFarFieldTransformer. The simulation domain only needs to hold the object and the box; the far field is computed afterwards. The functions are in witwin.maxwell.postprocess and listed on the Postprocess API page.
Record a Huygens surface#
Add a ClosedSurfaceMonitor.box that encloses every source and object, with some clearance, and stays inside the physical domain (outside the PML):
import math
import torch
import witwin.maxwell as mw
f0 = 1.0e9
scene = mw.Scene(
domain=mw.Domain(bounds=((-0.4, 0.4), (-0.4, 0.4), (-0.4, 0.4))),
grid=mw.GridSpec.uniform(0.02),
boundary=mw.BoundarySpec.pml(num_layers=10),
device="cuda",
)
scene.add_source(
mw.PointDipole(position=(0.0, 0.0, 0.0), polarization="Ez",
source_time=mw.GaussianPulse(frequency=f0, fwidth=0.3e9), name="dipole")
)
scene.add_monitor(
mw.ClosedSurfaceMonitor.box("huygens", position=(0.0, 0.0, 0.0), size=(0.5, 0.5, 0.5), frequencies=(f0,))
)
result = mw.Simulation.fdtd(scene, frequencies=[f0]).run()The box records E and H on its six faces at each listed frequency.
Project to the far field#
from witwin.maxwell.postprocess import (
NearFieldFarFieldTransformer,
compute_directivity,
equivalent_surface_currents_from_monitor,
)
currents = equivalent_surface_currents_from_monitor(result, "huygens")
transformer = NearFieldFarFieldTransformer(currents, solver=result.solver)
theta = torch.linspace(0.0, math.pi, 91) # polar angle, rad
phi = torch.linspace(0.0, 2.0 * math.pi, 181) # azimuth, rad
far = transformer.transform(theta[:, None], phi[None, :], radius=100.0) # radius in mfar is a dictionary with the spherical components E_theta, E_phi, H_theta, H_phi, the Cartesian components, power_density (W/m²) and the sampling grid. Shape the angles as theta[:, None] and phi[None, :] to get a 2D (theta, phi) grid, which the pattern functions expect.
The transformer runs on CUDA by default. Pass device="cpu" to run it on a machine without a GPU, for example on a saved result.
Directivity and beam width#
metrics = compute_directivity(far)
print(float(metrics["D_max_db"])) # peak directivity, dBi
print(math.degrees(float(metrics["D_max_theta"]))) # direction of the peak
print(metrics["beam_width_e_plane"]) # half-power beam width in the phi = 0 cut, degreesFor this short dipole, the script prints a peak of about 2.2 dBi at θ = 90° and a 91° beam width, close to the 1.76 dBi and 90° of an ideal short dipole. The difference comes from the coarse 20 mm grid. The whole script runs in a few seconds on a recent GPU.
compute_directivity integrates over the angles you sample, so sample the whole sphere. Pass input_power= (W) to also get gain, G_max_db and radiation_efficiency. For an antenna fed by a port, result.antenna(surface="huygens", driven_port="feed") combines the port's accepted power with the surface and returns gain, realized gain and efficiency (see the Antennas and arrays API).
Bistatic radar cross section#
For a scatterer, illuminate it with a single PlaneWave (ideally with a TFSF box, see Choose sources and waveforms) and put the Huygens box outside the TFSF box, so it records only the scattered field. Then:
from witwin.maxwell.postprocess import compute_bistatic_rcs
rcs = compute_bistatic_rcs(far, result=result, solver=result.solver)
print(rcs["rcs_dbsm"]) # RCS in dB relative to 1 m², per (theta, phi)The incident amplitude is read from the scene's single plane wave (its source_time.amplitude). With a pulsed source, the recorded spectra carry the pulse spectrum instead, so either drive the plane wave with CW, or run with spectral_sampler=mw.SpectralSampler(normalize_source=True) to divide the spectra by the source spectrum. Without one of these, the RCS is off by orders of magnitude. rcs also holds rcs (m²) and normalized_rcs (RCS divided by the wavelength squared). transform_to_bistatic_rcs(transformer, theta, phi, result=result) does the projection and the RCS step in one call.
Warning
Check absolute RCS values against a known case before you rely on them. In tests of 0.5.1 with a wavelength-sized dielectric sphere, the pattern shape followed the Mie solution, but absolute values changed by up to a factor of four with the number of PML layers and the domain size at a fixed grid. Moving the Huygens box did not change them.
Other tools#
| Need | Use |
|---|---|
| Fields at a finite distance, including near-zone terms | StrattonChuPropagator(currents, ...) with propagate_points or propagate_plane |
| A pattern from one plane instead of a closed box (for example above a planar array) | equivalent_surface_currents_from_monitor on a plane monitor, then the same transformer |
| Beams from a phased array | result.array_basis(...) after a PortSweep run, then ArrayBasisData.combine(weights) (Antennas and arrays API) |
The closed-surface projection assumes the medium outside the box is homogeneous; it raises NotImplementedError otherwise. Keep substrates and ground planes inside the box, or use a plane monitor.