Skip to content

Commit 10c290f

Browse files
authored
Merge pull request #302 from HERA-Team/version030
Version 0.3.0
2 parents 128f66a + 13b9adb commit 10c290f

File tree

9 files changed

+334
-200
lines changed

9 files changed

+334
-200
lines changed

docs/conf.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@
2626
# Mock-import modules to allow build to complete without throwing errors
2727
import mock
2828
MOCK_MODULES = ['numpy', 'scipy', 'scipy.interpolate', 'scipy.integrate',
29-
'pyuvdata', 'h5py', 'aipy', 'omnical', 'linsolve', 'hera_qm',
30-
'uvtools', 'uvtools.dspec', 'hera_cal', 'hera_cal.utils', 'healpy',
31-
'scikit-learn', 'astropy', 'astropy.cosmology', 'astropy.units',
32-
'astropy.constants', 'matplotlib', 'matplotlib.pyplot',
33-
'pylab', 'yaml', 'pyuvdata.utils', ]
34-
29+
'pyuvdata', 'h5py', 'aipy', 'aipy.const', 'omnical', 'linsolve',
30+
'hera_qm', 'uvtools', 'uvtools.dspec', 'hera_cal',
31+
'hera_cal.utils', 'healpy', 'scikit-learn', 'astropy',
32+
'astropy.cosmology', 'astropy.units', 'astropy.constants',
33+
'matplotlib', 'matplotlib.pyplot', 'pylab', 'yaml',
34+
'pyuvdata.utils', 'hera_sim']
35+
3536
for mod_name in MOCK_MODULES:
3637
sys.modules[mod_name] = mock.Mock()
3738

hera_pspec/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.0
1+
0.3.0

