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:

CallReturnsDocumented on
result.antenna(surface=..., driven_port=...)AntennaDataResult
mw.postprocess.antenna_data_from_result(result, ...)AntennaDataPostprocess
mw.postprocess.compute_antenna_data(...)AntennaDataPostprocess
result.array_basis(monitor=...) after a PortSweep runArrayBasisDataResult
ArrayBasisData.combine(weights)BeamDatathis page
BeamData.max_hold(metric)MaxHoldCompositethis page
ArrayBasisData.mimo(environment)MIMODatathis 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.

FieldShapeMeaning
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 NoneFar-field components in V/m at observation_radius.
observation_radius[F, T, P] or NoneRadius of the far-field evaluation, in metres.
wave_impedance[F, T, P] or NoneWave 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 NoneLudwig-3 projections of the far field.
axial_ratio[F, T, P] or NoneLinear 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_basisLudwig3Basis used for the co/cross projection.
driven_port_namestrName of the driven port.
surface_currentstuple or NoneOne 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#

PropertyReturns
devicetorch.device of frequencies.
directivity_db10 * log10(directivity), shape [F, T, P].
gain_db10 * log10(gain), shape [F, T, P].
realized_gain_db10 * log10(realized_gain), shape [F, T, P].
axial_ratio_db20 * log10(axial_ratio), or None.
directivity_maxMaximum directivity over angles, shape [F].
gain_maxMaximum gain over angles, shape [F].
realized_gain_maxMaximum 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 objective

Ludwig3#

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.

reference_anglefloatdefault 0.0

Rotation 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.

e_thetatorch.Tensorrequired

Theta component with a leading frequency axis, shape [F, T, P].

e_phitorch.Tensorrequired

Phi component, same shape as e_theta.

phitorch.Tensorrequired

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.

networkNetworkDatarequired

Complete N-port network (every excitation column valid). See Networks.

embedded_patternsEmbeddedElementPatternDatarequired

Embedded element patterns. Device, complex dtype, port order, frequencies, phasor convention, and power-wave convention must match network.

fingerprintstrrequired

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.

metadataMapping[str, Any]default {}

Free-form metadata, copied into a plain dict.

radiated_power_matrixtorch.Tensor | Nonedefault None

Optional 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#

PropertyReturns
frequenciesnetwork.frequencies, shape [F].
port_namesnetwork.port_names, tuple of N names.
deviceDevice of the scattering matrix.
dtypeComplex dtype of the scattering matrix.
eepAlias for embedded_patterns.
cache_keySame as fingerprint. Beam weights never change it; geometry, material, port, frequency, or surface changes do.

ArrayBasisData.combine#

ArrayBasisData.combine(weights) -> BeamData

Combines incident power-wave weights with the stored basis. Runs no field-solver steps.

weightsBeamWeights | BeamCodebook | torch.Tensorrequired

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], ohms

ArrayBasisData.mimo#

ArrayBasisData.mimo(environment) -> MIMOData

Computes 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.

environmentMultipathEnvironmentrequired

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.Tensor

Returns 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.

columnsSequence[tuple[torch.Tensor, torch.Tensor]]required

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.

weightsBeamWeights | torch.Tensorrequired

Single-beam weights of shape [N] or [F, N]. Batched [B, F, N] weights are rejected.

parameterstorch.Tensor | Sequence[torch.Tensor]required

The trainable tensors to differentiate.

objectiveCallable | Nonedefault None

Function (E_theta, E_phi) -> real scalar tensor on the combined field. Give exactly one of objective or field_cotangents.

field_cotangentstuple[torch.Tensor, torch.Tensor] | Nonedefault None

Precomputed cotangents (cot_E_theta, cot_E_phi) on the combined field, in the torch.autograd.grad(L, E) convention, shape [F, T, P].

reduction_orderSequence[int] | Nonedefault None

A permutation of range(N) that fixes the order in which per-column gradients are summed.

retain_graphbooldefault False

Keep 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   # classmethod

save 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).

frequenciestorch.Tensorrequired

