Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FRF noise covariance calculation library #977

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions hera_cal/frf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import numpy as np
from hera_filters import dspec
from hera_cal.noise import predict_noise_variance_from_autos

from . import utils
from scipy.interpolate import interp1d
Expand Down Expand Up @@ -1998,3 +1999,233 @@
ap.add_argument("--equalize_interleave_ntimes", action="store_true", default=False, help=desc)

return ap

def get_frop_for_noise(times, filter_cent_use, filter_half_wid_use, freqs,
t_avg=300., cutoff=1e-9, weights=None, coherent_avg=True,
wgt_tavg_by_nsample=True, nsamples=None, rephase=True,
bl_vec=None, lat=-30.721526120689507, dlst=None):
"""
Get FRF + coherent time average operator for the purposes of noise
covariance calculation. Composes time averaging and the main lobe FRF into
a single operation.

Parameters:
times (array):
(Interleaved) Julian Dates on hd, converted to seconds.
filter_cent_use (float):
Filter center for main lobe filter, in Hz.
filter_half_wid_use (float):
Filter half width for main lobe filter, in Hz.
freqs (array):
Frequencies in the data (in Hz).
t_avg (float):
Desired coherent averaging length, in seconds.
cutoff (float):
Eigenvalue cutoff for DPSS modes.
weights (array):
Array of weights to use for main lobe filter.
None (default) uses uniform weights.
coherent_avg (bool):
Whether to coherently average on the t_avg cadence or not.
wgt_tavg_by_nsample (bool):
Whether to weight the time average by nsamples.
Otherwise do uniform weighting.
nsamples (array):
Array proportional to how many nights contributed to each sample.
rephase (bool):
Whether to rephase before averaging.
bl_vec (array):
Baseline vector in ENU
lat (float):
Array latitude in degrees North.
dlst (float or array_like):
Amount of LST to rephase by, in radians. If float, rephases all
times by the same amount.

Returns:
frop (array):
Filter operator. Shape (Ntimes_coarse, Ntimes_fine, Nfreqs.)
"""

# Time handling is a slightly modified port from hera_filters/hera_cal

Ntimes = len(times)
Nfreqs = len(freqs)

Check warning on line 2053 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2052-L2053

Added lines #L2052 - L2053 were not covered by tests

dmatr, _ = dspec.dpss_operator(times, np.array([filter_cent_use, ]),

Check warning on line 2055 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2055

Added line #L2055 was not covered by tests
np.array([filter_half_wid_use, ]),
eigenval_cutoff=np.array([cutoff, ]))
Nmodes = dmatr.shape[-1]

Check warning on line 2058 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2058

Added line #L2058 was not covered by tests

if weights is None:
weights = np.ones([Ntimes, Nfreqs])

Check warning on line 2061 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2060-L2061

Added lines #L2060 - L2061 were not covered by tests

#####Index Legend#####
# a = DPSS mode #
# f = frequency #
# t = fine time #
# T = coarse time #
#####Index Legend#####

ddagW = dmatr.T.conj()[:, np.newaxis] * weights.T # aft
ddagWd = ddagW @ dmatr # afa
lsq = np.linalg.solve(ddagWd.swapaxes(0,1), ddagW.swapaxes(0,1)) # fat

Check warning on line 2072 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2070-L2072

Added lines #L2070 - L2072 were not covered by tests

if coherent_avg:
dtime = np.median(np.abs(np.diff(times)))
chunk_size = int(np.round((t_avg / dtime)))
Nchunk = int(np.ceil(Ntimes / chunk_size))
chunk_remainder = Ntimes % chunk_size

Check warning on line 2078 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2074-L2078

Added lines #L2074 - L2078 were not covered by tests

tavg_weights = nsamples if wgt_tavg_by_nsample else np.where(weights, np.ones([Ntimes, Nfreqs]), 0)
if chunk_remainder > 0: # Stack some 0s that get 0 weight so we can do the reshaping below without incident

Check warning on line 2081 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2080-L2081

Added lines #L2080 - L2081 were not covered by tests

dmatr_stack_shape = [chunk_size - chunk_remainder, Nmodes]
weights_stack_shape = [chunk_size - chunk_remainder, Nfreqs]
dmatr = np.vstack((dmatr, np.zeros(dmatr_stack_shape, dtype=complex)))
tavg_weights = np.vstack((tavg_weights, np.zeros(weights_stack_shape, dtype=complex)))
dlst = np.append(dlst, np.zeros(chunk_size - chunk_remainder, dtype=float))