hera_pspec/grouping.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ def spherical_average(uvp_in, kbins, bin_widths, blpair_groups=None, time_avg=Fa
825825
# bin covariance: C_sph = H.T C_cyl H
826826
# cm shape (Npols, Ntimes, Ndlyblps, Ndlyblps)
827827
cm = np.moveaxis(cov_array_real[spw], -1, 0)
828-
cm = Ht @ cm @ H
828+
cm = Ht @ cm.clip(-1e40, 1e40) @ H # clip infs
829829
cov_array_real[spw] = np.moveaxis(cm, 0, -1)
830830
cov_array_imag[spw] = np.zeros_like(cov_array_real[spw])
831831

@@ -874,7 +874,7 @@ def spherical_average(uvp_in, kbins, bin_widths, blpair_groups=None, time_avg=Fa
874874
uvp.lst_2_array = np.unique(uvp_in.lst_2_array)
875875

876876
# Add to history
877-
uvp.history += version.history_string(notes=add_to_history)
877+
uvp.history = "Spherically averaged with hera_pspec [{}]\n{}\n{}\n{}".format(version.git_hash[:15], add_to_history, '-'*40, uvp.history)
878878

879879
# validity check
880880
if run_check:

hera_pspec/plot.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def delay_waterfall(uvp, blpairs, spw, pol, component='abs-real',
301301
deltasq=False, log=True, lst_in_hrs=True,
302302
vmin=None, vmax=None, cmap='YlGnBu', axes=None,
303303
figsize=(14, 6), force_plot=False, times=None,
304-
title_type='blpair', colorbar=True):
304+
title_type='blpair', colorbar=True, **kwargs):
305305
"""
306306
Plot a 1D delay spectrum waterfall (or spectra) for a group of baselines.
307307
@@ -374,6 +374,9 @@ def delay_waterfall(uvp, blpairs, spw, pol, component='abs-real',
374374
colorbar : bool, optional
375375
Whether to make a colorbar. Default: True
376376
377+
kwargs : keyword arguments
378+
Additional kwargs to pass to ax.matshow()
379+
377380
Returns
378381
-------
379382
fig : matplotlib.pyplot.Figure
@@ -558,7 +561,7 @@ def delay_waterfall(uvp, blpairs, spw, pol, component='abs-real',
558561
# plot waterfall
559562
cax = ax.matshow(waterfall[key], cmap=cmap, aspect='auto',
560563
vmin=vmin, vmax=vmax,
561-
extent=[x[0], x[-1], y[-1], y[0]])
564+
extent=[x[0], x[-1], y[-1], y[0]], **kwargs)
562565

563566
# ax config
564567
ax.xaxis.set_ticks_position('bottom')

hera_pspec/pspecdata.py

Lines changed: 209 additions & 141 deletions
Large diffs are not rendered by default.

hera_pspec/tests/test_grouping.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,13 @@ def test_spherical_average():
469469
# bug check: in the past, combine after spherical average erroneously changed dly_array
470470
assert sph == sph_c
471471

472+
# insert an inf into the arrays as a test
473+
uvp2 = copy.deepcopy(uvp)
474+
uvp2.cov_array_real[0][0], uvp2.cov_array_imag[0][0] = np.inf, np.inf
475+
uvp2.stats_array['err'][0][0] = np.inf
476+
sph = grouping.spherical_average(uvp, kbins, bin_widths)
477+
assert np.isfinite(sph.cov_array_real[0]).all()
478+
472479
# exceptions
473480
nt.assert_raises(AssertionError, grouping.spherical_average, uvp, kbins, 1.0)
474481

hera_pspec/tests/test_noise.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,22 +159,27 @@ def test_analytic_noise():
159159
# get P_N estimate
160160
auto_Tsys = utils.uvd_to_Tsys(uvd, beam, os.path.join(DATA_PATH, "test_uvd.uvh5"))
161161
assert os.path.exists(os.path.join(DATA_PATH, "test_uvd.uvh5"))
162-
utils.uvp_noise_error(uvp, auto_Tsys, err_type=['P_N','P_SN'])
162+
utils.uvp_noise_error(uvp, auto_Tsys, err_type=['P_N','P_SN'], P_SN_correction=False)
163163

164164
# check consistency of 1-sigma standard dev. to 1%
165165
cov_diag = uvp.cov_array_real[0][:, range(Nchan), range(Nchan)]
166166
stats_diag = uvp.stats_array['P_N'][0]
167167
frac_ratio = (cov_diag**0.5 - stats_diag) / stats_diag
168-
169168
assert np.abs(frac_ratio).mean() < 0.01
170169

171170
## check P_SN consistency of 1-sigma standard dev. to 1%
172171
cov_diag = uvp_fg.cov_array_real[0][:, range(Nchan), range(Nchan)]
173172
stats_diag = uvp.stats_array['P_SN'][0]
174173
frac_ratio = (cov_diag**0.5 - stats_diag) / stats_diag
175-
176174
assert np.abs(frac_ratio).mean() < 0.01
177175

176+
# now compute unbiased P_SN and check that it matches P_N at high-k
177+
utils.uvp_noise_error(uvp, auto_Tsys, err_type=['P_N','P_SN'], P_SN_correction=True)
178+
frac_ratio = (uvp.stats_array["P_SN"][0] - uvp.stats_array["P_N"][0]) / uvp.stats_array["P_N"][0]
179+
dlys = uvp.get_dlys(0) * 1e9
180+
select = np.abs(dlys) > 3000
181+
assert np.abs(frac_ratio[:, select].mean()) < 1 / np.sqrt(uvp.Nblpairts)
182+
178183
# clean up
179184
os.remove(os.path.join(DATA_PATH, "test_uvd.uvh5"))
180185

hera_pspec/utils.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,7 +1301,7 @@ def uvd_to_Tsys(uvd, beam, Tsys_outfile=None):
13011301
return uvd
13021302

13031303

1304-
def uvp_noise_error(uvp, auto_Tsys, err_type='P_N', precomp_P_N=None):
1304+
def uvp_noise_error(uvp, auto_Tsys=None, err_type='P_N', precomp_P_N=None, P_SN_correction=True):
13051305
"""
13061306
Calculate analytic thermal noise error for a UVPSpec object.
13071307
Adds to uvp.stats_array inplace.
@@ -1313,9 +1313,10 @@ def uvp_noise_error(uvp, auto_Tsys, err_type='P_N', precomp_P_N=None):
13131313
If err_type == 'P_SN', uvp should not have any
13141314
incoherent averaging applied.
13151315
1316-
auto_Tsys : UVData object
1316+
auto_Tsys : UVData object, optional
13171317
Holds autocorrelation Tsys estimates in Kelvin (see uvd_to_Tsys)
13181318
for all antennas and polarizations involved in uvp power spectra.
1319+
Needed for P_N computation, not needed if feeding precomp_P_N.
13191320
13201321
err_type : str or list of str, options = ['P_N', 'P_SN']
13211322
Type of thermal noise error to compute. P_N is the standard
@@ -1326,19 +1327,24 @@ def uvp_noise_error(uvp, auto_Tsys, err_type='P_N', precomp_P_N=None):
13261327
which uses uses Re[P(tau)] as a proxy for P_S.
13271328
To store both, feed as err_type = ['P_N', 'P_SN']
13281329
1329-
precomp_P_N : str
1330+
precomp_P_N : str, optional
13301331
If computing P_SN and P_N is already computed, use this key
13311332
to index stats_array for P_N rather than computing it from auto_Tsys.
1333+
1334+
P_SN_correctoin : bool, optional
1335+
Apply correction factor if computing P_SN to account for double
1336+
counting of noise.
13321337
"""
13331338
from hera_pspec import uvpspec_utils
13341339

13351340
# type checks
13361341
if isinstance(err_type, str):
13371342
err_type = [err_type]
13381343

1339-
# get metadata
1340-
lsts = np.unique(auto_Tsys.lst_array)
1341-
freqs = auto_Tsys.freq_array[0]
1344+
# get metadata if needed
1345+
if precomp_P_N is None:
1346+
lsts = np.unique(auto_Tsys.lst_array)
1347+
freqs = auto_Tsys.freq_array[0]
13421348

13431349
# iterate over spectral window
13441350
for spw in uvp.spw_array:
@@ -1393,10 +1399,47 @@ def uvp_noise_error(uvp, auto_Tsys, err_type='P_N', precomp_P_N=None):
13931399
if 'P_SN' in err_type:
13941400
# calculate P_SN: see Tan+2020 and
13951401
# H1C_IDR2/notebooks/validation/errorbars_with_systematics_and_noise.ipynb
1402+
# get signal proxy
13961403
P_S = uvp.get_data(key).real
1404+
# clip negative values
13971405
P_S[P_S < 0] = 0
13981406
P_SN = np.sqrt(np.sqrt(2) * P_S * P_N + P_N**2)
13991407
# catch nans, set to inf
14001408
P_SN[np.isnan(P_SN)] = np.inf
14011409
# set stats
14021410
uvp.set_stats('P_SN', key, P_SN)
1411+
1412+
# P_SN correction
1413+
if P_SN_correction and "P_SN" in err_type:
1414+
if precomp_P_N is None:
1415+
precomp_P_N = 'P_N'
1416+
apply_P_SN_correction(uvp, P_SN='P_SN', P_N=precomp_P_N)
1417+
1418+
1419+
def apply_P_SN_correction(uvp, P_SN='P_SN', P_N='P_N'):
1420+
"""
1421+
Apply correction factor to P_SN errorbar in stats_array to account
1422+
for double counting of noise by using data as proxy for signal.
1423+
See Jianrong Tan et al. 2021 (Errorbar methodologies).
1424+
Operates in place. Must have both P_SN and P_N in stats_array.
1425+
1426+
Args:
1427+
uvp : UVPSpec object
1428+
With P_SN and P_N errorbar (not variance) as a key in stats_array
1429+
P_SN : str
1430+
Key in stats_array for P_SN errorbar
1431+
P_N : str
1432+
Key in stats_array for P_N errorbar
1433+
"""
1434+
assert P_SN in uvp.stats_array
1435+
assert P_N in uvp.stats_array
1436+
for spw in uvp.spw_array:
1437+
# get P_SN and P_N
1438+
p_n = uvp.stats_array[P_N][spw]
1439+
p_sn = uvp.stats_array[P_SN][spw]
1440+
# derive correction
1441+
corr = 1 - (np.sqrt(1 / np.sqrt(np.pi) + 1) - 1) * p_n.real / p_sn.real.clip(1e-40, np.inf)
1442+
corr[np.isclose(corr, 0)] = np.inf
1443+
corr[corr < 0] = np.inf
1444+
# apply correction
1445+
uvp.stats_array[P_SN][spw] *= corr

hera_pspec/uvpspec.py

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -362,24 +362,22 @@ def get_integrations(self, key, omit_flags=False):
362362

363363
def get_r_params(self):
364364
"""
365-
decompress r_params dictionary (so it can be readily used).
366-
in a pspecdata object, r_parms are stored as a dictionary
367-
with one entry per baseline.
368-
In uvpspec, the dictionary is compressed so that a single r_param entry
369-
correspondsto multiple baselines and is stored as a json format string.
370-
get_r_params() reads the compressed string and returns the dictionary
371-
with the format
372-
r_params: dictionary with parameters for weighting matrix.
373-
Proper fields
374-
and formats depend on the mode of data_weighting.
375-
data_weighting == 'dayenu':
376-
dictionary with fields
377-
'filter_centers', list of floats (or float) specifying the (delay) channel numbers
378-
at which to center filtering windows. Can specify fractional channel number.
379-
'filter_half_widths', list of floats (or float) specifying the width of each
380-
filter window in (delay) channel numbers. Can specify fractional channel number.
381-
'filter_factors', list of floats (or float) specifying how much power within each filter window
382-
is to be suppressed.
365+
Return an `r_params` dictionary.
366+
367+
In a `PSpecData` object, the `r_params` are stored as a dictionary with
368+
one entry per baseline.
369+
370+
In a `UVPSpec` object, the dictionary is compressed so that a single
371+
`r_param` entry correspondsto multiple baselines and is stored as a
372+
JSON format string.
373+
374+
This function reads the compressed string and returns the dictionary
375+
with the correct following fields and structure.
376+
377+
Returns
378+
-------
379+
r_params : dict
380+
Dictionary of r_params for this object.
383381
"""
384382
return uvputils.decompress_r_params(self.r_params)
385383

@@ -1776,13 +1774,15 @@ def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None,
17761774
where scalar is the cosmological and beam scalar (i.e. X2Y * Omega_eff)
17771775
calculated from pspecbeam with noise_scalar = True, integration_time is
17781776
in seconds and comes from self.integration_array and Nincoherent is the
1779-
number of incoherent averaging samples and comes from
1780-
self.nsample_array. If component is 'real' or 'imag', P_N is divided by
1781-
an additional factor of sqrt(2).
1777+
number of incoherent averaging samples and comes from `self.nsample_array`.
1778+
If `component` is `real` or `imag`, P_N is divided by an additional
1779+
factor of sqrt(2).
17821780
17831781
If the polarizations specified are pseudo Stokes pol (I, Q, U or V)
17841782
then an extra factor of 2 is divided.
1785-
If form == 'DelSq' then a factor of |k|^3 / (2pi^2) is multiplied.
1783+
1784+
If `form` is `DelSq` then a factor of `|k|^3 / (2pi^2)` is multiplied.
1785+
17861786
If real is True, a factor of sqrt(2) is divided to account for
17871787
discarding imaginary noise component.
17881788
@@ -1822,8 +1822,9 @@ def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None,
18221822
Number of frequency bins to use in integrating power spectrum
18231823
scalar in pspecbeam. Default: 2000.
18241824
1825-
component : str, options=['real', 'imag', 'abs']
1826-
If component is real or imag, divide by an extra factor of sqrt(2)
1825+
component : str
1826+
Options=['real', 'imag', 'abs'].
1827+
If component is real or imag, divide by an extra factor of sqrt(2).
18271828
18281829
Returns
18291830
-------
@@ -1909,14 +1910,14 @@ def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None,
19091910

19101911
def average_spectra(self, blpair_groups=None, time_avg=False,
19111912
blpair_weights=None, error_field=None, error_weights=None,
1912-
inplace=True):
1913+
inplace=True, add_to_history=''):
19131914
"""
19141915
Average power spectra across the baseline-pair-time axis, weighted by
19151916
each spectrum's integration time.
19161917
19171918
This is an "incoherent" average, in the sense that this averages power
1918-
spectra, rather than visibility data. The 'nsample_array' and
1919-
'integration_array' will be updated to reflect the averaging.
1919+
spectra, rather than visibility data. The `nsample_array` and
1920+
`integration_array` will be updated to reflect the averaging.
19201921
19211922
In the case of averaging across baseline pairs, the resultant averaged
19221923
spectrum is assigned to the zeroth blpair in the group. In the case of
@@ -1929,14 +1930,15 @@ def average_spectra(self, blpair_groups=None, time_avg=False,
19291930
equivalent to cylindrical binning in 3D k-space.
19301931
19311932
If you want help constructing baseline-pair groups from baseline
1932-
groups, see self.get_blpair_groups_from_bl_groups.
1933+
groups, see `self.get_blpair_groups_from_bl_groups`.
19331934
19341935
Parameters
19351936
----------
19361937
blpair_groups : list
19371938
List of list of baseline-pair group tuples or integers. All power
19381939
spectra in a baseline-pair group are averaged together. If a
19391940
baseline-pair exists in more than one group, a warning is raised.
1941+
19401942
Examples::
19411943
19421944
blpair_groups = [ [((1, 2), (1, 2)), ((2, 3), (2, 3))],
@@ -1951,6 +1953,7 @@ def average_spectra(self, blpair_groups=None, time_avg=False,
19511953
blpair_weights : list, optional
19521954
List of float or int weights dictating the relative weight of each
19531955
baseline-pair when performing the average.
1956+
19541957
This is useful for bootstrapping. This should have the same shape
19551958
as blpair_groups if specified. The weights are automatically
19561959
normalized within each baseline-pair group. Default: None (all
@@ -1965,17 +1968,21 @@ def average_spectra(self, blpair_groups=None, time_avg=False,
19651968
19661969
error_weights: string, optional
19671970
error_weights specify which kind of errors we use for weights
1968-
during averaging power spectra.
1969-
The weights are defined as $w_i = 1/ sigma_i^2$,
1970-
where $sigma_i$ is taken from the relevant field of stats_array.
1971-
If `error_weight' is set to None, which means we just use the
1972-
integration time as weights. If error_weights is specified,
1973-
then it also gets appended to error_field as a list.
1974-
Default: None
1971+
during averaging power spectra. The weights are defined as
1972+
$w_i = 1/ sigma_i^2$, where $sigma_i$ is taken from the relevant
1973+
field of stats_array.
1974+
1975+
If `error_weight` is set to `None`, which means we just use the
1976+
integration time as weights. If `error_weights` is specified,
1977+
then it also gets appended to `error_field` as a list.
1978+
Default: None.
19751979
19761980
inplace : bool, optional
1977-
If True, edit data in self, else make a copy and return. Default:
1978-
True.
1981+
If True, edit data in self, else make a copy and return.
1982+
Default: True.
1983+
1984+
add_to_history : str, optional
1985+
Added text to add to file history.
19791986
19801987
Notes
19811988
-----
@@ -1991,14 +1998,14 @@ def average_spectra(self, blpair_groups=None, time_avg=False,
19911998
error_field=error_field,
19921999
error_weights=error_weights,
19932000
blpair_weights=blpair_weights,
1994-
inplace=True)
2001+
inplace=True, add_to_history=add_to_history)
19952002
else:
19962003
return grouping.average_spectra(self, blpair_groups=blpair_groups,
19972004
time_avg=time_avg,
19982005
error_field=error_field,
19992006
error_weights=error_weights,
20002007
blpair_weights=blpair_weights,
2001-
inplace=False)
2008+
inplace=False, add_to_history=add_to_history)
20022009

20032010
def fold_spectra(self):
20042011
"""

0 commit comments

Comments
 (0)