Frequencies in Hz, shape [F], real, finite, strictly positive, and strictly increasing.

port_namestuple[str, ...]required

One unique non-empty name per pattern column.

thetatorch.Tensorrequired

Polar angle in radians, shape [T, P], constant along phi, strictly increasing, from 0 to pi. At least 3 theta samples.

phitorch.Tensorrequired

Azimuth in radians, shape [T, P], constant along theta, strictly increasing, spanning exactly 2*pi. At least 2 phi samples.

e_thetatorch.Tensorrequired

Complex, finite, shape [F, N, T, P].

e_phitorch.Tensorrequired

Complex, finite, same shape, dtype, and device as e_theta.

phase_centertorch.Tensorrequired

Real tensor of shape [3], in metres.

frametorch.Tensorrequired

Real [3, 3] right-handed orthonormal basis; columns are the local axes.

observation_radiusfloat | torch.Tensordefault 1.0

Far-field radius in metres. Scalar or broadcastable to [F, T, P]; finite and positive. Stored as a [F, T, P] tensor.

wave_impedancefloat | torch.Tensordefault ETA_0

Wave impedance in ohms (free-space impedance by default). Same broadcasting rules as observation_radius.

polarization_basisLudwig3default Ludwig3()

Polarization basis carried with the patterns.

phase_center_sourcestrdefault '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.

valuestorch.Tensorrequired

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.

weightstorch.Tensorrequired

Complex, finite weights of shape [B, N] (same at every frequency) or [B, F, N].

namesSequence[str]required

One unique non-empty name per beam. Stored as a tuple of strings.

target_anglestorch.Tensor | Nonedefault None

Real tensor of shape [B, 2] holding (theta, phi) per beam, in radians.

metadataMapping[str, Any]default {}

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   # classmethod

Builds 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.

element_positionstorch.Tensorrequired

Real tensor of shape [N, 3], in metres, in port order.

frequenciestorch.Tensorrequired

Real tensor of frequencies in Hz, shape [F], on the same device.

thetatorch.Tensorrequired

Scan polar angles in radians, 1-D, one per beam.

phitorch.Tensorrequired

Scan azimuth angles in radians, same length as theta.

amplitudetorch.Tensor | floatdefault 1.0

Scalar or per-port vector of length N.

speed_of_lightfloatdefault C_0

Propagation speed in m/s used for the wavenumber.

name_prefixstrdefault '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.Tensor

Returns 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.

FieldMeaning
weightsIncident power waves a used for the combine, sqrt(W).
networkPort-side quantities (table below).
far_fieldCombined far field (table below).
antennaBeam antenna metrics (table below).
frequencies[F] in Hz.
namesBeam names from a codebook, else None.
metadataIncludes basis_fingerprint, solver_rerun (always False), and radiated_power_source.
unitsUnit 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') -> MaxHoldComposite

Takes 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.

metricstrdefault '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.

FieldMeaning
metricName of the reduced metric.
envelopePer-direction maximum, [F, T, P] (or [F] for eirp).
winning_beamIndex of the beam that wins each entry, same shape as envelope.
frequencies[F] in Hz.
namesBeam 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.

thetatorch.Tensorrequired

Full-sphere polar grid in radians, [T, P]. Same validation as EmbeddedElementPatternData.theta.

phitorch.Tensorrequired

Full-sphere azimuth grid in radians, [T, P].

power_densitytorch.Tensor | floatdefault 1.0

Angular 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.

cross_polar_ratio_dbfloatdefault 0.0

Cross-polar ratio (theta power over phi power) in dB. Must be finite.

polarization_correlationcomplexdefault 0.0

Correlation 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 frequency

MIMOData#

class MIMOData(correlation, ecc, diversity_gain, mean_effective_gain, frequencies,
               port_names, environment_metadata, source)

Frozen MIMO correlation metrics returned by ArrayBasisData.mimo.

FieldShapeMeaning
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_namestuplePort order.
environment_metadatadictMultipathEnvironment.metadata_snapshot() of the environment used.
sourcestr'dual_polarized_far_field_integral'.

Property device returns the device of correlation.