Check warning on line 2087 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2083-L2087

Added lines #L2083 - L2087 were not covered by tests

dres = dmatr.reshape(Nchunk, chunk_size, Nmodes)
wres = tavg_weights.reshape(Nchunk, chunk_size, Nfreqs)
wnorm = wres.sum(axis=1)[:, np.newaxis]

Check warning on line 2091 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2089-L2091

Added lines #L2089 - L2091 were not covered by tests
# normalize for an average
wres = np.where(wnorm > 0, wres / wnorm, 0)

Check warning on line 2093 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2093

Added line #L2093 was not covered by tests

# Apply the rephase to the weights matrix since it's mathematically equivalent and convenient
if rephase:
wres = lst_rephase(wres.reshape(Nchunk * chunk_size, 1, Nfreqs),

Check warning on line 2097 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2096-L2097

Added lines #L2096 - L2097 were not covered by tests
bl_vec, freqs, dlst, lat=lat, inplace=False)
wres = wres.reshape(Nchunk, chunk_size, Nfreqs)

Check warning on line 2099 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2099

Added line #L2099 was not covered by tests

# does "Ttf,Tta->Tfa" much faster than einsum and fancy indexing
dchunk = np.zeros([Nchunk, Nfreqs, Nmodes], dtype=complex)
for coarse_time_ind in range(Nchunk):
dchunk[coarse_time_ind] = np.tensordot(wres[coarse_time_ind].T,

Check warning on line 2104 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2102-L2104

Added lines #L2102 - L2104 were not covered by tests
dres[coarse_time_ind],
axes=1)

# does "Tfa,fat->Ttf" faster than einsum and fancy indexing
frop = np.zeros([Nchunk, Ntimes, Nfreqs], dtype=complex)
for freq_ind in range(Nfreqs):
frop[:, :, freq_ind] = np.tensordot(dchunk[:, freq_ind],

Check warning on line 2111 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2109-L2111

Added lines #L2109 - L2111 were not covered by tests
lsq[freq_ind],
axes=1)
else:
# ta,fat->ttf
frop = np.tensordot(dmatr, lsq.transpose(1, 2, 0), axes=1)

Check warning on line 2116 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2116

Added line #L2116 was not covered by tests

return frop

Check warning on line 2118 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2118

Added line #L2118 was not covered by tests

def prep_var_for_frop(data, nsamples, weights, cross_antpairpol, freq_slice,
auto_ant=None, default_value=0.):
"""
Wrapper around hera_cal.noise.predict_noise_variance_from_autos that preps
the noise variance calculation for FRF + coherent average.

Parameters:
data: DataContainer
Must contain the cross_antpairpol in question as well as some
corresponding autos.
nsamples: DataContainer
Contains the nsamples associated with the cross_antpairpol in question.
weights: DataContainer
Contains the weights associated with the cross_antpairpol in question.
cross_antpairpol: tuple
Tuple whose first two entries are antennas and last entry is a
polarization.
freq_slice: slice
A slice into the frequency axis of the data that all gets filtered
identically (a PS analysis band).
auto_ant: int
If not None, should be a single integer specifying a single antenna's
autos (this is used because the redundantly averaged data have a single auto file for all antennas).
default_value: (float)
The default variance to use in locations with 0 nsamples to avoid
nans.

Returns:
var: array
Noise variance (Ntimes, Nfreqs)
"""
var = predict_noise_variance_from_autos(cross_antpairpol, data,

Check warning on line 2151 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2151

Added line #L2151 was not covered by tests
nsamples=nsamples, auto_ant=auto_ant)
var = var[:, freq_slice]

Check warning on line 2153 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2153

Added line #L2153 was not covered by tests

# Check all the infs are weighted to zero and replace with default value
all_infs_zero = np.all(weights[cross_antpairpol][:, freq_slice][np.isinf(var)]) == 0
if not all_infs_zero:
print(f"Not all infinite variance locations are of zero weight!")

Check warning on line 2158 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2156-L2158

Added lines #L2156 - L2158 were not covered by tests

var[np.isinf(var)] = default_value

Check warning on line 2160 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2160

Added line #L2160 was not covered by tests

return var

Check warning on line 2162 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2162

Added line #L2162 was not covered by tests

