Antennas and arrays
Antenna gain and efficiency data, array embedded-pattern bases, beam combining, codebooks, and MIMO correlation metrics.
This page covers the antenna and array result types in witwin.maxwell. Import them from the top-level package (import witwin.maxwell as mw). You need them after an FDTD run that drives one or more lumped ports and records a ClosedSurfaceMonitor (a closed near-field box used for the near-to-far-field transform).
Note
These antenna and array contracts are experimental. Their fields and validation rules can change between releases.
You do not usually construct these classes yourself. They come from these calls:
| Call | Returns | Documented on |
|---|---|---|
result.antenna(surface=..., driven_port=...) | AntennaData | Result |
mw.postprocess.antenna_data_from_result(result, ...) | AntennaData | Postprocess |
mw.postprocess.compute_antenna_data(...) | AntennaData | Postprocess |
result.array_basis(monitor=...) after a PortSweep run | ArrayBasisData | Result |
ArrayBasisData.combine(weights) | BeamData | this page |
BeamData.max_hold(metric) | MaxHoldComposite | this page |
ArrayBasisData.mimo(environment) | MIMOData | this page |
All pattern tensors use the order [frequency, theta, phi], written [F, T, P]. Array tensors add a port axis N and, for codebooks, a beam axis B. Angles are in radians, powers in watts, fields in V/m. Tensors stay on the device of the run and keep their autograd graph.
import witwin.maxwell as mw
simulation = mw.Simulation.fdtd(
scene, # one LumpedPort "feed" and a ClosedSurfaceMonitor "radiation"
frequencies=[3.0e9],
excitations=mw.PortExcitation(
"feed", source_time=mw.GaussianPulse(frequency=3.0e9, fwidth=1.5e9)
),
)
result = simulation.run()
data = result.antenna(surface="radiation", driven_port="feed")
print(data.realized_gain_max, data.radiation_efficiency)AntennaData#
class AntennaData(frequencies, theta, phi, e_theta, e_phi, observation_radius,
wave_impedance, radiation_intensity, p_rad, p_accepted, p_incident,
directivity, gain, realized_gain, radiation_efficiency,
mismatch_efficiency, system_efficiency, eirp, co_polarized,
cross_polarized, axial_ratio, phase_center, frame,
polarization_basis, driven_port_name, surface_currents=None,
field_basis='spherical_theta_phi', power_normalization=..., phasor_convention=...)Frozen dataclass with frequency-domain antenna metrics for one driven port. All tensors must sit on one device; the constructor checks shapes and raises ValueError or TypeError on a mismatch.
Gain is referenced to accepted power, realized gain to incident power. Fields are peak phasors with exp(-i*omega*t) time dependence. Radiation intensity is U = r^2 * (|E_theta|^2 + |E_phi|^2) / (2 * eta) in W/sr.
| Field | Shape | Meaning |
|---|---|---|
frequencies | [F] | Frequencies in Hz (real floating point, non-empty). |
theta, phi | [T, P] | Observation angles in radians, in the local frame. |
e_theta, e_phi | [F, T, P] or None | Far-field components in V/m at observation_radius. |
observation_radius | [F, T, P] or None | Radius of the far-field evaluation, in metres. |
wave_impedance | [F, T, P] or None | Wave impedance in ohms used for radiation_intensity. |
radiation_intensity | [F, T, P] | U in W/sr. |
p_rad | [F] | Radiated power in W. |
p_accepted | [F] | Power accepted by the driven port in W. |
p_incident | [F] | Incident power at the driven port in W. |
directivity | [F, T, P] | 4*pi*U / p_rad (linear). |
gain | [F, T, P] | 4*pi*U / p_accepted (linear). |
realized_gain | [F, T, P] | 4*pi*U / p_incident (linear). |
radiation_efficiency | [F] | p_rad / p_accepted. |
mismatch_efficiency | [F] | p_accepted / p_incident. |
system_efficiency | [F] | p_rad / p_incident. |
eirp | [F] | Peak of 4*pi*U over all angles, in W. |
co_polarized, cross_polarized | [F, T, P] or None | Ludwig-3 projections of the far field. |
axial_ratio | [F, T, P] or None | Linear major/minor axial ratio; inf where the minor axis is unresolved. |
phase_center | [3] | Phase reference point in simulation coordinates, in metres. |
frame | [3, 3] | Local basis; columns are the local x, y, z axes. |
polarization_basis | Ludwig3 | Basis used for the co/cross projection. |
driven_port_name | str | Name of the driven port. |
surface_currents | tuple or None | One surface-current entry per frequency, when available. |
The optional fields e_theta, e_phi, observation_radius, wave_impedance, co_polarized, cross_polarized, and axial_ratio are either all present or all None. They are None when the data was built from radiation intensity alone.
AntennaData properties#
| Property | Returns |
|---|---|
device | torch.device of frequencies. |
directivity_db | 10 * log10(directivity), shape [F, T, P]. |
gain_db | 10 * log10(gain), shape [F, T, P]. |
realized_gain_db | 10 * log10(realized_gain), shape [F, T, P]. |
axial_ratio_db | 20 * log10(axial_ratio), or None. |
directivity_max | Maximum directivity over angles, shape [F]. |
gain_max | Maximum gain over angles, shape [F]. |
realized_gain_max | Maximum realized gain over angles, shape [F]. |
data = result.antenna(surface="radiation", driven_port="feed", theta_points=91, phi_points=73)
peak_dbi = 10.0 * torch.log10(data.gain_max)
loss = -data.realized_gain[:, 45, 0].mean() # differentiable objectiveLudwig3#
class Ludwig3(reference_angle=0.0)Frozen dataclass for the Ludwig-3 co/cross-polarization basis. With reference_angle=0, the co-polarized direction is the x-axis of AntennaData.frame at boresight.
0.0Rotation of the co-polarized reference, in radians. Must be finite; raises ValueError otherwise.
Ludwig3.project#
Ludwig3.project(e_theta, e_phi, phi) -> tuple[torch.Tensor, torch.Tensor]Projects spherical field components onto the Ludwig-3 basis.
Theta component with a leading frequency axis, shape [F, T, P].
Phi component, same shape as e_theta.
Phi angles in radians, shape [T, P].
Returns (co_polarized, cross_polarized), where co = e_theta*cos(a) - e_phi*sin(a) and cross = e_theta*sin(a) + e_phi*cos(a) with a = phi - reference_angle.
data = result.antenna(
surface="radiation", driven_port="feed", polarization=mw.Ludwig3(reference_angle=0.5 * math.pi)
)ArrayBasisData#
class ArrayBasisData(network, embedded_patterns, fingerprint, metadata={}, radiated_power_matrix=None)Frozen dataclass holding a reusable N-port basis for one physical scene: the full scattering matrix plus one power-normalized embedded element pattern per port. You get it from result.array_basis(monitor=...) after an FDTD PortSweep run. Once you have it, combine forms any beam without running the field solver again.
Complete N-port network (every excitation column valid). See Networks.
Embedded element patterns. Device, complex dtype, port order, frequencies, phasor convention, and power-wave convention must match network.
Non-empty content fingerprint. Result.array_basis computes it from the scene content, grid, ports, frequencies, monitor surface, angular grid, polarization, phase center, and dtype.
{}Free-form metadata, copied into a plain dict.
NoneOptional complex Hermitian matrix of shape [F, N, N] from the closed-surface Poynting integral. When present, combine computes radiated power as real(a^H Q a) instead of integrating the far field over angles.
Raises TypeError or ValueError when the network is incomplete, when network and patterns disagree, or when radiated_power_matrix has the wrong shape, dtype, device, or is not Hermitian.
sweep = mw.PortSweep(source_time=mw.GaussianPulse(frequency=2.5e9, fwidth=1.0e9))
result = mw.Simulation.fdtd(scene, frequency=2.5e9, excitations=sweep).run()
basis = result.array_basis(monitor="array_nf2ff", theta_points=31, phi_points=61)
print(basis.port_names, basis.eep.e_theta.shape) # [F, N, T, P]ArrayBasisData properties#
| Property | Returns |
|---|---|
frequencies | network.frequencies, shape [F]. |
port_names | network.port_names, tuple of N names. |
device | Device of the scattering matrix. |
dtype | Complex dtype of the scattering matrix. |
eep | Alias for embedded_patterns. |
cache_key | Same as fingerprint. Beam weights never change it; geometry, material, port, frequency, or surface changes do. |
ArrayBasisData.combine#
ArrayBasisData.combine(weights) -> BeamDataCombines incident power-wave weights with the stored basis. Runs no field-solver steps.
Complex incident power-wave amplitudes in sqrt(W), port axis last. Shape [N] (same at every frequency), [F, N] (exact per-frequency weights; no interpolation), or [B, F, N] (a batch of beams). A BeamCodebook is expanded to [B, F, N]. Device and dtype must match the basis.
Returns a BeamData. For [N] or [F, N] weights, the beam axis is dropped from every result tensor.
Raises ValueError if any beam has zero incident power at any frequency, or if the shape, device, or dtype is wrong.
weights = torch.ones(len(basis.port_names), device=basis.device, dtype=basis.dtype)
beam = basis.combine(weights)
print(beam.antenna.realized_gain.shape) # [F, T, P]
print(beam.network.active_impedance) # [F, N], ohmsArrayBasisData.mimo#
ArrayBasisData.mimo(environment) -> MIMODataComputes field-based MIMO metrics from the dual-polarized embedded patterns, integrated over the full sphere against the environment's angular power spectrum and cross-polar ratio.
Incident-field statistics. Its theta and phi must equal the embedded-pattern grid exactly.
Returns a MIMOData.
Raises ValueError if the angular grids differ, if the power density integrates to zero, or if any port's pattern carries no power.
ArrayBasisData.ecc_from_scattering#
ArrayBasisData.ecc_from_scattering() -> torch.TensorReturns the S-parameter envelope correlation coefficient approximation, shape [F, N, N]: ecc_ij = |sum_n conj(S_ni) S_nj|^2 / prod_k (1 - sum_n |S_nk|^2).
Warning
This formula assumes a lossless array. Ohmic or material loss does not show up in S, so a lossy array passes the input check and still gets a wrong result. Use mimo when loss matters.
Raises ValueError if 1 - sum_n |S_ni|^2 is zero or negative for any port.
ArrayBasisData.scene_gradient_vjp#
ArrayBasisData.scene_gradient_vjp(*, columns, weights, parameters, objective=None,
field_cotangents=None, reduction_order=None,
retain_graph=False)Aggregates scene-parameter gradients through the N-column basis. The stored basis is detached, so you must re-run the per-column forwards under autograd and pass the live columns here.
One (e_theta_n, e_phi_n) pair per port, each complex with shape [F, T, P], on the basis device and dtype, and differentiable with respect to parameters.
Single-beam weights of shape [N] or [F, N]. Batched [B, F, N] weights are rejected.
The trainable tensors to differentiate.
NoneFunction (E_theta, E_phi) -> real scalar tensor on the combined field. Give exactly one of objective or field_cotangents.
NonePrecomputed cotangents (cot_E_theta, cot_E_phi) on the combined field, in the torch.autograd.grad(L, E) convention, shape [F, T, P].
NoneA permutation of range(N) that fixes the order in which per-column gradients are summed.
FalseKeep the per-column autograd graphs after the last column.
Returns the gradient: one tensor if parameters is a single tensor, otherwise a tuple aligned with parameters.
Raises ValueError if both or neither of objective and field_cotangents are given, if the column count is not N, or if no column depends on parameters.
ArrayBasisData.save and load#
ArrayBasisData.save(path)
ArrayBasisData.load(path, map_location=None) -> ArrayBasisData # classmethodsave writes a detached CPU snapshot (network, embedded patterns, fingerprint, metadata, and radiated_power_matrix) and creates parent folders. The live autograd graph is not saved. load reads it with torch.load(weights_only=True) and moves tensors to map_location. Metadata must contain only primitive values and tensors.
basis.save("runs/array_basis.pt")
restored = mw.ArrayBasisData.load("runs/array_basis.pt", map_location="cuda")EmbeddedElementPatternData#
class EmbeddedElementPatternData(frequencies, port_names, theta, phi, e_theta, e_phi,
phase_center, frame, observation_radius=1.0,
wave_impedance=ETA_0, polarization_basis=Ludwig3(),
phase_center_source='explicit', ...)Frozen dataclass with one far-field pattern per port. Each column is the field radiated when that port receives a unit incident power wave (1 sqrt(W)) and every other port is matched. Field units are V/m per sqrt(W).
Frequencies in Hz, shape [F], real, finite, strictly positive, and strictly increasing.
One unique non-empty name per pattern column.
Polar angle in radians, shape [T, P], constant along phi, strictly increasing, from 0 to pi. At least 3 theta samples.
Azimuth in radians, shape [T, P], constant along theta, strictly increasing, spanning exactly 2*pi. At least 2 phi samples.
Complex, finite, shape [F, N, T, P].
Complex, finite, same shape, dtype, and device as e_theta.
Real tensor of shape [3], in metres.
Real [3, 3] right-handed orthonormal basis; columns are the local axes.
1.0Far-field radius in metres. Scalar or broadcastable to [F, T, P]; finite and positive. Stored as a [F, T, P] tensor.
ETA_0Wave impedance in ohms (free-space impedance by default). Same broadcasting rules as observation_radius.
Ludwig3()Polarization basis carried with the patterns.
'explicit'Either 'explicit' or 'array_aabb'.
The remaining fields (field_basis, power_normalization, phasor_convention, power_wave_convention, field_units) record conventions. The constructor accepts only their default values.
Properties: device, dtype (complex dtype of the fields), and the aliases E_theta and E_phi. save(path) and the classmethod load(path, map_location=None) behave like the ArrayBasisData versions.
BeamWeights#
class BeamWeights(values)Frozen wrapper for complex incident power-wave amplitudes, port axis last.
Complex, finite tensor of shape [N], [F, N], or [B, F, N], in sqrt(W).
beam = basis.combine(mw.BeamWeights(torch.ones(4, dtype=basis.dtype, device=basis.device)))BeamCodebook#
class BeamCodebook(weights, names, target_angles=None, metadata={})A named set of beam weight vectors for one basis. It holds no solver state; pass it to ArrayBasisData.combine.
Complex, finite weights of shape [B, N] (same at every frequency) or [B, F, N].
One unique non-empty name per beam. Stored as a tuple of strings.
NoneReal tensor of shape [B, 2] holding (theta, phi) per beam, in radians.
{}Free-form metadata, copied into a plain dict.
Property beam_count returns B.
BeamCodebook.from_scan_angles#
BeamCodebook.from_scan_angles(*, element_positions, frequencies, theta, phi,
amplitude=1.0, speed_of_light=C_0,
name_prefix='scan') -> BeamCodebook # classmethodBuilds progressive-phase steering weights a[b, n] = amplitude[n] * exp(+j k (r_n . d_b)), where d_b is the unit vector toward scan direction b. Element positions are not read from the scene; you supply them.
Real tensor of shape [N, 3], in metres, in port order.
Real tensor of frequencies in Hz, shape [F], on the same device.
Scan polar angles in radians, 1-D, one per beam.
Scan azimuth angles in radians, same length as theta.
1.0Scalar or per-port vector of length N.
C_0Propagation speed in m/s used for the wavenumber.
'scan'Prefix for generated beam names, which also encode theta and phi.
Returns a codebook with weights [B, F, N] (complex128 for float64 positions, otherwise complex64) and target_angles set.
codebook = mw.BeamCodebook.from_scan_angles(
element_positions=positions, # [N, 3], metres
frequencies=basis.frequencies,
theta=torch.full((16,), math.pi / 2, dtype=torch.float64),
phi=torch.linspace(-0.5, 0.5, 16, dtype=torch.float64),
)
beams = basis.combine(codebook)
envelope = beams.max_hold("realized_gain")BeamCodebook.to_weights#
BeamCodebook.to_weights(*, frequency_count, port_count, device, dtype) -> torch.TensorReturns the weights as [B, F, N], expanding frequency-flat weights. Raises ValueError or TypeError if the shape, device, or dtype does not match.
BeamData#
class BeamData(weights, network, far_field, antenna, frequencies, names=None, metadata={}, ...)Frozen result of ArrayBasisData.combine. You read it; you do not build it. When you combine a batch, every tensor below has a leading beam axis B; for a single beam it does not.
| Field | Meaning |
|---|---|
weights | Incident power waves a used for the combine, sqrt(W). |
network | Port-side quantities (table below). |
far_field | Combined far field (table below). |
antenna | Beam antenna metrics (table below). |
frequencies | [F] in Hz. |
names | Beam names from a codebook, else None. |
metadata | Includes basis_fingerprint, solver_rerun (always False), and radiated_power_source. |
units | Unit strings for weights, power, impedance, far field, intensity, gain, and EIRP. |
network fields: a, b (reflected waves), incident_power_per_port, reflected_power_per_port, accepted_power_per_port (shape [..., F, N], W), incident_power, reflected_power, accepted_power (shape [..., F], W), active_reflection and active_impedance (ohms; NaN for ports with zero drive), active_mask, z0, port_names, frequencies.
far_field fields: e_theta, e_phi ([..., F, T, P], V/m) with aliases E_theta, E_phi, plus theta, phi, phase_center, frame, observation_radius, wave_impedance, polarization_basis.
antenna fields: radiation_intensity, directivity, gain, realized_gain ([..., F, T, P]), and p_rad, radiation_efficiency, mismatch_efficiency, system_efficiency, eirp, radiation_valid, accepted_power_valid ([..., F]). Directivity and gain are NaN where radiated or accepted power is not positive.
Properties: device, and is_batched (True when weights has rank 3).
BeamData.max_hold#
BeamData.max_hold(metric='realized_gain') -> MaxHoldCompositeTakes the per-direction maximum of a metric across the beam axis. The envelope value is differentiable through the winning beam; the winning-beam index is not.
'realized_gain'One of 'realized_gain', 'gain', 'directivity', 'radiation_intensity', or 'eirp'. Angular metrics reduce [B, F, T, P] to [F, T, P]; 'eirp' reduces [B, F] to [F].
Raises ValueError if the result is not batched, if metric is unknown, or if any beam has a NaN value for the metric.
MaxHoldComposite#
class MaxHoldComposite(metric, envelope, winning_beam, frequencies, names=None)Frozen envelope returned by BeamData.max_hold.
| Field | Meaning |
|---|---|
metric | Name of the reduced metric. |
envelope | Per-direction maximum, [F, T, P] (or [F] for eirp). |
winning_beam | Index of the beam that wins each entry, same shape as envelope. |
frequencies | [F] in Hz. |
names | Beam names, or None. |
Property device returns the envelope device.
MultipathEnvironment#
class MultipathEnvironment(theta, phi, power_density=1.0, cross_polar_ratio_db=0.0,
polarization_correlation=0.0)Frozen description of the angular power spectrum and polarization statistics of the incident field, used by ArrayBasisData.mimo.
Full-sphere polar grid in radians, [T, P]. Same validation as EmbeddedElementPatternData.theta.
Full-sphere azimuth grid in radians, [T, P].
1.0Angular power density. A scalar means uniform; a tensor must have shape [T, P]. Must be finite and non-negative. mimo normalizes it to integrate to one.
0.0Cross-polar ratio (theta power over phi power) in dB. Must be finite.
0.0Correlation between the theta and phi incident components; magnitude at most 1.
Public helper methods: resolved_power_density(*, device, dtype) returns the [T, P] density tensor, cross_polar_ratio(*, device, dtype) returns the linear ratio, polarization_correlation_value(*, device, dtype) returns the complex scalar, and metadata_snapshot() returns a dict describing the environment.
environment = mw.MultipathEnvironment(
theta=basis.eep.theta, phi=basis.eep.phi, cross_polar_ratio_db=6.0
)
mimo = basis.mimo(environment)
print(mimo.ecc[0]) # [N, N] at the first frequencyMIMOData#
class MIMOData(correlation, ecc, diversity_gain, mean_effective_gain, frequencies,
port_names, environment_metadata, source)Frozen MIMO correlation metrics returned by ArrayBasisData.mimo.
| Field | Shape | Meaning |
|---|---|---|
correlation | [F, N, N] | Complex Hermitian correlation matrix of the embedded patterns. |
ecc | [F, N, N] | Envelope correlation coefficient, abs(R_ij)^2 / (R_ii R_jj). |
diversity_gain | [F, N, N] | Apparent diversity gain, 10 * sqrt(1 - ecc). |
mean_effective_gain | [F, N] | Mean effective gain per port (linear). |
frequencies | [F] | Frequencies in Hz. |
port_names | tuple | Port order. |
environment_metadata | dict | MultipathEnvironment.metadata_snapshot() of the environment used. |
source | str | 'dual_polarized_far_field_integral'. |
Property device returns the device of correlation.