Gain Components
- class tabascal.components.gains.ConstGains[source]
A single complex direction-independent (DIE) gain per antenna.
g_pis 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 - 2parameters (2 n_ant - 1with 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 modelComplexRFIVarAntthe 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-freeA_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:FixedDiscreteSkywithast_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 othern_ant - 1phases 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 - 1parameters 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 oflog|g|, which agrees with a fractional spread to first order and keeps the gain positive.gains.amp_meanis 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 andamp_meanonly sets the scale the percentage is taken of, so a non-unit value there warns.gains.fix_flux_scale: falselifts 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.initoptionally starts the fit at a previously measured gain — an.npz(keygain, 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.
- 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 normalz, and the width is what carries it to the gain — throughexp(amp_std z)inConstGains. A narrow prior is therefore a short lever, and the optimiser’s step inzis set byopt.epsilonrather than by the gradient. (UnitaryGainsfits 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 within2 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 normalzbysigmadirectly (phase = mean + sigma zinConstGains), so the likelihood —2 pi-periodic in the phase — is periodic inzwith period2 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| <= 1cover 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
gainssection of the config: no prior width, no mean and no seed applies to a gain that is fixed at unity.
- 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, andtabascal.importsaccepts 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.npzor a calibration table.The
.npzform carries the gains under the keygainand is already one value per antenna; anything else is read as a calibration table and reduced byreduce_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;
ConstGainsis 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_stdis given as a percentage ofamp_meanandphase_stdin degrees; both come back in the units the model works in — a fraction and radians.ConstGainsis the only component that reads them:UnitaryGainsfits no gain and reads nothing from the gains section at all.A key is defaulted when, and only when, it is
Noneor absent, so a literal 0 is taken at its word:r_seed: 0is the seed it says, and a zeroamp_stdorphase_stdis 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
kiskentries of1/sqrt(k(k+1))followed by-k/sqrt(k(k+1)). Each column sums to zero, andH.T @ His the identity whileH @ H.Tis the centring projectorI - J/n.This is what lets the zero-sum log amplitude be parameterised rather than projected. Writing
log_amp = sigma (I - J/n) zwithnparameters gives the same values and the same prior, but leavesz -> z + can 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.