def get_FRF_cov(frop, var):
"""
Get noise covariance from noise variance and given FRF + coherent average operator.

Parameters:
frop: array
A linear operator that performs the FRF and coherent averaging
operations in one step.
var: array
Noise variance for a single antpairpol.

Returns:
cov: array
The desired covariance. Shape (Nfreqs, Ntimes, Ntimes)
"""
Nfreqs = frop.shape[2]
Ncoarse = frop.shape[0]
cov = np.zeros([Nfreqs, Ncoarse, Ncoarse], dtype=complex)
for freq_ind in range(Nfreqs):
cov[freq_ind] = np.tensordot((frop[:, :, freq_ind] * var[:, freq_ind]), frop[:, :, freq_ind].T.conj(), axes=1)

Check warning on line 2183 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2179-L2183

Added lines #L2179 - L2183 were not covered by tests

return cov

Check warning on line 2185 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2185

Added line #L2185 was not covered by tests

def get_corr(cov):
"""
Get correlation matrices from a sequence of covariance matrices.

Parameters:
cov: array
The covariance matrices.
Returns:
corr: array
The corresponding correlation matrices.
"""
Ntimes = cov.shape[1]
diags = cov[:, np.arange(Ntimes), np.arange(Ntimes)]
corr = cov / np.sqrt(diags[:, None] * diags[:, :, None])

Check warning on line 2200 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2198-L2200

Added lines #L2198 - L2200 were not covered by tests

return corr

Check warning on line 2202 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2202

Added line #L2202 was not covered by tests

def get_correction_factor_from_cov(cov, tslc=None):
"""
Get a correction factor for PS noise variance prediction in the absence
of propagating the noise covariances all the way to delay space.

Parameters:
cov: array
The time-time covariance matrix at every frequency
tslc: slice
A time slice to use in case some times should be excluded from the
calculation of this factor.

Returns:
correction_factor: array
The correction factor.
"""
corr = get_corr(cov)

Check warning on line 2220 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2220

Added line #L2220 was not covered by tests

if tslc is None:
Ncotimes = corr.shape[1]
Neff = Ncotimes**2 / np.sum(np.abs(corr)**2, axis=(1, 2))

Check warning on line 2224 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2222-L2224

Added lines #L2222 - L2224 were not covered by tests
else:
Ncotimes = tslc.stop - tslc.start
Neff = Ncotimes**2 / np.sum(np.abs(corr[:, tslc, tslc])**2, axis=(1, 2))
correction_factor = np.mean(Ncotimes / Neff) # Average over frequency since they are independent.

Check warning on line 2228 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2226-L2228

Added lines #L2226 - L2228 were not covered by tests

return correction_factor

Check warning on line 2230 in hera_cal/frf.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/frf.py#L2230

Added line #L2230 was not covered by tests

14 changes: 12 additions & 2 deletions hera_cal/noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
raise ValueError('Cannot infer dt when all len(times_by_bl) == 1 or fewer.')


def predict_noise_variance_from_autos(bl, data, dt=None, df=None, nsamples=None):
def predict_noise_variance_from_autos(bl, data, dt=None, df=None, nsamples=None,
auto_ant=None):
'''Predict the noise variance on a baseline using autocorrelation data
using the formla sigma^2 = Vii * Vjj / Delta t / Delta nu.

Expand All @@ -81,6 +82,11 @@
from the frequencies stored in the DataContainer
nsamples: DataContainer mapping bl tuples to numpy arrays of the number
integrations for that given baseline. Must include nsamples[bl].
auto_ant: int
For some cases, like redundant averaging, the auto corresponding
to the individual antennas is not available. In this case, the
user should use this keyword to specify a single auto antenna from
which to derive this statistic.

Returns:
Noise variance predicted on baseline bl in units of data squared.
Expand All @@ -92,7 +98,11 @@
df = np.median(np.ediff1d(data.freqs))

ap1, ap2 = split_pol(bl[2])
auto_bl1, auto_bl2 = (bl[0], bl[0], join_pol(ap1, ap1)), (bl[1], bl[1], join_pol(ap2, ap2))
if auto_ant is None:
auto_bl1, auto_bl2 = (bl[0], bl[0], join_pol(ap1, ap1)), (bl[1], bl[1], join_pol(ap2, ap2))
else:
auto_bl1, auto_bl2 = (auto_ant, auto_ant, join_pol(ap1, ap1)), (auto_ant, auto_ant, join_pol(ap2, ap2))

Check warning on line 104 in hera_cal/noise.py

View check run for this annotation

Codecov / codecov/patch

hera_cal/noise.py#L104

Added line #L104 was not covered by tests

var = np.abs(data[auto_bl1] * data[auto_bl2] / dt / df)
if nsamples is not None:
return var / nsamples[bl]
Expand Down
Loading