Gain Components

class tabascal.components.gains.ConstGains[source]

A single complex direction-independent (DIE) gain per antenna.

g_p is constant across time and frequency — the static DIE gain the array is known to have — and is FITTED, one complex number per antenna:

vis_obs[p, q] = g_p conj(g_q) (vis_ast[p, q] + vis_rfi[p, q])

Unlike a gain that varies over the observation this adds only 2 n_ant - 2 parameters (2 n_ant - 1 with the flux scale freed), and unlike a fixed gain it is constrained by the data.

What has to be true for it to be identifiable (issue #124). A gain is only constrained by a model term the gain cannot deform:

  • Pair it with rfi_signal:ComplexRFIConstAnt. With the per-antenna RFI model ComplexRFIVarAnt the gain is an exact no-op on the RFI term — g_p A_p conj(g_q A_q) is a reparametrisation of an already-free A_p — so setup warns when the two are combined.

  • The astronomical GP (ast_vis:GPVisAst) has per-baseline freedom and absorbs a gain in the same way. A rigid sky — ast_signal:FixedDiscreteSky with ast_vis:DiscreteSkyVis — is what anchors the gain’s overall scale.

The gauge. The gain is purely RELATIVE and carries no absolute flux scale:

  • the overall PHASE is unobservable, so gains.ref_ant’s phase is pinned to 0 and the other n_ant - 1 phases are free. The default reference is the first antenna with any unflagged data. One reference only pins one connected group of antennas, so setup also refuses an array the unflagged baselines split in two;

  • the overall AMPLITUDE is degenerate with the RFI source amplitude and the astronomical amplitude, so it is REMOVED by construction: the log amplitudes are carried by n_ant - 1 parameters on an orthonormal basis of the zero-sum subspace (zero_sum_basis()), giving a geometric mean |g| of exactly 1. Left free, the fit simply drifts (it settled at a median |g| of 0.70 in an earlier run, with the sky model absorbing the reciprocal) — a nuisance direction that buys nothing and slows convergence.

Both removed directions are removed from the parameters, not merely from the value they map to, so no latent coordinate is invisible to the data: such a coordinate is flat in the likelihood however much data there is, which ruins the conditioning of the fit and makes a likelihood-only Fisher matrix singular.

The prior on |g_p| is lognormal: gains.amp_std (a percentage) is used as the standard deviation of log|g|, which agrees with a fractional spread to first order and keeps the gain positive. gains.amp_mean is then the median of that prior — its centre in log space, not its arithmetic mean — and it is that only when the flux scale is free: under the zero-sum gauge the geometric mean is 1 by construction and amp_mean only sets the scale the percentage is taken of, so a non-unit value there warns.

gains.fix_flux_scale: false lifts the amplitude constraint, and is accepted only with a fixed-flux sky in the model, which is the one thing that can set the scale. gains.init optionally starts the fit at a previously measured gain — an .npz (key gain, shape (n_ant,)) or a calibration table — which is a much better starting point than the prior mean. Any such gain is projected into the gauge above rather than taken as given.

It binds the observation shapes and the amplitude and phase scales of the prior, and nothing else: the correlation lengths that used to sit beside them belonged to the Gaussian process gain removed in #129, and a constant gain has none.

build_constants()[source]

Return arrays that do not change during the forward pass.

Returns a dict of array_name -> array_value. These will be stored in constants as “_c/<ClassName>/array_name” by Model.__init__.

build_forward()[source]

Return pure, JIT-compatible function

build_set_params()[source]

Build parameter sampling function (optional)

setup(tab_config: TabConfig)[source]

All validation and error-prone operations here

tabascal.components.gains.DEFAULT_AMP_STD_PERCENT = 20.0

The width of the amplitude prior when the config leaves it unset, as a percentage of amp_mean. A real array’s antennas differ in sensitivity by far more than a percent, so a prior that narrow is a claim about the array rather than an absence of one, and it is also the fit’s own ceiling: the fitted parameter is always a standard normal z, and the width is what carries it to the gain — through exp(amp_std z) in ConstGains. A narrow prior is therefore a short lever, and the optimiser’s step in z is set by opt.epsilon rather than by the gradient. (UnitaryGains fits no gain and reads neither width.)

tabascal.components.gains.DEFAULT_PHASE_STD_DEGREES = 180.0

The width of the phase prior when the config leaves it unset, in degrees. Chosen to say as close to nothing about the phase as a Gaussian on an angle can.

The prior the model sees is the WRAPPED normal, whose density is (1 + 2 sum_k exp(-(k sigma)^2 / 2) cos(k t)) / 2 pi — uniform to within 2 exp(-sigma^2 / 2), which is 1.4 % at half a turn and 5e-9 at a full one. The extra six decades buy nothing: no fit is sensitive to a 1.4 % tilt in the prior over the circle, and the full turn costs something. The phase is carried from the fitted standard normal z by sigma directly (phase = mean + sigma z in ConstGains), so the likelihood — 2 pi-periodic in the phase — is periodic in z with period 2 pi / sigma, which halves as sigma doubles, putting twice as many whole-turn copies of every optimum inside the prior’s bulk. Half a turn also makes |z| <= 1 cover the whole circle, so any phase, including one read from a measured gain, starts the fit inside the prior rather than tens of sigma outside it.

class tabascal.components.gains.UnitaryGains[source]

A perfect gain of 1 on every antenna, over the whole time–frequency grid.

Nothing is fitted:

vis_obs[p, q] = vis_ast[p, q] + vis_rfi[p, q]

Which is why it reads nothing from the gains section of the config: no prior width, no mean and no seed applies to a gain that is fixed at unity.

build_forward()[source]

Build the forward computation function

setup(tab_config: TabConfig)[source]

All validation and error-prone operations here

tabascal.components.gains.antenna_connectivity(flags: Array, a1: Array, a2: Array, n_ant: int) Tuple[NDArray, List[List[int]]][source]

Which antennas carry unflagged data, and the groups the baselines join them into.

Two things a gain phase needs, which are not the same thing:

  • an antenna every one of whose baselines is flagged everywhere contributes nothing to the likelihood, so its own gain is unconstrained;

  • a phase is only ever measured relative to another antenna’s, along a chain of baselines that carry data. Antennas in different connected components of that graph share no reference at all, so pinning one component’s phase says nothing about another’s, whose overall phase stays flat however the reference is chosen.

Returns the per-antenna data mask and the connected components, each a sorted antenna list, ordered by their first antenna. Antennas with no data at all are left out rather than counted as components of their own.

tabascal.components.gains.component_class_names(tab_config) List[str][source]

The bare class names of model.components, however they were written.

"gains:ConstGains" and "gains.ConstGains" are the same component, and tabascal.imports accepts either, so a rule keyed on which components are present has to read them the same way.

tabascal.components.gains.read_const_gain(path: str, n_ant: int) NDArray[source]

A measured (n_ant,) complex gain, from an .npz or a calibration table.

The .npz form carries the gains under the key gain and is already one value per antenna; anything else is read as a calibration table and reduced by reduce_caltable_gains().

tabascal.components.gains.reduce_caltable_gains(path: str, n_ant: int) NDArray[source]

One complex gain per antenna from a calibration table.

A caltable is resolved over frequency and time; ConstGains is not, so the table is reduced to the median |g| and the mean phase direction over the valid samples of each antenna. The phase is reduced as a direction rather than as a number — the median of a wrapped quantity is not well defined, and a component-wise median of the complex value biases the amplitude low wherever the phase varies, which is exactly the case worth reporting rather than hiding.

Flagged solutions come back from read_caltable() as NaN and are dropped. An antenna with no solution at all falls back to unit gain, with a warning: a dead antenna in the table is usually dead in the data too, and unit gain is the one value that says nothing about it.

tabascal.components.gains.validate_gain_scales(gains_config: Dict) Dict[source]

Validate and normalise the scale parameters of the gain prior, in place.

amp_std is given as a percentage of amp_mean and phase_std in degrees; both come back in the units the model works in — a fraction and radians. ConstGains is the only component that reads them: UnitaryGains fits no gain and reads nothing from the gains section at all.

A key is defaulted when, and only when, it is None or absent, so a literal 0 is taken at its word: r_seed: 0 is the seed it says, and a zero amp_std or phase_std is an error rather than a width — a zero-width prior pins every gain to its mean, which is a mistake worth naming rather than a default worth guessing.

tabascal.components.gains.zero_sum_basis(n: int) NDArray[source]

An orthonormal (n, n - 1) basis of the zero-sum subspace of R^n.

The Helmert contrasts: column k is k entries of 1/sqrt(k(k+1)) followed by -k/sqrt(k(k+1)). Each column sums to zero, and H.T @ H is the identity while H @ H.T is the centring projector I - J/n.

This is what lets the zero-sum log amplitude be parameterised rather than projected. Writing log_amp = sigma (I - J/n) z with n parameters gives the same values and the same prior, but leaves z -> z + c an exact null direction of the forward model: a coordinate no visibility can see, curved only by the prior, which ruins the conditioning of the fit and makes a likelihood-only Fisher matrix singular.