diff --git a/examples/Inverse_sinc_weighting_example.ipynb b/examples/Inverse_sinc_weighting_example.ipynb new file mode 100644 index 00000000..21a42b02 --- /dev/null +++ b/examples/Inverse_sinc_weighting_example.ipynb @@ -0,0 +1,624 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this notebook, I demonstrate the efficacy of inverse sinc weighting in subtracting foreground side-lobes associated with flagged channels. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:45:44.704609Z", + "start_time": "2019-07-08T17:45:43.401110Z" + } + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "from pyuvdata import UVData\n", + "import hera_pspec as hp\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import copy, os, itertools, inspect\n", + "from hera_pspec.data import DATA_PATH\n", + "from hera_sim.simulate import Simulator\n", + "import hera_sim\n", + "from hera_sim import noise\n", + "import hera_pspec as hp" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will demonstrate the method on simulated visibilities generated by `hera_sim`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:45:45.604167Z", + "start_time": "2019-07-08T17:45:44.707380Z" + } + }, + "outputs": [], + "source": [ + "dfile = os.path.join(DATA_PATH, 'zen.all.xx.LST.1.06964.uvA')\n", + "simulated_data = Simulator(dfile)\n", + "simulated_eor = Simulator(dfile)\n", + "\n", + "simulated_data.data.data_array[:] = 0.+0j\n", + "simulated_eor.data.data_array[:] = 0.+0j" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, lets generate some random flags inside of our spectral windows (channels 300-400 and 600-721). These flags will generate nasty frequency side-lobes many orders of magnitude above the level of the signal. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:45:45.610650Z", + "start_time": "2019-07-08T17:45:45.605675Z" + } + }, + "outputs": [], + "source": [ + "nflags = 15\n", + "for i in range(nflags):\n", + " ind1 = np.random.randint(300,400)\n", + " ind2 = np.random.randint(600,721)\n", + " simulated_data.data.flag_array[:,:,ind1,:] = True\n", + " simulated_data.data.flag_array[:,:,ind2,:] = True\n", + " \n", + " simulated_eor.data.flag_array[:,:,ind1,:] = True\n", + " simulated_eor.data.flag_array[:,:,ind2,:] = True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we generate some model visibilities comprising of mock EoR signal and foregrounds. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:45:47.706310Z", + "start_time": "2019-07-08T17:45:45.614304Z" + } + }, + "outputs": [], + "source": [ + "Tsky_mdl = noise.HERA_Tsky_mdl['xx']\n", + "\n", + "\n", + "fg_model = lambda lsts, fqs, bl_vec: hera_sim.foregrounds.diffuse_foreground(lsts, fqs, bl_vec, Tsky_mdl)\\\n", + " +hera_sim.foregrounds.pntsrc_foreground(lsts, fqs, bl_vec)\n", + "\n", + "noise_model = lambda lsts, fqs: noise.thermal_noise(lsts, fqs, Tsky_mdl)\n", + "\n", + "eor_model = lambda lsts, fqs, bl_vec: hera_sim.eor.noiselike_eor(lsts, fqs, bl_vec, eor_amp = 3e-2)\n", + "\n", + "simulated_eor.add_eor(eor_model)\n", + "simulated_data.data.data_array = copy.copy(simulated_eor.data.data_array)\n", + "\n", + "\n", + "simulated_data.add_foregrounds(fg_model)\n", + "\n", + "uvd = simulated_data.data\n", + "uvd_eor = simulated_eor.data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set up our dspec object." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have simulated visibilities, we define a cosmology, load up a beam file, split the data into even and odd time steps, scale to mK units and create a delay spectrum object to read in the simulated data and perform the power spectrum calculations." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:45:47.833095Z", + "start_time": "2019-07-08T17:45:47.708590Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: taking power spectra between LST bins misaligned by more than 15 seconds\n", + "Warning: taking power spectra between LST bins misaligned by more than 15 seconds\n" + ] + } + ], + "source": [ + "# Instantiate a Cosmo Conversions object\n", + "# we will need this cosmology to put the power spectra into cosmological units\n", + "cosmo = hp.conversions.Cosmo_Conversions()\n", + "\n", + "# List of beamfile to load. This is a healpix map.\n", + "beamfile = os.path.join(DATA_PATH, 'HERA_NF_dipole_power.beamfits')\n", + "\n", + "# intantiate beam and pass cosmology, if not fed, a default Planck cosmology will be assumed\n", + "uvb = hp.pspecbeam.PSpecBeamUV(beamfile, cosmo=cosmo)\n", + "\n", + "\n", + "# find conversion factor from Jy to mK\n", + "Jy_to_mK = uvb.Jy_to_mK(np.unique(uvd.freq_array), pol='XX')\n", + "\n", + "\n", + "# reshape to appropriately match a UVData.data_array object and multiply in!\n", + "uvd.data_array *= Jy_to_mK[None, None, :, None]\n", + "uvd_eor.data_array *= Jy_to_mK[None, None, :, None]\n", + "\n", + "# slide the time axis of uvd by one integration\n", + "uvd1 = uvd.select(times=np.unique(uvd.time_array)[:-1:2], inplace=False)\n", + "uvd2 = uvd.select(times=np.unique(uvd.time_array)[1::2], inplace=False)\n", + "\n", + "\n", + "uvd_eor1 = uvd_eor.select(times=np.unique(uvd_eor.time_array)[:-1:2], inplace=False)\n", + "uvd_eor2 = uvd_eor.select(times=np.unique(uvd_eor.time_array)[1::2], inplace=False)\n", + "\n", + "\n", + "# Create a new PSpecData object, and don't forget to feed the beam object\n", + "ds = hp.PSpecData(dsets=[uvd1, uvd2], wgts=[None, None], beam=uvb)\n", + "ds_eor = hp.PSpecData(dsets=[uvd_eor1, uvd_eor2], wgts=[None, None], beam=uvb)\n", + "\n", + "\n", + "# Because the LST integrations are offset by more than ~15 seconds we will get a warning\n", + "# but this is okay b/c it is still **significantly** less than the beam-crossing time and we are using short\n", + "# baselines...\n", + "\n", + "# here we phase all datasets in dsets to the zeroth dataset\n", + "ds.rephase_to_dset(0)\n", + "ds_eor.rephase_to_dset(0)\n", + "\n", + "# change units of UVData objects\n", + "ds.dsets[0].vis_units = 'mK'\n", + "ds.dsets[1].vis_units = 'mK'\n", + "\n", + "ds_eor.dsets[0].vis_units = 'mK'\n", + "ds_eor.dsets[1].vis_units = 'mK'\n", + "\n", + "# Specify which baselines to include\n", + "baselines = [(24,25), (37,38), (38,39)]\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Construct an r_params dictionary." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`r_params` specifies the center, width, and level of each filtering window on each visibility. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:45:47.847930Z", + "start_time": "2019-07-08T17:45:47.839248Z" + } + }, + "outputs": [], + "source": [ + "\n", + "rp = {'filter_centers':[0.],\n", + " 'filter_widths':[250e-9],\n", + " 'filter_factors':[1e-9]}\n", + " \n", + "r_params = {}\n", + "\n", + "for bl in baselines:\n", + " key1 = (0,) + bl + ('xx',)\n", + " key2 = (1,) + bl + ('xx',)\n", + " r_params[key1] = rp\n", + " r_params[key2] = rp" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To perform inverse sinc weighting, just set `input_data_weight='sinc_downweight'` and `r_params= r_params` when calling `pspecdata.pspec()`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:45:58.026674Z", + "start_time": "2019-07-08T17:45:47.854182Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: taking power spectra between LST bins misaligned by more than 15 seconds\n", + "\n", + "Setting spectral range: (300, 400)\n", + "\n", + "Using polarization pair: ('xx', 'xx')\n", + "\n", + "(bl1, bl2) pair: ((24, 25), (24, 25))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((37, 38), (37, 38))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((38, 39), (38, 39))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "Setting spectral range: (600, 721)\n", + "\n", + "Using polarization pair: ('xx', 'xx')\n", + "\n", + "(bl1, bl2) pair: ((24, 25), (24, 25))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((37, 38), (37, 38))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((38, 39), (38, 39))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "{0: {'filter_centers': [0.0], 'filter_widths': [2.5e-07], 'filter_factors': [1e-09], 'baselines': [(0, 24, 25, 'xx'), (1, 24, 25, 'xx'), (0, 37, 38, 'xx'), (1, 37, 38, 'xx'), (0, 38, 39, 'xx'), (1, 38, 39, 'xx')]}}\n", + "{\"0\": {\"filter_centers\": [0.0], \"filter_widths\": [2.5e-07], \"filter_factors\": [1e-09], \"baselines\": [[0, 24, 25, \"xx\"], [1, 24, 25, \"xx\"], [0, 37, 38, \"xx\"], [1, 37, 38, \"xx\"], [0, 38, 39, \"xx\"], [1, 38, 39, \"xx\"]]}}\n" + ] + } + ], + "source": [ + "# we will use the baselines list to produce 3 power spectra\n", + "# whose data will be drawn from the dsets[0] and dsets[1]\n", + "# across two spectral windows with identity weighting and a blackman-harris taper\n", + "uvp_isw = ds.pspec(baselines, baselines, (0, 1), [('xx', 'xx')], spw_ranges=[(300, 400), (600,721)], \n", + " norm='I', taper='none', verbose=True,\n", + " input_data_weight = 'sinc_downweight', r_params = r_params)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will compare the impact of filtering versus no filtering by creating an unfiltered `uvpspec` object, `uvp`. We also compare the filtered signal to the underlying 21cm signal by generating `uvp_eor` from visibilities that only contain the the 21cm realizations (no foregrounds). " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:46:07.190642Z", + "start_time": "2019-07-08T17:45:58.028604Z" + }, + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: taking power spectra between LST bins misaligned by more than 15 seconds\n", + "\n", + "Setting spectral range: (300, 400)\n", + "\n", + "Using polarization pair: ('xx', 'xx')\n", + "\n", + "(bl1, bl2) pair: ((24, 25), (24, 25))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((37, 38), (37, 38))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((38, 39), (38, 39))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "Setting spectral range: (600, 721)\n", + "\n", + "Using polarization pair: ('xx', 'xx')\n", + "\n", + "(bl1, bl2) pair: ((24, 25), (24, 25))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((37, 38), (37, 38))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((38, 39), (38, 39))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "Warning: taking power spectra between LST bins misaligned by more than 15 seconds\n", + "\n", + "Setting spectral range: (300, 400)\n", + "\n", + "Using polarization pair: ('xx', 'xx')\n", + "\n", + "(bl1, bl2) pair: ((24, 25), (24, 25))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((37, 38), (37, 38))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((38, 39), (38, 39))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "Setting spectral range: (600, 721)\n", + "\n", + "Using polarization pair: ('xx', 'xx')\n", + "\n", + "(bl1, bl2) pair: ((24, 25), (24, 25))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building G...\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((37, 38), (37, 38))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n", + "\n", + "(bl1, bl2) pair: ((38, 39), (38, 39))\n", + "pol: (-5, -5)\n", + "WARNING: Number of unflagged chans for key1 and/or key2 < n_dlys\n", + " which may lead to normalization instabilities.\n", + " Building q_hat...\n", + " Normalizing power spectrum...\n", + " Computing and multiplying scalar...\n" + ] + } + ], + "source": [ + "# we will use the baselines list to produce 3 power spectra\n", + "# whose data will be drawn from the dsets[0] and dsets[1]\n", + "# across two spectral windows with identity weighting and a blackman-harris taper\n", + "uvp = ds.pspec(baselines, baselines, (0, 1), [('xx', 'xx')], spw_ranges=[(300, 400), (600,721)], input_data_weight='identity',\n", + " norm='I', taper='none', verbose=True)\n", + "\n", + "uvp_eor = ds_eor.pspec(baselines, baselines, (0, 1), [('xx', 'xx')], spw_ranges=[(300, 400), (600,721)], input_data_weight='identity',\n", + " norm='I', taper='none', verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:46:07.930169Z", + "start_time": "2019-07-08T17:46:07.192386Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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5WNxIVDmXERERC4omZmZPPDAA5w6dQofHx+6d+/OnDlzuP766wHo2LEjTzzxBCNGjKBDhw707duXkJCQCtu86aabuPbaaxk6dChRUVH07t3bY/0dPHgwM2fOZPjw4QDccccdDBo0qCjYdmvTpg3z5s1jxowZ5OTkAPDss8/Ss2dP3njjDa688kpatWrFqFGjyt3Wfffdxz333ENkZCQ+Pj7MmzcPPz8/Ro8eTUREBJGRkfTv35/BgwdX2u/XXnuNG2+8kddee43rrruuaHpMTAwvvvgivr6+BAUF8d5779XgqDRdprJLN03Z0KFDrfuubKlfMTExREdHN3Q3pI7o9W2cEhISCAgIqHU7O3bsoF+/2pcjZGVl0b1791q3I55V15/fXbt20adPnzpr31MyMzMJCgoiPz+fKVOmcNtttzFlypSG7laNuffn9OnTPPbYY/To0YOHH364obvVZJX1PjfGbLLWVjpGpspaREREREqZNWtW0VB/ERERTJ48uaG7VCv/+Mc/iIqKYvjw4aSnp3P33Xc3dJekHCprERERESnlpZdeaugueNTDDz/Mww8/TEZGhkZHaeSUORcRERERaSQUnIuIiIiINBIKzkVEREREGgkF5yIiIiIijYSCcxEREbmgzJ49m379+jFgwACioqJYv3494IwfvnPnTo9vLygoqNZtzJo1i44dOxIVFUVUVBSPPfaYB3rWOERHR1PVoa3LOw7FX7vw8HBSU1M5deoUb7zxRp31OyYmhmuuucbj7Wq0FhEREblgrF27lq+++orNmzfj5+dHamoqubm5ALz99tsN0qeYmBjmzZvHvHnzKlzu4Ycf5je/+U212y8oKMDb27uGvav9+p5W1nEo67VzB+f33Xdfldu21mKtxcur4fLXypyLiIjIBePIkSOEhYXh5+cHQFhYGB06dABKZnDfeecdevbsSXR0NHfeeSf3338/ADNnzuTBBx/kkksuoVu3bnzyySeA85Cf8ePHM3jwYCIjI/niiy/qZX+WLVvGoEGDiIyM5Lbbbit6Mmh4eDhPP/00l156KR9//DF79+7lyiuvZOzYsYwZM4a4uDgA9u7dy8iRIxk2bBh/+MMfirL8MTExXHbZZdx4441ERkYC8PLLL9O/f3/69+/Pq6++CkBiYiL9+/cv6s9LL73ErFmzAOd4PvroowwfPpyePXvy3XffAXD27FluuOEGBgwYwPTp0zl79izgnATMnDmT/v37ExkZySuvvFLl41BW9v2xxx5j7969REVF8dvf/haAF198kWHDhjFgwAD++Mc/Fu1Dnz59uO+++xg8eDBJSUksWbKEUaNGMXjwYKZNm0ZmZiYA3377Lb179+bSSy/ls88+q3L/qkOZcxEREWkY/3kMjm7zbJvtI+Gnz5c7e+LEiTz99NP07NmTCRMmMH36dMaNG1dimcOHD/PMM8+wefNmgoODufzyyxk4cGDR/CNHjrB69Wri4uKYNGkS119/Pf7+/ixatIgWLVqQmprKyJEjmTRpEsYYj+3aK6+8wvvvvw/ACy+8wLhx45g5cybLli2jZ8+e3HLLLbz55pv86le/AsDf35/Vq1cDMH78eN566y3at2/Pzp07ue+++1i+fDkPPfQQDz30EDNmzOCtt94qsb0NGzawfft2IiIi2LRpE3PnzmX9+vVYaxkxYgTjxo2jVatWFfY5Pz+fDRs28M033/DUU0+xdOlS3nzzTQICAti6dStbt25l8ODBAMTGxnLo0CG2b98OOJnvqhyHK664oszlnn/+ebZv305sbCwAS5YsYc+ePWzYsAFrLZMmTWLVqlV06dKF3bt3M3fuXN544w1SU1N59tlnWbp0KYGBgbzwwgu8/PLLPPLII9x5550sX76c7t27M3369Epfs5pQ5lxEREQuGEFBQWzatIk5c+bQpk0bpk+ffk45yYYNGxg3bhytW7fG19eXadOmlZg/efJkvLy86Nu3L8eOHQOccognnniCAQMGMGHCBA4dOlQ0rzwjRowgKiqKO+64g3//+99FddSLFy8uc/mHH36Y2NhYYmNjueKKK9i9ezcRERH07NkTgF/84hesWrWqaHl38JiZmcn333/PtGnTGD16NHfffTdHjhwBnDIf9/7deOONJbY3fPhwIiIiAFi9ejVTpkwhMDCQoKAgpk6dWpQJr8jUqVMBGDJkCImJiQCsWrWKm2++GYABAwYwYMAAALp168a+fft44IEH+Pbbb2nRokWVjkNVLVmyhCVLljBo0CAGDx5MXFwce/bsAaBr166MHDkSgHXr1rFz505Gjx5NVFQU8+fP58CBA8TFxREREUGPHj0wxhTtg6cpcy4iIiINo4IMd13y9vYmOjqa6OhoIiMjmT9/PjNnziyab62tcH13SUzxZRcsWEBKSgqbNm3C19eX8PBwsrOzK2zHfSNqVWvOS6usn4GBgQAUFhbSsmVLYmNjq/WEUPf6FW3Lx8eHwsLCot9L77P7WHl7e5Ofn180vawrCq1ateLHH39k8eLFvP766/zrX//i3XffrVJfq8Jay+OPP87dd99dYnpiYuI5+/qTn/yEDz/8sMRysbGxHr0SUh5lzkVEROSCsXv37qJsKTgBV9euXUssM3z4cFauXMnJkyfJz8/n008/rbTd9PR02rZti6+vLytWrODAgQMe73tpvXv3JjExkYSEBAD++c9/nlOiA9CiRQsiIiL4+OOPASf4/PHHHwEYOXJk0f4tXLiw3G2NHTuWzz//nKysLM6cOcOiRYsYM2YM7dq14/jx46SlpZGTk8NXX31Vab/Hjh3LggULANi+fTtbt24FIDU1lcLCQq677rqisqLaCA4OJiMjo+j3K664gnfffbeofvzQoUMcP378nPVGjhzJmjVrio5rVlYW8fHx9O7dm/3797N3716Ac4J3T1HmXERERC4YmZmZPPDAA5w6dQofHx+6d+/OnDlzSizTsWNHnnjiCUaMGEGHDh3o27cvISEhFbZ70003ce211zJ06FCioqLo3bt3Xe4G4NSUz507l2nTppGfn8+wYcO45557ylx2wYIF3HvvvTz99NMUFBRwww03MHDgQF599VVuvvlm/vznP3P11VeXu5+DBw9m5syZDB8+HHCGLhw0aBAAf/jDHxgxYgQRERFV2u97772XW2+9tWgoS3ebhw4d4tZbby3KxP/pT3+q9jEpLjQ0lNGjR9O/f39++tOf8uKLL7Jr1y5GjRoFOCVO77///jkj0bRp04Z58+YxY8aMohtsn332WXr27MmcOXO4+uqrCQsL49JLLy2qj/ckU9klkaZs6NChtqrjaopnxcTEEB0d3dDdkDqi17dxSkhIICAgoNbt7Nixg379+tW6naysLLp3717rdsSz6vrzu2vXLvr06VNn7XtKZmYmQUFB5OfnM2XKFG677TamTJnS0N2qtdJlLVlZWTRv3hxjDAsXLuTDDz+st5FmmrKy3ufGmE3W2qGVravMuYiIiEgps2bNYunSpWRnZzNx4kQmT57c0F2qE5s2beL+++/HWkvLli09WuMtNaPgXERERKSUl156qaG7UC/GjBlTVH8ujYNuCBURERERaSQUnIuIiIiI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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# plot power spectrum of spectral window 1\n", + "fig, ax = plt.subplots(figsize=(12,8))\n", + "\n", + "plt.fill_between([-300,300],[1e-10,1e-10],[1e30,1e30],color='grey',alpha=.2)\n", + "spw = 1\n", + "blp =((24, 25), (24,25))\n", + "key = (spw, blp, 'xx')\n", + "dlys = uvp.get_dlys(spw) * 1e9\n", + "power = np.abs(np.real(uvp.get_data(key)))\n", + "power_isw = np.abs(np.real(uvp_isw.get_data(key)))\n", + "power_eor = np.abs(np.real(uvp_eor.get_data(key)))\n", + "\n", + "p1 = ax.plot(dlys, power.mean(axis=0),label='Signal+Foregrounds')\n", + "p2 = ax.plot(dlys, power_isw.mean(axis=0), label='Signal + Foregrounds Filtered')\n", + "p2 = ax.plot(dlys, power_eor.mean(axis=0),label='Signal Only')\n", + "\n", + "ax.set_yscale('log')\n", + "ax.grid()\n", + "ax.set_xlabel(\"delay [ns]\", fontsize=14)\n", + "ax.set_ylabel(r\"$P(k)\\ \\rm [mK^2\\ h^{-3}\\ Mpc^3]$\", fontsize=14)\n", + "ax.set_title(\"spw : {}, blpair : {}, pol : {}\".format(*key), fontsize=14)\n", + "plt.ylim(1e3,1e18)\n", + "plt.legend(loc='best')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Comparing the \"Signal+Foregrounds\" line in the above plot to the \"Signal Only\" line, we see that flagging side-lobes of the foregrounds dwarf the underlying signal by roughly nine orders of magnitude. When we apply inverse sinc filtering out to the width of the grey shaded region, we remove the foregrounds and their flagging side-lobes to a level where we recover an unbiased estimate of the 21cm signal. Rigorous studies of signal loss and residual biases are ongoing. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-08T17:46:07.936058Z", + "start_time": "2019-07-08T17:46:07.932264Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"0\": {\"filter_centers\": [0.0], \"filter_widths\": [2.5e-07], \"filter_factors\": [1e-09], \"baselines\": [[0, 24, 25, \"xx\"], [1, 24, 25, \"xx\"], [0, 37, 38, \"xx\"], [1, 37, 38, \"xx\"], [0, 38, 39, \"xx\"], [1, 38, 39, \"xx\"]]}}\n" + ] + } + ], + "source": [ + "print(uvp_isw.r_params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/hera_pspec/pspecdata.py b/hera_pspec/pspecdata.py index db4684e5..55817ec8 100644 --- a/hera_pspec/pspecdata.py +++ b/hera_pspec/pspecdata.py @@ -13,11 +13,12 @@ import ast import glob import warnings - +import uvtools.dspec as dspec +import json class PSpecData(object): - def __init__(self, dsets=[], wgts=None, dsets_std=None, labels=None, + def __init__(self, dsets=[], wgts=None, dsets_std=None, labels=None, beam=None): """ Object to store multiple sets of UVData visibilities and perform @@ -32,7 +33,7 @@ def __init__(self, dsets=[], wgts=None, dsets_std=None, labels=None, dsets_std: list or dict of UVData objects, optional Set of UVData objects containing the standard deviations of each - data point in UVData objects in dsets. If specified as a dict, + data point in UVData objects in dsets. If specified as a dict, the key names will be used to tag each dataset. Default: []. wgts : list or dict of UVData objects, optional @@ -56,16 +57,18 @@ def __init__(self, dsets=[], wgts=None, dsets_std=None, labels=None, self.spw_range = None self.spw_Nfreqs = None self.spw_Ndlys = None - + self.r_params = {} #r_params is a dictionary that stores parameters for + #parametric R matrices. + self.cov_regularization = 0. # set data weighting to identity by default # and taper to none by default self.data_weighting = 'identity' self.taper = 'none' - + # Set all weights to None if wgts=None if wgts is None: wgts = [None for dset in dsets] - + # set dsets_std to None if any are None. if not dsets_std is None and None in dsets_std: dsets_std = None @@ -90,7 +93,7 @@ def add(self, dsets, wgts, labels=None, dsets_std=None): wgts : UVData or list or dict UVData object or list of UVData objects containing weights to add to the collection. Must be the same length as dsets. If a weight is - set to None, the flags of the corresponding dset are used. + set to None, the flags of the corresponding dset are used. labels : list of str An ordered list of names/labels for each dataset, if dsets was @@ -173,7 +176,7 @@ def add(self, dsets, wgts, labels=None, dsets_std=None): if self.labels is None: self.labels = [] if labels is None: - labels = ["dset{:d}".format(i) + labels = ["dset{:d}".format(i) for i in range(len(self.dsets), len(dsets)+len(self.dsets))] self.labels += labels @@ -250,8 +253,8 @@ def validate_datasets(self, verbose=True): raise ValueError("all dsets must have the same Ntimes") # raise warnings if times don't match - lst_diffs = np.array( [ np.unique(self.dsets[0].lst_array) - - np.unique(dset.lst_array) + lst_diffs = np.array( [ np.unique(self.dsets[0].lst_array) + - np.unique(dset.lst_array) for dset in self.dsets[1:]] ) if np.max(np.abs(lst_diffs)) > 0.001: raise_warning("Warning: taking power spectra between LST bins " @@ -259,8 +262,8 @@ def validate_datasets(self, verbose=True): verbose=verbose) # raise warning if frequencies don't match - freq_diffs = np.array( [ np.unique(self.dsets[0].freq_array) - - np.unique(dset.freq_array) + freq_diffs = np.array( [ np.unique(self.dsets[0].freq_array) + - np.unique(dset.freq_array) for dset in self.dsets[1:]] ) if np.max(np.abs(freq_diffs)) > 0.001e6: raise_warning("Warning: taking power spectra between frequency " @@ -279,14 +282,14 @@ def validate_datasets(self, verbose=True): if 'phased' in set(phase_types): phase_ra = [d.phase_center_ra_degrees for d in self.dsets] phase_dec = [d.phase_center_dec_degrees for d in self.dsets] - max_diff_ra = np.max( [np.diff(d) + max_diff_ra = np.max( [np.diff(d) for d in itertools.combinations(phase_ra, 2)]) - max_diff_dec = np.max([np.diff(d) + max_diff_dec = np.max([np.diff(d) for d in itertools.combinations(phase_dec, 2)]) max_diff = np.sqrt(max_diff_ra**2 + max_diff_dec**2) if max_diff > 0.15: raise_warning("Warning: maximum phase-center difference " - "between datasets is > 10 arcmin", + "between datasets is > 10 arcmin", verbose=verbose) def check_key_in_dset(self, key, dset_ind): @@ -344,6 +347,8 @@ def clear_cache(self, keys=None): except(KeyError): pass try: del(self._iC[k]) except(KeyError): pass + try: del(self.r_params[k]) + except(KeyError): pass try: del(self._Y[k]) except(KeyError): pass try: del(self._R[k]) @@ -520,6 +525,7 @@ def C_model(self, key, model='empirical'): model : string, optional Type of covariance model to calculate, if not cached. options=['empirical'] + Returns ------- C : array-like @@ -529,7 +535,6 @@ def C_model(self, key, model='empirical'): assert isinstance(key, tuple), "key must be fed as a tuple" assert isinstance(model, (str, np.str)), "model must be a string" assert model in ['empirical'], "didn't recognize model {}".format(model) - # parse key dset, bl = self.parse_blkey(key) key = (dset,) + (bl,) @@ -545,7 +550,7 @@ def C_model(self, key, model='empirical'): return self._C[Ckey] - def cross_covar_model(self, key1, key2, model='empirical', + def cross_covar_model(self, key1, key2, model='empirical', conj_1=False, conj_2=True): """ Return a covariance model having specified a key and model type. @@ -558,17 +563,18 @@ def cross_covar_model(self, key1, key2, model='empirical', subsequent indices specify the baseline index, in _key2inds format. model : string, optional - Type of covariance model to calculate, if not cached. + Type of covariance model to calculate, if not cached. options=['empirical'] conj_1 : boolean, optional - Whether to conjugate first copy of data in covar or not. + Whether to conjugate first copy of data in covar or not. Default: False conj_2 : boolean, optional - Whether to conjugate second copy of data in covar or not. + Whether to conjugate second copy of data in covar or not. Default: True + Returns ------- cross_covar : array-like, spw_Nfreqs x spw_Nfreqs @@ -629,7 +635,7 @@ def iC(self, key, model='empirical'): subsequent indices specify the baseline index, in _key2inds format. model : string - Type of covariance model to calculate, if not cached. + Type of covariance model to calculate, if not cached. options=['empirical'] Returns @@ -735,7 +741,8 @@ def R(self, key): R = sqrt(T^t) sqrt(Y^t) K sqrt(Y) sqrt(T) where T is a diagonal matrix holding the taper and Y is a diagonal - matrix holding flag weights. The K matrix comes from either I or iC + matrix holding flag weights. The K matrix comes from either `I` or `iC` + or a `sinc_downweight` depending on self.data_weighting, T is informed by self.taper and Y is taken from self.Y(). @@ -759,11 +766,11 @@ def R(self, key): else: sqrtT = np.sqrt(aipy.dsp.gen_window(self.spw_Nfreqs, self.taper)).reshape(1, -1) - # get flag weight vector: straight multiplication of vectors + # get flag weight vector: straight multiplication of vectors # mimics matrix multiplication sqrtY = np.sqrt(self.Y(key).diagonal().reshape(1, -1)) - # replace possible nans with zero (when something dips negative + # replace possible nans with zero (when something dips negative # in sqrt for some reason) sqrtT[np.isnan(sqrtT)] = 0.0 sqrtY[np.isnan(sqrtY)] = 0.0 @@ -775,6 +782,25 @@ def R(self, key): elif self.data_weighting == 'iC': self._R[Rkey] = sqrtT.T * sqrtY.T * self.iC(key) * sqrtY * sqrtT + elif self.data_weighting == 'sinc_downweight': + r_param_key = (self.data_weighting,) + key + if not r_param_key in self.r_params: + raise ValueError("Error: no filter params specified for " + "sinc weights! ") + else: + r_params = self.r_params[r_param_key] + + #This line retrieves a the psuedo-inverse of a lazy covariance + #matrix given by dspec.sinc_downweight_mat_inv. + # Note that we multiply sqrtY inside of the pinv + #to apply flagging weights before taking psuedo inverse. + self._R[Rkey] = sqrtT.T * np.linalg.pinv(sqrtY.T * \ + dspec.sinc_downweight_mat_inv(nchan = self.spw_Nfreqs, + df = np.median(np.diff(self.freqs)), + filter_centers = r_params['filter_centers'], + filter_widths = r_params['filter_widths'], + filter_factors = r_params['filter_factors'])* sqrtY) * sqrtT + return self._R[Rkey] def set_weighting(self, data_weighting): @@ -784,10 +810,37 @@ def set_weighting(self, data_weighting): Parameters ---------- data_weighting : str - Type of data weightings. Options=['identity', 'iC'] + Type of data weightings. Options=['identity', 'iC', 'sinc_downweight'] """ self.data_weighting = data_weighting + def set_r_param(self, key, r_params): + """ + Set the weighting parameters for baseline at (dset,bl, [pol]) + + Parameters + ---------- + key: tuple (dset, bl, [pol]), where dset is the index of the dataset + bl is a 2-tuple + pol is a float or string specifying polarization + + r_params: dictionary with parameters for weighting matrix. + Proper fields + and formats depend on the mode of data_weighting. + data_weighting == 'sinc_downweight': + dictionary with fields + 'filter_centers', list of floats (or float) specifying the (delay) channel numbers + at which to center filtering windows. Can specify fractional channel number. + 'filter_widths', list of floats (or float) specifying the width of each + filter window in (delay) channel numbers. Can specify fractional channel number. + 'filter_factors', list of floats (or float) specifying how much power within each filter window + is to be suppressed. + Absence of r_params dictionary will result in an error! + """ + key = self.parse_blkey(key) + key = (self.data_weighting,) + key + self.r_params[key] = r_params + def set_taper(self, taper): """ Set data tapering type. @@ -858,7 +911,8 @@ def cov_q_hat(self, key1, key2, time_indices=None): Returns ------- - cov_q_hat: matrix with covariances between un-normalized band powers + cov_q_hat: array_like + Matrix with covariances between un-normalized band powers (Ntimes, Nfreqs, Nfreqs) """ # type check if time_indices is None: @@ -958,15 +1012,15 @@ def q_hat(self, key1, key2, allow_fft=False, exact_norm = False, pol=False): of delay bins is equal to the number of frequencies. Default: False. exact_norm: bool, optional - If True, beam and spectral window factors are taken - in the computation of Q_matrix (dC/dp = Q, and not Q_alt) - (HERA memo #44, Eq. 11). Q matrix, for each delay mode, - is weighted by the integral of beam over theta,phi. + If True, beam and spectral window factors are taken + in the computation of Q_matrix (dC/dp = Q, and not Q_alt) + (HERA memo #44, Eq. 11). Q matrix, for each delay mode, + is weighted by the integral of beam over theta,phi. Therefore the output power spectra is, by construction, normalized. If True, it returns normalized power spectrum, except for X2Y term. - If False, Q_alt is used (HERA memo #44, Eq. 16), and the power + If False, Q_alt is used (HERA memo #44, Eq. 16), and the power spectrum is normalized separately. - + pol: str/int/bool, optional Used only if exact_norm is True. This argument is passed to get_Q to extract the requested beam polarization. Default is the first @@ -982,28 +1036,28 @@ def q_hat(self, key1, key2, allow_fft=False, exact_norm = False, pol=False): # Calculate R x_1 if isinstance(key1, list): - for _key in key1: + for _key in key1: Rx1 += np.dot(self.R(_key), self.x(_key)) - R1 += self.R(_key) + R1 += self.R(_key) else: Rx1 = np.dot(self.R(key1), self.x(key1)) R1 = self.R(key1) # Calculate R x_2 if isinstance(key2, list): - for _key in key2: + for _key in key2: Rx2 += np.dot(self.R(_key), self.x(_key)) - R2 += self.R(_key) + R2 += self.R(_key) else: Rx2 = np.dot(self.R(key2), self.x(key2)) R2 = self.R(key2) - # The set of operations for exact_norm == True are drawn from Equations - # 11(a) and 11(b) from HERA memo #44. We are incorporating the - # multiplicatives to the exponentials, and sticking to quantities in + # The set of operations for exact_norm == True are drawn from Equations + # 11(a) and 11(b) from HERA memo #44. We are incorporating the + # multiplicatives to the exponentials, and sticking to quantities in # their physical units. - - if exact_norm and allow_fft: #exact_norm approach is meant to enable non-uniform binnning as well, where FFT is not + + if exact_norm and allow_fft: #exact_norm approach is meant to enable non-uniform binnning as well, where FFT is not #applicable. As of now, we are using uniform binning. raise NotImplementedError("Exact normalization does not support FFT approach at present") @@ -1012,20 +1066,20 @@ def q_hat(self, key1, key2, allow_fft=False, exact_norm = False, pol=False): del_tau = np.median(np.diff(self.delays()))*1e-9 #Get del_eta in Eq.11(a) (HERA memo #44) (seconds) Q_matrix_all_delays = np.zeros((self.spw_Ndlys,self.spw_Nfreqs,self.spw_Nfreqs), dtype='complex128') for i in range(self.spw_Ndlys): - # Ideally, del_tau should be part of get_Q. We use it here to + # Ideally, del_tau should be part of get_Q. We use it here to # avoid its repeated computation Q = del_tau * self.get_Q(i, pol) Q_matrix_all_delays[i] = Q QRx2 = np.dot(Q, Rx2) - + # Square and sum over columns qi = 0.5 * np.einsum('i...,i...->...', Rx1.conj(), QRx2) q.append(qi) q = np.asarray(q) #(Ndlys X Ntime) q_norm = np.zeros_like(q, dtype='complex128') - wt_norm = np.zeros(len(q), dtype='complex128') - + wt_norm = np.zeros(len(q), dtype='complex128') + # One normalization for each delay bin for i in range(self.spw_Ndlys): for j in range(self.spw_Ndlys): @@ -1034,7 +1088,7 @@ def q_hat(self, key1, key2, allow_fft=False, exact_norm = False, pol=False): [R1, Q_matrix_all_delays[i], \ R2, Q_matrix_all_delays[j]] ) ) q_norm[i] = (q[i])/(0.5 * wt_norm[i]) - + # Return normalized band powers return q_norm @@ -1165,7 +1219,7 @@ def get_H(self, key1, key2, sampling=False): if self.spw_Ndlys == None: raise ValueError("Number of delay bins should have been set" "by now! Cannot be equal to None.") - + H = np.zeros((self.spw_Ndlys, self.spw_Ndlys), dtype=np.complex) R1 = self.R(key1) R2 = self.R(key2) @@ -1518,17 +1572,17 @@ def get_Q_alt(self, mode, allow_fft=True): def get_Q(self, mode, pol=False): ''' - Computes Q_alpha(i,j), which is the response of the data covariance to the bandpower (dC/dP_alpha). + Computes Q_alpha(i,j), which is the response of the data covariance to the bandpower (dC/dP_alpha). This includes contributions from primary beam. - + Parameters ---------- mode : int Central wavenumber (index) of the bandpower, p_alpha. pol : str/int/bool, optional - Which beam polarization to use. If the specified polarization doesn't exist, - a uniform isotropic beam (with integral 4pi for all frequencies) is assumed. + Which beam polarization to use. If the specified polarization doesn't exist, + a uniform isotropic beam (with integral 4pi for all frequencies) is assumed. Default: False (uniform beam). Return @@ -1536,7 +1590,7 @@ def get_Q(self, mode, pol=False): Q : array_like Response matrix for bandpower p_alpha. ''' - + if self.spw_Ndlys == None: self.set_Ndlys() if mode >= self.spw_Ndlys: @@ -1546,10 +1600,10 @@ def get_Q(self, mode, pol=False): nu = self.freqs[self.spw_range[0]:self.spw_range[1]] # in Hz try: - beam_res, beam_omega, N = self.primary_beam.beam_normalized_response(pol, nu) + beam_res, beam_omega, N = self.primary_beam.beam_normalized_response(pol, nu) #Get beam response in (frequency, pixel), beam area(freq) and Nside, used in computing dtheta. - prod = (1.0/beam_omega) - beam_prod = beam_res * prod[:, np.newaxis] + prod = (1.0/beam_omega) + beam_prod = beam_res * prod[:, np.newaxis] integral_beam = (np.pi/(3.0*(N)**2))* \ np.dot(beam_prod, beam_prod.T) #beam_prod has omega subsumed, but taper is still part of R matrix # the nside terms is dtheta^2, where dtheta is the resolution in healpix map @@ -1559,7 +1613,7 @@ def get_Q(self, mode, pol=False): eta_int = np.exp(-2j * np.pi * tau * nu) #exponential part of the expression Q_alt = np.einsum('i,j', eta_int.conj(), eta_int) # dot it with its conjugate - Q = Q_alt * integral_beam + Q = Q_alt * integral_beam return Q def p_hat(self, M, q): @@ -1733,7 +1787,7 @@ def delays(self): return utils.get_delays(self.freqs[self.spw_range[0]:self.spw_range[1]], n_dlys=self.spw_Ndlys) * 1e9 # convert to ns - def scalar(self, polpair, little_h=True, num_steps=2000, beam=None, + def scalar(self, polpair, little_h=True, num_steps=2000, beam=None, taper_override='no_override', exact_norm=False): """ Computes the scalar function to convert a power spectrum estimate @@ -1749,8 +1803,8 @@ def scalar(self, polpair, little_h=True, num_steps=2000, beam=None, ---------- polpair: tuple, int, or str Which pair of polarizations to compute the beam scalar for, - e.g. ('pI', 'pI') or ('XX', 'YY'). If string, will assume that - the specified polarization is to be cross-correlated with + e.g. ('pI', 'pI') or ('XX', 'YY'). If string, will assume that + the specified polarization is to be cross-correlated with itself, e.g. 'XX' implies ('XX', 'XX'). little_h : boolean, optional @@ -1792,7 +1846,7 @@ def scalar(self, polpair, little_h=True, num_steps=2000, beam=None, "Polarizations don't match. Beam scalar can only be " "calculated for auto-polarization pairs at the moment.") pol = polpair[0] - + # set spw_range and get freqs freqs = self.freqs[self.spw_range[0]:self.spw_range[1]] start = freqs[0] @@ -1936,11 +1990,11 @@ def validate_pol(self, dsets, pol_pair): return valid - def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, - input_data_weight='identity', norm='I', taper='none', - sampling=False, little_h=True, spw_ranges=None, + def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, + input_data_weight='identity', norm='I', taper='none', + sampling=False, little_h=True, spw_ranges=None, baseline_tol=1.0, store_cov=False, verbose=True, - exact_norm=False, history=''): + exact_norm=False, history='', r_params = None): """ Estimate the delay power spectrum from a pair of datasets contained in this object, using the optimal quadratic estimator of arXiv:1502.06016. @@ -1975,14 +2029,14 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, pols : tuple or list of tuple Contains polarization pairs to use in forming power spectra - e.g. ('XX','XX') or [('XX','XX'),('pI','pI')] or a list of - polarization pairs. Individual strings are also supported, and will - be expanded into a matching pair of polarizations, e.g. 'xx' - becomes ('xx', 'xx'). - - If a primary_beam is specified, only equal-polarization pairs can - be cross-correlated, as the beam scalar normalization is only - implemented in this case. To obtain unnormalized spectra for pairs + e.g. ('XX','XX') or [('XX','XX'),('pI','pI')] or a list of + polarization pairs. Individual strings are also supported, and will + be expanded into a matching pair of polarizations, e.g. 'xx' + becomes ('xx', 'xx'). + + If a primary_beam is specified, only equal-polarization pairs can + be cross-correlated, as the beam scalar normalization is only + implemented in this case. To obtain unnormalized spectra for pairs of different polarizations, set the primary_beam to None. n_dlys : list of integer, optional @@ -2018,28 +2072,41 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, Each tuple should contain a start (inclusive) and stop (exclusive) channel used to index the `freq_array` of each dataset. The default (None) is to use the entire band provided in each dataset. - + baseline_tol : float, optional - Distance tolerance for notion of baseline "redundancy" in meters. + Distance tolerance for notion of baseline "redundancy" in meters. Default: 1.0. - + store_cov : boolean, optional If True, calculate an analytic covariance between bandpowers given an input visibility noise model, and store the output in the UVPSpec object. - + verbose : bool, optional If True, print progress, warnings and debugging info to stdout. exact_norm : bool, optional - If True, estimates power spectrum using Q instead of Q_alt - (HERA memo #44). The default options is False. Beware that + If True, estimates power spectrum using Q instead of Q_alt + (HERA memo #44). The default options is False. Beware that turning this True would take ~ 7 sec for computing power spectrum for 100 channels per time sample per baseline. history : str, optional history string to attach to UVPSpec object + r_params: dictionary with parameters for weighting matrix. + Proper fields + and formats depend on the mode of data_weighting. + data_weighting == 'sinc_downweight': + dictionary with fields + 'filter_centers', list of floats (or float) specifying the (delay) channel numbers + at which to center filtering windows. Can specify fractional channel number. + 'filter_widths', list of floats (or float) specifying the width of each + filter window in (delay) channel numbers. Can specify fractional channel number. + 'filter_factors', list of floats (or float) specifying how much power within each filter window + is to be suppressed. + Absence of r_params dictionary will result in an error! + Returns ------- uvp : UVPSpec object @@ -2142,29 +2209,29 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, # configure spectral window selections if spw_ranges is None: spw_ranges = [(0, self.Nfreqs)] - + # convert to list if only a tuple was given if isinstance(spw_ranges, tuple): spw_ranges = [spw_ranges,] - - # Check that spw_ranges is list of len-2 tuples + + # Check that spw_ranges is list of len-2 tuples assert np.isclose([len(t) for t in spw_ranges], 2).all(), \ "spw_ranges must be fed as a list of length-2 tuples" - # if using default setting of number of delay bins equal to number + # if using default setting of number of delay bins equal to number # of frequency channels if n_dlys is None: n_dlys = [None for i in range(len(spw_ranges))] elif isinstance(n_dlys, (int, np.integer)): n_dlys = [n_dlys] - # if using the whole band in the dataset, then there should just be + # if using the whole band in the dataset, then there should just be # one n_dly parameter specified if spw_ranges is None and n_dlys != None: assert len(n_dlys) == 1, \ "Only one spw, so cannot specify more than one n_dly value" - # assert that the same number of ndlys has been specified as the + # assert that the same number of ndlys has been specified as the # number of spws assert len(spw_ranges) == len(n_dlys), \ "Need to specify number of delay bins for each spw" @@ -2242,7 +2309,7 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, print("Polarization pair: {} failed the validation test, " "continuing...".format(p_str)) continue - + spw_polpair.append( uvputils.polpair_tuple2int(p) ) pol_data = [] pol_wgts = [] @@ -2251,21 +2318,21 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, # Compute scalar to convert "telescope units" to "cosmo units" if self.primary_beam is not None: - + # Raise error if cross-pol is requested if (p[0] != p[1]): raise NotImplementedError( "Visibilities with different polarizations can only " "be cross-correlated if primary_beam = None. Cannot " "compute beam scalar for mixed polarizations.") - - # using zero'th indexed polarization, as cross-polarized + + # using zero'th indexed polarization, as cross-polarized # beams are not yet implemented if norm == 'H^-1': - # If using decorrelation, the H^-1 normalization - # already deals with the taper, so we need to override + # If using decorrelation, the H^-1 normalization + # already deals with the taper, so we need to override # the taper when computing the scalar - scalar = self.scalar(p, little_h=little_h, + scalar = self.scalar(p, little_h=little_h, taper_override='none', exact_norm=exact_norm) else: @@ -2309,17 +2376,29 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, print("WARNING: Number of unflagged chans for key1 " "and/or key2 < n_dlys\n which may lead to " "normalization instabilities.") + #if using inverse sinc weighting, set r_params + if input_data_weight == 'sinc_downweight': + key1 = (dsets[0],) + blp[0] + (p_str[0],) + key2 = (dsets[1],) + blp[1] + (p_str[1],) + if not key1 in r_params: + raise ValueError("No r_param dictionary supplied" + " for baseline %s"%(str(key1))) + if not key2 in r_params: + raise ValueError("No r_param dictionary supplied" + " for baseline %s"%(str(key2))) + self.set_r_param(key1,r_params[key1]) + self.set_r_param(key2,r_params[key2]) # Build Fisher matrix if input_data_weight == 'identity': # in this case, all Gv and Hv differ only by flagging pattern # so check if we've already computed this # First: get flag weighting matrices given key1 & key2 - Y = np.vstack([self.Y(key1).diagonal(), + Y = np.vstack([self.Y(key1).diagonal(), self.Y(key2).diagonal()]) # Second: check cache for Y - matches = [np.isclose(Y, y).all() + matches = [np.isclose(Y, y).all() for y in self._identity_Y.values()] if True in matches: # This Y exists, so pick appropriate G and H and continue @@ -2348,7 +2427,7 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, # Normalize power spectrum estimate if exact_norm: - # The output would be a normalized spectrum, so we + # The output would be a normalized spectrum, so we # would skip external normalization pv = qv else: @@ -2366,7 +2445,7 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, if verbose: print(" Computing and multiplying scalar...") pv *= scalar - # Wide bin adjustment of scalar, which is only needed for + # Wide bin adjustment of scalar, which is only needed for # the diagonal norm matrix mode (i.e., norm = 'I') if norm == 'I' and not(exact_norm): pv *= self.scalar_delay_adjustment(Gv=Gv, Hv=Hv) @@ -2420,7 +2499,7 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, pol_ints.extend(1./np.mean([1./integ1, 1./integ2], axis=0)) # combined weight is geometric mean - pol_wgts.extend(np.concatenate([wgts1[:, :, None], + pol_wgts.extend(np.concatenate([wgts1[:, :, None], wgts2[:, :, None]], axis=2)) # insert time and blpair info only once per blpair @@ -2468,7 +2547,7 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, uvp.time_avg_array = np.mean([uvp.time_1_array, uvp.time_2_array], axis=0) uvp.lst_1_array = np.array(lst1) uvp.lst_2_array = np.array(lst2) - uvp.lst_avg_array = np.mean([np.unwrap(uvp.lst_1_array), + uvp.lst_avg_array = np.mean([np.unwrap(uvp.lst_1_array), np.unwrap(uvp.lst_2_array)], axis=0) \ % (2*np.pi) uvp.blpair_array = np.array(blp_arr) @@ -2513,6 +2592,10 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, "".format(datetime.datetime.utcnow(), version.git_hash, '-'*20, filename1, label1, dset1.history, '-'*20, filename2, label2, dset2.history, '-'*20) + + + uvp.r_params = uvputils.compress_r_params(r_params) + uvp.taper = taper uvp.norm = norm @@ -2533,7 +2616,7 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, uvp.integration_array = integration_array uvp.wgt_array = wgt_array uvp.nsample_array = dict( - [ (k, np.ones_like(uvp.integration_array[k], np.float)) + [ (k, np.ones_like(uvp.integration_array[k], np.float)) for k in uvp.integration_array.keys() ] ) # run check @@ -2544,17 +2627,17 @@ def pspec(self, bls1, bls2, dsets, pols, n_dlys=None, def rephase_to_dset(self, dset_index=0, inplace=True): """ - Rephase visibility data in self.dsets to the LST grid of + Rephase visibility data in self.dsets to the LST grid of dset[dset_index] using hera_cal.utils.lst_rephase. - - Each integration in all other dsets is phased to the center of the + + Each integration in all other dsets is phased to the center of the corresponding LST bin (by index) in dset[dset_index]. - Will only phase if the dataset's phase type is 'drift'. This is because - the rephasing algorithm assumes the data is drift-phased when applying + Will only phase if the dataset's phase type is 'drift'. This is because + the rephasing algorithm assumes the data is drift-phased when applying the phasor term. - Note that PSpecData.Jy_to_mK() must be run *after* rephase_to_dset(), + Note that PSpecData.Jy_to_mK() must be run *after* rephase_to_dset(), if one intends to use the former capability at any point. Parameters @@ -2630,10 +2713,10 @@ def rephase_to_dset(self, dset_index=0, inplace=True): for j, k in enumerate(data.keys()): # get blts indices of basline indices = dset.antpair2ind(k[:2], ordered=False) - + # get index in polarization_array for this polarization polind = pol_list.index(uvutils.polstr2num(k[-1])) - + # insert into dset dset.data_array[indices, 0, :, polind] = data[k] @@ -2646,8 +2729,8 @@ def rephase_to_dset(self, dset_index=0, inplace=True): def Jy_to_mK(self, beam=None): """ - Convert internal datasets from a Jy-scale to mK scale using a primary - beam model if available. Note that if you intend to rephase_to_dset(), + Convert internal datasets from a Jy-scale to mK scale using a primary + beam model if available. Note that if you intend to rephase_to_dset(), Jy to mK conversion must be done *after* that step. Parameters @@ -2691,9 +2774,9 @@ def Jy_to_mK(self, beam=None): def trim_dset_lsts(self, lst_tol=6): """ - Assuming all datasets in self.dsets are locked to the same LST grid - (but each may have a constant offset), trim LSTs from each dset that - aren't found in all other dsets (within some decimal tolerance + Assuming all datasets in self.dsets are locked to the same LST grid + (but each may have a constant offset), trim LSTs from each dset that + aren't found in all other dsets (within some decimal tolerance specified by lst_tol). Warning: this edits the data in dsets in-place, and is not reversible. @@ -2716,7 +2799,7 @@ def trim_dset_lsts(self, lst_tol=6): lst_arrs = [] common_lsts = set() for i, dset in enumerate(self.dsets): - lsts = ["{lst:0.{tol}f}".format(lst=l, tol=lst_tol) + lsts = ["{lst:0.{tol}f}".format(lst=l, tol=lst_tol) for l in dset.lst_array] lst_arrs.append(lsts) if i == 0: @@ -2724,7 +2807,7 @@ def trim_dset_lsts(self, lst_tol=6): else: common_lsts = common_lsts.intersection(set(lsts)) - # iterate through dsets and trim off integrations whose lst isn't + # iterate through dsets and trim off integrations whose lst isn't # in common_lsts for i, dset in enumerate(self.dsets): trim_inds = np.array([l not in common_lsts for l in lst_arrs[i]]) @@ -2732,18 +2815,18 @@ def trim_dset_lsts(self, lst_tol=6): self.dsets[i].select(times=dset.time_array[~trim_inds]) -def pspec_run(dsets, filename, dsets_std=None, groupname=None, - dset_labels=None, dset_pairs=None, psname_ext=None, +def pspec_run(dsets, filename, dsets_std=None, groupname=None, + dset_labels=None, dset_pairs=None, psname_ext=None, spw_ranges=None, n_dlys=None, pol_pairs=None, blpairs=None, input_data_weight='identity', norm='I', taper='none', exclude_auto_bls=False, exclude_permutations=True, - Nblps_per_group=None, bl_len_range=(0, 1e10), + Nblps_per_group=None, bl_len_range=(0, 1e10), bl_deg_range=(0, 180), bl_error_tol=1.0, - beam=None, cosmo=None, rephase_to_dset=None, + beam=None, cosmo=None, rephase_to_dset=None, trim_dset_lsts=False, broadcast_dset_flags=True, - time_thresh=0.2, Jy2mK=False, overwrite=True, - file_type='miriad', verbose=True, store_cov=False, - history=''): + time_thresh=0.2, Jy2mK=False, overwrite=True, + file_type='miriad', verbose=True, store_cov=False, + history='', r_params=None): """ Create a PSpecData object, run OQE delay spectrum estimation and write results to a PSpecContainer object. @@ -2837,7 +2920,7 @@ def pspec_run(dsets, filename, dsets_std=None, groupname=None, bl_deg_range : len-2 float tuple A tuple containing the min and max baseline angle (ENU frame in degrees) - to use in utils.calc_blpair_reds. Total range is between 0 and 180 + to use in utils.calc_blpair_reds. Total range is between 0 and 180 degrees. bl_error_tol : float @@ -2881,9 +2964,9 @@ def pspec_run(dsets, filename, dsets_std=None, groupname=None, overwrite : boolean If True, overwrite outputs if they exist on disk. - + file_type : str, optional - If dsets passed as a list of filenames, specify which file format + If dsets passed as a list of filenames, specify which file format the files use. Default: 'miriad'. verbose : boolean @@ -2891,15 +2974,33 @@ def pspec_run(dsets, filename, dsets_std=None, groupname=None, history : str String to add to history of each UVPSpec object. - + + r_params: dict, optional + Dictionary with parameters for weighting matrix. Required fields and + formats depend on the mode of `data_weighting`. Default: None. + + - `sinc_downweight` fields: + - `filter_centers`: list of floats (or float) specifying the + (delay) channel numbers at which to center + filtering windows. Can specify fractional + channel number. + + - `filter_widths`: list of floats (or float) specifying the width + of each filter window in (delay) channel + numbers. Can specify fractional channel number. + + - `filter_factors`: list of floats (or float) specifying how much + power within each filter window is to be + suppressed. + Returns ------- psc : PSpecContainer object - A container for the output UVPSpec objects, which themselves contain + A container for the output UVPSpec objects, which themselves contain the power spectra and their metadata. ds : PSpecData object - The PSpecData object used for OQE of power spectrum, with cached + The PSpecData object used for OQE of power spectrum, with cached weighting matrices. """ # type check @@ -2970,12 +3071,12 @@ def pspec_run(dsets, filename, dsets_std=None, groupname=None, try: # load data into UVData objects if fed as list of strings t0 = time.time() - dsets_std = _load_dsets(dsets_std, bls=bls, pols=pols, + dsets_std = _load_dsets(dsets_std, bls=bls, pols=pols, verbose=verbose) utils.log("Loaded data in %1.1f sec." % (time.time() - t0), lvl=1, verbose=verbose) except ValueError: - # at least one of the dsets_std loads failed due to no data + # at least one of the dsets_std loads failed due to no data # being present utils.log("One of the dsets_std loads failed due to no data " "overlap given the bls and pols selection", @@ -3001,7 +3102,7 @@ def pspec_run(dsets, filename, dsets_std=None, groupname=None, beam.cosmo = cosmo # package into PSpecData - ds = PSpecData(dsets=dsets, wgts=[None for d in dsets], labels=dset_labels, + ds = PSpecData(dsets=dsets, wgts=[None for d in dsets], labels=dset_labels, dsets_std=dsets_std, beam=beam) # Rephase if desired @@ -3047,7 +3148,7 @@ def pspec_run(dsets, filename, dsets_std=None, groupname=None, xants2) = utils.calc_blpair_reds( dsets[dsetp[0]], dsets[dsetp[1]], filter_blpairs=True, - exclude_auto_bls=exclude_auto_bls, + exclude_auto_bls=exclude_auto_bls, exclude_permutations=exclude_permutations, Nblps_per_group=Nblps_per_group, bl_len_range=bl_len_range, @@ -3085,14 +3186,14 @@ def pspec_run(dsets, filename, dsets_std=None, groupname=None, # Run OQE uvp = ds.pspec(bls1_list[i], bls2_list[i], dset_idxs, pol_pairs, - spw_ranges=spw_ranges, n_dlys=n_dlys, - store_cov=store_cov, input_data_weight=input_data_weight, + spw_ranges=spw_ranges, n_dlys=n_dlys, r_params = r_params, + store_cov=store_cov, input_data_weight=input_data_weight, norm=norm, taper=taper, history=history, verbose=verbose) # Store output psname = '{}_x_{}{}'.format(dset_labels[dset_idxs[0]], dset_labels[dset_idxs[1]], psname_ext) - + psc.set_pspec(group=groupname, psname=psname, pspec=uvp, overwrite=overwrite) @@ -3160,11 +3261,11 @@ def validate_blpairs(blpairs, uvd1, uvd2, baseline_tol=1.0, verbose=True): See docstring of PSpecData.pspec() for details on format. uvd1, uvd2 : UVData - UVData instances containing visibility data that first/second bl in + UVData instances containing visibility data that first/second bl in blpair will draw from - + baseline_tol : float, optional - Distance tolerance for notion of baseline "redundancy" in meters. + Distance tolerance for notion of baseline "redundancy" in meters. Default: 1.0. verbose : bool, optional @@ -3191,7 +3292,7 @@ def validate_blpairs(blpairs, uvd1, uvd2, baseline_tol=1.0, verbose=True): ap = ap1 ap.update(ap2) - # iterate through baselines and check baselines crossed with each other + # iterate through baselines and check baselines crossed with each other # are within tolerance for i, blg in enumerate(blpairs): if isinstance(blg, tuple): @@ -3212,7 +3313,7 @@ def raise_warning(warning, verbose=True): print(warning) -def _load_dsets(fnames, bls=None, pols=None, logf=None, verbose=True, +def _load_dsets(fnames, bls=None, pols=None, logf=None, verbose=True, file_type='miriad'): """ Helper function for loading UVData-compatible datasets in pspec_run. @@ -3220,12 +3321,12 @@ def _load_dsets(fnames, bls=None, pols=None, logf=None, verbose=True, dsets = [] Ndsets = len(fnames) for i, dset in enumerate(fnames): - utils.log("Reading {} / {} datasets...".format(i+1, Ndsets), + utils.log("Reading {} / {} datasets...".format(i+1, Ndsets), f=logf, lvl=1, verbose=verbose) - + # read data uvd = UVData() - uvd.read(glob.glob(dset), bls=bls, polarizations=pols, + uvd.read(glob.glob(dset), bls=bls, polarizations=pols, file_type=file_type) dsets.append(uvd) return dsets diff --git a/hera_pspec/testing.py b/hera_pspec/testing.py index 49b1d546..92613175 100644 --- a/hera_pspec/testing.py +++ b/hera_pspec/testing.py @@ -6,11 +6,12 @@ from pyuvdata import UVData from hera_cal.utils import JD2LST from scipy import stats +import hera_pspec.uvpspec_utils as uvputils def build_vanilla_uvpspec(beam=None): """ - Build an example vanilla UVPSpec object from scratch, with all necessary + Build an example vanilla UVPSpec object from scratch, with all necessary metadata. Parameters @@ -56,7 +57,7 @@ def build_vanilla_uvpspec(beam=None): spw_freq_array = np.tile(np.arange(Nspws), Nfreqs) spw_dly_array = np.tile(np.arange(Nspws), Ndlys) spw_array = np.arange(Nspws) - freq_array = np.repeat(np.linspace(100e6, 105e6, Nfreqs, endpoint=False), + freq_array = np.repeat(np.linspace(100e6, 105e6, Nfreqs, endpoint=False), Nspws) dly_array = np.repeat(utils.get_delays(freq_array, n_dlys=Ndlys), Nspws) polpair_array = np.array([1515,]) # corresponds to ('xx','xx') @@ -72,6 +73,7 @@ def build_vanilla_uvpspec(beam=None): scalar_array = np.ones((Nspws, Npols), np.float) label1 = 'red' label2 = 'blue' + r_params = '' labels = np.array([label1, label2]) label_1_array = np.ones((Nspws, Nblpairts, Npols), np.int) * 0 label_2_array = np.ones((Nspws, Nblpairts, Npols), np.int) * 1 @@ -94,25 +96,25 @@ def build_vanilla_uvpspec(beam=None): data_array[s] = np.ones((Nblpairts, Ndlys, Npols), dtype=np.complex) \ * blpair_array[:, None, None] / 1e9 wgt_array[s] = np.ones((Nblpairts, Nfreqs, 2, Npols), dtype=np.float) - # NB: The wgt_array has dimensions Nfreqs rather than Ndlys; it has the + # NB: The wgt_array has dimensions Nfreqs rather than Ndlys; it has the # dimensions of the input visibilities, not the output delay spectra integration_array[s] = np.ones((Nblpairts, Npols), dtype=np.float) nsample_array[s] = np.ones((Nblpairts, Npols), dtype=np.float) cov_array[s] =np.moveaxis(np.array([[np.identity(Ndlys,dtype=np.complex)\ - for m in range(Nblpairts)] + for m in range(Nblpairts)] for n in range(Npols)]), 0, -1) - params = ['Ntimes', 'Nfreqs', 'Nspws', 'Nspwdlys', 'Nspwfreqs', 'Nspws', - 'Nblpairs', 'Nblpairts', 'Npols', 'Ndlys', 'Nbls', - 'blpair_array', 'time_1_array', 'time_2_array', + params = ['Ntimes', 'Nfreqs', 'Nspws', 'Nspwdlys', 'Nspwfreqs', 'Nspws', + 'Nblpairs', 'Nblpairts', 'Npols', 'Ndlys', 'Nbls', + 'blpair_array', 'time_1_array', 'time_2_array', 'lst_1_array', 'lst_2_array', 'spw_array', - 'dly_array', 'freq_array', 'polpair_array', 'data_array', - 'wgt_array', + 'dly_array', 'freq_array', 'polpair_array', 'data_array', + 'wgt_array', 'r_params', 'integration_array', 'bl_array', 'bl_vecs', 'telescope_location', - 'vis_units', 'channel_width', 'weighting', 'history', 'taper', - 'norm', 'git_hash', 'nsample_array', 'time_avg_array', - 'lst_avg_array', 'cosmo', 'scalar_array', 'labels', 'norm_units', - 'labels', 'label_1_array', 'label_2_array', 'store_cov', + 'vis_units', 'channel_width', 'weighting', 'history', 'taper', + 'norm', 'git_hash', 'nsample_array', 'time_avg_array', + 'lst_avg_array', 'cosmo', 'scalar_array', 'labels', 'norm_units', + 'labels', 'label_1_array', 'label_2_array', 'store_cov', 'cov_array', 'spw_dly_array', 'spw_freq_array'] if beam is not None: @@ -127,9 +129,9 @@ def build_vanilla_uvpspec(beam=None): return uvp, cosmo -def uvpspec_from_data(data, bl_grps, data_std=None, spw_ranges=None, - beam=None, taper='none', cosmo=None, n_dlys=None, - verbose=False): +def uvpspec_from_data(data, bl_grps, data_std=None, spw_ranges=None, + beam=None, taper='none', cosmo=None, n_dlys=None, + r_params = None, verbose=False): """ Build an example UVPSpec object from a visibility file and PSpecData. @@ -139,16 +141,16 @@ def uvpspec_from_data(data, bl_grps, data_std=None, spw_ranges=None, This can be a UVData object or a string filepath to a miriad file. bl_grps : list - This is a list of baseline groups (e.g. redundant groups) to form + This is a list of baseline groups (e.g. redundant groups) to form blpairs from. Ex: [[(24, 25), (37, 38), ...], [(24, 26), (37, 39), ...], ... ] data_std: UVData object or str, optional - Can be UVData object or a string filepath to a miriad file. + Can be UVData object or a string filepath to a miriad file. Default: None. spw_ranges : list, optional - List of spectral window tuples. See PSpecData.pspec docstring for + List of spectral window tuples. See PSpecData.pspec docstring for details. Default: None. beam : PSpecBeamBase subclass or str, optional @@ -160,11 +162,22 @@ def uvpspec_from_data(data, bl_grps, data_std=None, spw_ranges=None, cosmo : Cosmo_Conversions object Cosmology object. - + n_dlys : int, optional - Number of delay bins to use. Default: None (uses as many delay bins as + Number of delay bins to use. Default: None (uses as many delay bins as frequency channels). + r_params: dictionary with parameters for weighting matrix. + Proper fields + and formats depend on the mode of data_weighting. + data_weighting == 'sinc_downweight': + dictionary with fields + 'filter_centers', list of floats (or float) specifying the (delay) channel numbers + at which to center filtering windows. Can specify fractional channel number. + 'filter_widths', list of floats (or float) specifying the width of each + filter window in (delay) channel numbers. Can specify fractional channel number. + 'filter_factors', list of floats (or float) specifying how much power within each filter window + is to be suppressed. verbose : bool, optional if True, report feedback to standard output. Default: False. @@ -201,7 +214,7 @@ def uvpspec_from_data(data, bl_grps, data_std=None, spw_ranges=None, beam.cosmo = cosmo # instantiate pspecdata - ds = pspecdata.PSpecData(dsets=[uvd, uvd], dsets_std=[uvd_std, uvd_std], + ds = pspecdata.PSpecData(dsets=[uvd, uvd], dsets_std=[uvd_std, uvd_std], wgts=[None, None], labels=['d1', 'd2'], beam=beam) # get blpair groups @@ -213,16 +226,16 @@ def uvpspec_from_data(data, bl_grps, data_std=None, spw_ranges=None, "bl_grps must be fed as a list of lists of tuples" bls1, bls2 = [], [] for blgrp in bl_grps: - _bls1, _bls2, _ = utils.construct_blpairs(blgrp, exclude_auto_bls=True, + _bls1, _bls2, _ = utils.construct_blpairs(blgrp, exclude_auto_bls=True, exclude_permutations=True) bls1.extend(_bls1) bls2.extend(_bls2) # run pspec - uvp = ds.pspec(bls1, bls2, (0, 1), (pol, pol), input_data_weight='identity', - spw_ranges=spw_ranges, taper=taper, verbose=verbose, - store_cov=store_cov, n_dlys=n_dlys) - + uvp = ds.pspec(bls1, bls2, (0, 1), (pol, pol), input_data_weight='identity', + spw_ranges=spw_ranges, taper=taper, verbose=verbose, + store_cov=store_cov, n_dlys=n_dlys, r_params = r_params) + return uvp @@ -292,7 +305,7 @@ def noise_sim(data, Tsys, beam=None, Nextend=0, seed=None, inplace=False, if beam is not None: if isinstance(beam, (str, np.str)): beam = pspecbeam.PSpecBeamUV(beam) - assert isinstance(beam, pspecbeam.PSpecBeamBase) + assert isinstance(beam, pspecbeam.PSpecBeamBase) # Extend times Nextend = int(Nextend) @@ -342,4 +355,3 @@ def noise_sim(data, Tsys, beam=None, Nextend=0, seed=None, inplace=False, if not inplace: return data - diff --git a/hera_pspec/tests/test_pspecdata.py b/hera_pspec/tests/test_pspecdata.py index 1dcda95e..0a872a13 100644 --- a/hera_pspec/tests/test_pspecdata.py +++ b/hera_pspec/tests/test_pspecdata.py @@ -240,7 +240,7 @@ def test_str(self): print(ds) # print empty psd ds.add(self.uvd, None) print(ds) # print populated psd - + def test_get_Q_alt(self): @@ -318,21 +318,21 @@ def test_get_Q_alt(self): Q_matrix = self.ds.get_Q_alt(alpha, allow_fft=False) Q_diff_norm = np.linalg.norm(Q_matrix - Q_matrix_fft) self.assertLessEqual(Q_diff_norm, multiplicative_tolerance) - + # Check for error handling nt.assert_raises(ValueError, self.ds.set_Ndlys, vect_length+100) def test_get_Q(self): """ Test the Q = dC_ij/dp function. - + A general comment here: I would really want to do away with try and exception statements. The reason to use them now was that current unittests throw in empty datasets to these functions. Given that we are computing the actual value of tau/freq/taper etc. we do need datasets! Currently, if there is no dataset, Q_matrix is simply an identity matrix with same dimensions as that of vector length. It will be very helpful if we can have more elegant solution for this. - + """ vect_length = 50 x_vect = np.random.normal(size=vect_length) \ @@ -341,7 +341,7 @@ def test_get_Q(self): + 1.j * np.random.normal(size=vect_length) self.ds.spw_Nfreqs = vect_length - pol = 'xx' + pol = 'xx' #Test if there is a warning if user does not pass the beam key1 = (0, 24, 38) key2 = (1, 24, 38) @@ -586,10 +586,10 @@ def test_get_MW(self): M, W = self.ds.get_MW(random_G, random_H, mode=mode) self.assertEqual(diagonal_or_not(M), True) elif mode == 'L^-1': - # Test that Cholesky mode is disabled - nt.assert_raises(NotImplementedError, + # Test that Cholesky mode is disabled + nt.assert_raises(NotImplementedError, self.ds.get_MW, random_G, random_H, mode=mode) - + # Test sizes for everyone self.assertEqual(M.shape, (n,n)) self.assertEqual(W.shape, (n,n)) @@ -625,8 +625,14 @@ def test_cov_q(self, ndlys=13): key2 = (1, 25, 38) print(cov_analytic) - for input_data_weight in ['identity','iC']: + for input_data_weight in ['identity','iC','sinc_downweight']: self.ds.set_weighting(input_data_weight) + #check error raised + if input_data_weight == 'sinc_downweight': + nt.assert_raises(ValueError,self.ds.R, key1) + rpk = {'filter_centers':[0.],'filter_widths':[0.],'filter_factors':[0.]} + self.ds.set_r_param(key1,rpk) + self.ds.set_r_param(key2,rpk) for taper in taper_selection: qc = self.ds.cov_q_hat(key1,key2) self.assertTrue(np.allclose(np.array(list(qc.shape)), @@ -649,6 +655,7 @@ def test_cov_q(self, ndlys=13): self.assertRaises(ValueError, self.ds.cov_q_hat, key1, key2, 200) self.assertRaises(ValueError, self.ds.cov_q_hat, key1, key2, "watch out!") + def test_cov_p_hat(self): """ Test cov_p_hat, verify on identity. @@ -680,9 +687,13 @@ def test_q_hat(self): key3 = [(0, 24, 38), (0, 24, 38)] key4 = [(1, 25, 38), (1, 25, 38)] - for input_data_weight in ['identity', 'iC']: + for input_data_weight in ['identity', 'iC','sinc_downweight']: self.ds.set_weighting(input_data_weight) - + if input_data_weight == 'sinc_downweight': + nt.assert_raises(ValueError,self.ds.R, key1) + rpk = {'filter_centers':[0.],'filter_widths':[0.],'filter_factors':[0.]} + self.ds.set_r_param(key1,rpk) + self.ds.set_r_param(key2,rpk) # Loop over list of taper functions for taper in taper_selection: self.ds.set_taper(taper) @@ -719,7 +730,7 @@ def test_q_hat(self): self.ds.spw_Ndlys = Nfreq # Check that the slow method is the same as the FFT method - for input_data_weight in ['identity', 'iC']: + for input_data_weight in ['identity', 'iC','sinc_downweight']: self.ds.set_weighting(input_data_weight) # Loop over list of taper functions for taper in taper_selection: @@ -743,8 +754,13 @@ def test_get_H(self): key1 = (0, 24, 38) key2 = (1, 25, 38) - for input_data_weight in ['identity','iC']: + for input_data_weight in ['identity','iC','sinc_downweight']: self.ds.set_weighting(input_data_weight) + if input_data_weight == 'sinc_downweight': + nt.assert_raises(ValueError,self.ds.R, key1) + rpk = {'filter_centers':[0.],'filter_widths':[0.],'filter_factors':[0.]} + self.ds.set_r_param(key1,rpk) + self.ds.set_r_param(key2,rpk) for taper in taper_selection: self.ds.set_taper(taper) @@ -766,8 +782,13 @@ def test_get_G(self): key1 = (0, 24, 38) key2 = (1, 25, 38) - for input_data_weight in ['identity','iC']: + for input_data_weight in ['identity','iC','sinc_downweight']: self.ds.set_weighting(input_data_weight) + if input_data_weight == 'sinc_downweight': + nt.assert_raises(ValueError,self.ds.R, key1) + rpk = {'filter_centers':[0.],'filter_widths':[0.],'filter_factors':[0.]} + self.ds.set_r_param(key1,rpk) + self.ds.set_r_param(key2,rpk) for taper in taper_selection: self.ds.clear_cache() self.ds.set_taper(taper) @@ -911,7 +932,7 @@ def test_scalar(self): gauss = pspecbeam.PSpecBeamGauss(0.8, np.linspace(115e6, 130e6, 50, endpoint=False)) ds2 = pspecdata.PSpecData(dsets=self.d, wgts=self.w, beam=gauss) - + # Check normal execution scalar = self.ds.scalar(('xx','xx')) scalar_xx = self.ds.scalar('xx') # Can use single pol string as shorthand @@ -920,7 +941,7 @@ def test_scalar(self): scalar = self.ds.scalar(('xx','xx'), taper_override='none') scalar = self.ds.scalar(('xx','xx'), beam=gauss) nt.assert_raises(NotImplementedError, self.ds.scalar, ('xx','yy')) - + # Precomputed results in the following test were done "by hand" # using iPython notebook "Scalar_dev2.ipynb" in the tests/ directory # FIXME: Uncomment when pyuvdata support for this is ready @@ -933,34 +954,34 @@ def test_scalar(self): def test_validate_datasets(self): # test freq exception uvd = copy.deepcopy(self.d[0]) - uvd2 = uvd.select(frequencies=np.unique(uvd.freq_array)[:10], + uvd2 = uvd.select(frequencies=np.unique(uvd.freq_array)[:10], inplace=False) ds = pspecdata.PSpecData(dsets=[uvd, uvd2], wgts=[None, None]) nt.assert_raises(ValueError, ds.validate_datasets) - + # test time exception uvd2 = uvd.select(times=np.unique(uvd.time_array)[:10], inplace=False) ds = pspecdata.PSpecData(dsets=[uvd, uvd2], wgts=[None, None]) nt.assert_raises(ValueError, ds.validate_datasets) - + # test std exception ds.dsets_std=ds.dsets_std[:1] nt.assert_raises(ValueError, ds.validate_datasets) - + # test wgt exception ds.wgts = ds.wgts[:1] nt.assert_raises(ValueError, ds.validate_datasets) - + # test warnings uvd = copy.deepcopy(self.d[0]) uvd2 = copy.deepcopy(self.d[0]) - uvd.select(frequencies=np.unique(uvd.freq_array)[:10], + uvd.select(frequencies=np.unique(uvd.freq_array)[:10], times=np.unique(uvd.time_array)[:10]) - uvd2.select(frequencies=np.unique(uvd2.freq_array)[10:20], + uvd2.select(frequencies=np.unique(uvd2.freq_array)[10:20], times=np.unique(uvd2.time_array)[10:20]) ds = pspecdata.PSpecData(dsets=[uvd, uvd2], wgts=[None, None]) ds.validate_datasets() - + # test phasing uvd = copy.deepcopy(self.d[0]) uvd2 = copy.deepcopy(self.d[0]) @@ -969,15 +990,15 @@ def test_validate_datasets(self): nt.assert_raises(ValueError, ds.validate_datasets) uvd2.phase_to_time(Time(2458042.5, format='jd')) ds.validate_datasets() - + # test polarization ds.validate_pol((0,1), ('xx', 'xx')) - + # test channel widths uvd2.channel_width *= 2. ds2 = pspecdata.PSpecData(dsets=[uvd, uvd2], wgts=[None, None]) nt.assert_raises(ValueError, ds2.validate_datasets) - + def test_rephase_to_dset(self): # generate two uvd objects w/ different LST grids @@ -1055,7 +1076,7 @@ def test_units(self): vis_u, norm_u = ds.units() nt.assert_equal(vis_u, "UNCALIB") nt.assert_equal(norm_u, "Hz str [beam normalization not specified]") - ds_b = pspecdata.PSpecData(dsets=[self.uvd, self.uvd], + ds_b = pspecdata.PSpecData(dsets=[self.uvd, self.uvd], wgts=[None, None], beam=self.bm) vis_u, norm_u = ds_b.units(little_h=False) nt.assert_equal(norm_u,"Mpc^3") @@ -1082,7 +1103,7 @@ def test_check_in_dset(self): nt.assert_false(ds.check_key_in_dset((24, 26, 'yy'), 0)) # check exception nt.assert_raises(KeyError, ds.check_key_in_dset, (1,2,3,4,5), 0) - + # test dset_idx nt.assert_raises(TypeError, ds.dset_idx, (1,2)) @@ -1105,12 +1126,35 @@ def test_pspec(self): ds.pspec(bls, bls, (0, 1), ('xx','xx'), n_dlys=10, spw_ranges=[(10,20)]) ds.pspec(bls, bls, (0, 1), ('xx','xx'), n_dlys=1) + my_r_params = {} + my_r_params_dset0_only = {} + rp = {'filter_centers':[0.], + 'filter_widths':[250e-9], + 'filter_factors':[1e-9]} + for bl in bls: + key1 = (0,) + bl + ('xx',) + key2 = (1,) + bl + ('xx',) + my_r_params[key1] = rp + my_r_params_dset0_only[key1] = rp + my_r_params[key2] = rp + #test inverse sinc weighting. + ds.pspec(bls,bls,(0, 1), ('xx','xx'), + spw_ranges = (10,20), input_data_weight = 'sinc_downweight', + r_params = my_r_params) + #test value error + nt.assert_raises(ValueError, ds.pspec, bls, bls, (0, 1), ('xx','xx'), + spw_ranges = (10,20), input_data_weight = 'sinc_downweight', r_params = {}) + #test value error no dset1 keys + nt.assert_raises(ValueError, ds.pspec, bls, bls, (0, 1), ('xx','xx'), + spw_ranges = (10,20), input_data_weight = 'sinc_downweight', + r_params = my_r_params_dset0_only) + #assert error if baselines are not provided in the right format - nt.assert_raises(NotImplementedError, ds.pspec, [[(24,25),(38,39)]],[[(24,25),(38,39)]], + nt.assert_raises(NotImplementedError, ds.pspec, [[(24,25),(38,39)]],[[(24,25),(38,39)]], (0,1),[('xx','xx')]) # compare the output of get_Q function with analytical estimates - + ds_Q = pspecdata.PSpecData(dsets=[uvd, uvd], wgts=[None, None],beam=self.bm_Q) bls_Q = [(24, 25)] uvp = ds_Q.pspec(bls_Q, bls_Q, (0, 1), [('xx', 'xx')], input_data_weight='identity', @@ -1120,7 +1164,7 @@ def test_pspec(self): nt.assert_equal(np.shape(Q_sample), (ds_Q.spw_range[1] - ds_Q.spw_range[0],\ ds_Q.spw_range[1] - ds_Q.spw_range[0])) #Check for the right shape - estimated_Q = (1.0/(4*np.pi)) * np.ones_like(Q_sample) + estimated_Q = (1.0/(4*np.pi)) * np.ones_like(Q_sample) nt.assert_true(np.allclose(np.real(estimated_Q), np.real(Q_sample), rtol=1e-05)) @@ -1136,7 +1180,7 @@ def test_pspec(self): key = (spw, blp, 'xx') power_real_new = (np.real(uvp_new.get_data(key))) power_real_ext = (np.real(uvp_ext.get_data(key))) - + diff = np.median((power_real_new-power_real_ext)/power_real_ext) nt.assert_true((diff <= 0.05)) @@ -1239,6 +1283,9 @@ def test_pspec(self): little_h=True, verbose=True, spw_ranges=[(10,14)], store_cov=True) nt.assert_true(hasattr(uvp, 'cov_array')) + uvp = ds.pspec(bls1, bls2, (0, 1), ('xx','xx'), input_data_weight='identity', norm='I', taper='none', + little_h=True, verbose=True, spw_ranges=[(10,14)], store_cov=True) + nt.assert_true(hasattr(uvp, 'cov_array')) # test identity_Y caching works ds = pspecdata.PSpecData(dsets=[copy.deepcopy(self.uvd), copy.deepcopy(self.uvd)], wgts=[None, None], beam=self.bm) @@ -1447,20 +1494,20 @@ def test_validate_blpairs(self): pspecdata.validate_blpairs(blpairs, uvd, uvd) def test_pspec_run(): - fnames = [os.path.join(DATA_PATH, d) + fnames = [os.path.join(DATA_PATH, d) for d in ['zen.even.xx.LST.1.28828.uvOCRSA', 'zen.odd.xx.LST.1.28828.uvOCRSA']] beamfile = os.path.join(DATA_PATH, "HERA_NF_dipole_power.beamfits") - fnames_std = [os.path.join(DATA_PATH,d) + fnames_std = [os.path.join(DATA_PATH,d) for d in ['zen.even.std.xx.LST.1.28828.uvOCRSA', 'zen.odd.std.xx.LST.1.28828.uvOCRSA']] # test basic execution if os.path.exists("./out.hdf5"): os.remove("./out.hdf5") - psc, ds = pspecdata.pspec_run(fnames, "./out.hdf5", Jy2mK=False, + psc, ds = pspecdata.pspec_run(fnames, "./out.hdf5", Jy2mK=False, verbose=False, overwrite=True, - bl_len_range=(14, 15), bl_deg_range=(50, 70), + bl_len_range=(14, 15), bl_deg_range=(50, 70), psname_ext='_0') nt.assert_true(isinstance(psc, container.PSpecContainer)) nt.assert_equal(psc.groups(), ['dset0_dset1']) diff --git a/hera_pspec/tests/test_utils.py b/hera_pspec/tests/test_utils.py index 4e32552e..da9ba38d 100644 --- a/hera_pspec/tests/test_utils.py +++ b/hera_pspec/tests/test_utils.py @@ -37,17 +37,17 @@ def test_load_config(): """ fname = os.path.join(DATA_PATH, '_test_utils.yaml') cfg = utils.load_config(fname) - + # Check that expected keys exist assert('data' in cfg.keys()) assert('pspec' in cfg.keys()) - + # Check that boolean values are read in correctly assert(cfg['pspec']['overwrite'] == True) - + # Check that lists are read in as lists assert(len(cfg['data']['subdirs']) == 1) - + # Check that missing files cause an error nt.assert_raises(IOError, utils.load_config, "file_that_doesnt_exist") @@ -64,9 +64,9 @@ class Test_Utils(unittest.TestCase): def setUp(self): # Load data into UVData object self.uvd = UVData() - self.uvd.read_miriad(os.path.join(DATA_PATH, + self.uvd.read_miriad(os.path.join(DATA_PATH, "zen.2458042.17772.xx.HH.uvXA")) - + # Create UVPSpec object self.uvp, cosmo = testing.build_vanilla_uvpspec() @@ -75,78 +75,78 @@ def tearDown(self): def runTest(self): pass - + def test_spw_range_from_freqs(self): """ - Test that spectral window ranges are correctly recovered from UVData and + Test that spectral window ranges are correctly recovered from UVData and UVPSpec files. """ # Check that type errors and bounds errors are raised - nt.assert_raises(AttributeError, utils.spw_range_from_freqs, np.arange(3), + nt.assert_raises(AttributeError, utils.spw_range_from_freqs, np.arange(3), freq_range=(100e6, 110e6)) for obj in [self.uvd, self.uvp]: - nt.assert_raises(ValueError, utils.spw_range_from_freqs, obj, + nt.assert_raises(ValueError, utils.spw_range_from_freqs, obj, freq_range=(98e6, 110e6)) # lower bound - nt.assert_raises(ValueError, utils.spw_range_from_freqs, obj, + nt.assert_raises(ValueError, utils.spw_range_from_freqs, obj, freq_range=(190e6, 202e6)) # upper bound - nt.assert_raises(ValueError, utils.spw_range_from_freqs, obj, + nt.assert_raises(ValueError, utils.spw_range_from_freqs, obj, freq_range=(190e6, 180e6)) # wrong order - + # Check that valid frequency ranges are returned freq_list = [(100e6, 120e6), (120e6, 140e6), (140e6, 160e6)] spw1 = utils.spw_range_from_freqs(self.uvd, freq_range=(110e6, 130e6)) spw2 = utils.spw_range_from_freqs(self.uvd, freq_range=freq_list) - spw3 = utils.spw_range_from_freqs(self.uvd, freq_range=(98e6, 120e6), + spw3 = utils.spw_range_from_freqs(self.uvd, freq_range=(98e6, 120e6), bounds_error=False) spw4 = utils.spw_range_from_freqs(self.uvd, freq_range=(100e6, 120e6)) - + # Make sure tuple vs. list arguments were handled correctly nt.ok_( isinstance(spw1, tuple) ) nt.ok_( isinstance(spw2, list) ) nt.ok_( len(spw2) == len(freq_list) ) - + # Make sure that bounds_error=False works nt.ok_( spw3 == spw4 ) - + # Make sure that this also works for UVPSpec objects spw5 = utils.spw_range_from_freqs(self.uvp, freq_range=(100e6, 104e6)) nt.ok_( isinstance(spw5, tuple) ) nt.ok_( spw5[0] is not None ) - + def test_spw_range_from_redshifts(self): """ - Test that spectral window ranges are correctly recovered from UVData and + Test that spectral window ranges are correctly recovered from UVData and UVPSpec files (when redshift range is specified). """ # Check that type errors and bounds errors are raised - nt.assert_raises(AttributeError, utils.spw_range_from_redshifts, + nt.assert_raises(AttributeError, utils.spw_range_from_redshifts, np.arange(3), z_range=(9.7, 12.1)) for obj in [self.uvd, self.uvp]: - nt.assert_raises(ValueError, utils.spw_range_from_redshifts, obj, + nt.assert_raises(ValueError, utils.spw_range_from_redshifts, obj, z_range=(5., 8.)) # lower bound - nt.assert_raises(ValueError, utils.spw_range_from_redshifts, obj, + nt.assert_raises(ValueError, utils.spw_range_from_redshifts, obj, z_range=(10., 20.)) # upper bound - nt.assert_raises(ValueError, utils.spw_range_from_redshifts, obj, + nt.assert_raises(ValueError, utils.spw_range_from_redshifts, obj, z_range=(11., 10.)) # wrong order - + # Check that valid frequency ranges are returned z_list = [(6.5, 7.5), (7.5, 8.5), (8.5, 9.5)] spw1 = utils.spw_range_from_redshifts(self.uvd, z_range=(7., 8.)) spw2 = utils.spw_range_from_redshifts(self.uvd, z_range=z_list) - spw3 = utils.spw_range_from_redshifts(self.uvd, z_range=(12., 14.), + spw3 = utils.spw_range_from_redshifts(self.uvd, z_range=(12., 14.), bounds_error=False) spw4 = utils.spw_range_from_redshifts(self.uvd, z_range=(6.2, 7.2)) - + # Make sure tuple vs. list arguments were handled correctly nt.ok_( isinstance(spw1, tuple) ) nt.ok_( isinstance(spw2, list) ) nt.ok_( len(spw2) == len(z_list) ) - + # Make sure that this also works for UVPSpec objects spw5 = utils.spw_range_from_redshifts(self.uvp, z_range=(13.1, 13.2)) nt.ok_( isinstance(spw5, tuple) ) nt.ok_( spw5[0] is not None ) - + def test_calc_blpair_reds(self): fname = os.path.join(DATA_PATH, 'zen.all.xx.LST.1.06964.uvA') @@ -155,7 +155,7 @@ def test_calc_blpair_reds(self): # basic execution (bls1, bls2, blps, xants1, - xants2) = utils.calc_blpair_reds(uvd, uvd, filter_blpairs=True, exclude_auto_bls=False, exclude_permutations=True) + xants2) = utils.calc_blpair_reds(uvd, uvd, filter_blpairs=True, exclude_auto_bls=False, exclude_permutations=True) nt.assert_equal(len(bls1), len(bls2), 15) nt.assert_equal(blps, list(zip(bls1, bls2))) nt.assert_equal(xants1, xants2) @@ -164,24 +164,24 @@ def test_calc_blpair_reds(self): # test xant_flag_thresh (bls1, bls2, blps, xants1, xants2) = utils.calc_blpair_reds(uvd, uvd, filter_blpairs=True, exclude_auto_bls=True, exclude_permutations=True, - xant_flag_thresh=0.0) + xant_flag_thresh=0.0) nt.assert_equal(len(bls1), len(bls2), 0) # test bl_len_range (bls1, bls2, blps, xants1, xants2) = utils.calc_blpair_reds(uvd, uvd, filter_blpairs=True, exclude_auto_bls=False, exclude_permutations=True, - bl_len_range=(0, 15.0)) + bl_len_range=(0, 15.0)) nt.assert_equal(len(bls1), len(bls2), 12) (bls1, bls2, blps, xants1, xants2) = utils.calc_blpair_reds(uvd, uvd, filter_blpairs=True, exclude_auto_bls=True, exclude_permutations=True, - bl_len_range=(0, 15.0)) + bl_len_range=(0, 15.0)) nt.assert_equal(len(bls1), len(bls2), 5) nt.assert_true(np.all([bls1[i] != bls2[i] for i in range(len(blps))])) # test grouping (bls1, bls2, blps, xants1, xants2) = utils.calc_blpair_reds(uvd, uvd, filter_blpairs=True, exclude_auto_bls=False, exclude_permutations=True, - Nblps_per_group=2) + Nblps_per_group=2) nt.assert_equal(len(blps), 10) nt.assert_true(isinstance(blps[0], list)) nt.assert_equal(blps[0], [((24, 37), (25, 38)), ((24, 37), (24, 37))]) @@ -191,17 +191,17 @@ def test_calc_blpair_reds(self): uvd2.select(bls=[(24, 25), (37, 38), (24, 39)]) (bls1, bls2, blps, xants1, xants2) = utils.calc_blpair_reds(uvd2, uvd2, filter_blpairs=True, exclude_auto_bls=True, exclude_permutations=True, - bl_len_range=(10.0, 20.0)) + bl_len_range=(10.0, 20.0)) nt.assert_equal(blps, [((24, 25), (37, 38))]) # test exceptions uvd2 = copy.deepcopy(uvd) uvd2.antenna_positions[0] += 2 nt.assert_raises(AssertionError, utils.calc_blpair_reds, uvd, uvd2) - + def test_get_delays(self): utils.get_delays(np.linspace(100., 200., 50)*1e6) - + def test_get_reds(self): fname = os.path.join(DATA_PATH, 'zen.all.xx.LST.1.06964.uvA') uvd = UVData() @@ -266,7 +266,7 @@ def test_config_pspec_blpairs(self): # test exceptions nt.assert_raises(AssertionError, utils.config_pspec_blpairs, uv_template, [('xx', 'xx'), ('xx', 'xx')], [('even', 'odd')], verbose=False) - + def test_log(): """ @@ -328,7 +328,6 @@ def test_get_blvec_reds(): red_bl_tag) = utils.get_blvec_reds(uvp, bl_error_tol=1.0, match_bl_lens=True) nt.assert_equal(len(red_bl_grp), 1) - def test_job_monitor(): # open empty files datafiles = ["./{}".format(i) for i in ['a', 'b', 'c', 'd']] @@ -366,4 +365,3 @@ def run_func(i, datafiles=datafiles): # remove files for df in datafiles: os.remove(df) - diff --git a/hera_pspec/tests/test_uvpspec.py b/hera_pspec/tests/test_uvpspec.py index 0eca35cb..c8b88660 100644 --- a/hera_pspec/tests/test_uvpspec.py +++ b/hera_pspec/tests/test_uvpspec.py @@ -10,7 +10,7 @@ import h5py from collections import OrderedDict as odict from pyuvdata import UVData - +import json class Test_UVPSpec(unittest.TestCase): @@ -82,7 +82,7 @@ def test_get_funcs(self): nt.assert_equal(blps, [((1, 2), (1, 2)), ((1, 3), (1, 3)), ((2, 3), (2, 3))]) # test get all keys keys = self.uvp.get_all_keys() - nt.assert_equal(keys, [(0, ((1, 2), (1, 2)), ('xx','xx')), + nt.assert_equal(keys, [(0, ((1, 2), (1, 2)), ('xx','xx')), (0, ((1, 3), (1, 3)), ('xx','xx')), (0, ((2, 3), (2, 3)), ('xx','xx'))]) # test omit_flags @@ -110,14 +110,14 @@ def test_stats_array(self): u2 = uvp.average_spectra([blpairs], time_avg=True, error_field=["errors", "who?"], inplace=False) nt.assert_true(np.all( u.get_stats("errors", keys[0]) == u.get_stats("who?", keys[0]))) u.select(times=np.unique(u.time_avg_array)[:20]) - + u3 = uvp.average_spectra([blpairs], time_avg=True, inplace=False) nt.assert_raises(KeyError, uvp.average_spectra, [blpairs], time_avg=True, inplace=False, error_field=["..............."]) nt.assert_false(hasattr(u3, "stats_array")) if os.path.exists('./ex.hdf5'): os.remove('./ex.hdf5') u.write_hdf5('./ex.hdf5') u.read_hdf5('./ex.hdf5') - os.remove('./ex.hdf5') + os.remove('./ex.hdf5') # test folding uvp = copy.deepcopy(self.uvp) @@ -155,14 +155,14 @@ def test_indices_funcs(self): spw, blpairts, pol = self.uvp.key_to_indices( (0, ((1,2),(1,2)), 1515) ) nt.assert_equal(spw, 0) nt.assert_equal(pol, 0) - nt.assert_true(np.isclose(blpairts, + nt.assert_true(np.isclose(blpairts, np.array([0,3,6,9,12,15,18,21,24,27])).min()) spw, blpairts, pol = self.uvp.key_to_indices( (0, 101102101102, ('xx','xx')) ) nt.assert_equal(spw, 0) nt.assert_equal(pol, 0) - nt.assert_true(np.isclose(blpairts, + nt.assert_true(np.isclose(blpairts, np.array([0,3,6,9,12,15,18,21,24,27])).min()) - + # Check different polpair specification methods give the same results s1, b1, p1 = self.uvp.key_to_indices( (0, ((1,2),(1,2)), 1515) ) s2, b2, p2 = self.uvp.key_to_indices( (0, ((1,2),(1,2)), ('xx','xx')) ) @@ -226,13 +226,25 @@ def test_select(self): uvd.read_miriad(os.path.join(DATA_PATH, 'zen.even.xx.LST.1.28828.uvOCRSA')) beam = pspecbeam.PSpecBeamUV(os.path.join(DATA_PATH, "HERA_NF_dipole_power.beamfits")) bls = [(37, 38), (38, 39), (52, 53)] - uvp1 = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(20, 30), (60, 90)], beam=beam) + rp = {'filter_centers':[0.], + 'filter_widths':[250e-9], + 'filter_factors':[1e-9]} + r_params = {} + for bl in bls: + key1 = bl + ('xx',) + r_params[key1] = rp + + uvp1 = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(20, 30), (60, 90)], beam=beam, + r_params = r_params) uvp2 = uvp1.select(spws=0, inplace=False) nt.assert_equal(uvp2.Nspws, 1) uvp2 = uvp2.select(bls=[(37, 38), (38, 39)], inplace=False) nt.assert_equal(uvp2.Nblpairs, 1) nt.assert_equal(uvp2.data_array[0].shape, (10, 10, 1)) nt.assert_almost_equal(uvp2.data_array[0][0,0,0], (-3831605.3903496987+8103523.9604128916j)) + nt.assert_equal(len(uvp2.get_r_params().keys()), 2) + for rpkey in uvp2.get_r_params(): + nt.assert_true(rpkey == (37, 38, 'xx') or rpkey == (38, 39, 'xx')) # blpair select uvp = copy.deepcopy(self.uvp) @@ -250,7 +262,7 @@ def test_select(self): # test pol and blpair select, and check dimensionality of output uvp = copy.deepcopy(self.uvp) uvp.set_stats('hi', uvp.get_all_keys()[0], np.ones(300).reshape(10, 30)) - uvp2 = uvp.select(blpairs=uvp.get_blpairs(), polpairs=uvp.polpair_array, + uvp2 = uvp.select(blpairs=uvp.get_blpairs(), polpairs=uvp.polpair_array, inplace=False) nt.assert_equal(uvp2.data_array[0].shape, (30, 30, 1)) nt.assert_equal(uvp2.stats_array['hi'][0].shape, (30, 30, 1)) @@ -261,7 +273,7 @@ def test_select(self): uvp3.polpair_array[0] = 1313 uvp4.polpair_array[0] = 1212 uvp = uvp + uvp2 + uvp3 + uvp4 - uvp5 = uvp.select(blpairs=[101102101102], polpairs=[1515, 1414, 1313], + uvp5 = uvp.select(blpairs=[101102101102], polpairs=[1515, 1414, 1313], inplace=False) nt.assert_equal(uvp5.data_array[0].shape, (10, 30, 3)) @@ -304,6 +316,24 @@ def test_clear(self): nt.assert_false(hasattr(uvp, 'Ntimes')) nt.assert_false(hasattr(uvp, 'data_array')) + def test_get_r_params(self): + + # inplace vs not inplace, spw selection + uvd = UVData() + uvd.read_miriad(os.path.join(DATA_PATH, 'zen.even.xx.LST.1.28828.uvOCRSA')) + beam = pspecbeam.PSpecBeamUV(os.path.join(DATA_PATH, "HERA_NF_dipole_power.beamfits")) + bls = [(37, 38), (38, 39), (52, 53)] + rp = {'filter_centers':[0.], + 'filter_widths':[250e-9], + 'filter_factors':[1e-9]} + r_params = {} + for bl in bls: + key1 = bl + ('xx',) + r_params[key1] = rp + uvp = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(20, 30), (60, 90)], beam=beam, + r_params = r_params) + nt.assert_equal(r_params, uvp.get_r_params()) + def test_write_read_hdf5(self): # test basic write execution uvp = copy.deepcopy(self.uvp) @@ -366,9 +396,9 @@ def test_sense(self): def test_average_spectra(self): uvp = copy.deepcopy(self.uvp) # test blpair averaging - blpairs = uvp.get_blpair_groups_from_bl_groups([[101102, 102103, 101103]], + blpairs = uvp.get_blpair_groups_from_bl_groups([[101102, 102103, 101103]], only_pairs_in_bls=False) - uvp2 = uvp.average_spectra(blpair_groups=blpairs, time_avg=False, + uvp2 = uvp.average_spectra(blpair_groups=blpairs, time_avg=False, inplace=False) nt.assert_equal(uvp2.Nblpairs, 1) nt.assert_true(np.isclose(uvp2.get_nsamples((0, 101102101102, ('xx','xx'))), 3.0).all()) @@ -390,13 +420,13 @@ def test_average_spectra(self): uvp3a.get_data((0, 101102101102, ('xx','xx'))), uvp3b.get_data((0, 101102101102, ('xx','xx')))).all()) #nt.assert_equal(uvp2.get_data((0, 101102101102, 'xx')).shape, (10, 30)) - + # test time averaging uvp2 = uvp.average_spectra(time_avg=True, inplace=False) nt.assert_true(uvp2.Ntimes, 1) nt.assert_true(np.isclose( uvp2.get_nsamples((0, 101102101102, ('xx','xx'))), 10.0).all()) - nt.assert_true(uvp2.get_data((0, 101102101102, ('xx','xx'))).shape, + nt.assert_true(uvp2.get_data((0, 101102101102, ('xx','xx'))).shape, (1, 30)) # ensure averaging works when multiple repeated baselines are present, but only # if time_avg = True @@ -418,10 +448,10 @@ def test_fold_spectra(self): uvd_std = UVData() uvd.read_miriad(os.path.join(DATA_PATH, 'zen.even.xx.LST.1.28828.uvOCRSA')) uvd_std.read_miriad(os.path.join(DATA_PATH,'zen.even.xx.LST.1.28828.uvOCRSA')) - beam = pspecbeam.PSpecBeamUV(os.path.join(DATA_PATH, + beam = pspecbeam.PSpecBeamUV(os.path.join(DATA_PATH, "HERA_NF_dipole_power.beamfits")) bls = [(37, 38), (38, 39), (52, 53)] - uvp1 = testing.uvpspec_from_data(uvd, bls, data_std=uvd_std, + uvp1 = testing.uvpspec_from_data(uvd, bls, data_std=uvd_std, spw_ranges=[(0,17)], beam=beam) uvp1.fold_spectra() cov_folded = uvp1.get_cov((0, ((37, 38), (38, 39)), ('xx','xx'))) @@ -467,20 +497,36 @@ def test_combine_uvpspec(self): # setup uvp build uvd = UVData() uvd.read_miriad(os.path.join(DATA_PATH, 'zen.even.xx.LST.1.28828.uvOCRSA')) - beam = pspecbeam.PSpecBeamUV(os.path.join(DATA_PATH, + beam = pspecbeam.PSpecBeamUV(os.path.join(DATA_PATH, "HERA_NF_dipole_power.beamfits")) bls = [(37, 38), (38, 39), (52, 53)] - uvp1 = testing.uvpspec_from_data(uvd, bls, - spw_ranges=[(20, 30), (60, 90)], - beam=beam) - + + rp = {'filter_centers':[0.], + 'filter_widths':[250e-9], + 'filter_factors':[1e-9]} + + r_params = {} + + for bl in bls: + key1 = bl + ('xx',) + r_params[key1] = rp + + #create an r_params copy with inconsistent weighting to test + #error case + r_params_inconsistent = copy.deepcopy(r_params) + r_params[key1]['filter_widths'] = [100e-9] + + uvp1 = testing.uvpspec_from_data(uvd, bls, + spw_ranges=[(20, 30), (60, 90)], + beam=beam, r_params = r_params) + print("uvp1 unique blps:", np.unique(uvp1.blpair_array)) # test failure due to overlapping data uvp2 = copy.deepcopy(uvp1) - + print("uvp2 unique blps:", np.unique(uvp2.blpair_array)) - + nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) # test success across pol @@ -489,22 +535,34 @@ def test_combine_uvpspec(self): nt.assert_equal(out.Npols, 2) nt.assert_true(len(set(out.polpair_array) ^ set([1515, 1414])) == 0) key = (0, ((37, 38), (38, 39)), ('xx','xx')) - nt.assert_true(np.all(np.isclose(out.get_nsamples(key), + nt.assert_true(np.all(np.isclose(out.get_nsamples(key), np.ones(10, dtype=np.float64)))) - nt.assert_true(np.all(np.isclose(out.get_integrations(key), + nt.assert_true(np.all(np.isclose(out.get_integrations(key), 190 * np.ones(10, dtype=np.float64), atol=5, rtol=2))) + #test errors when combining with pspecs without r_params + uvp3 = copy.deepcopy(uvp2) + uvp3.r_params = '' + nt.assert_raises(ValueError, uvpspec.combine_uvpspec, [uvp1, uvp3]) + #combining multiple uvp objects without r_params should run fine + uvp4 = copy.deepcopy(uvp1) + uvp4.r_params = '' + uvpspec.combine_uvpspec([uvp3, uvp4]) + #now test error case with inconsistent weightings. + uvp5 = copy.deepcopy(uvp2) + uvp5.r_params = uvputils.compress_r_params(r_params_inconsistent) + nt.assert_raises(ValueError, uvpspec.combine_uvpspec, [uvp1, uvp5]) # test multiple non-overlapping data axes uvp2.freq_array[0] = 0.0 nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) # test partial data overlap failure - uvp2 = testing.uvpspec_from_data(uvd, [(37, 38), (38, 39), (53, 54)], - spw_ranges=[(20, 30), (60, 90)], + uvp2 = testing.uvpspec_from_data(uvd, [(37, 38), (38, 39), (53, 54)], + spw_ranges=[(20, 30), (60, 90)], beam=beam) nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) - uvp2 = testing.uvpspec_from_data(uvd, bls, - spw_ranges=[(20, 30), (60, 105)], + uvp2 = testing.uvpspec_from_data(uvd, bls, + spw_ranges=[(20, 30), (60, 105)], beam=beam) nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) uvp2 = copy.deepcopy(uvp1) @@ -513,17 +571,17 @@ def test_combine_uvpspec(self): nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) # test concat across spw - uvp2 = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(85, 101)], - beam=beam) + uvp2 = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(85, 101)], + beam=beam, r_params = r_params) out = uvpspec.combine_uvpspec([uvp1, uvp2], verbose=False) nt.assert_equal(out.Nspws, 3) nt.assert_equal(out.Nfreqs, 51) nt.assert_equal(out.Nspwdlys, 56) # test concat across blpairts - uvp2 = testing.uvpspec_from_data(uvd, [(53, 54), (67, 68)], - spw_ranges=[(20, 30), (60, 90)], - beam=beam) + uvp2 = testing.uvpspec_from_data(uvd, [(53, 54), (67, 68)], + spw_ranges=[(20, 30), (60, 90)], + beam=beam, r_params = r_params) out = uvpspec.combine_uvpspec([uvp1, uvp2], verbose=False) nt.assert_equal(out.Nblpairs, 4) nt.assert_equal(out.Nbls, 5) @@ -553,15 +611,15 @@ def test_combine_uvpspec(self): uvp3.polpair_array[0] = 1313 out = uvp1 + uvp2 + uvp3 nt.assert_equal(out.Npols, 3) - + # Test whether n_dlys != Nfreqs works - uvp4 = testing.uvpspec_from_data(uvd, bls, beam=beam, - spw_ranges=[(20, 30), (60, 90)], + uvp4 = testing.uvpspec_from_data(uvd, bls, beam=beam, + spw_ranges=[(20, 30), (60, 90)], n_dlys=[5, 15]) uvp4b = copy.deepcopy(uvp4) uvp4b.polpair_array[0] = 1414 out = uvpspec.combine_uvpspec([uvp4, uvp4b], verbose=False) - + def test_combine_uvpspec_std(self): # setup uvp build @@ -573,8 +631,8 @@ def test_combine_uvpspec_std(self): beam = pspecbeam.PSpecBeamUV( os.path.join(DATA_PATH, "HERA_NF_dipole_power.beamfits")) bls = [(37, 38), (38, 39), (52, 53)] - uvp1 = testing.uvpspec_from_data(uvd, bls, data_std=uvd_std, - spw_ranges=[(20, 24), (64, 68)], + uvp1 = testing.uvpspec_from_data(uvd, bls, data_std=uvd_std, + spw_ranges=[(20, 24), (64, 68)], beam=beam) # test failure due to overlapping data uvp2 = copy.deepcopy(uvp1) @@ -589,13 +647,13 @@ def test_combine_uvpspec_std(self): nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) # test partial data overlap failure - uvp2 = testing.uvpspec_from_data(uvd, [(37, 38), (38, 39), (53, 54)], - data_std=uvd_std, - spw_ranges=[(20, 24), (64, 68)], + uvp2 = testing.uvpspec_from_data(uvd, [(37, 38), (38, 39), (53, 54)], + data_std=uvd_std, + spw_ranges=[(20, 24), (64, 68)], beam=beam) nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) - uvp2 = testing.uvpspec_from_data(uvd, bls, - spw_ranges=[(20, 24), (64, 68)], + uvp2 = testing.uvpspec_from_data(uvd, bls, + spw_ranges=[(20, 24), (64, 68)], data_std=uvd_std, beam=beam) nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) uvp2 = copy.deepcopy(uvp1) @@ -604,7 +662,7 @@ def test_combine_uvpspec_std(self): nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) # test concat across spw - uvp2 = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(85, 91)], + uvp2 = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(85, 91)], data_std=uvd_std, beam=beam) out = uvpspec.combine_uvpspec([uvp1, uvp2], verbose=False) nt.assert_equal(out.Nspws, 3) @@ -612,8 +670,8 @@ def test_combine_uvpspec_std(self): nt.assert_equal(out.Nspwdlys, 14) # test concat across blpairts - uvp2 = testing.uvpspec_from_data(uvd, [(53, 54), (67, 68)], - spw_ranges=[(20, 24), (64, 68)], + uvp2 = testing.uvpspec_from_data(uvd, [(53, 54), (67, 68)], + spw_ranges=[(20, 24), (64, 68)], data_std=uvd_std, beam=beam) out = uvpspec.combine_uvpspec([uvp1, uvp2], verbose=False) nt.assert_equal(out.Nblpairs, 4) @@ -628,7 +686,7 @@ def test_combine_uvpspec_std(self): nt.assert_raises(AssertionError, uvpspec.combine_uvpspec, [uvp1, uvp2]) # test feed as strings - uvp1 = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(20, 30)], + uvp1 = testing.uvpspec_from_data(uvd, bls, spw_ranges=[(20, 30)], data_std=uvd_std, beam=beam) uvp2 = copy.deepcopy(uvp1) uvp2.polpair_array[0] = 1414 @@ -638,7 +696,7 @@ def test_combine_uvpspec_std(self): nt.assert_true(out.Npols, 2) for ff in ['uvp1.hdf5', 'uvp2.hdf5']: if os.path.exists(ff): os.remove(ff) - + # test UVPSpec __add__ uvp3 = copy.deepcopy(uvp1) uvp3.polpair_array[0] = 1313 @@ -661,4 +719,3 @@ def test_conj_blpair(): blpair = uvputils._conj_blpair(101102103104, which='both') nt.assert_equal(blpair, 102101104103) nt.assert_raises(ValueError, uvputils._conj_blpair, 102101103104, which='foo') - diff --git a/hera_pspec/tests/test_uvpspec_utils.py b/hera_pspec/tests/test_uvpspec_utils.py index 7418ffe3..7c22987b 100644 --- a/hera_pspec/tests/test_uvpspec_utils.py +++ b/hera_pspec/tests/test_uvpspec_utils.py @@ -7,6 +7,7 @@ from hera_pspec.data import DATA_PATH import os import copy +import json def test_select_common(): @@ -17,63 +18,62 @@ def test_select_common(): beamfile = os.path.join(DATA_PATH, 'HERA_NF_dipole_power.beamfits') beam = pspecbeam.PSpecBeamUV(beamfile) uvp, cosmo = testing.build_vanilla_uvpspec(beam=beam) - # Carve up some example UVPSpec objects - uvp1 = uvp.select(times=np.unique(uvp.time_avg_array)[:-1], + uvp1 = uvp.select(times=np.unique(uvp.time_avg_array)[:-1], inplace=False) - uvp2 = uvp.select(times=np.unique(uvp.time_avg_array)[1:], + uvp2 = uvp.select(times=np.unique(uvp.time_avg_array)[1:], inplace=False) - uvp3 = uvp.select(blpairs=np.unique(uvp.blpair_array)[1:], + uvp3 = uvp.select(blpairs=np.unique(uvp.blpair_array)[1:], inplace=False) - uvp4 = uvp.select(blpairs=np.unique(uvp.blpair_array)[:2], + uvp4 = uvp.select(blpairs=np.unique(uvp.blpair_array)[:2], inplace=False) - uvp5 = uvp.select(blpairs=np.unique(uvp.blpair_array)[:1], + uvp5 = uvp.select(blpairs=np.unique(uvp.blpair_array)[:1], inplace=False) - uvp6 = uvp.select(times=np.unique(uvp.time_avg_array)[:1], + uvp6 = uvp.select(times=np.unique(uvp.time_avg_array)[:1], inplace=False) - + # Check that selecting on common times works uvp_list = [uvp1, uvp2] - uvp_new = uvputils.select_common(uvp_list, spws=True, blpairs=True, + uvp_new = uvputils.select_common(uvp_list, spws=True, blpairs=True, times=True, polpairs=True, inplace=False) nt.assert_equal(uvp_new[0], uvp_new[1]) - np.testing.assert_array_equal(uvp_new[0].time_avg_array, + np.testing.assert_array_equal(uvp_new[0].time_avg_array, uvp_new[1].time_avg_array) - + # Check that selecting on common baseline-pairs works uvp_list_2 = [uvp1, uvp2, uvp3] - uvp_new_2 = uvputils.select_common(uvp_list_2, spws=True, blpairs=True, + uvp_new_2 = uvputils.select_common(uvp_list_2, spws=True, blpairs=True, times=True, polpairs=True, inplace=False) nt.assert_equal(uvp_new_2[0], uvp_new_2[1]) nt.assert_equal(uvp_new_2[0], uvp_new_2[2]) - np.testing.assert_array_equal(uvp_new_2[0].time_avg_array, + np.testing.assert_array_equal(uvp_new_2[0].time_avg_array, uvp_new_2[1].time_avg_array) - + # Check that zero overlap in times raises a ValueError - nt.assert_raises(ValueError, uvputils.select_common, [uvp2, uvp6], - spws=True, blpairs=True, times=True, + nt.assert_raises(ValueError, uvputils.select_common, [uvp2, uvp6], + spws=True, blpairs=True, times=True, polpairs=True, inplace=False) - - # Check that zero overlap in times does *not* raise a ValueError if + + # Check that zero overlap in times does *not* raise a ValueError if # not selecting on times - uvp_new_3 = uvputils.select_common([uvp2, uvp6], spws=True, - blpairs=True, times=False, + uvp_new_3 = uvputils.select_common([uvp2, uvp6], spws=True, + blpairs=True, times=False, polpairs=True, inplace=False) - + # Check that zero overlap in baselines raises a ValueError - nt.assert_raises(ValueError, uvputils.select_common, [uvp3, uvp5], - spws=True, blpairs=True, times=True, + nt.assert_raises(ValueError, uvputils.select_common, [uvp3, uvp5], + spws=True, blpairs=True, times=True, polpairs=True, inplace=False) - + # Check that matching times are ignored when set to False - uvp_new = uvputils.select_common(uvp_list, spws=True, blpairs=True, + uvp_new = uvputils.select_common(uvp_list, spws=True, blpairs=True, times=False, polpairs=True, inplace=False) - nt.assert_not_equal( np.sum(uvp_new[0].time_avg_array + nt.assert_not_equal( np.sum(uvp_new[0].time_avg_array - uvp_new[1].time_avg_array), 0.) nt.assert_equal(len(uvp_new), len(uvp_list)) - + # Check that in-place selection works - uvputils.select_common(uvp_list, spws=True, blpairs=True, + uvputils.select_common(uvp_list, spws=True, blpairs=True, times=True, polpairs=True, inplace=True) nt.assert_equal(uvp1, uvp2) @@ -93,7 +93,7 @@ def test_select_common(): # check pol overlap uvp7 = copy.deepcopy(uvp1) uvp7.polpair_array[0] = 1212 # = (-8,-8) - nt.assert_raises(ValueError, uvputils.select_common, [uvp1, uvp7], + nt.assert_raises(ValueError, uvputils.select_common, [uvp1, uvp7], polpairs=True) def test_get_blpairs_from_bls(): @@ -104,7 +104,7 @@ def test_get_blpairs_from_bls(): beamfile = os.path.join(DATA_PATH, 'HERA_NF_dipole_power.beamfits') beam = pspecbeam.PSpecBeamUV(beamfile) uvp, cosmo = testing.build_vanilla_uvpspec(beam=beam) - + # Check that bls can be specified in several different ways blps = uvputils._get_blpairs_from_bls(uvp, bls=101102) blps = uvputils._get_blpairs_from_bls(uvp, bls=(101,102)) @@ -119,14 +119,14 @@ def test_get_red_bls(): beamfile = os.path.join(DATA_PATH, 'HERA_NF_dipole_power.beamfits') beam = pspecbeam.PSpecBeamUV(beamfile) uvp, cosmo = testing.build_vanilla_uvpspec(beam=beam) - + # Get redundant baseline groups bls, lens, angs = uvp.get_red_bls() - + nt.assert_equal(len(bls), 3) # three red grps in this file nt.assert_equal(len(bls), len(lens)) # Should be one length for each group nt.assert_equal(len(bls), len(angs)) # Ditto, for angles - + # Check that number of grouped baselines = total no. of baselines num_bls = 0 for grp in bls: @@ -143,17 +143,17 @@ def test_get_red_blpairs(): beamfile = os.path.join(DATA_PATH, 'HERA_NF_dipole_power.beamfits') beam = pspecbeam.PSpecBeamUV(beamfile) uvp, cosmo = testing.build_vanilla_uvpspec(beam=beam) - + # Get redundant baseline groups blps, lens, angs = uvp.get_red_blpairs() - + nt.assert_equal(len(blps), 3) # three red grps in this file nt.assert_equal(len(blps), len(lens)) # Should be one length for each group nt.assert_equal(len(blps), len(angs)) # Ditto, for angles - + # Check output type nt.assert_equal(isinstance(blps[0][0], (np.int, int)), True) - + # Check that number of grouped blps = total no. of blps num_blps = 0 for grp in blps: @@ -167,30 +167,30 @@ def test_polpair_int2tuple(): Test conversion of polpair ints to tuples. """ # List of polpairs to test - polpairs = [('xx','xx'), ('xx','yy'), ('xy', 'yx'), + polpairs = [('xx','xx'), ('xx','yy'), ('xy', 'yx'), ('pI','pI'), ('pI','pQ'), ('pQ','pQ'), ('pU','pU'), ('pV','pV') ] - + # Check that lists and single items work pol_ints = uvputils.polpair_tuple2int(polpairs) uvputils.polpair_tuple2int(polpairs[0]) uvputils.polpair_int2tuple(1515) uvputils.polpair_int2tuple([1515,1414]) uvputils.polpair_int2tuple(np.array([1515,1414])) - + # Test converting to int and then back again pol_pairs_returned = uvputils.polpair_int2tuple(pol_ints, pol_strings=True) for i in range(len(polpairs)): nt.assert_equal(polpairs[i], pol_pairs_returned[i]) - + # Check that errors are raised appropriately nt.assert_raises(AssertionError, uvputils.polpair_int2tuple, ('xx','xx')) nt.assert_raises(AssertionError, uvputils.polpair_int2tuple, 'xx') nt.assert_raises(AssertionError, uvputils.polpair_int2tuple, 'pI') nt.assert_raises(ValueError, uvputils.polpair_int2tuple, 999) nt.assert_raises(ValueError, uvputils.polpair_int2tuple, [999,]) - - + + def test_subtract_uvp(): """ Test subtraction of two UVPSpec objects @@ -215,7 +215,7 @@ def test_subtract_uvp(): # check stats_array is np.sqrt(2) nt.assert_true(np.isclose(uvs.stats_array['mystat'][0], np.sqrt(2)).all()) - + def test_conj_blpair_int(): conj_blpair = uvputils._conj_blpair_int(101102103104) @@ -238,11 +238,11 @@ def test_conj_blpair(): def test_fast_is_in(): - blps = [ 102101103104, 102101103104, 102101103104, 102101103104, + blps = [ 102101103104, 102101103104, 102101103104, 102101103104, 101102104103, 101102104103, 101102104103, 101102104103, 102101104103, 102101104103, 102101104103, 102101104103 ] - times = [ 0.1, 0.15, 0.2, 0.25, - 0.1, 0.15, 0.2, 0.25, + times = [ 0.1, 0.15, 0.2, 0.25, + 0.1, 0.15, 0.2, 0.25, 0.1, 0.15, 0.3, 0.3, ] src_blpts = np.array(list(zip(blps, times))) @@ -251,17 +251,52 @@ def test_fast_is_in(): def test_fast_lookup_blpairts(): # Construct array of blpair-time tuples (including some out of order) - blps = [ 102101103104, 102101103104, 102101103104, 102101103104, + blps = [ 102101103104, 102101103104, 102101103104, 102101103104, 101102104103, 101102104103, 101102104103, 101102104103, 102101104103, 102101104103, 102101104103, 102101104103 ] - times = [ 0.1, 0.15, 0.2, 0.25, - 0.1, 0.15, 0.2, 0.25, + times = [ 0.1, 0.15, 0.2, 0.25, + 0.1, 0.15, 0.2, 0.25, 0.1, 0.15, 0.3, 0.3, ] src_blpts = np.array(list(zip(blps, times))) - + # List of blpair-times to look up query_blpts = [(102101103104, 0.1), (101102104103, 0.1), (101102104103, 0.25)] - + # Look up indices, compare with expected result idxs = uvputils._fast_lookup_blpairts(src_blpts, np.array(query_blpts)) np.testing.assert_array_equal(idxs, np.array([0, 4, 7])) + +def test_r_param_compression(): + baselines = [(24,25), (37,38), (38,39)] + + rp = {'filter_centers':[0.], + 'filter_widths':[250e-9], + 'filter_factors':[1e-9]} + + r_params = {} + + for bl in baselines: + key1 = bl + ('xx',) + key2 = bl + ('xx',) + r_params[key1] = rp + r_params[key2] = rp + + rp_str_1 = uvputils.compress_r_params(r_params) + rp = uvputils.decompress_r_params(rp_str_1) + for rpk in rp: + for rpfk in rp[rpk]: + nt.assert_true(rp[rpk][rpfk] == r_params[rpk][rpfk]) + for rpk in r_params: + for rpfk in r_params[rpk]: + nt.assert_true(r_params[rpk][rpfk] == rp[rpk][rpfk]) + + rp_str_2 = uvputils.compress_r_params(rp) + nt.assert_true(json.loads(rp_str_1) == json.loads(rp_str_2)) + + nt.assert_true(uvputils.compress_r_params({}) == '') + nt.assert_true(uvputils.decompress_r_params('') == {}) + + print("rp_str_1") + print(rp_str_1) + print("rp_str_2") + print(rp_str_2) diff --git a/hera_pspec/utils.py b/hera_pspec/utils.py index 01fc3345..ed1243d2 100644 --- a/hera_pspec/utils.py +++ b/hera_pspec/utils.py @@ -11,7 +11,6 @@ from pyuvdata import UVData from datetime import datetime - def cov(d1, w1, d2=None, w2=None, conj_1=False, conj_2=True): """ Computes an empirical covariance matrix from data vectors. If d1 is of size @@ -75,44 +74,44 @@ def cov(d1, w1, d2=None, w2=None, conj_1=False, conj_2=True): return C -def construct_blpairs(bls, exclude_auto_bls=False, exclude_permutations=False, +def construct_blpairs(bls, exclude_auto_bls=False, exclude_permutations=False, group=False, Nblps_per_group=1): """ - Construct a list of baseline-pairs from a baseline-group. This function - can be used to easily convert a single list of baselines into the input + Construct a list of baseline-pairs from a baseline-group. This function + can be used to easily convert a single list of baselines into the input needed by PSpecData.pspec(bls1, bls2, ...). Parameters ---------- bls : list of tuple - List of baseline tuples, Ex. [(1, 2), (2, 3), (3, 4)]. Baseline - integers are not supported, and must first be converted to tuples + List of baseline tuples, Ex. [(1, 2), (2, 3), (3, 4)]. Baseline + integers are not supported, and must first be converted to tuples using UVData.baseline_to_antnums(). exclude_auto_bls: bool, optional - If True, exclude all baselines crossed with themselves from the final + If True, exclude all baselines crossed with themselves from the final blpairs list. Default: False. exclude_permutations : bool, optional - If True, exclude permutations and only form combinations of the bls + If True, exclude permutations and only form combinations of the bls list. - - For example, if bls = [1, 2, 3] (note this isn't the proper form of - bls, but makes the example clearer) and exclude_permutations = False, - then blpairs = [11, 12, 13, 21, 22, 23,, 31, 32, 33]. If however + + For example, if bls = [1, 2, 3] (note this isn't the proper form of + bls, but makes the example clearer) and exclude_permutations = False, + then blpairs = [11, 12, 13, 21, 22, 23,, 31, 32, 33]. If however exclude_permutations = True, then blpairs = [11, 12, 13, 22, 23, 33]. - - Furthermore, if exclude_auto_bls = True then 11, 22, and 33 would + + Furthermore, if exclude_auto_bls = True then 11, 22, and 33 would also be excluded. - + Default: False. group : bool, optional - If True, group each consecutive Nblps_per_group blpairs into sub-lists. + If True, group each consecutive Nblps_per_group blpairs into sub-lists. Default: False. Nblps_per_group : int, optional - Number of baseline-pairs to put into each sub-group if group = True. + Number of baseline-pairs to put into each sub-group if group = True. Default: 1. Returns (bls1, bls2, blpairs) @@ -127,7 +126,7 @@ def construct_blpairs(bls, exclude_auto_bls=False, exclude_permutations=False, assert isinstance(bls, (list, np.ndarray)) and isinstance(bls[0], tuple), \ "bls must be fed as list or ndarray of baseline antnum tuples. Use " \ "UVData.baseline_to_antnums() to convert baseline integers to tuples." - + # form blpairs w/o explicitly forming auto blpairs # however, if there are repeated bl in bls, there will be auto bls in blpairs if exclude_permutations: @@ -169,26 +168,26 @@ def construct_blpairs(bls, exclude_auto_bls=False, exclude_permutations=False, return bls1, bls2, blpairs -def calc_blpair_reds(uvd1, uvd2, bl_tol=1.0, filter_blpairs=True, +def calc_blpair_reds(uvd1, uvd2, bl_tol=1.0, filter_blpairs=True, xant_flag_thresh=0.95, exclude_auto_bls=False, - exclude_permutations=True, Nblps_per_group=None, + exclude_permutations=True, Nblps_per_group=None, bl_len_range=(0, 1e10), bl_deg_range=(0, 180)): """ - Use hera_cal.redcal to get matching, redundant baseline-pair groups from - uvd1 and uvd2 within the specified baseline tolerance, not including + Use hera_cal.redcal to get matching, redundant baseline-pair groups from + uvd1 and uvd2 within the specified baseline tolerance, not including flagged ants. Parameters ---------- uvd1, uvd2 : UVData - UVData instances with visibility data for the first/second visibilities + UVData instances with visibility data for the first/second visibilities in the cross-spectra that will be formed. bl_tol : float, optional Baseline-vector redundancy tolerance in meters filter_blpairs : bool, optional - if True, calculate xants and filters-out baseline pairs based on + if True, calculate xants and filters-out baseline pairs based on xant lists and actual baselines in the data. xant_flag_thresh : float, optional @@ -200,7 +199,7 @@ def calc_blpair_reds(uvd1, uvd2, bl_tol=1.0, filter_blpairs=True, exclude_permutations : boolean, optional If True, exclude permutations and only form combinations of the bls list. - + For example, if bls = [1, 2, 3] (note this isn't the proper form of bls, but makes this example clearer) and exclude_permutations = False, then blpairs = [11, 12, 13, 21, 22, 23, 31, 32, 33]. If however @@ -212,11 +211,11 @@ def calc_blpair_reds(uvd1, uvd2, bl_tol=1.0, filter_blpairs=True, Default: None bl_len_range : tuple, optional - len-2 tuple containing minimum baseline length and maximum baseline + len-2 tuple containing minimum baseline length and maximum baseline length [meters] to keep in baseline type selection bl_deg_range : tuple, optional - len-2 tuple containing (minimum, maximum) baseline angle in degrees + len-2 tuple containing (minimum, maximum) baseline angle in degrees to keep in baseline selection Returns @@ -228,7 +227,7 @@ def calc_blpair_reds(uvd1, uvd2, bl_tol=1.0, filter_blpairs=True, blpairs : list of baseline-pair tuples Contains the baseline-pair tuples. i.e. zip(baselines1, baselines2) - xants1, xants2 : lists + xants1, xants2 : lists List of bad antenna integers for uvd1 and uvd2 """ # get antenna positions @@ -290,8 +289,8 @@ def calc_blpair_reds(uvd1, uvd2, bl_tol=1.0, filter_blpairs=True, baselines1, baselines2, blpairs = [], [], [] for r in reds: (bls1, bls2, - blps) = construct_blpairs(r, exclude_auto_bls=exclude_auto_bls, - group=False, + blps) = construct_blpairs(r, exclude_auto_bls=exclude_auto_bls, + group=False, exclude_permutations=exclude_permutations) if len(bls1) < 1: continue @@ -314,11 +313,11 @@ def calc_blpair_reds(uvd1, uvd2, bl_tol=1.0, filter_blpairs=True, # group if desired if Nblps_per_group is not None: Ngrps = int(np.ceil(float(len(blps)) / Nblps_per_group)) - bls1 = [bls1[Nblps_per_group*i:Nblps_per_group*(i+1)] + bls1 = [bls1[Nblps_per_group*i:Nblps_per_group*(i+1)] for i in range(Ngrps)] - bls2 = [bls2[Nblps_per_group*i:Nblps_per_group*(i+1)] + bls2 = [bls2[Nblps_per_group*i:Nblps_per_group*(i+1)] for i in range(Ngrps)] - blps = [blps[Nblps_per_group*i:Nblps_per_group*(i+1)] + blps = [blps[Nblps_per_group*i:Nblps_per_group*(i+1)] for i in range(Ngrps)] baselines1.extend(bls1) @@ -780,7 +779,7 @@ def get_blvec_reds(blvecs, bl_error_tol=1.0, match_bl_lens=False): uvp = blvecs bls = uvp.bl_array bl_vecs = uvp.get_ENU_bl_vecs()[:, :2] - blvecs = dict(list(zip( [uvp.bl_to_antnums(_bls) for _bls in bls], + blvecs = dict(list(zip( [uvp.bl_to_antnums(_bls) for _bls in bls], bl_vecs ))) # get baseline-pairs blpairs = uvp.get_blpairs() diff --git a/hera_pspec/uvpspec.py b/hera_pspec/uvpspec.py index 6a41b5bb..b9f37737 100644 --- a/hera_pspec/uvpspec.py +++ b/hera_pspec/uvpspec.py @@ -8,6 +8,7 @@ import h5py import operator import warnings +import json class UVPSpec(object): @@ -15,7 +16,7 @@ class UVPSpec(object): An object for storing power spectra generated by hera_pspec and a file-format for its data and meta-data. """ - + def __init__(self): """ An object for storing power spectra and associated metadata generated @@ -32,7 +33,7 @@ def __init__(self): self._Nfreqs = PSpecParam("Nfreqs", description="Number of unique frequency bins in the data.", expected_type=int) self._Npols = PSpecParam("Npols", description="Number of polarizations in the data.", expected_type=int) self._history = PSpecParam("history", description='The file history.', expected_type=str) - + self._r_params = PSpecParam("r_params", description = 'r_params.', expected_type = str) # Data attributes desc = "Power spectrum data dictionary with spw integer as keys and values as complex ndarrays." self._data_array = PSpecParam("data_array", description=desc, expected_type=np.complex128, form="(Nblpairts, spw_Ndlys, Npols)") @@ -97,10 +98,10 @@ def __init__(self): # self.check() self._req_params = ["Ntimes", "Nblpairts", "Nblpairs", "Nspws", "Ndlys", "Npols", "Nfreqs", "history", - "Nspwdlys", "Nspwfreqs", + "Nspwdlys", "Nspwfreqs", "r_params", "data_array", "wgt_array", "integration_array", - "spw_array", "freq_array", "dly_array", - "polpair_array", + "spw_array", "freq_array", "dly_array", + "polpair_array", "lst_1_array", "lst_2_array", "time_1_array", "time_2_array", "blpair_array", "Nbls", "bl_vecs", "bl_array", "channel_width", @@ -113,13 +114,13 @@ def __init__(self): # All parameters must fall into one and only one of the following # groups, which are used in __eq__ self._immutables = ["Ntimes", "Nblpairts", "Nblpairs", "Nspwdlys", - "Nspwfreqs", "Nspws", "Ndlys", "Npols", "Nfreqs", - "history", + "Nspwfreqs", "Nspws", "Ndlys", "Npols", "Nfreqs", + "history", "r_params", "Nbls", "channel_width", "weighting", "vis_units", "norm", "norm_units", "taper", "cosmo", "beamfile", 'folded'] - self._ndarrays = ["spw_array", "freq_array", "dly_array", - "polpair_array", "lst_1_array", "lst_avg_array", + self._ndarrays = ["spw_array", "freq_array", "dly_array", + "polpair_array", "lst_1_array", "lst_avg_array", "time_avg_array", "lst_2_array", "time_1_array", "time_2_array", "blpair_array", "OmegaP", "OmegaPP", "beam_freqs", @@ -130,7 +131,7 @@ def __init__(self): "nsample_array", "cov_array"] self._dicts_of_dicts = ["stats_array"] - # define which attributes are considered meta data. Large attrs should + # define which attributes are considered meta data. Large attrs should # be constructed as datasets self._meta_dsets = ["lst_1_array", "lst_2_array", "time_1_array", "time_2_array", "blpair_array", "bl_vecs", @@ -149,11 +150,11 @@ def __init__(self): # Default parameter values self.folded = False - + # Set function docstrings self.get_red_bls.__func__.__doc__ = uvputils._get_red_bls.__doc__ self.get_red_blpairs.__func__.__doc__ = uvputils._get_red_blpairs.__doc__ - + def get_cov(self, key, omit_flags=False): """ @@ -165,7 +166,7 @@ def get_cov(self, key, omit_flags=False): (spw, blpair-integer, pol-integer) where spw is the spectral window integer, ant1 etc. are integers, - and pol is either a tuple of polarization strings (ex. ('XX', 'YY')) + and pol is either a tuple of polarization strings (ex. ('XX', 'YY')) or integers (ex. (-5,-5)). Parameters @@ -184,7 +185,7 @@ def get_cov(self, key, omit_flags=False): Shape (Ntimes, Ndlys, Ndlys) """ spw, blpairts, polpair = self.key_to_indices(key, omit_flags=omit_flags) - + # Need to deal with folded data! # if data has been folded, return only positive delays if hasattr(self,'cov_array'): @@ -207,7 +208,7 @@ def get_data(self, key, omit_flags=False): (spw, blpair-integer, polpair) where spw is the spectral window integer, ant1 etc. are integers, - and polpair is either a tuple of polarization strings/ints or a + and polpair is either a tuple of polarization strings/ints or a polarizatio-pair integer. Parameters @@ -301,6 +302,13 @@ def get_integrations(self, key, omit_flags=False): return self.integration_array[spw][blpairts, polpair] + def get_r_params(self): + """ + decompress r_params dictionary (so it can be readily used). + """ + return uvputils.decompress_r_params(self.r_params) + + def get_nsamples(self, key, omit_flags=False): """ Slice into nsample_array with a specified data key in the format @@ -361,7 +369,7 @@ def get_blpair_seps(self): # get list of bl separations bl_vecs = self.get_ENU_bl_vecs() bls = self.bl_array.tolist() - blseps = np.array([np.linalg.norm(bl_vecs[bls.index(bl)]) + blseps = np.array([np.linalg.norm(bl_vecs[bls.index(bl)]) for bl in self.bl_array]) # construct empty blp_avg_sep array @@ -437,7 +445,7 @@ def get_kparas(self, spw, little_h=True): "wave-vectors. See self.set_cosmology()" # calculate mean redshift of band - avg_z = self.cosmo.f2z( + avg_z = self.cosmo.f2z( np.mean(self.freq_array[self.spw_to_freq_indices(spw)]) ) # get kparas @@ -449,12 +457,12 @@ def get_blpairs(self): """ Returns a list of all blpair tuples in the data_array. """ - return [self.blpair_to_antnums(blp) + return [self.blpair_to_antnums(blp) for blp in np.unique(self.blpair_array)] def get_all_keys(self): """ - Returns a list of all possible tuple keys in the data_array, in the + Returns a list of all possible tuple keys in the data_array, in the format: (spectral window, baseline-pair, polarization-string) @@ -496,7 +504,7 @@ def get_spw_ranges(self, spws=None): if spws is None: spws = np.arange(self.Nspws) spw_ranges = [] - + # iterate over spectral windows for spw in spws: spw_freqs = self.freq_array[self.spw_to_freq_indices(spw)] @@ -527,7 +535,7 @@ def get_stats(self, stat, key, omit_flags=False): Returns ------- statistic : array_like - A 2D (Ntimes, Ndlys) complex array holding desired bandpower + A 2D (Ntimes, Ndlys) complex array holding desired bandpower statistic. """ if not hasattr(self, "stats_array"): @@ -565,7 +573,7 @@ def set_stats(self, stat, key, statistic): spw, blpairts, polpair = self.key_to_indices(key) statistic = np.asarray(statistic) data_shape = self.data_array[spw][blpairts, :, polpair].shape - + if data_shape != statistic.shape: print(data_shape, statistic.shape) errmsg = "Input array shape {} must match " \ @@ -578,25 +586,25 @@ def set_stats(self, stat, key, statistic): dtype = statistic.dtype if stat not in self.stats_array.keys(): self.stats_array[stat] = \ - odict([[i, + odict([[i, np.nan * np.ones(self.data_array[i].shape, dtype=dtype)] for i in range(self.Nspws)]) self.stats_array[stat][spw][blpairts, :, polpair] = statistic - - + + def convert_to_deltasq(self, little_h=True, inplace=True): """ Convert from P(k) to Delta^2(k) by multiplying by k^3 / (2pi^2). - The units of the output is therefore the current units (self.units) + The units of the output is therefore the current units (self.units) times h^3 Mpc^-3, where the h^3 is only included if little_h == True. Parameters ---------- little_h : bool, optional Whether to use h^-1 units. Default: True. - + inplace : bool, optional If True edit and overwrite arrays in self, else make a copy of self and return. Default: True. @@ -622,8 +630,8 @@ def convert_to_deltasq(self, little_h=True, inplace=True): if inplace == False: return uvp - - + + def blpair_to_antnums(self, blpair): """ Convert baseline-pair integer to nested tuple of antenna numbers. @@ -640,8 +648,8 @@ def blpair_to_antnums(self, blpair): Ex. ((ant1, ant2), (ant3, ant4)) """ return uvputils._blpair_to_antnums(blpair) - - + + def antnums_to_blpair(self, antnums): """ Convert nested tuple of antenna numbers to baseline-pair integer. @@ -658,11 +666,11 @@ def antnums_to_blpair(self, antnums): baseline-pair integer """ return uvputils._antnums_to_blpair(antnums) - - + + def bl_to_antnums(self, bl): """ - Convert baseline (antenna-pair) integer to nested tuple of antenna + Convert baseline (antenna-pair) integer to nested tuple of antenna numbers. Parameters @@ -676,8 +684,8 @@ def bl_to_antnums(self, bl): tuple containing baseline antenna numbers. Ex. (ant1, ant2) """ return uvputils._bl_to_antnums(bl) - - + + def antnums_to_bl(self, antnums): """ Convert tuple of antenna numbers to baseline integer. @@ -694,8 +702,8 @@ def antnums_to_bl(self, antnums): Baseline integer. """ return uvputils._antnums_to_bl(antnums) - - + + def blpair_to_indices(self, blpair): """ Convert a baseline-pair nested tuple ((ant1, ant2), (ant3, ant4)) or @@ -721,8 +729,8 @@ def blpair_to_indices(self, blpair): "blpairs {} not all found in data".format(blpair) return np.arange(self.Nblpairts)[ np.logical_or.reduce([self.blpair_array == b for b in blpair])] - - + + def spw_to_freq_indices(self, spw): """ Convert a spectral window integer into a list of indices to index @@ -731,7 +739,7 @@ def spw_to_freq_indices(self, spw): Parameters ---------- spw : int or tuple - Spectral window index, or spw tuple from get_spw_ranges(), or list + Spectral window index, or spw tuple from get_spw_ranges(), or list of either. Returns @@ -754,12 +762,12 @@ def spw_to_freq_indices(self, spw): # get select boolean array select = np.logical_or.reduce([self.spw_freq_array == s for s in spw]) - + # get indices freq_indices = np.arange(self.Nspwfreqs)[select] return freq_indices - - + + def spw_to_dly_indices(self, spw): """ Convert a spectral window integer into a list of indices to index @@ -768,7 +776,7 @@ def spw_to_dly_indices(self, spw): Parameters ---------- spw : int or tuple - Spectral window index, or spw tuple from get_spw_ranges(), or list + Spectral window index, or spw tuple from get_spw_ranges(), or list of either. Returns @@ -791,7 +799,7 @@ def spw_to_dly_indices(self, spw): # get select boolean array select = np.logical_or.reduce([self.spw_dly_array == s for s in spw]) - + if self.folded: select[self.dly_array < 1e-10] = False @@ -808,7 +816,7 @@ def spw_indices(self, spw): Parameters ---------- spw : int or tuple - Spectral window index, or spw tuple from get_spw_ranges(), or list + Spectral window index, or spw tuple from get_spw_ranges(), or list of either. Returns @@ -832,13 +840,13 @@ def spw_indices(self, spw): # get select boolean array #select = reduce(operator.add, [self.spw_array == s for s in spw]) select = np.logical_or.reduce([self.spw_array == s for s in spw]) - + # get array spw_indices = np.arange(self.Nspws)[select] return spw_indices - - + + def polpair_to_indices(self, polpair): """ Map a polarization-pair integer or tuple to its index in polpair_array. @@ -846,10 +854,10 @@ def polpair_to_indices(self, polpair): Parameters ---------- polpair : (list of) int or tuple or str - Polarization-pair integer or tuple of polarization strings/ints. - - Alternatively, if a single string is given, will assume that the - specified polarization is to be cross-correlated withitself, + Polarization-pair integer or tuple of polarization strings/ints. + + Alternatively, if a single string is given, will assume that the + specified polarization is to be cross-correlated withitself, e.g. 'XX' implies ('XX', 'XX'). Returns @@ -862,24 +870,24 @@ def polpair_to_indices(self, polpair): polpair = [polpair,] elif not isinstance(polpair, (list, np.ndarray)): raise TypeError("polpair must be list of tuple or int or str") - + # Convert strings to ints - polpair = [uvputils.polpair_tuple2int((p,p)) if isinstance(p, str) else p + polpair = [uvputils.polpair_tuple2int((p,p)) if isinstance(p, str) else p for p in polpair] - + # Convert list items to standard format (polpair integers) - polpair = [uvputils.polpair_tuple2int(p) if isinstance(p, tuple) else p + polpair = [uvputils.polpair_tuple2int(p) if isinstance(p, tuple) else p for p in polpair] # Ensure all pols exist in data assert np.array([p in self.polpair_array for p in polpair]).all(), \ "pols {} not all found in data".format(polpair) - + idxs = np.logical_or.reduce([self.polpair_array == pp for pp in polpair]) indices = np.arange(self.polpair_array.size)[idxs] return indices - - + + def time_to_indices(self, time, blpairs=None): """ Convert a time [Julian Date] from self.time_avg_array to an array that @@ -940,7 +948,7 @@ def key_to_indices(self, key, omit_flags=False): Parameters ---------- key : tuple - Baseline-pair, spw, and pol-pair key. + Baseline-pair, spw, and pol-pair key. omit_flags : bool, optional If True, remove time integrations (or spectra) that @@ -967,19 +975,19 @@ def key_to_indices(self, key, omit_flags=False): spw_ind = key[0] blpair = key[1] polpair = key[2] - - + + # assert types assert isinstance(spw_ind, (int, np.integer)), "spw must be an integer" assert isinstance(blpair, (int, np.integer, tuple)), \ "blpair must be an integer or nested tuple" assert isinstance(polpair, (tuple, np.integer, int, str)), \ "polpair must be a tuple, integer, or str: %s / %s" % (polpair, key) - + # convert polpair string to tuple if isinstance(polpair, str): polpair = (polpair, polpair) - + # convert blpair to int if not int if isinstance(blpair, tuple): blpair = self.antnums_to_blpair(blpair) @@ -1016,7 +1024,7 @@ def key_to_indices(self, key, omit_flags=False): def select(self, spws=None, bls=None, only_pairs_in_bls=True, blpairs=None, - times=None, lsts=None, polpairs=None, inplace=True, + times=None, lsts=None, polpairs=None, inplace=True, run_check=True): """ Select function for selecting out certain slices of the data. @@ -1052,10 +1060,10 @@ def select(self, spws=None, bls=None, only_pairs_in_bls=True, blpairs=None, all lsts are kept. Default: None. polpairs : list of tuple or int or str, optional - List of polarization-pairs to keep. If None, all polarizations + List of polarization-pairs to keep. If None, all polarizations are kept. Default: None. - - (Strings are expanded into polarization pairs, and are not treated + + (Strings are expanded into polarization pairs, and are not treated as individual polarizations, e.g. 'XX' becomes ('XX,'XX').) inplace : bool, optional @@ -1078,13 +1086,13 @@ def select(self, spws=None, bls=None, only_pairs_in_bls=True, blpairs=None, uvputils._select(uvp, spws=spws, bls=bls, only_pairs_in_bls=only_pairs_in_bls, - blpairs=blpairs, times=times, lsts=lsts, + blpairs=blpairs, times=times, lsts=lsts, polpairs=polpairs) if run_check: uvp.check() if inplace == False: return uvp - - + + def get_ENU_bl_vecs(self): """ Return baseline vector array in TOPO (ENU) frame in meters, with @@ -1098,8 +1106,8 @@ def get_ENU_bl_vecs(self): return uvutils.ENU_from_ECEF( (self.bl_vecs + self.telescope_location), \ *uvutils.LatLonAlt_from_XYZ(self.telescope_location[None])) - - + + def read_from_group(self, grp, just_meta=False, spws=None, bls=None, blpairs=None, times=None, lsts=None, polpairs=None, only_pairs_in_bls=False): @@ -1136,8 +1144,8 @@ def read_from_group(self, grp, just_meta=False, spws=None, bls=None, lsts from the lst_avg_array to keep. polpairs : list of tuple or int or str - List of polarization-pair integers, tuples, or strings to keep. - Strings are expanded into polarization pairs, and are not treated + List of polarization-pair integers, tuples, or strings to keep. + Strings are expanded into polarization pairs, and are not treated as individual polarizations, e.g. 'XX' becomes ('XX,'XX'). only_pairs_in_bls : bool, optional @@ -1162,18 +1170,18 @@ def read_from_group(self, grp, just_meta=False, spws=None, bls=None, for k in grp: if k in self._meta_dsets: setattr(self, k, grp[k][:]) - + # Backwards compatibility: pol_array exists (not polpair_array) if 'pol_array' in grp.attrs: warnings.warn("Stored UVPSpec contains pol_array attr, which has " "been superseded by polpair_array. Converting " "automatically.", UserWarning) pol_arr = grp.attrs['pol_array'] - + # Convert to polpair array polpair_arr = [uvputils.polpair_tuple2int((p,p)) for p in pol_arr] setattr(self, 'polpair_array', np.array(polpair_arr)) - + # Use _select() to pick out only the requested baselines/spws if just_meta: uvputils._select(self, spws=spws, bls=bls, lsts=lsts, @@ -1228,9 +1236,9 @@ def read_hdf5(self, filepath, just_meta=False, spws=None, bls=None, lsts from the lst_avg_array to keep. polpairs : list of polpair ints or tuples or str - List of polarization-pair integers or tuples to keep. - - Strings are expanded into polarization pairs, and are not treated + List of polarization-pair integers or tuples to keep. + + Strings are expanded into polarization pairs, and are not treated as individual polarizations, e.g. 'XX' becomes ('XX,'XX'). only_pairs_in_bls : bool, optional @@ -1239,11 +1247,11 @@ def read_hdf5(self, filepath, just_meta=False, spws=None, bls=None, """ # Open file descriptor and read data with h5py.File(filepath, 'r') as f: - self.read_from_group(f, just_meta=just_meta, spws=spws, bls=bls, + self.read_from_group(f, just_meta=just_meta, spws=spws, bls=bls, times=times, lsts=lsts, polpairs=polpairs, only_pairs_in_bls=only_pairs_in_bls) - - + + def write_to_group(self, group, run_check=True): """ Write UVPSpec data into an HDF5 group. @@ -1273,14 +1281,14 @@ def write_to_group(self, group, run_check=True): group.attrs['cosmo'] = str(getattr(self, k).get_params()) continue this_attr = getattr(self, k) - + # Do Unicode/string conversions, as HDF5 struggles with them if k == 'labels': - this_attr = [np.string_(lbl) for lbl in this_attr] - + this_attr = [np.string_(lbl) for lbl in this_attr] + # Store attribute in group group.attrs[k] = this_attr - + for k in self._meta_dsets: if hasattr(self, k): group.create_dataset(k, data=getattr(self, k)) @@ -1315,8 +1323,8 @@ def write_to_group(self, group, run_check=True): # denote as a uvpspec object group.attrs['pspec_type'] = self.__class__.__name__ - - + + def write_hdf5(self, filepath, overwrite=False, run_check=True): """ Write a UVPSpec object to HDF5 file. @@ -1342,8 +1350,8 @@ def write_hdf5(self, filepath, overwrite=False, run_check=True): # Write file with h5py.File(filepath, 'w') as f: self.write_to_group(f, run_check=run_check) - - + + def set_cosmology(self, new_cosmo, overwrite=False, new_beam=None, verbose=True): """ @@ -1430,8 +1438,8 @@ def set_cosmology(self, new_cosmo, overwrite=False, new_beam=None, # Update self.units if pspectra were not originally in cosmological units if "Mpc" not in self.norm_units: self.norm_units = "h^-3 Mpc^3" - - + + def check(self, just_meta=False): """ Run checks to make sure metadata and data arrays are properly defined @@ -1444,7 +1452,7 @@ def check(self, just_meta=False): """ # iterate over all possible parameters for p in self._all_params: - + # only enforce existance if not just_meta if not just_meta: if p in self._req_params: @@ -1456,18 +1464,18 @@ def check(self, just_meta=False): a = getattr(self, '_' + p) if hasattr(a, 'expected_type'): err_msg = "attribute {} does not have expected data type {}".format(p, a.expected_type) - + # ndarrays if p in self._ndarrays: assert isinstance(getattr(self, p), np.ndarray), \ "attribute {} needs to be an ndarray, {}".format(p, getattr(self, p).tostring()) - if issubclass(getattr(self, p).dtype.type, + if issubclass(getattr(self, p).dtype.type, a.expected_type): pass else: # try to cast into its dtype try: - setattr(self, p, + setattr(self, p, getattr(self, p).astype(a.expected_type)) except: raise ValueError(err_msg) @@ -1501,8 +1509,8 @@ def check(self, just_meta=False): raise AssertionError(err_msg) # check spw convention assert set(self.spw_array) == set(np.arange(self.Nspws)), "spw_array must be np.arange(Nspws)" - - + + def _clear(self): """ Clear UVPSpec of all parameters. Warning: this cannot be undone. @@ -1574,8 +1582,8 @@ def units(self): """ return "({})^2 {}".format(self.vis_units, self.norm_units) - def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None, - little_h=True, form='Pk', num_steps=2000, + def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None, + little_h=True, form='Pk', num_steps=2000, component='real'): """ Generate the expected RMS noise power spectrum given a selection of @@ -1611,7 +1619,7 @@ def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None, polpair : tuple or int or str Polarization-pair selection in form of tuple (e.g. ('I', 'Q')) or - polpair int. Strings are expanded into polarization pairs, e.g. + polpair int. Strings are expanded into polarization pairs, e.g. 'XX' becomes ('XX,'XX'). Tsys : dictionary, float or array @@ -1647,7 +1655,7 @@ def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None, # Check for str polpair and convert to integer if isinstance(polpair, str): polpair = uvputils.polpair_tuple2int((polpair, polpair)) - + # Assert polarization-pair type if isinstance(polpair, tuple): polpair = uvputils.polpair_tuple2int(polpair) @@ -1689,7 +1697,7 @@ def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None, or Tsys[blp].shape[0] == self.Ntimes, \ "Tsys must be a float or an ndarray with shape[0] == Ntimes" P_blp = [] - + # iterate over time axis for j, ind in enumerate(inds): # get integration time and n_samp @@ -1704,7 +1712,7 @@ def generate_noise_spectra(self, spw, polpair, Tsys, blpairs=None, # Get noise power spectrum pn = noise.calc_P_N(scalar, Tsys[blp][j], t_int, k=k, - Nincoherent=n_samp, form=form, + Nincoherent=n_samp, form=form, component=component) # Put into appropriate form @@ -1812,8 +1820,8 @@ def fold_spectra(self): WARNING: This operation cannot be undone. """ grouping.fold_spectra(self) - - + + def get_blpair_groups_from_bl_groups(self, blgroups, only_pairs_in_bls=False): """ Get baseline pair matches from a list of baseline groups. @@ -1844,13 +1852,13 @@ def get_blpair_groups_from_bl_groups(self, blgroups, only_pairs_in_bls=False): blpair_groups.append(blp) return blpair_groups - + def get_red_bls(self, bl_len_tol=1., bl_ang_tol=1.): - return uvputils._get_red_bls(self, bl_len_tol=bl_len_tol, + return uvputils._get_red_bls(self, bl_len_tol=bl_len_tol, bl_ang_tol=bl_ang_tol) - + def get_red_blpairs(self, bl_len_tol=1., bl_ang_tol=1.): - return uvputils._get_red_blpairs(self, bl_len_tol=bl_len_tol, + return uvputils._get_red_blpairs(self, bl_len_tol=bl_len_tol, bl_ang_tol=bl_ang_tol) def compute_scalar(self, spw, polpair, num_steps=1000, little_h=True, @@ -2013,13 +2021,35 @@ def combine_uvpspec(uvps, verbose=True): # get each uvp's data axes uvp_spws = [_uvp.get_spw_ranges() for _uvp in uvps] - uvp_blpts = [list(zip(_uvp.blpair_array, _uvp.time_avg_array)) + uvp_blpts = [list(zip(_uvp.blpair_array, _uvp.time_avg_array)) for _uvp in uvps] uvp_polpairs = [_uvp.polpair_array.tolist() for _uvp in uvps] # Construct dict of label indices, to be used for re-ordering later u_lbls = {lbl: ll for ll, lbl in enumerate(u.labels)} + # Concatenate r_params arrays + #check that r_params are either all empty or all full. + _r_param_strs = [_uvp.r_params for _uvp in uvps] + if '' in _r_param_strs: + if not np.all(np.asarray([rp == '' for rp in _r_param_strs])): + raise ValueError("All r_params must be set or empty." + "Combinging empty with non-empty r_params" + "is not yet supported.") + _r_params = [_uvp.get_r_params() for _uvp in uvps] + #check for conflicts by iterating through each key in the first _uvp, store + #in list of new keys. + r_params = {} + for _r_param in _r_params: + for rkey in _r_param: + if not rkey in r_params: + r_params[rkey] = _r_param[rkey] + elif r_params[rkey] != _r_param[rkey]: + #For now, we won't support inconsistent weightings. + raise ValueError("Conflict between weightings" + "Only consistent weightings are supported!") + + # fill in data arrays depending on concat ax if concat_ax == 'spw': @@ -2153,11 +2183,11 @@ def combine_uvpspec(uvps, verbose=True): u.lst_2_array[j] = uvps[0].lst_2_array[n] u.lst_avg_array[j] = uvps[0].lst_avg_array[n] u.blpair_array[j] = uvps[0].blpair_array[n] - + else: # Make sure we have properly identified the concat_ax raise ValueError("concat_ax {} not recognized.".format(concat_ax)) - + # Set baselines u.Nblpairs = len(np.unique(u.blpair_array)) uvp_bls = [uvp.bl_array for uvp in uvps] @@ -2174,6 +2204,8 @@ def combine_uvpspec(uvps, verbose=True): u.history = "".join([uvp.history for uvp in uvps]) u.labels = np.array(u.labels, np.str) + u.r_params = uvputils.compress_r_params(r_params) + for k in static_meta.keys(): setattr(u, k, static_meta[k]) @@ -2329,14 +2361,14 @@ def get_uvp_overlap(uvps, just_meta=True, verbose=True): # assert all uvp pairs have the same (single) non-overlap (concat) axis err_msg = "Non-overlapping data in uvps span multiple data axes:\n{}" \ - "".format("\n".join( ["{} & {}: {}".format(i[0][0],i[0][1],i[1]) + "".format("\n".join( ["{} & {}: {}".format(i[0][0],i[0][1],i[1]) for i in data_concat_axes.items()] )) assert len(set(data_concat_axes.values())) == 1, err_msg # perform additional checks given concat ax if concat_ax == 'blpairts': # check scalar_array - assert np.all([np.isclose(uvps[0].scalar_array, u.scalar_array) + assert np.all([np.isclose(uvps[0].scalar_array, u.scalar_array) for u in uvps[1:]]), \ "scalar_array must be the same for all uvps given " \ "concatenation along blpairts." diff --git a/hera_pspec/uvpspec_utils.py b/hera_pspec/uvpspec_utils.py index 766090d8..64d4ba98 100644 --- a/hera_pspec/uvpspec_utils.py +++ b/hera_pspec/uvpspec_utils.py @@ -3,17 +3,18 @@ from . import utils from collections import OrderedDict as odict from pyuvdata.utils import polstr2num, polnum2str +import json def subtract_uvp(uvp1, uvp2, run_check=True, verbose=False): """ Subtract uvp2.data_array from uvp1.data_array. Subtract matching - spw, blpair-lst, polpair keys. For non-overlapping keys, remove from + spw, blpair-lst, polpair keys. For non-overlapping keys, remove from output uvp. Note: Entries in stats_array are added in quadrature. nsample_arrays, integration_arrays and wgt_arrays are inversely added in quadrature. If these arrays are identical in the two objects, this is equivalent - to multiplying (dividing) the stats (nsamp, int & wgt) arrays(s) by + to multiplying (dividing) the stats (nsamp, int & wgt) arrays(s) by sqrt(2). Parameters @@ -34,7 +35,7 @@ def subtract_uvp(uvp1, uvp2, run_check=True, verbose=False): """ # select out common parts uvp1, uvp2 = select_common([uvp1, uvp2], spws=True, blpairs=True, lsts=True, - polpairs=True, times=False, inplace=False, + polpairs=True, times=False, inplace=False, verbose=verbose) # get metadata @@ -64,17 +65,17 @@ def subtract_uvp(uvp1, uvp2, run_check=True, verbose=False): # add nsample inversely in quadrature uvp1.nsample_array[i][blp1_inds, j] \ - = np.sqrt( 1. / (1./uvp1.get_nsamples(key1)**2 + = np.sqrt( 1. / (1./uvp1.get_nsamples(key1)**2 + 1. / uvp2.get_nsamples(key2)**2) ) # add integration inversely in quadrature uvp1.integration_array[i][blp1_inds, j] \ - = np.sqrt(1. / (1./uvp1.get_integrations(key1)**2 + = np.sqrt(1. / (1./uvp1.get_integrations(key1)**2 + 1. / uvp2.get_integrations(key2)**2)) # add wgts inversely in quadrature uvp1.wgt_array[i][blp1_inds, :, :, j] \ - = np.sqrt(1. / (1./uvp1.get_wgts(key1)**2 + = np.sqrt(1. / (1./uvp1.get_wgts(key1)**2 + 1. / uvp2.get_wgts(key2)**2)) uvp1.wgt_array[i][blp1_inds, :, :, j] /= \ uvp1.wgt_array[i][blp1_inds, :, :, j].max() @@ -100,12 +101,92 @@ def subtract_uvp(uvp1, uvp2, run_check=True, verbose=False): if run_check: uvp1.check() return uvp1 +def compress_r_params(r_params_dict): + """ + Convert a dictionary of r_paramsters to a compressed string format + + Parameters + ---------- + r_params_dict: Dictionary + dictionary with parameters for weighting matrix. Proper fields + and formats depend on the mode of data_weighting. + data_weighting == 'sinc_downweight': + dictionary with fields + 'filter_centers', list of floats (or float) specifying the (delay) channel numbers + at which to center filtering windows. Can specify fractional channel number. + 'filter_widths', list of floats (or float) specifying the width of each + filter window in (delay) channel numbers. Can specify fractional channel number. + 'filter_factors', list of floats (or float) specifying how much power within each filter window + is to be suppressed. + Returns + ------- + string containing r_params dictionary in json format and only containing one + copy of each unique dictionary with a list of associated baselines. + """ + if r_params_dict == {} or r_params_dict is None: + return '' + else: + r_params_unique = {} + r_params_unique_bls = {} + r_params_index = -1 + for rp in r_params_dict: + #do not include data set in tuple key + already_in = False + for rpu in r_params_unique: + if r_params_unique[rpu] == r_params_dict[rp]: + r_params_unique_bls[rpu] += [rp,] + already_in = True + if not already_in: + r_params_index += 1 + r_params_unique[r_params_index] = copy.copy(r_params_dict[rp]) + r_params_unique_bls[r_params_index] = [rp,] + for rpi in r_params_unique: + r_params_unique[rpi]['baselines'] = r_params_unique_bls[rpi] + r_params_str = json.dumps(r_params_unique) + return r_params_str + +def decompress_r_params(r_params_str): + """ + Decompress json format r_params string into an r_params dictionary. + + Parameters + ---------- + r_params_str: String + string with compressed r_params in json format + + Returns + ------- + dictionary with parameters for weighting matrix. Proper fields + and formats depend on the mode of data_weighting. + data_weighting == 'sinc_downweight': + dictionary with fields + 'filter_centers', list of floats (or float) specifying the (delay) channel numbers + at which to center filtering windows. Can specify fractional channel number. + 'filter_widths', list of floats (or float) specifying the width of each + filter window in (delay) channel numbers. Can specify fractional channel number. + 'filter_factors', list of floats (or float) specifying how much power within each filter window + is to be suppressed. + """ + decompressed_r_params = {} + if r_params_str != '' and not r_params_str is None: + r_params = json.loads(r_params_str) + for rpi in r_params: + rp_dict = {} + for r_field in r_params[rpi]: + if not r_field == 'baselines': + rp_dict[r_field] = r_params[rpi][r_field] + for blkey in r_params[rpi]['baselines']: + decompressed_r_params[tuple(blkey)] = rp_dict + else: + decompressed_r_params = {} + return decompressed_r_params + def select_common(uvp_list, spws=True, blpairs=True, times=True, polpairs=True, lsts=False, inplace=False, verbose=False): """ - Find spectral windows, baseline-pairs, times, and/or polarization-pairs - that a set of UVPSpec objects have in common and return new UVPSpec objects + Find spectral windows, baseline-pairs, times, and/or polarization-pairs + that a set of UVPSpec objects have in common and return new UVPSpec objects that contain only those data. If there is no overlap, an error will be raised. @@ -140,7 +221,7 @@ def select_common(uvp_list, spws=True, blpairs=True, times=True, polpairs=True, have in common. Similar algorithm to times. Default: False. polpairs : bool, optional - Whether to retain only the polarization pairs that all UVPSpec objects + Whether to retain only the polarization pairs that all UVPSpec objects have in common. Default: True. inplace : bool, optional @@ -183,7 +264,7 @@ def select_common(uvp_list, spws=True, blpairs=True, times=True, polpairs=True, # Get polarization-pairs that are common to all if polpairs: common_polpairs = np.unique(uvp_list[0].polpair_array) - has_polpairs = [np.isin(common_polpairs, uvp.polpair_array) + has_polpairs = [np.isin(common_polpairs, uvp.polpair_array) for uvp in uvp_list] common_polpairs = common_polpairs[np.all(has_polpairs, axis=0)] if verbose: print("common_polpairs:", common_polpairs) @@ -192,7 +273,7 @@ def select_common(uvp_list, spws=True, blpairs=True, times=True, polpairs=True, # Each row of common_spws is a list of that spw's index in each UVPSpec if spws: common_spws = uvp_list[0].get_spw_ranges() - has_spws = [[x in uvp.get_spw_ranges() for x in common_spws] + has_spws = [[x in uvp.get_spw_ranges() for x in common_spws] for uvp in uvp_list] common_spws = [common_spws[i] for i, f in enumerate(np.all(has_spws, axis=0)) if f] if verbose: print("common_spws:", common_spws) @@ -236,64 +317,64 @@ def select_common(uvp_list, spws=True, blpairs=True, times=True, polpairs=True, def polpair_int2tuple(polpair, pol_strings=False): """ - Convert a pol-pair integer into a tuple pair of polarization + Convert a pol-pair integer into a tuple pair of polarization integers. See polpair_tuple2int for more details. - + Parameters ---------- polpair : int or list of int Integer representation of polarization pair. - + pol_strings : bool, optional - If True, return polarization pair tuples with polarization strings. + If True, return polarization pair tuples with polarization strings. Otherwise, use polarization integers. Default: True. - + Returns ------- polpair : tuple, length 2 - A length-2 tuple containing a pair of polarization + A length-2 tuple containing a pair of polarization integers, e.g. (-5, -5). """ # Recursive evaluation if isinstance(polpair, (list, np.ndarray)): return [polpair_int2tuple(p, pol_strings=pol_strings) for p in polpair] - + # Check for integer type assert isinstance(polpair, (int, np.integer)), \ "polpair must be integer: %s" % type(polpair) - + # Split into pol1 and pol2 integers pol1 = int(str(polpair)[:-2]) - 20 pol2 = int(str(polpair)[-2:]) - 20 - + # Check that pol1 and pol2 are in the allowed range (-8, 4) if (pol1 < -8 or pol1 > 4) or (pol2 < -8 or pol2 > 4): raise ValueError("polpair integer evaluates to an invalid " - "polarization pair: (%d, %d)" + "polarization pair: (%d, %d)" % (pol1, pol2)) # Convert to strings if requested if pol_strings: return (polnum2str(pol1), polnum2str(pol2)) else: return (pol1, pol2) - + def polpair_tuple2int(polpair): """ - Convert a tuple pair of polarization strings/integers into + Convert a tuple pair of polarization strings/integers into an pol-pair integer. - - The polpair integer is formed by adding 20 to each standardized - polarization integer (see polstr2num and AIPS memo 117) and - then concatenating them. For example, polarization pair - ('pI', 'pQ') == (1, 2) == 2122. - + + The polpair integer is formed by adding 20 to each standardized + polarization integer (see polstr2num and AIPS memo 117) and + then concatenating them. For example, polarization pair + ('pI', 'pQ') == (1, 2) == 2122. + Parameters ---------- polpair : tuple, length 2 - A length-2 tuple containing a pair of polarization strings + A length-2 tuple containing a pair of polarization strings or integers, e.g. ('XX', 'YY') or (-5, -5). - + Returns ------- polpair : int @@ -302,16 +383,16 @@ def polpair_tuple2int(polpair): # Recursive evaluation if isinstance(polpair, (list, np.ndarray)): return [polpair_tuple2int(p) for p in polpair] - + # Check types assert type(polpair) in (tuple,), "pol must be a tuple" assert len(polpair) == 2, "polpair tuple must have 2 elements" - + # Convert strings to ints if necessary pol1, pol2 = polpair if type(pol1) in (str, np.str): pol1 = polstr2num(pol1) if type(pol2) in (str, np.str): pol2 = polstr2num(pol2) - + # Convert to polpair integer ppint = (20 + pol1)*100 + (20 + pol2) return ppint @@ -319,21 +400,21 @@ def polpair_tuple2int(polpair): def _get_blpairs_from_bls(uvp, bls, only_pairs_in_bls=False): """ - Get baseline pair matches from a list of baseline antenna-pairs in a + Get baseline pair matches from a list of baseline antenna-pairs in a UVPSpec object. Parameters ---------- uvp : UVPSpec object - Must at least have meta-data in required params loaded in. If only + Must at least have meta-data in required params loaded in. If only meta-data is loaded in then h5file must be specified. bls : list of i6 baseline integers or baseline tuples - Select all baseline-pairs whose first _or_ second baseline are in bls + Select all baseline-pairs whose first _or_ second baseline are in bls list. This changes if only_pairs_in_bls == True. only_pairs_in_bls : bool - If True, keep only baseline-pairs whose first _and_ second baseline are + If True, keep only baseline-pairs whose first _and_ second baseline are both found in bls list. Returns @@ -355,10 +436,10 @@ def _get_blpairs_from_bls(uvp, bls, only_pairs_in_bls=False): bls = [uvp.antnums_to_bl(b) for b in bls] elif isinstance(bls, (int, np.integer)): bls = [bls] - + # get indices if only_pairs_in_bls: - blp_select = np.array( [np.bool((blp[0] in bls) * (blp[1] in bls)) + blp_select = np.array( [np.bool((blp[0] in bls) * (blp[1] in bls)) for blp in blpair_bls] ) else: blp_select = np.array( [np.bool((blp[0] in bls) + (blp[1] in bls)) @@ -454,11 +535,11 @@ def _select(uvp, spws=None, bls=None, only_pairs_in_bls=False, blpairs=None, if blpairs is not None: if bls is None: blp_select = np.zeros(uvp.Nblpairts, np.bool) - + # assert form assert isinstance(blpairs[0], (tuple, int, np.integer)), \ "blpairs must be fed as a list of baseline-pair tuples or baseline-pair integers" - + # if fed as list of tuples, convert to integers if isinstance(blpairs[0], tuple): blpairs = [uvp.antnums_to_blpair(blp) for blp in blpairs] @@ -469,8 +550,8 @@ def _select(uvp, spws=None, bls=None, only_pairs_in_bls=False, blpairs=None, if times is not None: if bls is None and blpairs is None: blp_select = np.ones(uvp.Nblpairts, np.bool) - time_select = np.logical_or.reduce( - [np.isclose(uvp.time_avg_array, t, rtol=1e-16) + time_select = np.logical_or.reduce( + [np.isclose(uvp.time_avg_array, t, rtol=1e-16) for t in times]) blp_select *= time_select @@ -479,7 +560,7 @@ def _select(uvp, spws=None, bls=None, only_pairs_in_bls=False, blpairs=None, if bls is None and blpairs is None: blp_select = np.ones(uvp.Nblpairts, np.bool) lst_select = np.logical_or.reduce( - [ np.isclose(uvp.lst_avg_array, t, rtol=1e-16) + [ np.isclose(uvp.lst_avg_array, t, rtol=1e-16) for t in lsts] ) blp_select *= lst_select @@ -528,17 +609,17 @@ def _select(uvp, spws=None, bls=None, only_pairs_in_bls=False, blpairs=None, "polpairs must be passed as a list or ndarray" assert isinstance(polpairs[0], (tuple, int, np.integer, str)), \ "polpairs must be fed as a list of tuples or pol integers/strings" - + # convert str to polpair integers - polpairs = [polpair_tuple2int((p,p)) if isinstance(p, str) + polpairs = [polpair_tuple2int((p,p)) if isinstance(p, str) else p for p in polpairs] - + # convert tuples to polpair integers - polpairs = [polpair_tuple2int(p) if isinstance(p, tuple) + polpairs = [polpair_tuple2int(p) if isinstance(p, tuple) else p for p in polpairs] - + # create selection - polpair_select = np.logical_or.reduce( [uvp.polpair_array == p + polpair_select = np.logical_or.reduce( [uvp.polpair_array == p for p in polpairs] ) # turn into slice object if possible @@ -548,8 +629,8 @@ def _select(uvp, spws=None, bls=None, only_pairs_in_bls=False, blpairs=None, polpair_select = slice(polpair_select[0], polpair_select[-1] + 1) elif len(set(np.diff(polpair_select))) == 1: # sliceable - polpair_select = slice(polpair_select[0], - polpair_select[-1] + 1, + polpair_select = slice(polpair_select[0], + polpair_select[-1] + 1, np.diff(polpair_select)[0]) # edit metadata @@ -584,7 +665,7 @@ def _select(uvp, spws=None, bls=None, only_pairs_in_bls=False, blpairs=None, # get stats_array keys if h5file if h5file is not None: - statnames = np.unique([f[f.find("_")+1: f.rfind("_")] + statnames = np.unique([f[f.find("_")+1: f.rfind("_")] for f in h5file.keys() if f.startswith("stats")]) else: @@ -662,6 +743,26 @@ def _select(uvp, spws=None, bls=None, only_pairs_in_bls=False, blpairs=None, if store_cov: uvp.cov_array = cov + #select r_params based on new bl_array + blp_keys = uvp.get_all_keys() + blkeys = [] + for blpkey in blp_keys: + key1 = blpkey[1][0] + (blpkey[2][0],) + key2 = blpkey[1][1] + (blpkey[2][1],) + if not key1 in blkeys: + blkeys += [key1,] + if not key2 in blkeys: + blkeys += [key2,] + new_r_params = {} + if not uvp.r_params == '': + r_params = uvp.get_r_params() + for rpkey in r_params: + if rpkey in blkeys: + new_r_params[rpkey] = r_params[rpkey] + else: + new_r_params = {} + uvp.r_params = compress_r_params(new_r_params) + def _blpair_to_antnums(blpair): """ @@ -675,7 +776,7 @@ def _blpair_to_antnums(blpair): Returns ------- antnums : tuple - nested tuple containing baseline-pair antenna numbers. + nested tuple containing baseline-pair antenna numbers. Ex. ((ant1, ant2), (ant3, ant4)) """ # get antennas @@ -936,7 +1037,7 @@ def _fast_lookup_blpairts(src_blpts, query_blpts, time_prec=8): # array lookup functions to be used src_blpts = np.asarray(src_blpts) query_blpts = np.asarray(query_blpts) - + src_blpts = src_blpts[:,0] + 1.j*np.around(src_blpts[:,1], time_prec) query_blpts = query_blpts[:,0] + 1.j*np.around(query_blpts[:,1], time_prec) # Applies rounding to time values to ensure reliable float comparisons @@ -951,142 +1052,142 @@ def _fast_lookup_blpairts(src_blpts, query_blpts, time_prec=8): def _get_red_bls(uvp, bl_len_tol=1., bl_ang_tol=1.): """ Get redundant baseline groups that are present in a UVPSpec object. - + Parameters ---------- uvp : UVPSpec UVPSpec object. - + bl_len_tol : float, optional - Maximum difference in length to use for grouping baselines. - This does not guarantee that the maximum length difference - between any two baselines in a group is less than bl_len_tol + Maximum difference in length to use for grouping baselines. + This does not guarantee that the maximum length difference + between any two baselines in a group is less than bl_len_tol however. Default: 1.0. - + bl_ang_tol : float, optional - Maximum separation in angle to use for grouping baselines. - This does not guarantee that the maximum angle between any - two baselines in a group is less than bl_ang_tol however. + Maximum separation in angle to use for grouping baselines. + This does not guarantee that the maximum angle between any + two baselines in a group is less than bl_ang_tol however. Default: 1.0. - + Returns ------- grp_bls : list of array_like - List of redundant baseline groups. Each list item contains - an array of baseline integers corresponding to the members + List of redundant baseline groups. Each list item contains + an array of baseline integers corresponding to the members of the group. - + grp_lens : list of float Average length of the baselines in each group. - + grp_angs : list of float Average angle of the baselines in each group. """ # Calculate length and angle of baseline vecs bl_vecs = uvp.get_ENU_bl_vecs() - + lens, angs = utils.get_bl_lens_angs(bl_vecs, bl_error_tol=bl_len_tol) - + # Baseline indices idxs = np.arange(len(lens)).astype(np.int) grp_bls = []; grp_len = []; grp_ang = [] - + # Group baselines by length and angle max_loops = idxs.size nloops = 0 while len(idxs) > 0 and nloops < max_loops: nloops += 1 - + # Match bls within some tolerance in length and angle matches = np.where(np.logical_and( np.abs(lens - lens[0]) < bl_len_tol, np.abs(angs - angs[0]) < bl_ang_tol) ) - + # Save info about this group grp_bls.append(uvp.bl_array[idxs[matches]]) grp_len.append(np.mean(lens[matches])) grp_ang.append(np.mean(angs[matches])) - + # Remove bls that were matched so we don't try to group them again idxs = np.delete(idxs, matches) lens = np.delete(lens, matches) angs = np.delete(angs, matches) - + return grp_bls, grp_len, grp_ang def _get_red_blpairs(uvp, bl_len_tol=1., bl_ang_tol=1.): """ - Group baseline-pairs from a UVPSpec object according to the + Group baseline-pairs from a UVPSpec object according to the redundant groups that their constituent baselines belong to. - - NOTE: Baseline-pairs made up of baselines from two different + + NOTE: Baseline-pairs made up of baselines from two different redundant groups are ignored. - + Parameters ---------- uvp : UVPSpec UVPSpec object. - + bl_len_tol : float, optional - Maximum difference in length to use for grouping baselines. - This does not guarantee that the maximum length difference - between any two baselines in a group is less than bl_len_tol + Maximum difference in length to use for grouping baselines. + This does not guarantee that the maximum length difference + between any two baselines in a group is less than bl_len_tol however. Default: 1.0. - + bl_ang_tol : float, optional - Maximum separation in angle to use for grouping baselines. - This does not guarantee that the maximum angle between any - two baselines in a group is less than bl_ang_tol however. + Maximum separation in angle to use for grouping baselines. + This does not guarantee that the maximum angle between any + two baselines in a group is less than bl_ang_tol however. Default: 1.0. - + Returns ------- grp_bls : list of array_like - List of redundant baseline groups. Each list item contains - an array of baseline-pair integers corresponding to the + List of redundant baseline groups. Each list item contains + an array of baseline-pair integers corresponding to the members of the group. - + grp_lens : list of float Average length of the baselines in each group. - + grp_angs : list of float Average angle of the baselines in each group. """ # Get redundant baseline groups - red_bls, red_lens, red_angs = _get_red_bls(uvp=uvp, - bl_len_tol=bl_len_tol, + red_bls, red_lens, red_angs = _get_red_bls(uvp=uvp, + bl_len_tol=bl_len_tol, bl_ang_tol=bl_ang_tol) - + # Get all available blpairs and convert to pairs of integers blps = [(uvp.antnums_to_bl(blp[0]), uvp.antnums_to_bl(blp[1])) for blp in uvp.get_blpairs()] bl1, bl2 = zip(*blps) - + # Build bl -> group index dict group_idx = {} for i, grp in enumerate(red_bls): for bl in grp: group_idx[bl] = i - + # Get red. group that each bl belongs to bl1_grp = np.array([group_idx[bl] for bl in bl1]) bl2_grp = np.array([group_idx[bl] for bl in bl2]) - + # Convert to arrays for easier indexing bl1 = np.array(bl1) bl2 = np.array(bl2) - + # Loop over redundant groups; assign blpairs to each group red_grps = [] grp_ids = np.arange(len(red_bls)) for i in grp_ids: # This line only keeps blpairs where both bls belong to the same red grp! matches = np.where(np.logical_and(bl1_grp == i, bl2_grp == i)) - + # Unpack into list of blpair integers - blpair_ints = [int("%d%d" % _blp) + blpair_ints = [int("%d%d" % _blp) for _blp in zip(bl1[matches], bl2[matches])] red_grps.append(blpair_ints) - + return red_grps, red_lens, red_angs