Skip to content

Commit

Permalink
Add FRF noise calculation library
Browse files Browse the repository at this point in the history
  • Loading branch information
Mike Wilensky committed Sep 26, 2024
1 parent 0529798 commit e6d8dca
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 2 deletions.
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 @@ def time_average_argparser():
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 @@ def infer_dt(times_by_bl, bl, default_dt=None):
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 @@ def predict_noise_variance_from_autos(bl, data, dt=None, df=None, nsamples=None)
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 @@ def predict_noise_variance_from_autos(bl, data, dt=None, df=None, nsamples=None)
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

0 comments on commit e6d8dca

Please sign in to comment.