Make one or more components available with a common timestamp.
+
If you are adding only one component, this function is equivalent to calling
+component.add(). However, multiple calls to component.add()
+generate a unique timestamp per call. To assign a single timestamp to many
+additions at once, use this function.
+
Examples
+
>>> lna_type=layout.component_type.get(name="LNA")
+>>> lna_rev=lna_type.rev.where(layout.component_type_rev.name=="B").get()
+>>> c=[]
+>>> foriinrange(0,10):
+... c.append(layout.component(sn="LNA%04dB"%(i),type=lna_type,rev=lna_rev))
+>>> layout.add_component(c,time=datetime(2014,10,10,11),notes="Adding many at once.")
+
+
+
+
Parameters:
+
+
comp (list of component objects) – The components to make available.
+
time (datetime.datetime) – The time at which to make the components available.
+
notes (string) – Any notes for the timestamp.
+
force (bool) – If True, then add any components that can be added, while doing
+nothing (except making note of such in the logger) for components whose
+addition would violate database integrity. If False,
+AlreadyExists is raised for any addition that violates database
+integrity.
See if two connexions are the same.
+Because the connexion could store the two components in different
+orders, or have different instances of the same component object, direct
+comparison may fail. This function explicitly compares both possible
+combinations of serial numbers.
To add or remove components, use the add() and remove() methods.
+There are also methods for getting and setting component properties, history
+and documents.
Initialize the connection to the CHIME data index database.
+
This function uses the current database connector from
+core to establish a connection to the CHIME data
+index. It must be called if you change the connection method after
+importing this module. Or if you wish to connect with both read and write
+privileges.
+
+
Parameters:
+
+
read_write (bool) – Whether to connect with read and write privileges.
Create a connexion.
+This method begins a connexion event at the specified time.
+
+
Parameters:
+
+
time (datetime.datetime) – The time at which to begin the connexion event.
+
permanent (bool) – If True, then make this a permanent connexion.
+
notes (string) – Any notes for the timestamp.
+
force (bool) – If False, then AlreadyExists will be raised if the connexion
+already exists; otherwise, conflicts will be ignored and nothing will be
+done.
Sever a connexion.
+This method ends a connexion event at the specified time.
+
+
Parameters:
+
+
time (datetime.datetime) – The time at which to end the connexion event.
+
notes (string) – Any notes for the timestamp.
+
force (bool) – If False, then DoesNotExists will be raised if the connexion
+does not exist; otherwise, conflicts will be ignored and nothing will be
+done.
Events are never deleted; rather, the active flag is switched off.
+This method first checks to see whether doing so would break database
+integrity, and only deactivates if it will not.
+
+
Raises:
+
:exc:LayoutIntegrity – if deactivating will compromise layout integrity.:
The class methods comp_avail(), connexion() and so on return
+event type instances, and internally store the result. Thus, subsequent calls
+do not generate more database queries. This can reduce overhead.
Parent table for any table that has events associated with it.
+This is a way to make the event table polymorphic. It points to this table,
+which shares (unique) primary keys with child tables (e.g., component). It
+only has one key: ID.
Connect one or more component pairs with a common timestamp.
+
If you are connecting only one pair, this function is equivalent to calling
+connexion.make(). However, multiple calls to connexion.make()
+generate a unique timestamp per call. To assign a single timestamp to many
+connexions at once, use this function.
comp (list of connexion objects) – The connexions to make.
+
time (datetime.datetime) – The time at which to end availability.
+
notes (string) – Any notes for the timestamp.
+
force (bool) – If True, then remove any components that can be removed, while doing
+nothing (except making note of such in the logger) for components whose
+removal would violate database integrity. If False,
+DoesNotExist is raised for any addition that violates database
+integrity.
The role of this component type:
+- T: terminate at type1 (type2 is left NULL).
+- H: hide type1 (type2 is left NULL).
+- O: only draw connexions one way between type1 and type2.
A list associating property types with components.
+A property can be for one or more component types. For example,
+“dist_from_n_end” is only a property of cassettes, but “termination” may be a
+property of LNA’s, FLA’s and so on. This is simply a table for matching
+property types to component types.
End availability of one or more components with a common timestamp.
+
If you are adding only one component, this function is equivalent to calling
+component.remove(). However, multiple calls to component.remove()
+generate a unique timestamp per call. To assign a single timestamp to many
+additions at once, use this function.
+
+
Parameters:
+
+
comp (list of component objects) – The components to end availability of.
+
time (datetime.datetime) – The time at which to end availability.
+
notes (string) – Any notes for the timestamp.
+
force (bool) – If True, then remove any components that can be removed, while doing
+nothing (except making note of such in the logger) for components whose
+removal would violate database integrity. If False,
+DoesNotExist is raised for any addition that violates database
+integrity.
Set a property value for one or more components with a common timestamp.
+
Passing None for the property value erases that property from the
+component.
+
If you altering only one component, this function is equivalent to calling
+component.set_property(). However, multiple calls to
+component.set_property() generate a unique timestamp per call. To
+assign a single timestamp to many additions at once, use this function.
+
+
Parameters:
+
+
comp (list of component objects) – The components to assign the property to.
time (datetime.datetime) – The time at which to end availability.
+
notes (string) – Any notes for the timestamp.
+
force (bool) – If False, then complain if altering the property does nothing (e.g.,
+because the property value would be unchanged for a certain component);
+otherwise, ignore such situations and merely issue logging information on
+them.
+
+
+
Raises:
+
+
:exc:ValueError:, if value does not conform to the property type's regular –
+
expression; :exc:PropertyUnchanged – if force is False: and a:
+
component's property value would remain unaltered. –
All events recorded in the database are associated with a user, and not all
+users have all permissions. You must call this function before making any
+changes to the database.
+
+
Parameters:
+
u (string or integer) –
+
One of:
+
your CHIMEwiki username (string). Use an initial capital letter.
+This is the recommended input.
+
the name entered into the “real name” field in your CHIMEwiki profile
Sever one or more component pairs with a common timestamp.
+
If you are severing only one pair, this function is equivalent to calling
+connexion.sever(). However, multiple calls to connexion.sever()
+generate a unique timestamp per call. To assign a single timestamp to many
+connexion severances at once, use this function.
comp (list of connexion objects) – The connexions to sever.
+
time (datetime.datetime) – The time at which to end availability.
+
notes (string) – Any notes for the timestamp.
+
force (bool) – If True, then sever any connexions that can be severed, while doing
+nothing (except making note of such in the logger) for connexions whose
+severence would violate database integrity. If False,
+DoesNotExist is raised for any severence that violates database
+integrity.
This is intended to be the main data class for the post
+acquisition/real-time analysis parts of the pipeline. This class is laid
+out very similarly to how the data is stored in analysis format hdf5 files
+and the data in this class can be optionally stored in such an hdf5 file
+instead of in memory.
+
+
Parameters:
+
h5_data (h5py.Group, memh5.MemGroup or hdf5 filename, optional) – Underlying h5py like data container where data will be stored. If not
+provided a new caput.memh5.MemGroup instance will be created.
+
+
+
Used to pick which subclass to instantiate based on attributes in
+data.
Convert acquisition format hdf5 data to analysis data object.
+
Reads hdf5 data produced by the acquisition system and converts it to
+analysis format in memory.
+
+
Parameters:
+
+
acq_files (filename, h5py.File or list there-of or filename pattern) – Files to convert from acquisition format to analysis format.
+Filename patterns with wild cards (e.g. “foo*.h5”) are supported.
+
start (integer, optional) – What frame to start at in the full set of files.
+
stop (integer, optional) – What frame to stop at in the full set of files.
+
datasets (list of strings) – Names of datasets to include from acquisition files. Default is to
+include all datasets found in the acquisition files.
+
out_group (h5py.Group, hdf5 filename or memh5.Group) – Underlying hdf5 like container that will store the data for the
+BaseData instance.
Parses and stores meta-data from file headers allowing for the
+interpretation and selection of the data without reading it all from disk.
+
+
Parameters:
+
files (filename, h5py.File or list there-of or filename pattern) – Files containing data. Filename patterns with wild cards (e.g.
+“foo*.h5”) are supported.
Convert acquisition format hdf5 data to analysis data object.
+
This method overloads the one in BaseData.
+
Changed Jan. 22, 2016: input arguments are now (acq_files,start,
+stop,**kwargs) instead of (acq_files,start,stop,prod_sel,
+freq_sel,datasets,out_group).
+
Reads hdf5 data produced by the acquisition system and converts it to
+analysis format in memory.
+
+
Parameters:
+
+
acq_files (filename, h5py.File or list there-of or filename pattern) – Files to convert from acquisition format to analysis format.
+Filename patterns with wild cards (e.g. “foo*.h5”) are supported.
+
start (integer, optional) – What frame to start at in the full set of files.
+
stop (integer, optional) – What frame to stop at in the full set of files.
+
stack_sel (valid numpy index) – Used to select a subset of the stacked correlation products.
+Only one of stack_sel, prod_sel, and input_sel may be
+specified, with prod_sel preferred over input_sel and
+stack_sel proferred over both.
+h5py fancy indexing supported but to be used with caution
+due to poor reading performance.
+
prod_sel (valid numpy index) – Used to select a subset of correlation products.
+Only one of stack_sel, prod_sel, and input_sel may be
+specified, with prod_sel preferred over input_sel and
+stack_sel proferred over both.
+h5py fancy indexing supported but to be used with caution
+due to poor reading performance.
+
input_sel (valid numpy index) – Used to select a subset of correlator inputs.
+Only one of stack_sel, prod_sel, and input_sel may be
+specified, with prod_sel preferred over input_sel and
+stack_sel proferred over both.
+h5py fancy indexing supported but to be used with caution
+due to poor reading performance.
+
freq_sel (valid numpy index) – Used to select a subset of frequencies.
+h5py fancy indexing supported but to be used with caution
+due to poor reading performance.
+
datasets (list of strings) – Names of datasets to include from acquisition files. Default is to
+include all datasets found in the acquisition files.
+
out_group (h5py.Group, hdf5 filename or memh5.Group) – Underlying hdf5 like container that will store the data for the
+BaseData instance.
+
apply_gain (boolean, optional) – Whether to apply the inverse gains to the visibility datasets.
+
renormalize (boolean, optional) – Whether to renormalize for dropped packets.
+
distributed (boolean, optional) – Load data into a distributed dataset.
+
comm (MPI.Comm) – Communicator to distributed over. Use MPI.COMM_WORLD if not set.
Efficiently read a CorrData file in a distributed fashion.
+
This reads a single file from disk into a distributed container. In
+contrast to to CorrData.from_acq_h5 it is more restrictive,
+allowing only contiguous slices of the frequency and time axes,
+and no down selection of the input/product/stack axis.
+
+
Parameters:
+
+
fname (str) – File name to read. Only supports one file at a time.
+
comm (MPI.Comm, optional) – MPI communicator to distribute over. By default this will
+use MPI.COMM_WORLD.
+
freq_sel (slice, optional) – A selection over the frequency axis. Only slice objects
+are supported. If not set, read all frequencies.
+
start (int, optional) – Start and stop indexes of the time selection.
+
stop (int, optional) – Start and stop indexes of the time selection.
A pair of input indices representative of those in the stack.
+
Note, these are correctly conjugated on return, and so calculations
+of the baseline and polarisation can be done without additionally
+looking up the stack conjugation.
Frequencies selected will have bin centres bracked by provided range.
+
+
Parameters:
+
+
freq_low (float) – Lower end of the frequency range in MHz. Default is the lower edge
+of the band.
+
freq_high (float) – Upper end of the frequency range in MHz. Default is the upper edge
+of the band.
+
freq_step (float) – How much bandwidth to skip over between samples in MHz. This value
+is approximate. Default is to include all samples in given range.
Subclass of BaseData for gain, digitalgain, and flag input acquisitions.
+
These acquisitions consist of a collection of updates to the real-time pipeline ordered
+chronologically. In most cases the updates do not occur at a regular cadence.
+The time that each update occured can be accessed via self.index_map[‘update_time’].
+In addition, each update is given a unique update ID that can be accessed via
+self.datasets[‘update_id’] and can be searched using the self.search_update_id method.
+
Used to pick which subclass to instantiate based on attributes in
+data.
Convert acquisition format hdf5 data to analysis data object.
+
This method overloads the one in BaseData.
+
Reads hdf5 data produced by the acquisition system and converts it to
+analysis format in memory.
+
+
Parameters:
+
+
acq_files (filename, h5py.File or list there-of or filename pattern) – Files to convert from acquisition format to analysis format.
+Filename patterns with wild cards (e.g. “foo*.h5”) are supported.
+
start (integer, optional) – What frame to start at in the full set of files.
+
stop (integer, optional) – What frame to stop at in the full set of files.
+
datasets (list of strings) – Names of datasets to include from acquisition files. Default is to
+include all datasets found in the acquisition files.
+
out_group (h5py.Group, hdf5 filename or memh5.Group) – Underlying hdf5 like container that will store the data for the
+BaseData instance.
Convenience access to a single time-ordered datastream (TOD).
+
+
Parameters:
+
+
chan (int) – A channel number. (Generally, they should be in the range 0–7 for
+non-multiplexed data and 0–15 for multiplexed data.)
+
mux (int) – A mux number. For housekeeping files with no multiplexing (e.g.,
+FLA’s), leave this as -1.
+
+
+
Returns:
+
tod – A 1D array of values for the requested channel/mux combination. Note
+that a reference to the data in the dataset is returned; this method
+does not make a copy.
+
+
Return type:
+
numpy.array
+
+
Raises:
+
ValueError – Raised if one of chan or mux is not present in any dataset.
This internally uses the Pandas resampling functionality so that
+documentation is a useful reference. This will return the metric with
+the labels as a series of multi-level columns.
+
+
Parameters:
+
+
metric_name (str) – Name of metric to resample.
+
rule (str) – The set of times to resample onto (example ‘30S’, ‘1Min’, ‘2D’). See
+the pandas docs for a full description.
+
how (str or callable, optional) – How should we combine samples to regrid the data? This takes any
+valid argument for the the pandas apply method. Useful options are
+‘mean’, ‘sum’, ‘min’, ‘max’ and ‘std’.
+
unstack (bool, optional) – Unstack the data, i.e. return with the labels as hierarchial columns.
+
kwargs – Any remaining kwargs are passed to the pandas.DataFrame.resample
+method to give fine grained control of the resampling.
+
+
+
Returns:
+
df – A dataframe resampled onto a regular grid. Labels now appear as part
+of multi-level columns.
For easy access to outside weather station temperature.
+Needs to be able to extrac temperatures from both mingun_weather files
+and chime_weather files.
Returns a parameteric model for the map of a point source, consisting of the interpolated dirty beam along the y-axis and a sinusoid with gaussian envelope along the x-axis.
Base class for fitting models to point source transits.
+
The fit method should be used to populate the param, param_cov, chisq,
+and ndof attributes. The predict and uncertainty methods can then be used
+to obtain the model prediction for the response and uncertainty on this quantity
+at a given hour angle.
Apply subclass defined _fit method to multiple transits.
+
This function can be used to fit the transit for multiple inputs
+and frequencies. Populates the param, param_cov, chisq, and ndof
+attributes.
+
+
Parameters:
+
+
ha (np.ndarray[nha,]) – Hour angle in degrees.
+
resp (np.ndarray[..., nha]) – Measured response to the point source. Complex valued.
+
resp_err (np.ndarray[..., nha]) – Error on the measured response.
+
width (np.ndarray[...]) – Initial guess at the width (sigma) of the transit in degrees.
+
absolute_sigma (bool) – Set to True if the errors provided are absolute. Set to False if
+the errors provided are relative, in which case the parameter covariance
+will be scaled by the chi-squared per degree-of-freedom.
ha (np.ndarray[nha,] or float) – The hour angle in degrees.
+
elementwise (bool) – If False, then the model will be evaluated at the
+requested hour angles for every set of parameters.
+If True, then the model will be evaluated at a
+separate hour angle for each set of parameters
+(requires ha.shape == self.N).
+
+
+
Returns:
+
model – Model for the point source response at the requested
+hour angles. Complex valued.
Predict the uncertainty on the point source response.
+
+
Parameters:
+
+
ha (np.ndarray[nha,] or float) – The hour angle in degrees.
+
alpha (float) – Confidence level given by 1 - alpha.
+
elementwise (bool) – If False, then the uncertainty will be evaluated at
+the requested hour angles for every set of parameters.
+If True, then the uncertainty will be evaluated at a
+separate hour angle for each set of parameters
+(requires ha.shape == self.N).
+
+
+
Returns:
+
err – Uncertainty on the point source response at the
+requested hour angles.
Calculate robust, direction dependent estimate of scale.
+
+
Parameters:
+
+
z (np.ndarray) – 1D array containing the data.
+
c (float) – Cutoff in number of MAD. Data points whose absolute value is
+larger than c * MAD from the median are saturated at the
+maximum value in the estimator.
submap (np.ndarray[..., nra, ndec]) – Region of the ringmap around the point source.
+
rms (np.ndarray[..., nra]) – RMS error on the map.
+
flag (np.ndarray[..., nra, ndec]) – Boolean array that indicates which pixels to fit.
+
dirty_beam (np.ndarray[..., nra, ndec] or [ra, dec, dirty_beam]) – Fourier transform of the weighting function used to create
+the map. If input, then the interpolated dirty beam will be used
+as the model for the point source response in the declination direction.
+Can either be an array that is the same size as submap, or a list/tuple
+of length 3 that contains [ra, dec, dirty_beam] since the shape of the
+dirty beam is likely to be larger than the shape of the subregion of the
+map, at least in the declination direction.
+
+
+
Returns:
+
+
param_name (np.ndarray[nparam, ]) – Names of the parameters.
+
param (np.ndarray[…, nparam]) – Best-fit parameters for each item.
+
param_cov (np.ndarray[…, nparam, nparam]) – Parameter covariance for each item.
Returns a parameteric model for the map of a point source,
+consisting of the interpolated dirty beam along the y-axis
+and a gaussian along the x-axis.
+
This function is a wrapper that defines the interpolated
+dirty beam.
+
+
Parameters:
+
dirty_beam (scipy.interpolate.interp1d) – Interpolation function that takes as an argument el = sin(za)
+and outputs an np.ndarray[nel, nra] that represents the dirty
+beam evaluated at the same right ascension as the map.
+
+
Returns:
+
dirty_gauss – Model prediction for the map of the point source.
Returns a parameteric model for the map of a point source,
+consisting of the interpolated dirty beam along the y-axis
+and a sinusoid with gaussian envelope along the x-axis.
+
This function is a wrapper that defines the interpolated
+dirty beam.
+
+
Parameters:
+
dirty_beam (scipy.interpolate.interp1d) – Interpolation function that takes as an argument el = sin(za)
+and outputs an np.ndarray[nel, nra] that represents the dirty
+beam evaluated at the same right ascension as the map.
+
+
Returns:
+
real_dirty_gauss – Model prediction for the map of the point source.
For a given set of times determine when and how they were calibrated.
+
This uses the pre-calculated calibration time reference files.
+
+
Parameters:
+
+
times – Unix times of data points to be calibrated as floats.
+
cal_file – memh5 container which containes the reference times for calibration source
+transits.
+
logger – A logging object to use for messages. If not provided, use a module level
+logger.
+
+
+
Returns:
+
reftime_result – A dictionary containing four entries:
+
+
reftime: Unix time of same length as times. Reference times of transit of the
+source used to calibrate the data at each time in times. Returns NaN for
+times without a reference.
+
reftime_prev: The Unix time of the previous gain update. Only set for time
+samples that need to be interpolated, otherwise NaN.
+
interp_start: The Unix time of the start of the interpolation period. Only
+set for time samples that need to be interpolated, otherwise NaN.
+
interp_stop: The Unix time of the end of the interpolation period. Only
+set for time samples that need to be interpolated, otherwise NaN.
Provide rough estimate of the FWHM of the CHIME primary beam pattern.
+
It uses a linear fit to the median FWHM(nu) over all feeds of a given
+polarization for CygA transits. CasA and TauA transits also showed
+good agreement with this relationship.
+
+
Parameters:
+
+
freq (float or np.ndarray) – Frequency in MHz.
+
pol (string or bool) – Polarization, can be ‘X’/’E’ or ‘Y’/’S’
+
dec (float) – Declination of the source in radians. If this quantity
+is input, then the FWHM is divided by cos(dec) to account
+for the increased rate at which a source rotates across
+the sky. Default is do not correct for this effect.
+
sigma (bool) – Return the standard deviation instead of the FWHM.
+Default is to return the FWHM.
+
voltage (bool) – Return the value for a voltage beam, otherwise returns
+value for a power beam.
+
seconds (bool) – Convert to elapsed time in units of seconds.
+Otherwise returns in units of degrees on the sky.
+
+
+
Returns:
+
fwhm – Rough estimate of the FWHM (or standard deviation if sigma=True).
+
+
Return type:
+
float or np.ndarray
+
+
+
+
+
+
+ch_util.cal_utils.interpolate_gain(freq, gain, weight, flag=None, length_scale=30.0)[source]
+
Replace gain at flagged frequencies with interpolated values.
+
Uses a gaussian process regression to perform the interpolation
+with a Matern function describing the covariance between frequencies.
+
+
Parameters:
+
+
freq (np.ndarray[nfreq,]) – Frequencies in MHz.
+
gain (np.ndarray[nfreq, ninput]) – Complex gain for each input and frequency.
+
weight (np.ndarray[nfreq, ninput]) – Uncertainty on the complex gain, expressed as inverse variance.
+
flag (np.ndarray[nfreq, ninput]) – Boolean array indicating the good (True) and bad (False) gains.
+If not provided, then it will be determined by evaluating weight > 0.0.
+
length_scale (float) – Correlation length in frequency in MHz.
+
+
+
Returns:
+
+
interp_gain (np.ndarray[nfreq, ninput]) – For frequencies with flag = True, this will be equal to gain. For frequencies with
+flag = False, this will be an interpolation of the gains with flag = True.
+
interp_weight (np.ndarray[nfreq, ninput]) – For frequencies with flag = True, this will be equal to weight. For frequencies with
+flag = False, this will be the expected uncertainty on the interpolation.
This class provides the user interface to FeedLocator.
+
It initializes instances of FeedLocator (normally one per polarization)
+and returns results combined lists of results (good channels and positions,
+agreement/disagreement with the layout database, etc.)
+
Feed locator should not
+have to sepparate the visibilities in data to run the test on and data not to run the
+test on. ChanMonitor should make the sepparation and provide FeedLocator with the right
+data cube to test.
If self.finder exists, then it takes a deep copy of this object,
+further restricts the time range to include only src transits,
+and then queries the database to obtain a list of the acquisitions.
+If self.finder does not exist, then it creates a finder object,
+restricts the time range to include only src transits between
+self.t1 and self.t2, and then queries the database to obtain a list
+of the acquisitions.
This method uses the attributes ‘night_acq_list’ and
+‘acq_list’ to determine the srcs that transit
+in the available data. If these attributes do not
+exist, then the method ‘set_acq_list’ is called.
+If srcs is not specified, then it defaults to the
+brightest four radio point sources in the sky:
+CygA, CasA, TauA, and VirA.
This method sets four attributes. The first two attributes
+are ‘night_finder’ and ‘night_acq_list’, which are the
+finder object and list of acquisitions that
+contain all night time data between self.t1 and self.t2.
+The second two attributes are ‘finder’ and ‘acq_list’,
+which are the finder object and list of acquisitions
+that contain all data beween self.t1 and self.t2 with the
+sunrise, sun transit, and sunset removed.
This class contains functions that do all the computations to
+determine feed positions from data. It also determines the quality
+of data and returns a list of good inputs and frequencies.
+
Uppon initialization, it receives visibility data around one or two
+bright sources transits as well as corresponding meta-data.
+
+
Parameters:
+
+
vis1 (Visibility data around bright source transit)
+
[vis2] (Visibility data around bright source transit)
+
tm1 (Timestamp corresponding to vis1 [vis2])
+
[tm2] (Timestamp corresponding to vis1 [vis2])
+
src1 (Ephemeris astronomical object corresponding to the) – transit in vis1 [vis2]
+
[src2] (Ephemeris astronomical object corresponding to the) – transit in vis1 [vis2]
+
freqs (frequency axis of vis1 [and vis2])
+
prods (Product axis of vis1 [and vis2])
+
inputs (inputs loaded in vis1 [and vis2])
+
pstns0 (positions of inputs as obtained from the layout database)
+
bsipts (base inputs used to determine cross correlations loaded) – (might become unecessary in the future)
Call only if freqs are adjacent.
+Uses xdists (Earth coords) instead of c_xdists (cylinder coords)
+to allow for calling before ydists are computed. Doesn’t make any
+difference for this test. Results are used in computing y_dists.
Compliance of noise to the radiometer equation and
+
Goodness of fit to a template Tsky.
+
+
See Doclib:235
+file ‘data_quality.pdf’ for details on how the filters and tolerances work.
+
+
Parameters:
+
+
data (ch_util.andata.CorrData object) – Data to run test on.
+If andata object contains cross-correlations,
+test is performed on auto-correlations only.
+
gain_tol (float) – Tolerance for digital gains filter. Flag channels whose
+digital gain fractional absolute deviation
+is above ‘gain_tol’ (default is 10.)
+
noise_tol (float) – Tolerance for radiometer noise filter. Flag channels whose
+noise rms is higher then ‘noise_tol’ times the expected
+from the radiometer equation. (default = 2.)
+
fit_tol (float) – Tolerance for the fit-to-Tsky filter. Flag channels whose
+fractional rms for the ‘gain’ fit parameter is above
+‘fit_tol’ (default = 0.02)
+
test_freq (integer) – Index of frequency to test. Default is 0.
+
noise_synced (boolean) – Use this to force the code to call (or not call)
+ni_utils.process_synced_data(). If not given,
+the code will determine if syncronized noise injection was on.
+For acquisitions newer then 20150626T200540Z_pathfinder_corr,
+noise injection info is written in the attributes. For older
+acquisitions the function _check_ni() is called to determine
+if noise injection is On.
+
inputs (list of CorrInputs, optional) – List of CorrInput objects describing the channels in this
+dataset. This is optional, if not set (default), then it will
+look the data up in the database. This option just allows
+control of the database accesses.
+
res_plot (boolean, optional) – If True, a plot with all the tested channels and the
+Tsky fits is generated. File naming is
+plot_fit_{timestamp}.pdf
+
verbose (boolean, optional) – Print out useful output as the tests are run.
+
+
+
Returns:
+
+
good_gains (list of int) –
+
+
for channels that pass the gains filter, 0. otherwise.
+
+
+
good_noise (list of int) –
+
+
for channels that pass the noise filter, 0. otherwise.
+
+
+
good_fit (list of int) –
+
1. for channels that pass the fit-to-Tsky filter,
+0. otherwise.
+
+
test_chans (list of int) – A list of the channels tested in the same order as they
+appear in all the other lists returned
+
+
+
+
+
Examples
+
Run test on frequency index 3. data is an andata object:
The precession of the Earth’s axis gives noticeable shifts in object
+positions over the life time of CHIME. To minimise the effects of this we
+need to be careful and consistent with our ephemeris calculations.
+Historically Right Ascension has been given with respect to the Vernal
+Equinox which has a significant (and unnecessary) precession in the origin of
+the RA axis. To avoid this we use the new Celestial Intermediate Reference
+System which does not suffer from this issue.
+
Practically this means that when calculating RA, DEC coordinates for a source
+position at a given time you must be careful to obtain CIRS coordinates
+(and not equinox based ones). Internally using ephemeris.object_coords does
+exactly that for you, so for any lookup of coordinates you should use that on
+your requested body.
+
Note that the actual coordinate positions of sources must be specified using
+RA, DEC coordinates in ICRS (which is roughly equivalent to J2000). The
+purpose of object_coords is to transform into new RA, DEC coordinates taking
+into account the precession and nutation of the Earth’s polar axis since
+then.
+
These kind of coordinate issues are tricky, confusing and hard to debug years
+later, so if you’re unsure you are recommended to seek some advice.
+
Constants
+
+
CHIMELATITUDE
CHIME’s latitude [degrees].
+
+
CHIMELONGITUDE
CHIME’s longitude [degrees].
+
+
CHIMEALTITUDE
CHIME’s altitude [metres].
+
+
SIDEREAL_S
Number of SI seconds in a sidereal second [s/sidereal s]. You probably want
+STELLAR_S instead.
+
+
STELLAR_S
Number of SI seconds in a stellar second [s/stellar s].
Calculate pointing correction in declination for the Galt Telescope See description of the pointing model by Lewis Knee CHIME document library 754 https://bao.chimenet.ca/doc/documents/754
Calculate pointing correction in hour angle for the Galt Telescope See description of the pointing model by Lewis Knee CHIME document library 754 https://bao.chimenet.ca/doc/documents/754
Calculate pointing correction in declination for the Galt Telescope
+See description of the pointing model by Lewis Knee CHIME document library
+754 https://bao.chimenet.ca/doc/documents/754
+
+
Parameters:
+
+
ha (Skyfield Angle objects) – Target hour angle and declination
+
dec (Skyfield Angle objects) – Target hour angle and declination
+
b (list of floats) – List of coefficients (in arcmin) for the pointing model
+(NOTE: it is very unlikely that a user will want to change these
+from the defaults, which are taken from the pointing model as of
+2019-2-15)
Calculate pointing correction in hour angle for the Galt Telescope
+See description of the pointing model by Lewis Knee CHIME document library
+754 https://bao.chimenet.ca/doc/documents/754
+
+
Parameters:
+
+
ha (Skyfield Angle objects) – Target hour angle and declination
+
dec (Skyfield Angle objects) – Target hour angle and declination
+
a (list of floats) – List of coefficients (in arcmin) for the pointing model
+(NOTE: it is very unlikely that a user will want to change these
+from the defaults, which are taken from the pointing model as of
+2019-2-15)
Returns a dictionary containing skyfield.starlib.Star
+objects for common radio point sources. This is useful for
+obtaining the skyfield representation of a source from a string
+containing its name.
+
+
Parameters:
+
catalog_name (str) – Name of the catalog. This must be the basename of the json file
+in the ch_util/catalogs directory. Can take multiple catalogs,
+with the first catalog favoured for any overlapping sources.
+
+
Returns:
+
src_dict – Format is {‘SOURCE_NAME’: skyfield.starlib.Star, …}
Gives the ICRS coordinates if no date is given (=J2000), or if a date is
+specified gives the CIRS coordinates at that epoch.
+
This also returns the apparent position, including abberation and
+deflection by gravitational lensing. This shifts the positions by up to
+20 arcseconds.
+
+
Parameters:
+
+
body (skyfield source) – skyfield.starlib.Star or skyfield.vectorlib.VectorSum or
+skyfield.jpllib.ChebyshevPosition body representing the source.
+
date (float) – Unix time at which to determine ra of source If None, use Jan 01
+2000.
+
deg (bool) – Return RA ascension in degrees if True, radians if false (default).
+
obs (caput.time.Observer) – An observer instance to use. If not supplied use chime. For many
+calculations changing from this default will make little difference.
Calculates the RA where a source is expected to peak in the beam.
+Note that this is not the same as the RA where the source is at
+transit, since the pathfinder is rotated with respect to north.
+
+
Parameters:
+
+
body (ephem.FixedBody) – skyfield.starlib.Star or skyfield.vectorlib.VectorSum or
+skyfield.jpllib.ChebyshevPosition or Ephemeris body
+representing the source.
+
date (float) – Unix time at which to determine ra of source
+If None, use Jan 01 2000.
+Ignored if body is not a skyfield object
+
deg (bool) – Return RA ascension in degrees if True,
+radians if false (default).
Return the angular distance between two coordinates.
+
+
Parameters:
+
+
long1 (Skyfield Angle objects) – longitude and latitude of the first coordinate. Each should be the
+same length; can be one or longer.
+
lat1 (Skyfield Angle objects) – longitude and latitude of the first coordinate. Each should be the
+same length; can be one or longer.
+
long2 (Skyfield Angle objects) – longitude and latitude of the second coordinate. Each should be the
+same length. If long1, lat1 have length longer than 1, long2 and
+lat2 should either have the same length as coordinate 1 or length 1.
+
lat2 (Skyfield Angle objects) – longitude and latitude of the second coordinate. Each should be the
+same length. If long1, lat1 have length longer than 1, long2 and
+lat2 should either have the same length as coordinate 1 or length 1.
The andata.Reader is initialized with the filename list part
+of the data interval then the time range part of the data interval is
+used as an arguments to andata.Reader.select_time_range().
This class gives a convenient way to search and filter data acquisitions
+as well as time ranges of data within acquisitions. Search results
+constitute a list of files within an acquisition as well as a time range for
+the data within these files. Convenient methods are provided for loading
+the precise time range of constituting a search result.
+
This is intended to make the most common types of searches of CHIME data as
+convenient as possible. However for very complex searches, it may be
+necessary to resort to the lower level interface.
+
Searching the index
+
There are four ways that a search can be modified which may be combined in
+any way.
+
+
You can restrict the types of acquisition that are under
+consideration, using methods whose names begin with only_.
+In this way, one can consider only, say, housekeeping acquisitions.
+
The second is to adjust the total time range under consideration.
+This is achieved by assigning to time_range or calling
+methods beginning with set_time_range_. The total time range affects
+acquisitions under consideration as well as the data time ranges within
+the acquisitions. Subsequent changes to the total time range under
+consideration may only become more restrictive.
+
The data index may also be filtered by acquisition using methods whose
+names begin with filter_acqs. Again subsequent filtering are always
+combined to become more restrictive. The attribute acqs
+lists the acquisitions currently included in the search for convenience
+when searching interactively.
+
Time intervals within acquisitions are added using methods with names
+beginning with include_. Time intervals are defined in the
+time_intervals attribute, and are inclusive (you can
+add as many as you want).
+
Finally, upon calling :meth:get_results or :meth:get_results_acq,
+one can pass an arbitrary condition on individual files, thereby
+returning only a subset of files from each acquisition.
+
+
Getting results
+
Results of the search can be retrieved using methods whose names begin with
+get_results An individual search result is constituted of a list of file
+names and a time interval within these files. These can easily loaded into
+memory using helper functions (see BaseDataInterval and
+DataIntervalList).
+
+
Parameters:
+
+
acqs (list of chimedb.data_index.ArchiveAcq objects) – Acquisitions to initially include in data search. Default is to search
+all acquisitions.
+
node_spoof (dictionary) – Normally, the DB will be queried to find which nodes are mounted on your
+host. If you are on a machine that is cross-mounted, though, you can
+enter a dictionary of “node_name”: “mnt_root” pairs, specifying the
+nodes to search and where they are mounted on your host.
+
+
+
+
Examples
+
To find all the correlator data between two times.
condition (peewee comparison) – Condition on any on chimedb.data_index.ArchiveAcq or any
+class joined to chimedb.data_index.ArchiveAcq: using the
+syntax from the peewee module [1].
Filter the acquisitions by the properties of its files.
+
Because each acquisition has many files, this filter should be
+significantly slower than Finder.filter_acqs().
+
+
Parameters:
+
condition (peewee comparison) – Condition on any on chimedb.data_index.ArchiveAcq,
+chimedb.data_index.ArchiveFile or any class joined to
+chimedb.data_index.ArchiveFile using the syntax from the
+peewee module [2].
file_condition (peewee comparison) – Any additional condition for filtering the files within the
+acquisition. In general, this should be a filter on one of the file
+information tables, e.g., chimedb.data_index.CorrFileInfo.
Get search results restricted to a given acquisition.
+
+
Parameters:
+
+
acq_ind (int) – Index of Finder.acqs for the desired acquisition.
+
file_condition (peewee comparison) – Any additional condition for filtering the files within the
+acquisition. In general, this should be a filter on one of the file
+information tables, e.g., CorrFileInfo.
Defines how global flags are treated when finding data. There are three
+severities of global flag: comment, warning, and severe. There are
+four possible behaviours when a search result overlaps a global flag,
+represented by module constants:
+
+
GF_REJECT:
+
Reject any data overlapping flag silently.
+
+
GF_RAISE:
+
Raise an exception when retrieving data intervals.
+
+
GF_WARN:
+
Send a warning when retrieving data intervals but proceed.
+
+
GF_ACCEPT:
+
Accept the data silently, ignoring the flag.
+
+
+
The behaviour for all three severities is represented by a dictionary.
+If no mode is set, then the default behaviour is
+{‘comment’ : GF_ACCEPT, ‘warning’ : GF_WARN, ‘severe’ : GF_REJECT}.
Initialize Finder when not working on a storage node.
+
Normally only data that is available on the present host is searched,
+and as such Finder can’t be used to browse the index when you
+don’t have access to the acctual data. Initializing using this method
+spoofs the ‘gong’ and ‘niedermayer’ storage nodes (which should have a
+full copy of the archive) such that the data index can be search the
+full archive.
Print the acquisitions included in this search and thier properties.
+
This method is convenient when searching the data index interactively
+and you want to see what acquisitions remain after applying filters or
+restricting the time range.
This is a shortcut for specifying
+file_condition=(chimedb.data_index.HKFileInfo.atmel_name==name)
+in get_results_acq(). Instead, one can simply call this function
+with name as, e.g., “LNA”, “FLA”, and calls to
+get_results_acq() will be appropriately restricted.
This method updates the time_range property and also
+excludes any acquisitions that do not overlap with the new range. This
+method always narrows the time range under consideration, never expands
+it.
+
+
Parameters:
+
+
start_time (float or datetime.datetime) – Unix/POSIX time or UTC start of desired time range. Optional.
+
end_time (float or datetime.datetime) – Unix/POSIX time or UTC end of desired time range. Optional.
Catalog the measured flux densities of astronomical sources
+
This module contains tools for cataloging astronomical sources
+and predicting their flux density at radio frequencies based on
+previous measurements.
A base class for modeling and fitting spectra. Any spectral model
+used by FluxCatalog should be derived from this class.
+
The fit method should be used to populate the param, param_cov, and stats
+attributes. The predict and uncertainty methods can then be used to obtain
+the flux density and uncertainty at arbitrary frequencies.
Class for cataloging astronomical sources and predicting
+their flux density at radio frequencies based on spectral fits
+to previous measurements.
+
Class methods act upon and provide access to the catalog of
+all sources. Instance methods act upon and provide access
+to individual sources. All instances are stored in an
+internal class dictionary.
Dictionary that provides access to the various models that
+can be fit to the spectrum. These models should be
+subclasses of FitSpectrum.
+
+
Type:
+
dict
+
+
+
+
+
Instantiates a FluxCatalog object for an astronomical source.
+
+
Parameters:
+
+
name (string) – Name of the source. The convention for the source name is to
+use the MAIN_ID in the SIMBAD database in all uppercase letters
+with spaces replaced by underscores.
+
ra (float) – Right Ascension in degrees.
+
dec (float) – Declination in degrees.
+
alternate_names (list of strings) – Alternate names for the source. Ideally should include all alternate names
+present in the SIMBAD database using the naming convention specified above.
+
model (string) – Name of FitSpectrum subclass.
+
model_kwargs (dict) – Dictionary containing keywords required by the model.
+
stats (dict) – Dictionary containing statistics from model fit.
param_cov (2D-list, size nparam x nparam) – Estimate of covariance of fit parameters.
+
measurements (2D-list, size nmeas x 7) – List of measurements of the form:
+[freq, flux, eflux, flag, catalog, epoch, citation].
+Should use the add_measurement method to populate this list.
+
overwrite (int between 0 and 2) – Action to take in the event that this source is already in the catalog:
+- 0 - Return the existing entry.
+- 1 - Add the measurements to the existing entry.
+- 2 - Overwrite the existing entry.
+Default is 0.
Add entries to the list of measurements. Each argument/keyword
+can be a list of items with length equal to ‘len(flux)’, or
+alternatively a single item in which case the same value is used
+for all measurements.
+
+
Parameters:
+
+
freq (float, list of floats) – Frequency in MHz.
+
flux (float, list of floats) – Flux density in Jansky.
+
eflux (float, list of floats) – Uncertainty on flux density in Jansky.
+
flag (bool, list of bool) – If True, use this measurement in model fit.
+Default is True.
+
catalog (string or None, list of strings or Nones) – Name of the catalog from which this measurement originates.
+Default is None.
+
epoch (float or None, list of floats or Nones) – Year when this measurement was taken.
+Default is None.
+
citation (string or None, list of strings or Nones) – Citation where this measurement can be found
+(e.g., ‘Baars et al. (1977)’).
+Default is None.
Search the local directory for potential collections that
+can be loaded.
+
+
Returns:
+
collections – List containing a tuple for each collection. The tuple contains
+the filename of the collection (str) and the sources it contains
+(list of str).
Fit the measurements stored in the ‘measurements’ attribute with the
+spectral model specified in the ‘model’ attribute. This populates the
+‘param’, ‘param_cov’, and ‘stats’ attributes.
filename (str) – Valid path name. Should have .json or .pickle extension.
+
overwrite (int between 0 and 2) – Action to take in the event that this source is already in the catalog:
+- 0 - Return the existing entry.
+- 1 - Add any measurements to the existing entry.
+- 2 - Overwrite the existing entry.
+Default is 0.
+
set_globals (bool) – If True, this creates a variable in the global space
+for each source in the file. Default is False.
+
verbose (bool) – If True, print some basic info about the contents of
+the file as it is loaded. Default is False.
collections – List containing a tuple for each collection. The tuple contains
+the filename of the collection (str) and the sources it contains
+(list of str).
Plot the measurements, best-fit model, and confidence interval.
+
+
Parameters:
+
+
legend (bool) – Show legend. Default is True.
+
catalog (bool) – If True, then label and color code the measurements according to
+their catalog. If False, then label and color code the measurements
+according to their citation. Default is True.
+
residuals (bool) – Plot the residuals instead of the measurements and best-fit model.
+Default is False.
Constructor for JSONEncoder, with sensible defaults.
+
If skipkeys is false, then it is a TypeError to attempt
+encoding of keys that are not str, int, float or None. If
+skipkeys is True, such items are simply skipped.
+
If ensure_ascii is true, the output is guaranteed to be str
+objects with all incoming non-ASCII characters escaped. If
+ensure_ascii is false, the output can contain non-ASCII characters.
+
If check_circular is true, then lists, dicts, and custom encoded
+objects will be checked for circular references during encoding to
+prevent an infinite recursion (which would cause an RecursionError).
+Otherwise, no such check takes place.
+
If allow_nan is true, then NaN, Infinity, and -Infinity will be
+encoded as such. This behavior is not JSON specification compliant,
+but is consistent with most JavaScript based encoders and decoders.
+Otherwise, it will be a ValueError to encode such floats.
+
If sort_keys is true, then the output of dictionaries will be
+sorted by key; this is useful for regression tests to ensure
+that JSON serializations can be compared on a day-to-day basis.
+
If indent is a non-negative integer, then JSON array
+elements and object members will be pretty-printed with that
+indent level. An indent level of 0 will only insert newlines.
+None is the most compact representation.
+
If specified, separators should be an (item_separator, key_separator)
+tuple. The default is (’, ‘, ‘: ‘) if indent is None and
+(‘,’, ‘: ‘) otherwise. To get the most compact JSON representation,
+you should specify (‘,’, ‘:’) to eliminate whitespace.
+
If specified, default is a function that gets called for objects
+that can’t otherwise be serialized. It should return a JSON encodable
+version of the object or raise a TypeError.
Create holography database entry from .POST_REPORT log files
+generated by the nsched controller for the Galt Telescope.
+
+
Parameters:
+
+
logs (string) – list of paths to archives. Filenames should be, eg,
+01DEC17_1814.zip. Must be only one period in the filename,
+separating the extension.
+
start_tol (float (optional; default: 60.)) – Tolerance (in seconds) around which to search for duplicate
+operations.
+
dryrun (boolean (optional; default: True)) – Dry run only; do not add entries to database
+
replace_dup (boolean (optional; default: False)) – Delete existing duplicate entries and replace. Only has effect if
+dry_run == False
+
notes (string or list of strings (optional; default: None)) – notes to be added. If a string, the same note will be added to all
+observations. If a list of strings (must be same length as logs),
+each element of the list will be added to the corresponding
+database entry.
+Nota bene: the text “Added by create_from_post_reports” with the
+current date and time will also be included in the notes database
+entry.
Unzip and parse .ANT log file output by nsched for John Galt Telescope
+observations
+
+
Parameters:
+
logs (list of strings) –
.ZIP filenames. Each .ZIP archive should include a .ANT file and
+a .POST_REPORT file. This method unzips the archive, uses
+parse_post_report to read the .POST_REPORT file and extract
+the CHIME sidereal day corresponding to the DRAO sidereal day,
+and then reads the lines in the .ANT file to obtain the pointing
+history of the Galt Telescope during this observation.
+
(The DRAO sidereal day is days since the clock in Ev Sheehan’s
+office at DRAO was reset. This clock is typically only reset every
+few years, but it does not correspond to any defined date, so the
+date must be figured out from the .POST_REPORT file, which reports
+both the DRAO sidereal day and the UTC date and time.
+
Known reset dates: 2017-11-21, 2019-3-10)
+
+
+
Returns:
+
+
if output_params == False –
+
+
ant_data: A dictionary consisting of lists containing the LST,
hour angle, RA, and dec (all as Skyfield Angle objects),
+CHIME sidereal day, and DRAO sidereal day.
+
+
+
+
if output_params == True – output_params: dictionary returned by parse_post_report
+and
+ant_data: described above
+
Files
+
—–
+
the .ANT and .POST_REPORT files in the input .zip archive are
read a .POST_REPORT file from the nsched program which controls the
+John Galt Telescope and extract the source name, estimated start time,
+DRAO sidereal day, commanded duration, and estimated finish time
+
+
Parameters:
+
post_report_file (str) – path to the .POST_REPORT file to read
+
+
Returns:
+
output_params –
+
+
output_params[‘src’]HolographySource object or string
If the source is a known source in the holography database,
+return the HolographySource object. If not, return the name
+of the source as a string
+
+
output_params[‘SID’]int
DRAO sidereal day at the beginning of the observation
+
+
output_params[‘start_time’]skyfield time object
UTC time at the beginning of the observation
+
+
output_params[‘DURATION’]float
Commanded duration of the observation in sidereal hours
+
+
output_params[‘finish_time’]skyfield time object
Calculated UTC time at the end of the observation
+Calculated as start_time + duration * ephemeris.SIDEREAL_S
This module interfaces to the layout tables in the CHIME database.
+
The peewee module is used for the ORM to the MySQL database. Because the
+layouts are event-driven, you should never attempt to enter events by raw
+inserts to the event or timestamp tables, as you could create
+inconsistencies. Rather, use the methods which are described in this document to
+do such alterations robustly.
+
For most uses, you probably want to import the following:
The database must now be explicitly connected. This should not be done within
+an import statement.
+
+
+
Note
+
The logging module can be set to the level of your preference, or not
+imported altogether if you don’t want log messages from the layout
+module. Note that the peewee module sends a lot of messages to the
+DEBUG stream.
+
+
If you will be altering the layouts, you will need to register as a user:
+
>>> layout.set_user("Ahincks")
+
+
+
Use your CHIME wiki username here. Make sure it starts with a capital letter.
+Note that different users have different permissions, stored in the
+user_permission table. If you are simply reading from the layout,
+there is no need to register as a user.
This is a special mark-up language for quickly entering events. See the “help”
+box on the LTF page of the web interface for instructions.
+
+
Parameters:
+
+
ltf (string) – Pass either the path to a file containing the LTF, or a string containing
+the LTF.
+
time (datetime.datetime) – The time at which to apply the LTF.
+
notes (string) – Notes for the timestamp.
+
force (bool) – If True, then do nothing when events that would damage database
+integrity are encountered; skip over them. If False, then a bad
+propsed event will raise the appropriate exception.
There are some convenience methods for our implementation. For example, you
+can easily find components by component type:
+
>>> printg.component(type="reflector")
+[<layout.component object at 0x7fd1b2cda710>, <layout.component object at 0x7fd1b2cda810>, <layout.component object at 0x7fd1b2cfb7d0>]
+
+
+
Note that the graph nodes are component objects. You can also use the
+component() method to search for components by serial number:
+
>>> ant=g.component(comp="ANT0044B")
+
+
+
Node properties are stored as per usual for networkx.Graph objects:
Note, however, that there are some internally-used properties (starting with
+an underscore). The node_property() returns a dictionary of properties
+without these private memebers:
+
>>> forping.node_property(ant).values():
+... print"%s = %s%s"%(p.type.name,p.value,p.type.unitsifp.type.unitselse"")
+pol1_orient = S
+pol2_orient = E
+
+
+
To search the graph for the closest component of a given type to a single
+component, using closest_of_type():
Use of closest_of_type() can be subtle for components separated by long
+paths. See its documentation for more examples.
+
Subgraphs can be created using a subgraph specification, encoded in a
+subgraph_spec object. See the documentation for that class for
+details, but briefly, this allows one to create a smaller, more manageable
+graph containing only components and connexions you are interested in. Given a
+subgraph, the ltf() method can be useful.
Searches for the closest connected component of a given type.
+
Sometimes the closest component is through a long, convoluted path that you
+do not wish to explore. You can cut out these cases by including a list of
+component types that will block the search along a path.
+
The component may be passed by object or by serial number; similarly for
+component types.
+
+
Parameters:
+
+
comp (component or string or list of such) – The component to search from.
+
type (component_type or string) – The component type to find.
+
type_exclude (list of component_type or strings) – Any components of this type will prematurely cut off a line of
+investigation.
+
ignore_draws (boolean) – It is possible that there be more than one component of a given type the
+same distance from the starting component. If this parameter is set to
+True, then just return the first one that is found. If set to
+False, then raise an exception.
+
+
+
Returns:
+
comp – The closest component of the given type to start. If no component of
+type is found None is returned.
In general, though, you need to take care when
+using this method and make judicious use of the type_exclude parameter.
+For example, consider the following example:
Return a component or list of components from the graph.
+
The components exist as graph nodes. This method provides searchable access
+to them.
+
+
Parameters:
+
+
comp (string or component) – If not None, then return the component with this serial number, or
+None if it does not exist in the graph. If this parameter is set,
+then type is ignored. You can also pass a component object; the
+instance of that component with the same serial number will be returned if
+it exists in this graph.
+
type (string or component_type) – If not None, then only return components of this type. You may pass
+either the name of the component type or an object.
+
+
+
Returns:
+
If the sn parameter is passed, a single component object is
+returned. If the type parameter is passed, a list of
+component objects is returned.
This method is designed to be efficient. It has customised SQL calls so that
+only a couple of queries are required. Doing this with the standard peewee
+functionality requires many more calls.
+
This method will establish a connection to the database if it doesn’t
+already exist.
+
+
Parameters:
+
+
time (datetime.datetime) – The time at which the graph is valid. Default is now().
+
sg_spec (subgraph_spec) – The subgraph specificationto use; can be set to None.
+
sg_start_sn (string) – If a serial number is specified, then only the subgraph starting with that
+component will be returned. This parameter is ignored if sg_spec is
+None.
+
+
+
Returns:
+
If sg_spec is not None, and sg_start_sn is not specified, then
+a list of graph objects is returned instead.
Return the properties of a node excluding internally used properties.
+
If you iterate over a nodes properties, you will also get the
+internally-used properties (starting with an underscore). This method gets
+the dictionary of properties without these “private” properties.
+
+
Parameters:
+
node (node object) – The node for which to get the properties.
Searches for the shortest path to a component of a given type.
+
Sometimes the closest component is through a long, convoluted path that you
+do not wish to explore. You can cut out these cases by including a list of
+component types that will block the search along a path.
+
The component may be passed by object or by serial number; similarly for
+component types.
+
+
Parameters:
+
+
comp (component or string or list of one of these) – The component(s) to search from.
+
type (component_type or string) – The component type to find.
+
type_exclude (list of component_type or strings) – Any components of this type will prematurely cut off a line of
+investigation.
+
ignore_draws (boolean) – It is possible that there be more than one component of a given type the
+same distance from the starting component. If this parameter is set to
+True, then just return the first one that is found. If set to
+False, then raise an exception.
+
+
+
Returns:
+
comp – The closest component of the given type to start. If no path to a
+component of the specified type exists, return None.
Specifications for extracting a subgraph from a full graph.
+
The subgraph specification can be created from scratch by passing the
+appropriate parameters. They can also be pulled from the database using the
+class method FROM_PREDef().
+
The parameters can be passed as ID’s, names of compoenet types or
+component_type instances.
+
+
Parameters:
+
+
start (integer, component_type or string) – The component type for the start of the subgraph.
+
terminate (list of integers, of component_type or of strings) – Component type id’s for terminating the subgraph.
+
oneway (list of list of integer pairs, of component_type or of strings) – Pairs of component types for defining connexions that should only be
+traced one way when moving from the starting to terminating components.
+
hide (list of integers, of component_type or of strings) – Component types for components that should be hidden and skipped over in
+the subgraph.
+
+
+
+
Examples
+
To look at subgraphs of components between the outer bulkhead and the
+correlator inputs, one could create the following specification:
What did we do? We specified that the subgraph starts at the C-Can bulkhead.
+It terminates at the correlator input; in the other direction, it must also
+terminate at a 60 m coaxial cable plugged into the bulkhead. We hide the 60 m
+coaxial cable so that it doesn’t show up in the subgraph. We also hide the SMA
+cables so that they will be skipped over.
+
We can load all such subgraphs from the database now and see how many nodes
+they contain:
Most of them are as short as we would expect, but there are some
+complications. Let’s look at that first one by printing out its LTF:
+
>>> printsg[0].ltf
+# C-can thru to RFT thru.
+CANAD0B
+RFTA15B attenuation=10 therm_avail=ch7
+
+# RFT thru to HK preamp.
+RFTA15B attenuation=10 therm_avail=ch7
+CHB036C7
+HPA0002A
+
+# HK preamp to HK readout.
+HPA0002A
+ATMEGA49704949575721220150
+HKR00
+
+# HK readout to HK ATMega.
+HKR00
+ATMEGA50874956504915100100
+etc...
+etc...
+# RFT thru to FLA.
+RFTA15B attenuation=10 therm_avail=ch7
+FLA0159B
+
+
+
Some FLA’s are connected to HK hydra cables and we need to terminate on these
+as well. It turns out that some outer bulkheads are connected to 200 m
+coaxial cables, and some FLA’s are connected to 50 m delay cables, adding to
+the list of terminations. Let’s exclude these as well:
The remaining subgraphs with more than three components actually turn out to
+be errors in the layout! Let’s investigate the last one by removing any hidden
+components and printing its LTF.
It appears that CXS0016 mistakenly connects RFTQ00B to
+FLA0073B. This is an error that should be investigated and fixed. But
+by way of illustration, let’s cut this subgraph short by specifying a one-way
+connection, and not allowing the subgrapher to trace backwards from the inner
+bulkhead to an SMA cable:
Converts gain array to CHIME visibility format for all frequencies and
+time frames.
+
For every frequency and time frame, converts a gain vector into an outer
+product matrix and then vectorizes its upper triangle to obtain a vector in
+the same format as the CHIME visibility matrix.
+
Converting the gain arrays to CHIME visibility format makes easier to
+apply the gain corrections to the visibility data. See example below.
+
+
Parameters:
+
gains (3d array) – Input array with the gains for all frequencies, channels and time frames
+in the fromat of ni_gains_evalues_tf. g has dimensions
+[frequency, channels, time].
+
+
Returns:
+
G_ut – Output array with dimmensions [frequency, corr. number, time]. For
+every frequency and time frame, contains the vectorized form of upper
+triangle for the outer product of the respective gain vector.
+
+
Return type:
+
3d array
+
+
+
Example
+
To compute the gains from a set of noise injection pass0 data and apply the
+gains to the visibilities run:
Generates correlation product indices for selected channels.
+
For a correlation matrix with total_N_channels total number of channels,
+generates indices for correlation products corresponding to channels in
+the list channels_to_select.
+
+
Parameters:
+
+
channels_to_select (list of integers) – Indices of channels to select
+
total_N_channels (int) – Total number of channels
+
+
+
Returns:
+
prod_sel – indices of correlation products for channels in channels_to_select
Implementation of the Alternating Least Squares algorithm for noise
+injection.
+
Implements the Alternating Least Squares algorithm to recover the system
+gains, sky covariance matrix and system output noise covariance matrix
+from the data covariance matrix R. All the variables and definitions are as
+in http://bao.phas.ubc.ca/doc/library/doc_0103/rev_01/chime_calibration.pdf
+
+
Parameters:
+
+
R (2d array) – Data covariance matrix
+
g0 (1d array) – First estimate of system gains
+
Gamma (2d array) – Matrix that characterizes parametrization of sky covariance matrix
+
Upsilon (2d array) – Matrix characterizing parametrization of system noise covariance matrix
+
maxsteps (int) – Maximum number of iterations
+
abs_tol (float) – Absolute tolerance on error function
+
rel_tol (float) – Relative tolerance on error function
+
weighted_als (bool) – If True, perform weighted ALS
+
+
+
Returns:
+
+
g (1d array) – System gains
+
C (2d array) – Sky covariance matrix
+
N (2d array) – System output noise covariance matrix
Provides analysis utilities for CHIME noise injection data.
+
This is just a wrapper for all the utilities created in this module.
+
+
Parameters:
+
+
Reader_read_obj (andata.Reader.read() like object) – Contains noise injection data. Must have ‘vis’ and ‘timestamp’ property.
+Assumed to contain all the Nadc_channels*(Nadc_channels+1)/2 correlation
+products, in chime’s canonical vector, for an
+Nadc_channels x Nadc_channels correlation matrix
+
Nadc_channels (int) – Number of channels read in Reader_read_obj
+
adc_ch_ref (int in the range 0 <= adc_ch_ref <= Nadc_channels-1) – Reference channel (used to find on/off points).
+
fbin_ref (int in the range) – 0 <= fbin_ref <= np.size(Reader_read_obj.vis, 0)-1
+Reference frequency bin (used to find on/off points).
Basic algorithm to compute gains and evalues from noise injection data.
+
C is a correlation matrix from which the gains are calculated.
+If normalize_vis = True, the visibility matrix is weighted by the diagonal
+matrix that turns it into a crosscorrelation coefficient matrix before the
+gain calculation. The eigenvalues are not sorted. The returned gain solution
+vector is normalized (LA.norm(g) = 1.)
+
+
Parameters:
+
+
C (2d array) – Data covariance matrix from which the gains are calculated. It is
+assumed that both the sky and system noise contributions have already
+been subtracted using noise injection
+
normalize_vis (bool) – If True, the visibility matrix is weighted by the diagonal matrix that
+turns it into a crosscorrelation coefficient matrix before the
+gain calculation.
Computes gains and evalues from noise injection visibility data.
+
Gains and eigenvalues are calculated for all frames and
+frequencies in vis_gated. The returned gain solution
+vector is normalized (LA.norm(gains[f, :, t]) = 1.)
+
+
Parameters:
+
+
vis_gated (3d array) – Visibility array in chime’s canonical format. vis_gated has dimensions
+[frequency, corr. number, time]. It is assumed that both the sky and
+system noise contributions have already been subtracted using noise
+injection.
+
Nchannels (int) – Order of the visibility matrix (number of channels)
+
normalize_vis (bool) – If True, then the visibility matrix is weighted by the diagonal matrix that
+turns it into a crosscorrelation coefficient matrix before the
+gain calculation.
+
vis_on (3d array) – If input and normalize_vis is True, then vis_gated is weighted
+by the diagonal elements of the matrix vis_on.
+vis_on must be the same shape as vis_gated.
+
vis_off (3d array) – If input and normalize_vis is True, then vis_gated is weighted
+by the diagonal elements of the matrix: vis_on = vis_gated + vis_off.
+vis_off must be the same shape as vis_gated. Keyword vis_on
+supersedes keyword vis_off.
+
niter (0) – Number of iterations to perform. At each iteration, the diagonal
+elements of vis_gated are replaced with their rank 1 approximation.
+If niter == 0 (default), then no iterations are peformed and the
+autocorrelations are used instead.
Turn a synced noise source observation into gated form.
+
This will decimate the visibility to only the noise source off bins, and
+will add 1 or more gated on-off dataset according to the specification in
+doclib:5.
+
+
Parameters:
+
+
data (andata.CorrData) – Correlator data with noise source switched synchronously with the
+integration.
+
ni_params (dict) – Dictionary with the noise injection parameters. Optional
+for data after ctime=1435349183. ni_params has the following keys
+- ni_period: Noise injection period in GPU integrations.
+It is assummed to be the same for all the enabled noise sources
+- ni_on_bins: A list of lists, one per enabled noise source,
+with the corresponding ON gates (within a period). For each
+noise source, the list contains the indices of the time frames
+for which the source is ON.
+Example: For 3 GPU integration period (3 gates: 0, 1, 2), two enabled
+noise sources, one ON during gate 0, the other ON during gate 1,
+and both OFF during gate 2, then
+`
+ni_params={'ni_period':3,'ni_on_bins':[[0],[1]]}
+`
+
only_off (boolean) – Only return the off dataset. Do not return gated datasets.
+
+
+
Returns:
+
+
newdata (andata.CorrData) – Correlator data folded on the noise source.
+
Comments
+
——–
+
- The function assumes that the fpga frame counter, which is used to
+
determine the noise injection gating parameters, is unwrapped.
+
- For noise injection data before ctime=1435349183 (i.e. for noise
+
injection data before 20150626T200540Z_pathfinder_corr) the noise
+
injection information is not in the headers so this function cannot be
+
used to determine the noise injection parameters. A different method is
+
required. Although it is recommended to check the data directly in this
+
case, the previous version of this function assumed that
Removes sky and system noise contributions from noise injection visibility
+data.
+
By looking at the autocorrelation of the reference channel adc_ch_ref
+for frequency bin fbin_ref, finds timestamps indices for which the signal is
+on and off. For every noise signal period, the subcycles with the noise
+signal on and off are averaged separatedly and then subtracted.
+
It is assumed that there are at least 5 noise signal cycles in the data.
+The first and last noise on subcycles are discarded since those cycles may
+be truncated.
+
+
Parameters:
+
+
vis (3d array) – Noise injection visibility array in chime’s canonical format. vis has
+dimensions [frequency, corr. number, time].
+
Nchannels (int) – Order of the visibility matrix (number of channels)
+
timestamp (1d array) – Timestamps for the visibility array vis
+
adc_ch_ref (int in the range 0 <= adc_ch_ref <= N_channels-1) – Reference channel (typically, but not necessaritly the channel
+corresponding to the directly injected noise signal) used to find
+timestamps indices for which the signal is on and off.
+on and off.
+
fbin_ref (int in the range 0 <= fbin_ref <= np.size(vis, 0)-1) – frequency bin used to find timestamps indices for which the signal is
+on and off
+
+
+
Returns:
+
+
A dictionary with keys
+
time_index_on (1d array) – timestamp indices for noise signal on.
+
time_index_off (1d array) – timestamp indices for noise signal off.
+
timestamp_on_dec (1d array) – timestamps for noise signal on after averaging.
+
timestamp_off_dec (1d array) – timestamps for noise signal off after averaging.
+
timestamp_dec (1d array) – timestamps for visibility data after averaging and subtracting on and
+off subcycles. These timestaps represent the time for every noise cycle
+and thus, these are the timestaps for the gain solutions.
+
vis_on_dec (3d array) – visibilities for noise signal on after averaging.
+
vis_off_dec (3d array) – visibilities for noise signal off after averaging.
+
vis_dec_sub (3d array) – visibilities data after averaging and subtracting on and
+off subcycles.
+
cor_prod_ref (int) – correlation index corresponding to the autocorrelation of the reference
+channel
To make a plot normalized by a baseline of the median-filtered
+power spectrum averaged over 200 time bins starting at bin 0 with
+a median filter window of 40 bins:
+>>> data = andata.AnData.from_acq(”…”)
+>>> med_filt_arg = [‘new’,200,0,40]
+>>> waterfall(data, prod_sel=21, med_filt=med_filt_arg)
+
You can also make it save the calculated baseline to a file,
+by providing the filename:
+>>> data = andata.AnData.from_acq(”…”)
+>>> med_filt_arg = [‘new’,200,0,40,’base_filename.dat’]
+>>> waterfall(data, prod_sel=21, med_filt=med_filt_arg)
+
…or to use a previously obtained baseline to normalize data:
+(where bsln is either a numpy array or a list with length equal
+to the frequency axis of the data)
+>>> data = andata.AnData.from_acq(”…”)
+>>> med_filt_arg = [‘old’,bsln]
+>>> waterfall(data, prod_sel=21, med_filt=med_filt_arg)
+
To make a full day plot of 01/14/2014,
+rebinned to 4000 time bins:
+>>> data = andata.AnData.from_acq(”…”)
+>>> full_day_arg = [[2014,01,14],4000,’time’]
+>>> waterfall(data, prod_sel=21, full_day=full_day_arg)
This module contains tools for finding and removing Radio Frequency Interference
+(RFI).
+
Note that this generates masks where the elements containing RFI are marked as
+True, and the remaining elements are marked False. This is in
+contrast to the routines in ch_pipeline.rfi which generates a inverse
+noise weighting, where RFI containing elements are effectively False, and
+the remainder are True.
+
There are general purpose routines for flagging RFI in andata like datasets:
RFI flag the dataset. This function wraps number_deviations,
+and remains largely for backwards compatability. The pipeline code
+now calls number_deviations directly.
+
+
Parameters:
+
+
data (andata.CorrData) – Must contain vis and weight attribute that are both
+np.ndarray[nfreq, nprod, ntime]. Note that this
+function does not work with CorrData that has
+been stacked over redundant baselines.
+
freq_width (float) – Frequency interval in MHz to compare across.
+
time_width (float) – Time interval in seconds to compare.
+
threshold (float) – Threshold in MAD over which to cut out RFI.
+
rolling (bool) – Use a rolling window instead of distinct blocks.
+
flag1d (bool, optional) – Only apply the MAD cut in the time direction. This is useful if the
+frequency coverage is sparse.
+
+
+
Returns:
+
mask – RFI mask, output shape is the same as input visibilities.
Time dependent static RFI flags that affect the recent observations are added.
+
+
Parameters:
+
+
freq_centre – Centre of each frequency channel
+
freq_width – Width of each frequency channel. If None (default), calculate the width from
+the frequency centre separation. If supplied as an array it must be
+broadcastable
+against freq_centre.
+
timestamp – UNIX observing time. If None (default) mask all specified bands regardless of
+their start/end times, otherwise mask only timestamps within the band start and
+end times. If supplied as an array it must be broadcastable against
+freq_centre.
The stop band will range from [-tau_cut, tau_cut].
+DAYENU is used to construct the filter in the presence
+of masked frequencies. See Ewall-Wice et al. 2021
+(arXiv:2004.11397) for a description.
+
+
Parameters:
+
+
freq (np.ndarray[nfreq,]) – Frequency in MHz.
+
tau_cut (float) – The half width of the stop band in micro-seconds.
+
flag (np.ndarray[nfreq,]) – Boolean flag that indicates what frequencies are valid.
+
epsilon (float) – The stop-band rejection of the filter.
+
+
+
Returns:
+
pinv – High pass delay filter.
+
+
Return type:
+
np.ndarray[nfreq, nfreq]
+
+
+
+
+
+
+ch_util.rfi.iterative_hpf_masking(freq, y, flag=None, tau_cut=0.6, epsilon=1e-10, window=65, threshold=6.0, nperiter=1, niter=40, timestamp=None)[source]
+
Mask features in a spectrum that have significant power at high delays.
+
Uses the following iterative procedure to generate the mask:
+
+
+
Apply a high-pass filter to the spectrum.
+
For each frequency channel, calculate the median absolute
+deviation of nearby frequency channels to get an estimate
+of the noise. Divide the high-pass filtered spectrum by
+the noise estimate.
+
Mask excursions with the largest signal to noise.
+
Regenerate the high-pass filter using the new mask.
+
Repeat.
+
+
+
The procedure stops when the maximum number of iterations is reached
+or there are no excursions beyond some threshold.
+
+
Parameters:
+
+
freq (np.ndarray[nfreq,]) – Frequency in MHz.
+
y (np.ndarray[nfreq,]) – Spectrum to search for narrowband features.
+
flag (np.ndarray[nfreq,]) – Boolean flag where True indicates valid data.
+
tau_cut (float) – Cutoff of the high-pass filter in microseconds.
+
epsilon (float) – Stop-band rejection of the filter.
+
threshold (float) – Number of median absolute deviations beyond which
+a frequency channel is considered an outlier.
+
window (int) – Width of the window used to estimate the noise
+(by calculating a local median absolute deviation).
+
nperiter (int) – Maximum number of frequency channels to flag
+on any iteration.
+
niter (int) – Maximum number of iterations.
+
timestamp (float) – Start observing time (in unix time)
+
+
+
Returns:
+
+
yhpf (np.ndarray[nfreq,]) – The high-pass filtered spectrum generated using
+the mask from the last iteration.
+
flag (np.ndarray[nfreq,]) – Boolean flag where True indicates valid data.
+This is the logical complement to the mask
+from the last iteration.
+
rsigma (np.ndarray[nfreq,]) – The local median absolute deviation from the last
+iteration.
Mask out RFI using a median absolute deviation cut in the time direction.
+
This is useful for datasets with sparse frequency coverage. Functionally
+this routine is equivalent to mad_cut_2d() with fwidth = 1, but will
+be much faster.
+
+
Parameters:
+
+
data (np.ndarray[freq, time]) – Array of data to mask.
+
twidth (integer, optional) – Number of time samples to average median over.
+
threshold (scalar, optional) – Number of median deviations above which we cut the data.
+
mask (boolean, optional) – If True return the mask, if False return the number of
+median absolute deviations.
+
+
+
Returns:
+
mask – Mask or number of median absolute deviations for each sample.
Mask out RFI by placing a cut on the absolute deviation.
+Compared to mad_cut_2d, this function calculates
+the median and median absolute deviation using a rolling
+2D median filter, i.e., for every (freq, time) sample a
+separate estimates of these statistics is obtained for a
+window that is centered on that sample.
+
For sparsely sampled frequency axis, set fwidth = 1.
+
+
Parameters:
+
+
data (np.ndarray[freq, time]) – Array of data to mask.
+
fwidth (integer, optional) – Number of frequency samples to calculate median over.
+
twidth (integer, optional) – Number of time samples to calculate median over.
+
threshold (scalar, optional) – Number of median absolute deviations above which we cut the data.
+
freq_flat (boolean, optional) – Flatten in the frequency direction by dividing each frequency
+by the median over time.
+
mask (boolean, optional) – If True return the mask, if False return the number of
+median absolute deviations.
+
limit_range (slice, optional) – Data is limited to this range in the freqeuncy axis. Defaults to slice(None).
+
+
+
Returns:
+
mask – Mask or number of median absolute deviations for each sample.
Calculate the number of median absolute deviations (MAD)
+of the autocorrelations from the local median.
+
+
Parameters:
+
+
data (andata.CorrData) – Must contain vis and weight attributes that are both
+np.ndarray[nfreq, nprod, ntime].
+
freq_width (float) – Frequency interval in MHz to compare across.
+
time_width (float) – Time interval in seconds to compare across.
+
flag1d (bool) – Only apply the MAD cut in the time direction. This is useful if the
+frequency coverage is sparse.
+
apply_static_mask (bool) – Apply static mask obtained from frequency_mask before computing
+the median absolute deviation.
+
rolling (bool) – Use a rolling window instead of distinct blocks.
+
stack (bool) – Average over all autocorrelations.
+
normalize (bool) – Normalize by the median value over time prior to averaging over
+autocorrelations. Only relevant if stack is True.
+
fill_value (float) – Data that was already flagged as bad will be set to this value in
+the output array. Should be a large positive value that is greater
+than the threshold that will be placed. Default is float(‘Inf’).
+
+
+
Returns:
+
+
auto_ii (np.ndarray[ninput,]) – Index of the inputs that have been processed.
+If stack is True, then [0] will be returned.
+
auto_vis (np.ndarray[nfreq, ninput, ntime]) – The autocorrelations that were used to calculate
+the number of deviations.
+
ndev (np.ndarray[nfreq, ninput, ntime]) – Number of median absolute deviations of the autocorrelations
+from the local median.
Apply the SIR operator over the frequency and time axes for each product.
+
This is a wrapper for sir1d. It loops over times, applying sir1d
+across the frequency axis. It then loops over frequencies, applying sir1d
+across the time axis. It returns the logical OR of these two masks.
+
+
Parameters:
+
+
basemask (np.ndarray[nfreq, nprod, ntime] of boolean type) – The previously generated threshold mask.
+1 (True) for masked points, 0 (False) otherwise.
+
eta (float) – Aggressiveness of the method: with eta=0, no additional samples are
+flagged and the function returns basemask. With eta=1, all samples
+will be flagged.
+
only_freq (bool) – Only apply the SIR operator across the frequency axis.
+
only_time (bool) – Only apply the SIR operator across the time axis.
+
+
+
Returns:
+
mask – The mask after the application of the SIR operator.
Numpy implementation of the scale-invariant rank (SIR) operator.
+
For more information, see arXiv:1201.3364v2.
+
+
Parameters:
+
+
basemask (numpy 1D array of boolean type) – Array with the threshold mask previously generated.
+1 (True) for flagged points, 0 (False) otherwise.
+
eta (float) – Aggressiveness of the method: with eta=0, no additional samples are
+flagged and the function returns basemask. With eta=1, all samples
+will be flagged. The authors in arXiv:1201.3364v2 seem to be convinced
+that 0.2 is a mostly universally optimal value, but no optimization
+has been done on CHIME data.
+
+
+
Returns:
+
mask – The mask after the application of the (SIR) operator. Same shape and
+type as basemask.
This module contains tools for using noise sources to correct
+timing jitter and timing delay.
+
Example
+
The function construct_delay_template() generates a delay template from
+measurements of the visibility between noise source inputs, which can
+be used to remove the timing jitter in other data.
+
The user seldom needs to work with construct_delay_template()
+directly and can instead use several high-level functions and containers
+that load the timing data, derive the timing correction using
+construct_delay_template(), and then enable easy application of
+the timing correction to other data.
+
For example, to load the timing data and derive the timing correction from
+a list of timing acquisition files (i.e., YYYYMMSSTHHMMSSZ_chimetiming_corr),
+use the following:
This results in a andata.CorrData object that has additional
+methods avaiable for applying the timing correction to other data.
+For example, to obtain the complex gain for some freq, input, and time
+that upon multiplication will remove the timing jitter, use the following:
+
+
`tgain,tweight=tdata.get_gain(freq,input,time)`
+
+
To apply the timing correction to the visibilities in an andata.CorrData
+object called data, use the following:
+
+
`tdata.apply_timing_correction(data)`
+
+
The timing acquisitions must cover the span of time that you wish to correct.
+If you have a list of data acquisition files and would like to obtain
+the appropriate timing correction by searching the archive for the
+corresponding timing acquisitons files, then use:
Apply the timing correction to another visibility dataset.
+
This method uses the get_gain or get_stacked_tau method, depending
+on whether or not the visibilities have been stacked. It acccepts
+and passes along keyword arguments for those method.
+
+
Parameters:
+
+
timestream (andata.CorrData / equivalent or np.ndarray[nfreq, nprod, ntime]) – If timestream is an np.ndarray containing the visiblities, then you
+must also pass the corresponding freq, prod, input, and time axis as kwargs.
+Otherwise these quantities are obtained from the attributes of CorrData.
+If the visibilities have been stacked, then you must additionally pass the
+stack and reverse_stack axis as kwargs, and (optionally) the input flags.
+
copy (bool) – Create a copy of the input visibilities. Apply the timing correction to
+the copy and return it, leaving the original untouched. Default is False.
+
freq (np.ndarray[nfreq, ]) – Frequency in MHz.
+Must be passed as keyword argument if timestream is an np.ndarray.
+
prod (np.ndarray[nprod, ]) – Product map.
+Must be passed as keyword argument if timestream is an np.ndarray.
+
time (np.ndarray[ntime, ]) – Unix time.
+Must be passed as keyword argument if timestream is an np.ndarray.
+
input (np.ndarray[ninput, ] of dtype=('chan_id', 'correlator_input')) – Input axis.
+Must be passed as keyword argument if timestream is an np.ndarray.
+
stack (np.ndarray[nstack, ]) – Stack axis.
+Must be passed as keyword argument if timestream is an np.ndarray
+and the visibilities have been stacked.
+
reverse_stack (np.ndarray[nprod, ] of dtype=('stack', 'conjugate')) – The index of the stack axis that each product went into.
+Typically found in reverse_map[‘stack’] attribute.
+Must be passed as keyword argument if timestream is an np.ndarray
+and the visibilities have been stacked.
+
input_flags (np.ndarray [ninput, ntime]) – Array indicating which inputs were good at each time. Non-zero value
+indicates that an input was good. Optional. Only used for stacked visibilities.
+
+
+
Returns:
+
+
If copy == True –
+
+
visnp.ndarray[nfreq, nprod(nstack), ntime]
New set of visibilities with timing correction applied.
+
+
+
+
else –
+
+
None
Correction is applied to the input visibility data. Also,
+if timestream is an andata.CorrData instance and the gain dataset exists,
+then it will be updated with the complex gains that have been applied.
freq (np.ndarray[nfreq, ] of dtype=('centre', 'width')) – Frequencies in MHz that were used to construct the timing correction.
+
noise_source (np.ndarray[nsource,] of dtype=('chan_id', 'correlator_input')) – Correlator inputs that were used to construct the timing correction.
+
input (np.ndarray[ninput, ] of dtype=('chan_id', 'correlator_input')) – Correlator inputs to which the timing correction will be applied.
+
time (np.ndarray[ntime, ]) – Unix time.
+
param (np.ndarray[nparam, ]) – Parameters of the model fit to the static phase versus frequency.
+
tau (np.ndarray[nsource, ntime]) – The actual timing correction, which is the relative delay of each of the
+noise source inputs with respect to a reference input versus time.
+
weight_tau (np.ndarray[nsource, ntime]) – Estimate of the uncertainty (inverse variance) on the timing correction.
+
static_phi (np.ndarray[nfreq, nsource]) – The phase that was subtracted from each frequency and input prior to
+fitting for the timing correction. This is necessary to remove the
+approximately static ripple pattern caused by reflections.
+
weight_static_phi (np.ndarray[nfreq, nsource]) – Inverse variance on static_phi.
+
static_phi_fit (np.ndarray[nparam, nsource]) – Best-fit parameters of a fit to the static phase versus frequency
+for each of the noise source inputs.
+
alpha (np.ndarray[nsource, ntime]) – The coefficient of the spectral model of the amplitude variations of
+each of the noise source inputs versus time.
+
weight_alpha (np.ndarray[nsource, ntime]) – Estimate of the uncertainty (inverse variance) on the amplitude coefficients.
+
static_amp (np.ndarray[nfreq, nsource]) – The amplitude that was subtracted from each frequency and input prior to
+fitting for the amplitude variations. This is necessary to remove the
+approximately static ripple pattern caused by reflections.
+
weight_static_amp (np.ndarray[nfreq, nsource]) – Inverse variance on static_amp.
+
num_freq (np.ndarray[nsource, ntime]) – The number of frequencies used to determine the delay and alpha quantities.
+If num_freq is 0, then that time is ignored when deriving the timing correction.
+
coeff_tau (np.ndarray[ninput, nsource]) – If coeff is provided, then the timing correction applied to a particular
+input will be the linear combination of the tau correction from the
+noise source inputs, with the coefficients set by this array.
+
coeff_alpha (np.ndarray[ninput, nsource]) – If coeff is provided, then the timing correction applied to a particular
+input will be adjusted by the linear combination of the alpha correction
+from the noise source inputs, with the coefficients set by this array.
+
reference_noise_source (np.ndarray[ninput]) – The noise source input that was used as reference when fitting coeff_tau.
Return the amplitude variation for each noise source at the requested times.
+
Uses the TimingInterpolator to interpolate to the requested times.
+
+
Parameters:
+
+
timestamp (np.ndarray[ntime,]) – Unix timestamp.
+
interp (string) – Method to interpolate over time. Options include ‘linear’, ‘nearest’,
+‘zero’, ‘slinear’, ‘quadratic’, ‘cubic’, ‘previous’, and ‘next’.
+
extrap_limit (float) – Do not extrapolate the underlying data beyond its boundaries by this
+amount in seconds. Default is 2 integrations.
+
+
+
Returns:
+
+
alpha (np.ndarray[nsource, ntime]) – Amplitude coefficient as a function of time for each of the noise sources.
+
weight (np.ndarray[nsource, ntime]) – The uncertainty on the amplitude coefficient, expressed as an inverse variance.
Return the complex gain for the requested frequencies, inputs, and times.
+
Multiplying the visibilities by the outer product of these gains will remove
+the fluctuations in phase due to timing jitter. This method uses the
+get_tau method. It acccepts and passes along keyword arguments for that method.
+
+
Parameters:
+
+
freq (np.ndarray[nfreq, ]) – Frequency in MHz.
+
inputs (np.ndarray[ninput, ]) – Must contain ‘correlator_input’ field.
gain (np.ndarray[nfreq, ninput, ntime]) – Complex gain. Multiplying the visibilities by the
+outer product of this vector at a given time and
+frequency will correct for the timing jitter.
+
weight (np.ndarray[nfreq, ninput, ntime]) – Uncerainty on the gain expressed as an inverse variance.
Return the equivalent of get_stacked_tau for the noise source amplitude variations.
+
Averages the alphas from the noise source inputs that map to the set of redundant
+baseline included in each stacked visibility. If input_flags is provided, then the
+bad inputs that were excluded from the stack are also excluded from the alpha
+template averaging. This method can be used to generate a stacked alpha template
+that can be used to correct a stacked tau template for variations in the noise source
+distribution system. However, it is recommended that the tau template be corrected
+before stacking. This is accomplished by setting the amp_to_delay property
+prior to calling get_stacked_tau.
+
+
Parameters:
+
+
timestamp (np.ndarray[ntime,]) – Unix timestamp.
+
inputs (np.ndarray[ninput,]) – Must contain ‘correlator_input’ field.
+
prod (np.ndarray[nprod,]) – The products that were included in the stack.
+Typically found in the index_map[‘prod’] attribute of the
+andata.CorrData object.
+
reverse_stack (np.ndarray[nprod,] of dtype=('stack', 'conjugate')) – The index of the stack axis that each product went into.
+Typically found in reverse_map[‘stack’] attribute
+of the andata.CorrData.
+
input_flags (np.ndarray [ninput, ntime]) – Array indicating which inputs were good at each time.
+Non-zero value indicates that an input was good.
+
+
+
Returns:
+
alpha – Noise source amplitude variation as a function of time for each stacked visibility.
Return the appropriate delay for each stacked visibility at the requested time.
+
Averages the delays from the noise source inputs that map to the set of redundant
+baseline included in each stacked visibility. This yields the appropriate
+common-mode delay correction. If input_flags is provided, then the bad inputs
+that were excluded from the stack are also excluded from the delay template averaging.
+
+
Parameters:
+
+
timestamp (np.ndarray[ntime,]) – Unix timestamp.
+
inputs (np.ndarray[ninput,]) – Must contain ‘correlator_input’ field.
+
prod (np.ndarray[nprod,]) – The products that were included in the stack.
+Typically found in the index_map[‘prod’] attribute of the
+andata.CorrData object.
+
reverse_stack (np.ndarray[nprod,] of dtype=('stack', 'conjugate')) – The index of the stack axis that each product went into.
+Typically found in reverse_map[‘stack’] attribute
+of the andata.CorrData.
+
input_flags (np.ndarray [ninput, ntime]) – Array indicating which inputs were good at each time.
+Non-zero value indicates that an input was good.
+
+
+
Returns:
+
tau – Delay as a function of time for each stacked visibility.
Return the delay for each noise source at the requested times.
+
Uses the TimingInterpolator to interpolate to the requested times.
+
+
Parameters:
+
+
timestamp (np.ndarray[ntime,]) – Unix timestamp.
+
ignore_amp (bool) – Do not apply a noise source based amplitude correction, even if one exists.
+
interp (string) – Method to interpolate over time. Options include ‘linear’, ‘nearest’,
+‘zero’, ‘slinear’, ‘quadratic’, ‘cubic’, ‘previous’, and ‘next’.
+
extrap_limit (float) – Do not extrapolate the underlying data beyond its boundaries by this
+amount in seconds. Default is 2 integrations.
+
+
+
Returns:
+
+
tau (np.ndarray[nsource, ntime]) – Delay as a function of time for each of the noise sources.
+
weight (np.ndarray[nsource, ntime]) – The uncertainty on the delay, expressed as an inverse variance.
Return the phase correction from each noise source at the requested frequency and time.
+
Assumes the phase correction scales with frequency nu as phi = 2 pi nu tau and uses the
+get_tau method to interpolate over time. It acccepts and passes along keyword arguments
+for that method.
+
+
Parameters:
+
+
freq (np.ndarray[nfreq, ]) – Frequency in MHz.
+
timestamp (np.ndarray[ntime, ]) – Unix timestamp.
+
+
+
Returns:
+
+
gain (np.ndarray[nfreq, nsource, ntime]) – Complex gain containing a pure phase correction for each of the noise sources.
+
weight (np.ndarray[nfreq, nsource, ntime]) – Uncerainty on the gain for each of the noise sources, expressed as an inverse variance.
Provide convenience access to the noise source inputs.
+
Note that in older versions of the timing correction, the
+noise_source axis does not exist. Instead, the equivalent
+quantity is labeled as input. Since the addition of the
+coeff dataset it has become necessary to distinguish between the
+noise source inputs from which the timing correction is derived
+and the correlator inputs to which the timing correction is applied.
Setting the coefficients changes how the timing corretion for a particular
+correlator input is derived. Without coefficients, each input is matched
+to the timing correction from a single noise source input through the
+map_input_to_noise_source method. With coefficients, each input is a
+linear combination of the timing correction from all noise source inputs.
+
+
Parameters:
+
+
coeff_tau (np.ndarray[ninput, nsource]) – The timing correction applied to a particular input will be the
+linear combination of the tau correction from the noise source inputs,
+with the coefficients set by this array.
+
inputs (np.ndarray[ninput, ] of dtype=('chan_id', 'correlator_input')) – Correlator inputs to which the timing correction will be applied.
+
noise_source (np.ndarray[nsource,] of dtype=('chan_id', 'correlator_input')) – Correlator inputs that were used to construct the timing correction.
+
coeff_alpha (np.ndarray[ninput, nsource]) – The timing correction applied to a particular input will be adjusted by
+the linear combination of the alpha correction from the noise source inputs,
+with the coefficients set by this array.
+
reference_noise_source (np.ndarray[ninput,]) – For each input, the index into noise_source that was used as
+reference in the fit for coeff_tau.
Normalize the delay and alpha template to the value at a single time.
+
Useful for referencing the template to the value at the time that
+you plan to calibrate.
+
+
Parameters:
+
+
tref (unix time) – Reference the templates to the values at this time.
+
window (float) – Reference the templates to the median value over a window (in seconds)
+around tref. If nonzero, this will override the interpolate keyword.
+
interpolate (bool) – Interpolate the delay template to time tref. Otherwise take the measured time
+nearest to tref. The get_tau method is use to perform the interpolation, and
+kwargs for that method will be passed along.
Normalize the delay and alpha template to specific times.
+
Required if applying the timing correction to data that has
+already been calibrated.
+
+
Parameters:
+
+
tref (np.ndarray[nref]) – Reference the delays to the values at this unix time.
+
tstart (np.ndarray[nref]) – Begin transition to the reference delay at this unix time.
+
tend (np.ndarray[nref]) – Complete transition to the reference delay at this unix time.
+
tinit (float) – Use the delay at this time for the period before the first tstart.
+Takes prescendent over tau_init.
+
tau_init (np.ndarray[nsource]) – Use this delay for times before the first tstart. Must provide a value
+for each noise source input. If None, then will reference with respect
+to the average delay over the full time series.
+
alpha_init (np.ndarray[nsource]) – Use this alpha for times before the first tstart. Must provide a value
+for each noise source input. If None, then will reference with respect
+to the average alpha over the full time series.
+
interpolate (bool) – Interpolate the delay template to times tref. Otherwise take the measured
+times nearest to tref. The get_tau method is use to perform the
+interpolation, and kwargs for that method will be passed along.
summary – Contains useful information about the timing correction.
+Specifically contains for each noise source input the
+time averaged phase offset and delay. Also contains
+estimates of the variance in the timing for both the
+shortest and longest timescale probed by the underlying
+dataset. Meant to be joined with new lines and printed.
Automatically computes the timing correction when data is loaded and
+inherits the methods of TimingCorrection that enable the application
+of that correction to other datasets.
+
Used to pick which subclass to instantiate based on attributes in
+data.
Provide a summary of the timing data and correction.
+
+
Returns:
+
summary – Contains useful information about the timing correction
+and data. Includes the reduction in the standard deviation
+of the phase after applying the timing correction. This is
+presented as quantiles over frequency for each of the
+noise source products.
+
+
Return type:
+
list of strings
+
+
+
+
+
+
+
+
+classch_util.timing.TimingInterpolator(x, y, weight=None, flag=None, kind='linear', extrap_limit=None)[source]
+
Bases: object
+
Interpolation that is aware of flagged data and weights.
+
Flagged data is ignored during the interpolation. The weights from
+the data are propagated to obtain weights for the interpolated points.
+
Instantiate a callable TimingInterpolator object.
+
+
Parameters:
+
+
x (np.ndarray[nsample,]) – The points where the data was sampled.
+Must be monotonically increasing.
+
y (np.ndarray[..., nsample]) – The data to interpolate.
+
weight (np.ndarray[..., nsample]) – The uncertainty on the data, expressed as an
+inverse variance.
+
flag (np.ndarray[..., nsample]) – Boolean indicating if the data is to be
+included in the interpolation.
+
kind (str) – String that specifies the kind of interpolation.
+The value nearest, previous, next, and linear will use
+custom methods that propagate uncertainty to obtain the interpolated
+weights. The value zero, slinear, quadratic, and cubic
+will use spline interpolation from scipy.interpolation.interp1d
+and use the weight from the nearest point.
+
+
+
Returns:
+
interpolator – Callable that will interpolate the data that was provided
+to a new set of x values.
Correlation data. Must contain the following attributes:
+
freq: np.ndarray[nfreq, ]
Frequency in MHz.
+
+
vis: np.ndarray[nfreq, nprod, ntime]
Upper-triangle, product packed visibility matrix
+containing ONLY the noise source inputs.
+
+
weight: np.ndarray[nfreq, nprod, ntime]
Flag indicating the data points to fit.
+
+
flags/frac_lost: np.ndarray[nfreq, ntime]
Flag indicating the fraction of data lost.
+If provided, then data will be weighted by the
+fraction of data that remains when solving
+for the delay template.
+
+
+
+
+
+
min_frac_kept (float) – Do not include frequencies and times where the fraction
+of data that remains is less than this threshold.
+Default is 0.0.
+
threshold (float) – A (frequency, input) must pass the checks specified above
+more than this fraction of the time, otherwise it will be
+flaged as bad for all times. Default is 0.50.
+
min_freq (float) – Minimum frequency in MHz to include in the fit.
+Default is 420.
+
max_freq (float) – Maximum frequency in MHz to include in the fit.
+Default is 780.
+
mask_rfi (bool) – Mask frequencies that occur within known RFI bands. Note that the
+noise source data does not contain RFI, however the real-time pipeline
+does not distinguish between noise source inputs and sky inputs, and as
+a result will discard large amounts of data in these bands.
+
max_iter_weight (int) – The weight for each frequency is estimated from the variance of the
+residuals of the template fit from the previous iteration. Outliers
+are also flagged at each iteration with an increasingly aggresive threshold.
+This is the total number of times to iterate. Setting to 1 corresponds
+to linear least squares. Default is 1, unless check_amp or check_phi is True,
+in which case this defaults to the maximum number of thresholds provided.
+
check_amp (bool) – Do not fit frequencies and times where the residual amplitude is an outlier.
+Default is False.
+
nsigma_amp (list of float) – If check_amp is True, then residuals greater than this number of sigma
+will be considered an outlier. Provide a list containing the value to be used
+at each iteration. If the length of the list is less than max_iter_weight,
+then the last value in the list will be repeated for the remaining iterations.
+Default is [1000, 500, 200, 100, 50, 20, 10, 5].
+
check_phi (bool) – Do not fit frequencies and times where the residual phase is an outlier.
+Default is True.
+
nsigma_phi (list of float) – If check_phi is True, then residuals greater than this number of sigma
+will be considered an outlier. Provide a list containing the value to be used
+at each iteration. If the length of the list is less than max_iter_weight,
+then the last value in the list will be repeated for the remaining iterations.
+Default is [1000, 500, 200, 100, 50, 20, 10, 5].
+
nparam (int) – Number of parameters for polynomial fit to the
+time averaged phase versus frequency. Default is 2.
+
static_phi (np.ndarray[nfreq, nsource]) – Subtract this quantity from the noise source phase prior to fitting
+for the timing correction. If None, then this will be estimated from the median
+of the noise source phase over time.
+
weight_static_phi (np.ndarray[nfreq, nsource]) – Inverse variance of the time averaged phased. Set to zero for frequencies and inputs
+that are missing or should be ignored. If None, then this will be estimated from the
+residuals of the fit.
+
static_phi_fit (np.ndarray[nparam, nsource]) – Polynomial fit to static_phi versus frequency.
+
static_amp (np.ndarray[nfreq, nsource]) – Subtract this quantity from the noise source amplitude prior to fitting
+for the amplitude variations. If None, then this will be estimated from the median
+of the noise source amplitude over time.
+
weight_static_amp (np.ndarray[nfreq, nsource]) – Inverse variance of the time averaged amplitude. Set to zero for frequencies and inputs
+that are missing or should be ignored. If None, then this will be estimated from the
+residuals of the fit.
+
+
+
Returns:
+
+
phi (np.ndarray[nfreq, nsource, ntime]) – Phase of the signal from the noise source.
+
weight_phi (np.ndarray[nfreq, nsource, ntime]) – Inverse variance of the phase of the signal from the noise source.
+
tau (np.ndarray[nsource, ntime]) – Delay template for each noise source input.
+
weight_tau (np.ndarray[nfreq, nsource]) – Estimate of the uncertainty on the delay template (inverse variance).
+
static_phi (np.ndarray[nfreq, nsource]) – Time averaged phase versus frequency.
+
weight_static_phi (np.ndarray[nfreq, nsource]) – Inverse variance of the time averaged phase.
+
static_phi_fit (np.ndarray[nparam, nsource]) – Best-fit parameters of the polynomial fit to the
+time averaged phase versus frequency.
+
amp (np.ndarray[nfreq, nsource, ntime]) – Amplitude of the signal from the noise source.
+
weight_amp (np.ndarray[nfreq, nsource, ntime]) – Inverse variance of the amplitude of the signal from the noise source.
+
alpha (np.ndarray[nsource, ntime]) – Amplitude coefficient for each noise source input.
+
weight_alpha (np.ndarray[nfreq, nsource]) – Estimate of the uncertainty on the amplitude coefficient (inverse variance).
+
static_amp (np.ndarray[nfreq, nsource]) – Time averaged amplitude versus frequency.
+
weight_static_amp (np.ndarray[nfreq, nsource]) – Inverse variance of the time averaged amplitude.
+
num_freq (np.ndarray[nsource, ntime]) – Number of frequencies used to construct the delay and amplitude templates.
Eigenvalue decomposition of the visibility matrix.
+
+
Parameters:
+
+
vis (np.ndarray[nfreq, nprod, ntime]) – Upper-triangle, product packed visibility matrix.
+
flag (np.ndarray[nfreq, nsource, ntime] (optional)) – Array of 1 or 0 indicating the inputs that should be included
+in the eigenvalue decomposition for each frequency and time.
+
+
+
Returns:
+
resp – Eigenvector corresponding to the largest eigenvalue for
+each frequency and time.
pcov (np.ndarray[nparam, nparam]) – Covariance of the best-fit parameters.
+Assumes that it obtained a good fit
+and returns the errors
+necessary to achieve that.
Find and load the appropriate timing correction for a list of corr acquisition files.
+
For example, if the instrument keyword is set to ‘chime’,
+then this function will accept all types of chime corr acquisition files,
+such as ‘chimetiming’, ‘chimepb’, ‘chimeN2’, ‘chimecal’, and then find
+the relevant set of ‘chimetiming’ files to load.
+
Accepts and passes on all keyword arguments for the functions
+andata.CorrData.from_acq_h5 and construct_delay_template.
+
Should consider modifying this method to use Finder at some point in future.
+
+
Parameters:
+
+
files (string or list of strings) – Absolute path to corr acquisition file(s).
+
start (integer, optional) – What frame to start at in the full set of files.
+
stop (integer, optional) – What frame to stop at in the full set of files.
+
window (float) – Use the timing data -window from start and +window from stop.
+Default is 12 hours.
+
instrument (string) – Name of the instrument. Default is ‘chime’.
Query the layout database to find out what is ultimately connected at the end
+of correlator inputs. This is done by calling the routine
+get_correlator_inputs(), which returns a list of the inputs. Routines
+such as get_feed_positions() operate on this list.
Routines for undoing the phase rotation of a fixed celestial source. The
+routine fringestop() is an easy to use routine for fringestopping data
+given a list of the feeds in the data. For more advanced usage
+fringestop_phase() can be used.
graph (obj:layout.graph or datetime.datetime) – The graph in which to do the search. If you pass a time, then the graph
+will be constructed internally. (Note that the latter option will be
+quite slow if you do repeated calls!)
+
ant (layout.component) – The antenna.
+
pol (integer) – There can be up to two LNA’s connected to the two polarisation outputs
+of an antenna. Select which by passing 1 or 2. (Note that
+conversion to old-style naming ‘A’ and ‘B’ is done automatically.)
+
+
+
Returns:
+
lna – The LNA.
+
+
Return type:
+
layout.component or string
+
+
Raises:
+
layout.NotFound – Raised if the polarisation connector could not be found in the graph.
+
+
+
+
+
+
+ch_util.tools.apply_gain(vis, gain, axis=1, out=None, prod_map=None)[source]
+
Apply per input gains to a set of visibilities packed in upper
+triangular format.
+
This allows us to apply the gains while minimising the intermediate
+products created.
+
+
Parameters:
+
+
vis (np.ndarray[..., nprod, ...]) – Array of visibility products.
+
gain (np.ndarray[..., ninput, ...]) – Array of gains. One gain per input.
+
axis (integer, optional) – The axis along which the inputs (or visibilities) are
+contained. Currently only supports axis=1.
+
out (np.ndarray) – Array to place output in. If None create a new
+array. This routine can safely use out = vis.
+
prod_map (ndarray of integer pairs) – Gives the mapping from product axis to input pairs. If not supplied,
+icmap() is used.
+
+
+
Returns:
+
out – Visibility array with gains applied. Same shape as vis.
Equivalent to ch_util.tools.pack_product_array(arr, axis=0),
+but 10^5 times faster for full CHIME!
+
Currently assumes that arr is a 2D array of shape (nfeeds, nfeeds),
+and returns a 1D array of length (nfeed*(nfeed+1))/2. This case
+is all we need for phase calibration, but pack_product_array() is
+more general.
lay_time (layout.graph or datetime) – layout.graph object, layout tag id, or datetime.
+
correlator (str, optional) – Fetch only for specified correlator. Use the serial number in database,
+or pathfinder or chime, which will substitute the correct serial.
+If None return for all correlators.
+Option tone added for GBO 12 dish outrigger prototype array.
+
connect (bool, optional) – Connect to database and set the user to Jrs65 prior to query.
+Default is True.
+
+
+
Returns:
+
channels – List of CorrInput instances. Returns None for MPI ranks
+other than zero.
feeds (list of CorrInput) – List of feeds to compute positions of.
+
get_zpos (bool) – Return a third column with elevation information.
+
+
+
Returns:
+
positions – Array of feed positions. The first column is the E-W position
+(increasing to the E), and the second is the N-S position (increasing
+to the N). Non CHIME feeds get set to NaN.
Find what component a housekeeping channel is connected to.
+
This method is for finding either LNA or FLA’s that your housekeeping
+channel is connected to. (It currently cannot find accelerometers, other
+novel housekeeping instruments that may later exist; nor will it work if the
+FLA/LNA is connected via a very non-standard chain of components.)
+
+
Parameters:
+
+
graph (obj:layout.graph or datetime.datetime) – The graph in which to do the search. If you pass a time, then the graph
+will be constructed internally. (Note that the latter option will be
+quite slow if you do repeated calls!)
graph (obj:layout.graph or datetime.datetime) – The graph in which to do the search. If you pass a time, then the graph
+will be constructed internally. (Note that the latter option will be
+quite slow if you do repeated calls!)
Ensure that only baselines between array antennas are used to represent the stack.
+
The correlator will have inputs that are not connected to array antennas. These inputs
+are flagged as bad and are not included in the stack, however, products that contain
+their chan_id can still be used to represent a characteristic baseline in the stack
+index map. This method creates a new stack index map that, if possible, only contains
+products between two array antennas. This new stack index map should be used when
+calculating baseline distances to fringestop stacked data.
+
+
Parameters:
+
+
input_map (list of CorrInput) – List describing the inputs as they are in the file, output from
+tools.get_correlator_inputs
+
prod (np.ndarray[nprod,] of dtype=('input_a', 'input_b')) – The correlation products as pairs of inputs.
+
stack (np.ndarray[nstack,] of dtype=('prod', 'conjugate')) – The index into the prod axis of a characteristic baseline included in the stack.
+
reverse_stack (np.ndarray[nprod,] of dtype=('stack', 'conjugate')) – The index into the stack axis that each prod belongs.
+
+
+
Returns:
+
+
stack_new (np.ndarray[nstack,] of dtype=(‘prod’, ‘conjugate’)) – The updated stack index map, where each element is an index to a product
+consisting of a pair of array antennas.
+
stack_flag (np.ndarray[nstack,] of dtype=bool) – Boolean flag that is True if this element of the stack index map is now valid,
+and False if none of the baselines that were stacked contained array antennas.
Find what housekeeping channel a component is connected to.
+
+
Parameters:
+
+
graph (obj:layout.graph or datetime.datetime) – The graph in which to do the search. If you pass a time, then the graph
+will be constructed internally. (Note that the latter option will be
+quite slow if you do repeated calls!)
+
comp (layout.component or string) – The component to search for (you can pass by serial number if you wish).
+Currently, only components of type LNA, FLA and RFT thru are accepted.
+
+
+
Returns:
+
inp – The housekeeping input channel the sensor is connected to.
This turns an axis of the packed upper triangle set of products into the
+full correlation matrices. It replaces the specified product axis with two
+axes, one for each feed. By setting feeds this routine can also
+pull out a subset of feeds.
+"""
+Private module for defining the DB tables with the peewee ORM.
+
+These are imported into the layout and finder modules.
+"""
+
+importdatetime
+importre
+
+importchimedb.core
+importchimedb.data_index
+fromchimedb.coreimportAlreadyExistsErrorasAlreadyExists
+
+importpeeweeaspw
+importnumpyasnp
+
+# Logging
+# =======
+
+# Set default logging handler to avoid "No handlers could be found for logger
+# 'layout'" warnings.
+importlogging
+
+# All peewee-generated logs are logged to this namespace.
+logger=logging.getLogger("_db_tables")
+logger.addHandler(logging.NullHandler())
+
+
+# Global variables and constants.
+# ================================
+
+_property=property# Do this because we want a class named "property".
+_user=None
+
+#: Return events at the specified time.
+EVENT_AT=0
+#: Return events before the specified time.
+EVENT_BEFORE=1
+#: Return events after the specified time.
+EVENT_AFTER=2
+#: Return all events (and ignore any specified time).
+EVENT_ALL=3
+#: Order search results in ascending order.
+ORDER_ASC=0
+#: Order search results in descending order.
+ORDER_DESC=1
+
+# Exceptions
+# ==========
+
+
+
+[docs]
+classNoSubgraph(chimedb.core.CHIMEdbError):
+"""Raise when a subgraph specification is missing."""
+
+
+
+
+[docs]
+classBadSubgraph(chimedb.core.CHIMEdbError):
+"""Raise when an error in subgraph specification is made."""
+
+
+
+
+[docs]
+classDoesNotExist(chimedb.core.CHIMEdbError):
+"""The event does not exist at the specified time."""
+
+
+
+
+[docs]
+classUnknownUser(chimedb.core.CHIMEdbError):
+"""The user requested is unknown."""
+
+
+
+
+[docs]
+classNoPermission(chimedb.core.CHIMEdbError):
+"""User does not have permission for a task."""
+
+
+
+
+[docs]
+classLayoutIntegrity(chimedb.core.CHIMEdbError):
+"""Action would harm the layout integrity."""
+[docs]
+defconnect_peewee_tables(read_write=False,reconnect=False):
+"""Initialize the connection to the CHIME data index database.
+
+ This function uses the current database connector from
+ :mod:`~chimedb.core` to establish a connection to the CHIME data
+ index. It must be called if you change the connection method after
+ importing this module. Or if you wish to connect with both read and write
+ privileges.
+
+ Parameters
+ ----------
+ read_write : bool
+ Whether to connect with read and write privileges.
+ reconnect : bool
+ Force a reconnection.
+ """
+
+ chimedb.core.connect(read_write,reconnect)
+
+ # Set the default, no-permissions user.
+ set_user("Chime")
+
+
+
+
+[docs]
+defset_user(u):
+"""Identify yourself as a user, for record keeping.
+
+ All events recorded in the database are associated with a user, and not all
+ users have all permissions. You must call this function before making any
+ changes to the database.
+
+ Parameters
+ ----------
+ u : string or integer
+ One of:
+ - your CHIMEwiki username (string). Use an initial capital letter.
+ This is the recommended input.
+ - the name entered into the "real name" field in your CHIMEwiki profile
+ - your CHIMEwiki integer user_id (not easy to find)
+
+ Raises
+ ------
+ UnknownUser
+ """
+ global_user
+
+ _user=dict()
+
+ # Find the user.
+ ifisinstance(u,int):
+ q=chimedb.core.proxy.execute_sql(
+ "SELECT user_id FROM chimewiki.user WHERE user_id = %d;"%u
+ )
+ else:
+ q=chimedb.core.proxy.execute_sql(
+ "SELECT user_id FROM chimewiki.user "
+ "WHERE user_name = '%s' OR "
+ "user_real_name = '%s';"%(u,u)
+ )
+ r=q.fetchone()
+ ifnotr:
+ raiseUnknownUser("Could not find user.")
+ _user["id"]=r[0]
+ _user["perm"]=[]
+
+ # Get permissions.
+ forrin(
+ user_permission_type.select()
+ .join(user_permission)
+ .where(user_permission.user_id==_user["id"])
+ ):
+ _user["perm"].append(r.name)
+
+
+
+def_check_user(perm):
+ ifnot_user:
+ raiseUnknownUser(
+ "You must call layout.set_user() before attempting to alter the DB."
+ )
+ ifpermnotin_user["perm"]:
+ try:
+ p=(
+ user_permission_type.select()
+ .where(user_permission_type.name==perm)
+ .get()
+ )
+ raiseNoPermission("You do not have the permissions to %s."%p.long_name)
+ exceptpw.DoesNotExist:
+ raiseRuntimeError(
+ "Internal error: _check_user called with unknown permission: {}".format(
+ perm
+ )
+ )
+
+
+def_peewee_get_current_user():
+ # Get the current user for peewee, working around the issues with creating
+ # instances before the local user has been set. The particular issue here is
+ # that peewee creates an instance with the default values before setting the
+ # ones fetched from the database, so although it seems like you shouldn't
+ # need the user to have been set, you do.
+
+ if_userisNone:
+ returnNone
+
+ return_user["id"]
+
+
+# Tables in the DB pertaining to layouts
+# ======================================
+
+
+
+[docs]
+classgraph_obj(base_model):
+"""Parent table for any table that has events associated with it.
+ This is a way to make the event table polymorphic. It points to this table,
+ which shares (unique) primary keys with child tables (e.g., component). It
+ only has one key: ID.
+
+ Attributes
+ ----------
+ id
+ """
+
+ id=pw.AutoField()
+
+
+
+
+[docs]
+classglobal_flag_category(base_model):
+"""Categories for global flags.
+ Examples of component types are antennas, 60m coaxial cables, and so on.
+
+ Attributes
+ ----------
+ name : string
+ The name of the category.
+ notes : string
+ An (optional) description of the category.
+ """
+
+ name=pw.CharField(max_length=255)
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+
+
+[docs]
+classglobal_flag(event_table):
+"""A simple flag index for global flags.
+
+ Attributes
+ ----------
+ id : foreign key, primary key
+ The ID shared with parent table graph_obj.
+ category : foreign key
+ The category of flag.
+ severity : enum('comment', 'warning', 'severe')
+ An indication of how the data finder should react to this flag.
+ inst : foreign key
+ The acquisition instrument, if any, affected by this flag.
+ name : string
+ A short description of the flag.
+ notes : string
+ Notes about the global flag.
+ """
+
+ id=pw.ForeignKeyField(
+ graph_obj,column_name="id",primary_key=True,backref="global_flag"
+ )
+ category=pw.ForeignKeyField(global_flag_category,backref="flag")
+ severity=EnumField(["comment","warning","severe"])
+ inst=pw.ForeignKeyField(chimedb.data_index.ArchiveInst,backref="flag",null=True)
+ name=pw.CharField(max_length=255)
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+[docs]
+ defstart(self,time=datetime.datetime.now(),notes=None):
+"""Start this global flag.
+
+ Examples
+ --------
+
+ The following starts and ends a new global flag.
+
+ >>> cat = layout.global_flag_category.get(name = "pass")
+ >>> flag = layout.global_flag(category = cat, severity = "comment", name = "run_pass12_a").start(time = datetime.datetime(2015, 4, 1, 12))
+ >>> flag.end(time = datetime.datetime(2015, 4, 5, 15, 30))
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which the flag is to start.
+ notes : string
+ Any notes for the timestamp.
+
+ Returns
+ -------
+ self : :obj:`global_flag`
+
+ Raises
+ ------
+ :exc:`AlreadyExists` if the flag has already been started.
+ """
+ _check_user("global_flag")
+
+ # Ensure that it has not been started before---i.e., that there is not an
+ # active event associated with it.
+ g=None
+ try:
+ g=global_flag.get(id=self.id)
+ exceptpw.DoesNotExist:
+ pass
+ ifg:
+ try:
+ g.event(time,event_type.global_flag()).get()
+ raiseAlreadyExists("This flag has already been started.")
+ exceptpw.DoesNotExist:
+ pass
+
+ start=timestamp.create(time=time,notes=notes)
+ ifg:
+ o=g.id
+ else:
+ o=graph_obj.create()
+ self.id=o
+ self.save(force_insert=True)
+ g=self
+ e=event.create(
+ graph_obj=o,type=event_type.global_flag(),start=start,end=None
+ )
+ logger.info("Created global flag as event %d."%e.id)
+ returng
+
+
+
+[docs]
+ defend(self,time=datetime.datetime.now(),notes=None):
+"""End this global flag.
+
+ See :meth:`global_flag.start` for an example.
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which the flag is to end.
+ notes : string
+ Any notes for the timestamp.
+
+ Returns
+ -------
+ self : :obj:`global_flag`
+
+ Raises
+ ------
+ :exc:`AlreadyExists` if the flag has already been ended; :exc:`DoesNotExist`
+ if it has not been started.
+ """
+ _check_user("global_flag")
+
+ # Ensure that it has been started but not ended.
+ try:
+ g=global_flag.get(id=self.id)
+ exceptpw.DoesNotExist:
+ raiseDoesNotExist("This flag was never started.")
+ try:
+ e=g.event(time,event_type.global_flag()).get()
+ exceptpw.DoesNotExist:
+ raiseDoesNotExist("This flag was never started.")
+ try:
+ e.end
+ raiseAlreadyExists("This event has already been ended.")
+ exceptpw.DoesNotExist:
+ pass
+
+ end=timestamp.create(time=time,notes=notes)
+ e.end=end
+ e.save()
+ logger.info("Ended global flag.")
+ returnself
+
+
+
+
+
+[docs]
+classcomponent_type(name_table):
+"""A CHIME component type.
+ Examples of component types are antennas, 60m coaxial cables, and so on.
+
+ Attributes
+ ----------
+ name : string
+ The name of the component type.
+ notes : string
+ An (optional) description of the component type.
+ """
+
+ name=pw.CharField(max_length=255)
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+
+
+[docs]
+classcomponent_type_rev(name_table):
+"""A CHIME component type revision.
+
+ Component types can, optionally, have revisions. For example, when an antenna
+ design changes, a new revision is introduced.
+
+ Attributes
+ ----------
+ type : foreign key
+ The component type this revision applies to.
+ name : string
+ The name of the component type.
+ notes : string
+ An (optional) description of the component type.
+ """
+
+ type=pw.ForeignKeyField(component_type,backref="rev")
+ name=pw.CharField(max_length=255)
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+
+
+[docs]
+classexternal_repo(name_table):
+"""Information on an external repository.
+
+ Attributes
+ ----------
+ name : string
+ The name of the repository.
+ root : string
+ Its location, e.g., a URL onto which individual paths to files can be
+ appended.
+ notes : string
+ Any notes about this repository.
+ """
+
+ name=pw.CharField(max_length=255)
+ root=pw.CharField(max_length=255)
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+
+
+[docs]
+classcomponent(event_table):
+"""A CHIME component.
+
+ To add or remove components, use the :meth:`add` and :meth:`remove` methods.
+ There are also methods for getting and setting component properties, history
+ and documents.
+
+ Attributes
+ ----------
+ id : foreign key, primary key
+ The ID shared with parent table graph_obj.
+ sn : string, unique
+ The unique serial number of the component.
+ type : foreign key
+ The component type.
+ type_rev : foreign key
+ The revision of this component.
+ """
+
+ id=pw.ForeignKeyField(
+ graph_obj,column_name="id",primary_key=True,backref="component"
+ )
+ sn=pw.CharField(max_length=255,unique=True,index=True)
+ type=pw.ForeignKeyField(component_type,backref="component")
+ type_rev=pw.ForeignKeyField(component_type_rev,backref="component",null=True)
+
+ classMeta(object):
+ indexes=(("sn"),True)
+
+ def__hash__(self):
+ # Reimplement the hash function. Peewee's default implementation, while
+ # pretty sensible significantly slows down networkx when constructing a
+ # graph as it is called a huge number of times
+ returnid(self)
+
+
+[docs]
+ defget_connexion(
+ self,
+ comp=None,
+ time=datetime.datetime.now(),
+ when=EVENT_AT,
+ order=ORDER_ASC,
+ active=True,
+ ):
+"""Get connexions involving this component.
+
+ Parameters
+ ----------
+ comp : :obj:`component`
+ If this parameter is set, then search for connexions between this
+ component and *comp*.
+ time : datetime.datetime
+ Event time.
+ when : int
+ Event when.
+ order : int
+ Event order.
+ active : bool
+ Event active.
+
+ Returns
+ -------
+ A :obj:`peewee.SelectQuery` for :class:`connexion` entries.
+ """
+ c=_graph_obj_iter(connexion,event,time,when,order,active).where(
+ (connexion.comp1==self)|(connexion.comp2==self)
+ )
+ ifcomp:
+ returnc.where((connexion.comp1==comp)|(connexion.comp2==comp)).get()
+ else:
+ returnc
+
+
+
+[docs]
+ defget_history(
+ self,time=datetime.datetime.now(),when=EVENT_AT,order=ORDER_ASC,active=True
+ ):
+"""Get history items associated with this component.
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ Event time.
+ when : int
+ Event when.
+ order : int
+ Event order.
+ active : bool
+ Event active.
+
+ Returns
+ -------
+ A :obj:`peewee.SelectQuery` for :class:`history` entries.
+ """
+ return_graph_obj_iter(
+ component_history,event,time,EVENT_AT,None,True
+ ).where(component_history.comp==self)
+
+
+
+[docs]
+ defget_doc(self,time=datetime.datetime.now()):
+"""Get document pointers associated with this component.
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which the document events should be active.
+
+ Returns
+ -------
+ A :obj:`peewee.SelectQuery` for :class:`component_doc` entries.
+ """
+ return_graph_obj_iter(component_doc,event,time,EVENT_AT,None,True).where(
+ component_doc.comp==self
+ )
+
+
+
+[docs]
+ defadd(self,time=datetime.datetime.now(),notes=None,force=True):
+"""Add this component.
+
+ This triggers the "component available" event.
+
+ To add many components at once, see :func:`add_component`.
+
+ Examples
+ --------
+
+ The following makes a new LNA available:
+
+ >>> lna_type = layout.component_type.get(name = "LNA")
+ >>> lna_rev = lna_type.rev.where(layout.component_type_rev.name == "B").get()
+ >>> comp = layout.component(sn = "LNA0000A", type = lna_type, rev = lna_type.rev).add()
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which the component is to be made available.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`False`, then raise :exc:`AlreadyExists` if this event creates a
+ conflict; otherwise, do not add but ignore on conflict.
+
+ Returns
+ -------
+ self : :obj:`component`
+ """
+ add_component(self,time,notes,force)
+ returnself
+
+
+
+[docs]
+ defremove(self,time=datetime.datetime.now(),notes=None,force=False):
+"""Remove this component.
+
+ This ends the "component available" event.
+
+ To remove many components at once, see :func:`remove_component`.
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which the component is to be removed.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`False`, then raise :exc:`DoesNotExist` if this event creates a
+ conflict; otherwise, do not add but ignore on conflict.
+ """
+ remove_component(self,time,notes,force)
+
+
+
+[docs]
+ defadd_history(self,notes,time=datetime.datetime.now(),timestamp_notes=None):
+"""Add a history item for this component.
+
+ Parameters
+ ----------
+ notes : string
+ The history note.
+ time : datetime.datetime
+ The time at which the history is to be added.
+ timestamp_notes : string
+ Any notes for the timestamp.
+
+ Returns
+ -------
+ history : :obj:`component_history`
+ The newly-created component history object.
+ """
+ _check_user("comp_info")
+ o=graph_obj.create()
+ h=component_history.create(id=o,comp=self,notes=notes)
+ t_stamp=timestamp.create(time=time,notes=timestamp_notes)
+ e=event.create(graph_obj=o,type=event_type.comp_history(),start=t_stamp)
+ logger.info("Added component history as event %d."%e.id)
+ returnh
+
+
+
+[docs]
+ defadd_doc(self,repo,ref,time=datetime.datetime.now(),notes=None):
+"""Add a document pointer for this component.
+
+ Parameters
+ ----------
+ repo : :obj:`external_repo`
+ The place where the document is.
+ ref : string
+ A path or similar pointer, relevative to the root of *repo*.
+ time : datetime.datetime
+ The time at which the document pointer is to be added.
+ notes : string
+ Any notes for the timestamp.
+
+ Returns
+ -------
+ history : :obj:`component_doc`
+ The newly-created document pointer object.
+ """
+
+ _check_user("comp_info")
+ try:
+ external_repo.get(id=repo.id)
+ exceptpw.DoesNotExist:
+ raiseDoesNotExist("Repository does not exist in the DB. Create it first.")
+ o=graph_obj.create()
+ d=component_doc.create(id=o,comp=self,repo=repo,ref=ref)
+ t_stamp=timestamp.create(time=time,notes=notes)
+ e=event.create(graph_obj=o,type=event_type.comp_doc(),start=t_stamp)
+ logger.info("Added component document as event %d."%e.id)
+ returnd
+
+
+
+[docs]
+ defget_property(self,type=None,time=datetime.datetime.now()):
+"""Get a property for this component.
+
+ Parameters
+ ----------
+ type : :obj:`property_type`
+ The property type to search for.
+ time : obj:`datetime.datetime`
+ The time at which to get the property.
+
+ Returns
+ -------
+ property : string
+ If no property is set, then :obj:`None` is returned.
+ """
+ p=_graph_obj_iter(property,event,time,EVENT_AT,None,True).where(
+ property.comp_sn==self.sn
+ )
+ iftype:
+ returnp.where(property.type==type).get()
+ else:
+ returnp.get()
+
+
+
+[docs]
+ defset_property(self,type,value,time=datetime.datetime.now(),notes=None):
+"""Set a property for this component.
+
+ Parameters
+ ----------
+ type : :obj:`property_type`
+ The property type to search for.
+ value : string
+ The value to set.
+ time : obj:`datetime.datetime`
+ The time at which to get the property.
+ notes : string
+ Notes for the timestamp.
+
+ Raises
+ ------
+ :exc:ValueError:, if *value* does not conform to the property regular
+ expression.
+ """
+ set_property(self,type,value,time=time,notes=notes,force=True)
+
+
+ def__repr__(self):
+ # Format a representation of the object
+ # At the moment, cannot include the type info as it generates another query
+ fmt="<component serial='%s'>"
+ returnfmt%self.sn
+
+
+
+
+[docs]
+classcomponent_history(event_table):
+"""For providing history information on a component.
+
+ Attributes
+ ----------
+ id : foreign key, primary key
+ The ID shared with parent table graph_obj.
+ comp : foreign key
+ The component linked to the history.
+ notes : string
+ The history information.
+ """
+
+ id=pw.ForeignKeyField(
+ graph_obj,column_name="id",primary_key=True,backref="history"
+ )
+ comp=pw.ForeignKeyField(
+ component,column_name="comp_sn",field="sn",backref="history"
+ )
+ notes=pw.CharField(max_length=65000)
+
+
+
+
+[docs]
+classcomponent_doc(event_table):
+"""For linking a component to a document in an external repository.
+
+ Attributes
+ ----------
+ id : foreign key, primary key
+ The ID shared with parent table graph_obj.
+ comp : foreign key
+ The component linked to the document.
+ repo : foreign key
+ The repository holding the document.
+ ref : string
+ The location of the document within the repository (e.g., a filename).
+ """
+
+ id=pw.ForeignKeyField(
+ graph_obj,column_name="id",primary_key=True,backref="doc"
+ )
+ comp=pw.ForeignKeyField(
+ component,column_name="comp_sn",field="sn",backref="doc"
+ )
+ repo=pw.ForeignKeyField(external_repo,backref="doc")
+ ref=pw.CharField(max_length=65000)
+
+
+
+
+[docs]
+classconnexion(event_table):
+"""A connexion between two components.
+
+ This should always be instatiated using the from_pair() method.
+
+ Attributes
+ ----------
+ id : foreign key, primary key
+ The ID shared with parent table graph_obj.
+ comp1 : foreign key
+ The first component in the connexion.
+ comp2 : foreign key
+ The second component in the connexion.
+ """
+
+ id=pw.ForeignKeyField(
+ graph_obj,column_name="id",primary_key=True,backref="connexion"
+ )
+ comp1=pw.ForeignKeyField(
+ component,column_name="comp_sn1",field="sn",backref="conn1"
+ )
+ comp2=pw.ForeignKeyField(
+ component,column_name="comp_sn2",field="sn",backref="conn2"
+ )
+
+ classMeta(object):
+ indexes=(("component_sn1","component_sn2"),True)
+
+
+[docs]
+ @classmethod
+ deffrom_pair(cls,comp1,comp2,allow_new=True):
+"""Get a :obj:`connexion` given a pair of components.
+
+ Parameters
+ ----------
+ comp1 : str or :obj:`component`
+ Pass either the serial number or a :obj:`component` object.
+ comp2 : str or :obj:`component`
+ Pass either the serial number or a :obj:`component` object.
+ allow_new : bool
+ If :obj:`False`, then raise :exc:`peewee.DoesNotExist` if the connexion
+ does not exist at all in the database.
+
+ Returns
+ -------
+ connexion : :obj:`connexion`
+ """
+ pair=[]
+ forcompin(comp1,comp2):
+ ifisinstance(comp,component):
+ comp=comp.sn
+ try:
+ pair.append(component.get(sn=comp))
+ exceptpw.DoesNotExist:
+ raiseDoesNotExist("Component %s does not exist."%comp)
+ q=cls.select().where(
+ ((cls.comp1==pair[0])&(cls.comp2==pair[1]))
+ |((cls.comp1==pair[1])&(cls.comp2==pair[0]))
+ )
+ ifallow_new:
+ try:
+ returnq.get()
+ except:
+ returncls(comp1=pair[0],comp2=pair[1])
+ else:
+ returnq.get()
+
+
+
+[docs]
+ defis_connected(self,time=datetime.datetime.now()):
+"""See if a connexion exists.
+
+ Connexions whose events have been deactivated are not included.
+
+ Parameters
+ ----------
+ time : datetime
+ The time at which to check whether the connexion exists.
+
+ Returns
+ -------
+ connected : bool
+ :obj:`True` if there is a connexion, otherwise :obj:`False`.
+
+ Raises
+ ------
+ peewee.DoesNotExist
+ Raised if one or both of the components does not exist.
+ """
+ try:
+ self.event(
+ time=time,
+ type=(event_type.connexion(),event_type.perm_connexion()),
+ when=EVENT_AT,
+ ).get()
+ returnTrue
+ exceptpw.DoesNotExist:
+ returnFalse
+
+
+
+[docs]
+ defis_permanent(self,time=datetime.datetime.now()):
+"""See if a permenant connexion exists.
+
+ Connexions whose events have been deactivated are not included.
+
+ Parameters
+ ----------
+ time : datetime
+ The time at which to check whether the connexion exists.
+
+ Returns
+ -------
+ connected : bool
+ :obj:`True` if there is a permanent connexion, otherwise :obj:`False`.
+
+ Raises
+ ------
+ peewee.DoesNotExist
+ Raised if one or both of the components does not exist.
+ """
+ try:
+ self.event(time=time,type=event_type.perm_connexion(),when=EVENT_AT).get()
+ returnTrue
+ exceptpw.DoesNotExist:
+ returnFalse
+
+
+
+[docs]
+ defother_comp(self,comp):
+"""Given one component in the connexion, return the other.
+
+ Parameters
+ ----------
+ comp : :obj:`component`
+ The component you know in the connexion.
+
+ Returns
+ -------
+ :obj:`component`
+ The other component in the connexion, i.e., the one that isn't *comp*.
+
+ Raises
+ ------
+ :exc:`DoesNotExist`
+ If *comp* is not part of this connexion, an exception occurs.
+ """
+ ifself.comp1==comp:
+ returnself.comp2
+ elifself.comp2==comp:
+ returnself.comp1
+ else:
+ raiseDoesNotExist(
+ "The component you passed is not part of this connexion."
+ )
+
+
+
+[docs]
+ defmake(
+ self,time=datetime.datetime.now(),permanent=False,notes=None,force=False
+ ):
+"""Create a connexion.
+ This method begins a connexion event at the specified time.
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which to begin the connexion event.
+ permanent : bool
+ If :obj:`True`, then make this a permanent connexion.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`False`, then :exc:`AlreadyExists` will be raised if the connexion
+ already exists; otherwise, conflicts will be ignored and nothing will be
+ done.
+
+ Returns
+ -------
+ connexion : :obj:`connexion`
+ """
+ make_connexion(self,time,permanent,notes,force)
+ returnconnexion.from_pair(self.comp1,self.comp2)
+
+
+
+[docs]
+ defsever(self,time=datetime.datetime.now(),notes=None,force=False):
+"""Sever a connexion.
+ This method ends a connexion event at the specified time.
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which to end the connexion event.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`False`, then :exc:`DoesNotExists` will be raised if the connexion
+ does not exist; otherwise, conflicts will be ignored and nothing will be
+ done.
+ """
+ sever_connexion(self,time,notes,force)
+
+
+
+
+
+[docs]
+classproperty_type(name_table):
+"""A component property type.
+
+ Attributes
+ ----------
+ name : string
+ The name of the property type (e.g., "attenuation").
+ units : string
+ The (optional) units of the property (e.g., "dB").
+ regex : string
+ An (optional) regular expression for controlling allowed property values.
+ notes : string
+ Any (optional) notes further explaining the property.
+ """
+
+ name=pw.CharField(max_length=255)
+ units=pw.CharField(max_length=255,null=True)
+ regex=pw.CharField(max_length=255,null=True)
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+
+
+[docs]
+classproperty_component(base_model):
+"""A list associating property types with components.
+ A property can be for one or more component types. For example,
+ "dist_from_n_end" is only a property of cassettes, but "termination" may be a
+ property of LNA's, FLA's and so on. This is simply a table for matching
+ property types to component types.
+
+ Attributes
+ ----------
+ prop_type : foreign key
+ The property type to be mapped.
+ comp_type : foreign key
+ The component type to be mapped.
+ """
+
+ prop_type=pw.ForeignKeyField(property_type,backref="property_component")
+ comp_type=pw.ForeignKeyField(component_type,backref="property_component")
+
+ classMeta(object):
+ indexes=(("prop_type","comp_type"),True)
+
+
+
+
+[docs]
+classproperty(event_table):
+"""A property associated with a particular component.
+
+ Attributes
+ ----------
+ id : foreign key, primary key
+ The ID shared with parent table graph_obj.
+ comp : foreign key
+ The component to which this property belongs.
+ type : foreign key
+ The property type.
+ value : string
+ The actual property.
+ """
+
+ id=pw.ForeignKeyField(
+ graph_obj,column_name="id",primary_key=True,backref="property"
+ )
+ comp=pw.ForeignKeyField(
+ component,column_name="comp_sn",backref="property",field="sn"
+ )
+ type=pw.ForeignKeyField(property_type,backref="property")
+ value=pw.CharField(max_length=255)
+
+ classMeta(object):
+ indexes=(("comp_sn, type_id"),False)
+
+
+
+
+[docs]
+classevent_type(name_table):
+"""For differentiating event types.
+
+ The class methods :meth:`comp_avail`, :meth:`connexion` and so on return
+ event type instances, and internally store the result. Thus, subsequent calls
+ do not generate more database queries. This can reduce overhead.
+
+ Attributes
+ ----------
+ name : string
+ The name of the event type.
+ human_name : string
+ A proper, English name.
+ assoc_table : string
+ The (optional) table that this event is about; it should be a child of
+ graph_obj.
+ no_end : enum('Y', 'N')
+ If 'Y', then this is an "instantaneous" event, i.e., there will never be
+ recorded an end.
+ require_notes : enum('Y', 'N')
+ If 'Y', then the notes of the event _must_ be set.
+ notes : string
+ Any notes about this event type.
+ """
+
+ name=pw.CharField(max_length=255)
+ human_name=pw.CharField(max_length=255)
+ assoc_table=pw.CharField(max_length=255,null=True)
+ no_end=EnumField(["Y","N"],default="N")
+ require_notes=EnumField(["Y","N"],default="N")
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+[docs]
+ @classmethod
+ defcomp_avail(cls):
+"""For getting the component available event type."""
+ returncls.from_name("comp_avail")
+[docs]
+classtimestamp(base_model):
+"""A timestamp.
+
+ Attributes
+ ----------
+ time : datetime
+ The timestamp.
+ entry_time : datetime
+ The creation time of the timestamp.
+ user_id : foreign key
+ In the actual DB, this is a foreign key to chimewiki.user(user_id), but
+ peewee doesn't support foreign keys to different schemas.
+ notes : string
+ Any (optional) notes about the timestamp.
+ """
+
+ # Removed problematic constructor and replaced functionality with
+ # the fact that peewee supports callables as default arguments.
+
+ time=pw.DateTimeField(default=datetime.datetime.now)
+ entry_time=pw.DateTimeField(default=datetime.datetime.now)
+ user_id=pw.IntegerField(default=_peewee_get_current_user)
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+
+
+[docs]
+classevent(base_model):
+"""An event, or timestamp, for something graphy.
+
+ *Never* manually create, delete or alter events! Doing so can damage the
+ integrity of the database.
+
+ To interact with events, use:
+
+ * :meth:`component.add`, :func:`add_component`, :meth:`component.remove` and
+ :func:`remove_component` for starting and ending component
+ * :meth:`connexion.make` and :meth:`connexion.sever` for making and severing
+ connexions
+ * :meth:`component.set_property` for starting/ending component properties
+ * :meth:`component.add_history` and :meth:`component.add_doc` for adding
+ component history and documents.
+ * :meth:`global_flag.start`, :meth:`global_flag.end` to set a global flag.
+
+ You can safely deactivate an event using :meth:`deactivate`; this method only
+ allows deactivation if it will not damage the database integrity.
+
+ Attributes
+ ----------
+ active : bool
+ Is this event active? (Instead of deleting events, we deactivate them.)
+ replaces : foreign key
+ Instead of editing events, we replace them, so that we have a history of
+ event edits. This key indicates which event (if any) this event replaces.
+ graph_obj : foreign key
+ Which graph object is this event about?
+ type : foreign key
+ What kind of event is it?
+ start : foreign key
+ The timestamp for the event start.
+ end : foreign key
+ The timestamp for the event end.
+ """
+
+ active=pw.BooleanField(default=True)
+ replaces=pw.ForeignKeyField("self",null=True,backref="replacement")
+ graph_obj=pw.ForeignKeyField(graph_obj,backref="event")
+ type=pw.ForeignKeyField(event_type,backref="event")
+ start=pw.ForeignKeyField(timestamp,backref="event_start")
+ end=pw.ForeignKeyField(timestamp,backref="event_end")
+
+ classMeta(object):
+ indexes=((("type_id"),False),(("start","end"),False))
+
+ def_event_permission(self):
+ t=self.type
+ ift==event_type.comp_avail():
+ _check_user("comp_avail")
+ elift==event_type.connexion()ort==event_type.perm_connexion():
+ _check_user("connexion")
+ elift==event_type.comp_history()ort==event_type.comp_doc():
+ _check_user("comp_info")
+ elift==event_type.property():
+ _check_user("property")
+ elift==event_type.global_flag():
+ _check_user("global_flag")
+ # Layout notes need to be reworked.
+ # elif t == event_type.layout_note():
+ # _check_user("layout_note")
+ else:
+ raiseNoPermission("This layout type cannot be deactivated.")
+
+
+[docs]
+ defdeactivate(self):
+"""Deactivate an event.
+
+ Events are never deleted; rather, the :attr:`active` flag is switched off.
+ This method first checks to see whether doing so would break database
+ integrity, and only deactivates if it will not.
+
+ Raises
+ ------
+ :exc:LayoutIntegrity: if deactivating will compromise layout integrity.
+ """
+ self._event_permission()
+ fail=[]
+
+ ifnotself.active:
+ logger.info("Event %d is already deactivated."%(self.id))
+ return
+
+ # If this is about component availability, do not deactivate if it is
+ # connected, or if it has any properties, history or documents.
+ ifself.type==event_type.comp_avail():
+ comp=self.graph_obj.component.get()
+
+ # Check history.
+ forein_graph_obj_iter(
+ event,component_history,None,EVENT_ALL,None,True
+ ).where(component_history.comp==comp):
+ fail.append(str(e.id))
+ _check_fail(
+ fail,
+ False,
+ LayoutIntegrity,
+ "Cannot deactivate because "
+ "the following history event%s%s set for this "
+ "component"%(_plural(fail),_are(fail)),
+ )
+
+ # Check documents.
+ forein_graph_obj_iter(
+ event,component_doc,None,EVENT_ALL,None,True
+ ).where(component_doc.comp==comp):
+ fail.append(str(e.id))
+ _check_fail(
+ fail,
+ False,
+ LayoutIntegrity,
+ "Cannot deactivate because "
+ "the following document event%s%s set for this "
+ "component"%(_plural(fail),_are(fail)),
+ )
+
+ # Check properties.
+ forein_graph_obj_iter(
+ event,property,None,EVENT_ALL,None,True
+ ).where(property.comp==comp):
+ fail.append(str(e.id))
+ _check_fail(
+ fail,
+ False,
+ LayoutIntegrity,
+ "Cannot deactivate because "
+ "the following property event%s%s set for this "
+ "component"%(_plural(fail),_are(fail)),
+ )
+
+ # Check connexions.
+ forconnincomp.get_connexion(when=EVENT_ALL):
+ fail.append("%s<->%s"%(conn.comp1.sn,conn.comp2.sn))
+ _check_fail(
+ fail,
+ False,
+ LayoutIntegrity,
+ "Cannot deactivate because "
+ "the following component%s are connected"%(_plural(fail)),
+ )
+
+ self.active=False
+ self.save()
+ logger.info("Deactivated event %d."%self.id)
+
+
+ def_replace(self,start=None,end=None,force_end=False):
+"""Replace one or both timestamps for an event.
+
+ Currently, the following is not supported and will raise a
+ :exc:`RuntimeError` exception:
+
+ - replacing the end time of a component availability event;
+ - replacing the start time of a component availability event if the start
+ time is *later* than the current start time.
+
+ Parameters
+ ----------
+ start : :obj:`timestamp`
+ The new starting timestamp. If :obj:`None`, then the starting timestamp is
+ not altered.
+ end : :obj:`timestamp`
+ The new end timestamp. If this is set to :obj:`None` *and* **force_end**
+ is :obj:`True`, then the event will be set with no end time.
+ force_end : bool
+ If :obj:`True`, then a value of :obj:`None` for **end** is interpreted as
+ having no end time; otherwise, :obj:`None` for **end** will not change the
+ end timestamp.
+
+ Returns
+ -------
+ event : :obj:`event`
+ The modified event.
+ """
+ self._event_permission()
+ ifself.type==event_type.comp_avail():
+ ifendorforce_end:
+ raiseRuntimeError(
+ "This method does not currently support ending "
+ "component availability events."
+ )
+ ifstart.time>self.start.time:
+ raiseRuntimeError(
+ "This method does not currently support moving a "
+ "component availability event later."
+ )
+ ifstart==None:
+ start=self.start
+ else:
+ try:
+ timestamp.get(id=start.id)
+ exceptpw.DoesNotExist:
+ start.save()
+ ifend==None:
+ ifnotforce_end:
+ end=_pw_getattr(self,"end",None)
+ else:
+ ifend.time<start.time:
+ raiseLayoutIntegrity("End time cannot be earlier than start time.")
+ try:
+ timestamp.get(id=end.id)
+ exceptpw.DoesNotExist:
+ end.save()
+ self.active=False
+ self.save()
+
+ new=event.create(
+ replaces=self,
+ graph_obj=self.graph_obj,
+ type=self.type,
+ start=start,
+ end=end,
+ )
+ self=new
+ returnself
+
+
+
+
+[docs]
+classpredef_subgraph_spec(name_table):
+"""A specification for a subgraph of a full graph.
+
+ Attributes
+ ----------
+ name : string
+ The name of this subgraph specification.
+ start_type : foreign key
+ The starting component type.
+ notes : string
+ Optional notes about this specification.
+ """
+
+ name=pw.CharField(max_length=255)
+ start_type=pw.ForeignKeyField(
+ component_type,backref="predef_subgraph_spec_start"
+ )
+ notes=pw.CharField(max_length=65000,null=True)
+
+
+
+
+[docs]
+classpredef_subgraph_spec_param(base_model):
+"""Parameters for a subgraph specification.
+
+ Attributes
+ ----------
+ predef_subgraph_spec : foreign key
+ The subgraph which this applies.
+ type1 : foreign key
+ A component type.
+ type2 : foreign key
+ A component type.
+ action : enum('T', 'H', 'O')
+ The role of this component type:
+ - T: terminate at type1 (type2 is left NULL).
+ - H: hide type1 (type2 is left NULL).
+ - O: only draw connexions one way between type1 and type2.
+ """
+
+ predef_subgraph_spec=pw.ForeignKeyField(predef_subgraph_spec,backref="param")
+ type1=pw.ForeignKeyField(component_type,backref="subgraph_param1")
+ type2=pw.ForeignKeyField(component_type,backref="subgraph_param2",null=True)
+ action=EnumField(["T","H","O"])
+
+ classMeta(object):
+ indexes=(("predef_subgraph_spec","type","action"),False)
+
+
+
+
+[docs]
+classuser_permission_type(name_table):
+"""Defines permissions for the DB interface.
+
+ Attributes
+ ----------
+ name : string
+ The name of the permission.
+ notes : string
+ An (optional) description of the permission.
+ peewee doesn't support foreign keys to different schemas.
+ """
+
+ name=pw.CharField(max_length=255)
+ long_name=pw.CharField(max_length=65000)
+
+
+
+
+[docs]
+classuser_permission(base_model):
+"""Specifies users' permissions.
+
+ Attributes
+ ----------
+ user_id : foreign key
+ In the actual DB, this is a foreign key to chimewiki.user(user_id), but
+ peewee doesn't support foreign keys to different schemas.
+ type : foreign key
+ The permission type to grant to the user.
+ """
+
+ user_id=pw.IntegerField()
+ type=pw.ForeignKeyField(user_permission_type,backref="user")
+
+ classMeta(object):
+ indexes=(("user_id","type"),False)
+[docs]
+defcompare_connexion(conn1,conn2):
+"""See if two connexions are the same.
+ Because the :class:`connexion` could store the two components in different
+ orders, or have different instances of the same component object, direct
+ comparison may fail. This function explicitly compares both possible
+ combinations of serial numbers.
+
+ Parameters
+ ----------
+ conn1 : :obj:`connexion`
+ The first connexion object.
+ conn2 : :obj:`connexion`
+ The second connexion object.
+
+ Returns
+ -------
+ :obj:`True` if the connexions are the same, :obj:`False` otherwise.
+ """
+ sn11=conn1.comp1.sn
+ sn12=conn1.comp2.sn
+ sn21=conn2.comp1.sn
+ sn22=conn2.comp2.sn
+
+ if(sn11==sn21andsn12==sn22)or(sn11==sn22andsn12==sn21):
+ returnTrue
+ else:
+ returnFalse
+
+
+
+
+[docs]
+defadd_component(comp,time=datetime.datetime.now(),notes=None,force=False):
+"""Make one or more components available with a common timestamp.
+
+ If you are adding only one component, this function is equivalent to calling
+ :meth:`component.add`. However, multiple calls to :meth:`component.add`
+ generate a unique timestamp per call. To assign a single timestamp to many
+ additions at once, use this function.
+
+ Examples
+ --------
+ >>> lna_type = layout.component_type.get(name = "LNA")
+ >>> lna_rev = lna_type.rev.where(layout.component_type_rev.name == "B").get()
+ >>> c = []
+ >>> for i in range(0, 10):
+ ... c.append(layout.component(sn = "LNA%04dB" % (i), type = lna_type, rev = lna_rev))
+ >>> layout.add_component(c, time = datetime(2014, 10, 10, 11), notes = "Adding many at once.")
+
+ Parameters
+ ----------
+ comp : list of :obj:`component` objects
+ The components to make available.
+ time : datetime.datetime
+ The time at which to make the components available.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`True`, then add any components that can be added, while doing
+ nothing (except making note of such in the logger) for components whose
+ addition would violate database integrity. If :obj:`False`,
+ :exc:`AlreadyExists` is raised for any addition that violates database
+ integrity.
+ """
+ importcopy
+
+ _check_user("comp_avail")
+ ifisinstance(comp,component):
+ comp_list=[comp]
+ else:
+ comp_list=comp
+
+ # First check to see that the component does not already exist at this time.
+ fail=[]
+ to_add=[]
+ to_add_sn=[]
+ forcompincomp_list:
+ try:
+ c=component.get(sn=comp.sn)
+ exceptpw.DoesNotExist:
+ to_add.append(comp)
+ to_add_sn.append(comp.sn)
+ continue
+ try:
+ c.event(time,event_type.comp_avail(),EVENT_AT).get()
+ fail.append(c.sn)
+ except:
+ to_add.append(comp)
+ to_add_sn.append(comp.sn)
+
+ # Also add permanently connected components.
+ done=copy.deepcopy(to_add)
+ add=[]
+ sn=[]
+ forcinto_add:
+ try:
+ component.get(sn=c.sn)
+ this_add,this_sn=_get_perm_connexion_recurse(c,time,done)
+ add+=this_add
+ sn+=this_sn
+ to_add+=add
+ to_add_sn+=sn
+ exceptpw.DoesNotExist:
+ # If the component doesn't exist in the DB, then it can't have any
+ # permanent connexions.
+ pass
+
+ _check_fail(
+ fail,
+ force,
+ AlreadyExists,
+ "Aborting because the following "
+ "component%s%s already available at that time"%(_plural(fail),_are(fail)),
+ )
+
+ iflen(to_add):
+ t_stamp=timestamp.create(time=time,notes=notes)
+ forcompinto_add:
+ try:
+ comp=component.get(sn=comp.sn)
+ exceptpw.DoesNotExist:
+ o=graph_obj.create()
+ comp.id=o
+ comp.save(force_insert=True)
+
+ try:
+ # If the component is already available after this time, replace it with
+ # an event starting at this new time.
+ e_old=comp.event(
+ time,event_type.comp_avail(),EVENT_AFTER,ORDER_ASC
+ ).get()
+ e_old._replace(start=t_stamp)
+ logger.debug(
+ "Added %s by replacing previous event %d."%(comp.sn,e_old.id)
+ )
+ exceptpw.DoesNotExist:
+ e=event.create(
+ graph_obj=comp.id,type=event_type.comp_avail(),start=t_stamp
+ )
+ logger.debug("Added %s with new event %d."%(comp.sn,e.id))
+ iflen(to_add):
+ logger.info(
+ "Added %d new component%s: %s"
+ %(len(to_add),_plural(to_add),", ".join(to_add_sn))
+ )
+ else:
+ logger.info("Added no new component.")
+[docs]
+defremove_component(comp,time=datetime.datetime.now(),notes=None,force=False):
+"""End availability of one or more components with a common timestamp.
+
+ If you are adding only one component, this function is equivalent to calling
+ :meth:`component.remove`. However, multiple calls to :meth:`component.remove`
+ generate a unique timestamp per call. To assign a single timestamp to many
+ additions at once, use this function.
+
+ Parameters
+ ----------
+ comp : list of :obj:`component` objects
+ The components to end availability of.
+ time : datetime.datetime
+ The time at which to end availability.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`True`, then remove any components that can be removed, while doing
+ nothing (except making note of such in the logger) for components whose
+ removal would violate database integrity. If :obj:`False`,
+ :exc:`DoesNotExist` is raised for any addition that violates database
+ integrity.
+ """
+ _check_user("comp_avail")
+ ifisinstance(comp,component)orisinstance(comp,str):
+ comp_list=[comp]
+ else:
+ comp_list=comp
+
+ # First check to see that the component already exists at this time; also
+ # ensure that it is not connected to anything.
+ fail_avail=[]
+ fail_conn=[]
+ fail_perm_conn=[]
+ ev=[]
+ ev_comp_sn=[]
+ forcompincomp_list:
+ try:
+ c=component.get(sn=comp.sn)
+ e=c.event(time,event_type.comp_avail(),EVENT_AT).get()
+
+ found_conn=False
+ forconninc.get_connexion(time=time):
+ ifnotconn.is_permanent():
+ fail_conn.append("%s<->%s"%(conn.comp1.sn,conn.comp2.sn))
+ found_conn=True
+
+ perm_ev,perm_ev_sn,perm_fail=_check_perm_connexion_recurse(c,time)
+ iflen(perm_fail):
+ fail_perm_conn+=perm_fail
+ found_conn=True
+ eliflen(perm_ev):
+ ev+=perm_ev
+ ev_comp_sn+=perm_ev_sn
+ found_conn=True
+
+ ifnotfound_conn:
+ ev.append(c.event(time,event_type.comp_avail(),EVENT_AT).get())
+ ev_comp_sn.append(c.sn)
+ exceptpw.DoesNotExist:
+ fail_avail.append(c.sn)
+ pass
+
+ _check_fail(
+ fail_avail,
+ force,
+ LayoutIntegrity,
+ "The following component%s "
+ "%s not available at that time, or you have specified an "
+ "end time earlier than %s start time%s"
+ %(
+ _plural(fail_avail),
+ _are(fail_avail),
+ "its"iflen(fail_avail)==1else"their",
+ _plural(fail_avail),
+ ),
+ )
+ _check_fail(
+ fail_conn,
+ force,
+ LayoutIntegrity,
+ "Cannot remove because the "
+ "following component%s%s connected"%(_plural(fail_conn),_are(fail_conn)),
+ )
+ _check_fail(
+ fail_perm_conn,
+ force,
+ LayoutIntegrity,
+ "Cannot remove because "
+ "the following component%s%s connected (via permanent "
+ "connexions)"%(_plural(fail_perm_conn),_are(fail_perm_conn)),
+ )
+
+ t_stamp=timestamp.create(time=time,notes=notes)
+ foreinev:
+ e.end=t_stamp
+ e.save()
+ logger.debug("Removed component by ending event %d."%e.id)
+ iflen(ev):
+ logger.info(
+ "Removed %d component%s: %s."
+ %(len(ev),_plural(ev),", ".join(ev_comp_sn))
+ )
+ else:
+ logger.info("Removed no component.")
+
+
+
+
+[docs]
+defset_property(
+ comp,type,value,time=datetime.datetime.now(),notes=None,force=False
+):
+"""Set a property value for one or more components with a common timestamp.
+
+ Passing :obj:`None` for the property value erases that property from the
+ component.
+
+ If you altering only one component, this function is equivalent to calling
+ :meth:`component.set_property`. However, multiple calls to
+ :meth:`component.set_property` generate a unique timestamp per call. To
+ assign a single timestamp to many additions at once, use this function.
+
+ Parameters
+ ----------
+ comp : list of :obj:`component` objects
+ The components to assign the property to.
+ type : :obj:`property_type`
+ The property type.
+ value : str
+ The property value to assign.
+ time : datetime.datetime
+ The time at which to end availability.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`False`, then complain if altering the property does nothing (e.g.,
+ because the property value would be unchanged for a certain component);
+ otherwise, ignore such situations and merely issue logging information on
+ them.
+
+ Raises
+ ------
+ :exc:ValueError:, if *value* does not conform to the property type's regular
+ expression; :exc:PropertyUnchanged: if *force* is :obj:`False`: and a
+ component's property value would remain unaltered.
+ """
+ _check_user("property")
+ ifisinstance(comp,component)orisinstance(comp,str):
+ comp_list=[comp]
+ else:
+ comp_list=comp
+ forcompincomp_list:
+ _check_property_type(type,comp.type)
+ iftype.regexandvalue!=None:
+ ifnotre.match(re.compile(type.regex),value):
+ raiseValueError(
+ 'Value "%s" does not conform to regular '
+ "expression %s."%(value,type.regex)
+ )
+
+ fail=[]
+ to_end=[]
+ to_end_sn=[]
+ to_set=[]
+ to_set_sn=[]
+ forcompincomp_list:
+ try:
+ # If this property type is already set, then end it---unless the value is
+ # exactly the same, in which case don't do anything.
+ p=(
+ _graph_obj_iter(property,event,time,EVENT_AT,None,True)
+ .where((property.comp==comp)&(property.type==type))
+ .get()
+ )
+ ifp.value==value:
+ fail.append(comp.sn)
+ else:
+ to_end.append(p)
+ to_end_sn.append(comp.sn)
+ to_set.append(comp)
+ to_set_sn.append(comp.sn)
+ exceptpw.DoesNotExist:
+ ifnotvalue:
+ fail.append(comp.sn)
+ to_set.append(comp)
+ to_set_sn.append(comp.sn)
+
+ _check_fail(
+ fail,
+ force,
+ PropertyUnchanged,
+ "The following component%s "
+ "property does not change"%("'s"iflen(fail)==1else"s'"),
+ )
+
+ # End any events that need to be ended.
+ iflen(to_end):
+ t_stamp=timestamp.create(time=time)
+ forpinto_end:
+ e=p.event(time,event_type.property(),EVENT_AT).get()
+ e.end=t_stamp
+ e.save()
+
+ # If no value was passed, then we are done.
+ ifnotvalue:
+ logger.info(
+ "Removed property %s from the following %d component%s: %s."
+ %(type.name,len(to_end),_plural(to_end),", ".join(to_end_sn))
+ )
+ return
+
+ # Start the event with a common timestamp.
+ iflen(to_set):
+ t_stamp=timestamp.create(time=time,notes=notes)
+ forcompinto_set:
+ o=graph_obj.create()
+ p=property.create(id=o,comp=comp,type=type,value=value)
+ e=event.create(graph_obj=o,type=event_type.property(),start=t_stamp)
+ logger.info(
+ "Added property %s=%s to the following component%s: %s."
+ %(type.name,value,_plural(to_set),", ".join(to_set_sn))
+ )
+ else:
+ logger.info("No component property was changed.")
+
+
+
+
+[docs]
+defmake_connexion(
+ conn,time=datetime.datetime.now(),permanent=False,notes=None,force=False
+):
+"""Connect one or more component pairs with a common timestamp.
+
+ If you are connecting only one pair, this function is equivalent to calling
+ :meth:`connexion.make`. However, multiple calls to :meth:`connexion.make`
+ generate a unique timestamp per call. To assign a single timestamp to many
+ connexions at once, use this function.
+
+ Examples
+ --------
+ >>> conn = []
+ >>> for i in range(0, 10):
+ ... comp1 = layout.component.get(sn = "LNA%04dB" % (i))
+ ... comp2 = layout.component.get(sn = "CXA%04dB"% (i))
+ ... conn.append(layout.connexion.from_pair(comp1, comp2))
+ >>> layout.make_connexion(conn, time = datetime(2013, 10, 11, 23, 15), notes = "Making multiple connexions at once.")
+
+ Parameters
+ ----------
+ comp : list of :obj:`connexion` objects
+ The connexions to make.
+ time : datetime.datetime
+ The time at which to end availability.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`True`, then remove any components that can be removed, while doing
+ nothing (except making note of such in the logger) for components whose
+ removal would violate database integrity. If :obj:`False`,
+ :exc:`DoesNotExist` is raised for any addition that violates database
+ integrity.
+ """
+ _check_user("connexion")
+ ifisinstance(conn,connexion):
+ conn=[conn]
+
+ # Check that the connexions do not yet exist.
+ fail=[]
+ to_conn=[]
+ to_conn_sn=[]
+ forcinconn:
+ ifc.is_connected(time):
+ fail.append("%s<=>%s"%(c.comp1.sn,c.comp2.sn))
+ else:
+ to_conn.append(c)
+ to_conn_sn.append("%s<=>%s"%(c.comp1.sn,c.comp2.sn))
+ iflen(fail):
+ _check_fail(
+ fail,
+ force,
+ AlreadyExists,
+ "Cannot connect because the following connexions already exist",
+ )
+
+ t_stamp=timestamp.create(time=time,notes=notes)
+ forcinto_conn:
+ try:
+ # If there is a connexion after this time, replace it with an event
+ # starting at this new time.
+ e_old=c.event(time,EVENT_AFTER,ORDER_ASC).get()
+ e_old._replace(start=t_stamp)
+ logger.debug("Added connexion by replacing previous event %d."%e_old.id)
+ exceptpw.DoesNotExist:
+ try:
+ conn=connexion.from_pair(c.comp1,c.comp2,allow_new=False)
+ o=conn.id
+ except:
+ o=graph_obj.create()
+ conn=connexion.create(id=o,comp1=c.comp1,comp2=c.comp2)
+ ifpermanent:
+ e_type=event_type.perm_connexion()
+ else:
+ e_type=event_type.connexion()
+ e=event.create(graph_obj=o,type=e_type,start=t_stamp)
+ logger.debug("Added connexion with new event %d."%e.id)
+ iflen(to_conn):
+ logger.info(
+ "Added %d new connexion%s: %s"
+ %(len(to_conn),_plural(to_conn),", ".join(to_conn_sn))
+ )
+ else:
+ logger.info("Added no new connexions.")
+
+
+
+
+[docs]
+defsever_connexion(conn,time=datetime.datetime.now(),notes=None,force=False):
+"""Sever one or more component pairs with a common timestamp.
+
+ If you are severing only one pair, this function is equivalent to calling
+ :meth:`connexion.sever`. However, multiple calls to :meth:`connexion.sever`
+ generate a unique timestamp per call. To assign a single timestamp to many
+ connexion severances at once, use this function.
+
+ Examples
+ --------
+ >>> conn = []
+ >>> for i in range(0, 10):
+ ... comp1 = layout.component.get(sn = "LNA%04dB" % (i))
+ ... comp2 = layout.component.get(sn = "CXA%04dB"% (i))
+ ... conn.append(layout.connexion.from_pair(comp1, comp2))
+ >>> layout.sever_connexion(conn, time = datetime(2014, 10, 11, 23, 15), notes = "Severing multiple connexions at once.")
+
+ Parameters
+ ----------
+ comp : list of :obj:`connexion` objects
+ The connexions to sever.
+ time : datetime.datetime
+ The time at which to end availability.
+ notes : string
+ Any notes for the timestamp.
+ force : bool
+ If :obj:`True`, then sever any connexions that can be severed, while doing
+ nothing (except making note of such in the logger) for connexions whose
+ severence would violate database integrity. If :obj:`False`,
+ :exc:`DoesNotExist` is raised for any severence that violates database
+ integrity.
+ """
+ _check_user("connexion")
+ ifisinstance(conn,connexion):
+ conn=[conn]
+
+ # Check that the connexions actually exist.
+ fail_conn=[]
+ fail_perm=[]
+ ev=[]
+ ev_conn_sn=[]
+ forcinconn:
+ try:
+ ev.append(
+ c.event(time=time,type=event_type.connexion(),when=EVENT_AT).get()
+ )
+ ev_conn_sn.append("%s<=>%s"%(c.comp1.sn,c.comp2.sn))
+ exceptpw.DoesNotExist:
+ try:
+ c.event(
+ time=time,type=event_type.perm_connexion(),when=EVENT_AT
+ ).get()
+ fail_perm.append("%s<=>%s"%(c.comp1.sn,c.comp2.sn))
+ exceptpw.DoesNotExist:
+ fail_conn.append("%s<=>%s"%(c.comp1.sn,c.comp2.sn))
+ _check_fail(
+ fail_conn,
+ force,
+ AlreadyExists,
+ "Cannot disconnect because "
+ "the following connexion%s%s not exist at that time"
+ %(_plural(fail_conn),_does(fail_conn)),
+ )
+ _check_fail(
+ fail_perm,force,LayoutIntegrity,"Cannot disconnect permanent connexions"
+ )
+
+ t_stamp=timestamp.create(time=time,notes=notes)
+ foreinev:
+ e.end=t_stamp
+ e.save()
+ logger.debug("Severed connexion by ending event %d."%e.id)
+ iflen(ev):
+ logger.info(
+ "Severed %d connexion%s: %s."
+ %(len(ev),_plural(ev),", ".join(ev_conn_sn))
+ )
+ else:
+ logger.info("Severed no connexion.")
+"""Analysis data format"""
+
+importwarnings
+importglob
+fromosimportpath
+importposixpath
+importre
+
+importnumpyasnp
+importh5py
+frombitshuffleimporth5
+
+tmp=h5# To appease linters who complain about unused imports.
+
+# If the `caput` package is available, get `memh5` from there. Otherwise, use
+# the version of memh5 that ships with `ch_util`, eliminating the dependency.
+try:
+ fromcaputimportmemh5,tod
+exceptImportError:
+ raiseImportError("Could not import memh5 or tod. Have you installed caput?")
+
+
+ni_msg="Ask Kiyo to implement this."
+
+
+# Datasets in the Acq files whose shape is the same as the visibilities.
+# Variable only used for legacy archive version 1.
+ACQ_VIS_SHAPE_DATASETS=("vis","vis_flag","vis_weight")
+
+# Datasets in the Acq files that are visibilities or gated visibilities
+ACQ_VIS_DATASETS="^vis$|^gated_vis[0-9]$"
+
+# Datasets in the HK files that are data.
+HK_DATASET_NAMES=("data","^mux[0-9]{2}$")
+
+# List of axes over which we can concatenate datasets. To be concatenated, all
+# datasets must have one and only one of these in their 'axes' attribute.
+CONCATENATION_AXES=(
+ "time",
+ "gated_time0",
+ "gated_time1",
+ "gated_time2",
+ "gated_time3",
+ "gated_time4",
+ "snapshot",
+ "update_time",
+ "station_time_blockhouse",
+)
+
+ANDATA_VERSION="3.1.0"
+
+
+# Main Class Definition
+# ---------------------
+
+
+
+[docs]
+classBaseData(tod.TOData):
+"""CHIME data in analysis format.
+
+ Inherits from :class:`caput.memh5.BasicCont`.
+
+ This is intended to be the main data class for the post
+ acquisition/real-time analysis parts of the pipeline. This class is laid
+ out very similarly to how the data is stored in analysis format hdf5 files
+ and the data in this class can be optionally stored in such an hdf5 file
+ instead of in memory.
+
+ Parameters
+ ----------
+ h5_data : h5py.Group, memh5.MemGroup or hdf5 filename, optional
+ Underlying h5py like data container where data will be stored. If not
+ provided a new :class:`caput.memh5.MemGroup` instance will be created.
+ """
+
+ time_axes=CONCATENATION_AXES
+
+ # Convert strings to/from unicode on load and save
+ convert_attribute_strings=True
+ convert_dataset_strings=True
+
+ def__new__(cls,h5_data=None,**kwargs):
+"""Used to pick which subclass to instantiate based on attributes in
+ data."""
+
+ new_cls=subclass_from_obj(cls,h5_data)
+
+ self=super(BaseData,new_cls).__new__(new_cls)
+ returnself
+
+ def__init__(self,h5_data=None,**kwargs):
+ super(BaseData,self).__init__(h5_data,**kwargs)
+ ifself._data.file.mode=="r+":
+ self._data.require_group("cal")
+ self._data.require_group("flags")
+ self._data.require_group("reverse_map")
+ self.attrs["andata_version"]=ANDATA_VERSION
+
+ # - The main interface - #
+
+ @property
+ defdatasets(self):
+"""Stores hdf5 datasets holding all data.
+
+ Each dataset can reference a calibration scheme in
+ ``datasets[name].attrs['cal']`` which refers to an entry in
+ :attr:`~BaseData.cal`.
+
+ Do not try to add a new dataset by assigning to an item of this
+ property. Use `create_dataset` instead.
+
+ Returns
+ -------
+ datasets : read only dictionary
+ Entries are :mod:`h5py` or :mod:`caput.memh5` datasets.
+
+ """
+
+ out={}
+ forname,valueinself._data.items():
+ ifnotmemh5.is_group(value):
+ out[name]=value
+ returnmemh5.ro_dict(out)
+
+ @property
+ defflags(self):
+"""Datasets representing flags and data weights.
+
+ Returns
+ -------
+ flags : read only dictionary
+ Entries are :mod:`h5py` or :mod:`caput.memh5` datasets.
+
+ """
+
+ try:
+ g=self._data["flags"]
+ exceptKeyError:
+ returnmemh5.ro_dict({})
+
+ out={}
+ forname,valueing.items():
+ ifnotmemh5.is_group(value):
+ out[name]=value
+ returnmemh5.ro_dict(out)
+
+ @property
+ defcal(self):
+"""Stores calibration schemes for the datasets.
+
+ Each entry is a calibration scheme which itself is a dict storing
+ meta-data about calibration.
+
+ Do not try to add a new entry by assigning to an element of this
+ property. Use :meth:`~BaseData.create_cal` instead.
+
+ Returns
+ -------
+ cal : read only dictionary
+ Calibration schemes.
+
+ """
+
+ out={}
+ forname,valueinself._data["cal"].items():
+ out[name]=value.attrs
+ returnmemh5.ro_dict(out)
+
+ # - Methods used by base class to control container structure. - #
+
+
+[docs]
+ defdataset_name_allowed(self,name):
+"""Permits datasets in the root and 'flags' groups."""
+
+ parent_name,name=posixpath.split(name)
+ returnTrueifparent_name=="/"orparent_name=="/flags"elseFalse
+
+
+
+[docs]
+ defgroup_name_allowed(self,name):
+"""Permits only the "flags" group."""
+
+ returnTrueifname=="/flags"elseFalse
+
+
+ # - Methods for manipulating and building the class. - #
+
+
+[docs]
+ defcreate_cal(self,name,cal=None):
+"""Create a new cal entry."""
+
+ ifcalisNone:
+ cal={}
+ self._data["cal"].create_group(name)
+ forkey,valueincal.items():
+ self._data["cal"][name].attrs[key]=value
+
+
+
+[docs]
+ defcreate_flag(self,name,*args,**kwargs):
+"""Create a new flags dataset."""
+ returnself.create_dataset("flags/"+name,*args,**kwargs)
+
+
+
+[docs]
+ defcreate_reverse_map(self,axis_name,reverse_map):
+"""Create a new reverse map."""
+ returnself._data["reverse_map"].create_dataset(axis_name,data=reverse_map)
+
+
+
+[docs]
+ defdel_reverse_map(self,axis_name):
+"""Delete a reverse map."""
+ delself._data["reverse_map"][axis_name]
+
+
+ # - These describe the various data axes. - #
+
+ @property
+ defntime(self):
+"""Length of the time axis of the visibilities."""
+
+ returnlen(self.index_map["time"])
+
+ @property
+ deftime(self):
+"""The 'time' axis centres as Unix/POSIX time."""
+
+ if(
+ self.index_map["time"].dtype==np.float32
+ orself.index_map["time"].dtype==np.float64
+ ):
+ # Already a calculated timestamp.
+ returnself.index_map["time"][:]
+
+ else:
+ time=_timestamp_from_fpga_cpu(
+ self.index_map["time"]["ctime"],0,self.index_map["time"]["fpga_count"]
+ )
+
+ alignment=self.index_attrs["time"].get("alignment",0)
+
+ ifalignment!=0:
+ time=time+alignment*abs(np.median(np.diff(time))/2)
+
+ returntime
+
+ @classmethod
+ def_interpret_and_read(cls,acq_files,start,stop,datasets,out_group):
+ # Save a reference to the first file to get index map information for
+ # later.
+ f_first=acq_files[0]
+
+ andata_objs=[cls(d)fordinacq_files]
+ data=concatenate(
+ andata_objs,
+ out_group=out_group,
+ start=start,
+ stop=stop,
+ datasets=datasets,
+ convert_attribute_strings=cls.convert_attribute_strings,
+ convert_dataset_strings=cls.convert_dataset_strings,
+ )
+ fork,vinf_first["index_map"].attrs.items():
+ data.create_index_map(k,v)
+ returndata
+
+
+[docs]
+ @classmethod
+ deffrom_acq_h5(
+ cls,acq_files,start=None,stop=None,datasets=None,out_group=None,**kwargs
+ ):
+"""Convert acquisition format hdf5 data to analysis data object.
+
+ Reads hdf5 data produced by the acquisition system and converts it to
+ analysis format in memory.
+
+ Parameters
+ ----------
+ acq_files : filename, `h5py.File` or list there-of or filename pattern
+ Files to convert from acquisition format to analysis format.
+ Filename patterns with wild cards (e.g. "foo*.h5") are supported.
+ start : integer, optional
+ What frame to start at in the full set of files.
+ stop : integer, optional
+ What frame to stop at in the full set of files.
+ datasets : list of strings
+ Names of datasets to include from acquisition files. Default is to
+ include all datasets found in the acquisition files.
+ out_group : `h5py.Group`, hdf5 filename or `memh5.Group`
+ Underlying hdf5 like container that will store the data for the
+ BaseData instance.
+
+ Examples
+ --------
+ Examples are analogous to those of :meth:`CorrData.from_acq_h5`.
+
+ """
+
+ # Make sure the input is a sequence and that we have at least one file.
+ acq_files=tod.ensure_file_list(acq_files)
+ ifnotacq_files:
+ raiseValueError("Acquisition file list is empty.")
+
+ to_close=[False]*len(acq_files)
+ try:
+ # Open the files while keeping track of this so that we can close
+ # them later.
+ _open_files(acq_files,to_close)
+
+ # Now read them in: the functionality here is provided by the
+ # overloaded method in the inherited class. If this method is
+ # called on this base class, an exception will be raised.
+ data=cls._interpret_and_read(
+ acq_files=acq_files,
+ start=start,
+ stop=stop,
+ datasets=datasets,
+ out_group=out_group,
+ **kwargs
+ )
+
+ # Set an attribute on the time axis specifying alignment
+ if"time"indata.index_map:
+ data.index_attrs["time"]["alignment"]=1
+
+ finally:
+ # Close any files opened in this function.
+ foriiinrange(len(acq_files)):
+ iflen(to_close)>iiandto_close[ii]:
+ acq_files[ii].close()
+
+ returndata
+
+
+ @property
+ deftimestamp(self):
+"""Deprecated name for :attr:`~BaseData.time`."""
+
+ returnself.time
+
+
+[docs]
+classCorrData(BaseData):
+"""Subclass of :class:`BaseData` for correlation data."""
+
+ @property
+ defvis(self):
+"""Convenience access to the visibilities array.
+
+ Equivalent to `self.datasets['vis']`.
+ """
+ returnself.datasets["vis"]
+
+ @property
+ defgain(self):
+"""Convenience access to the gain dataset.
+
+ Equivalent to `self.datasets['gain']`.
+ """
+ returnself.datasets["gain"]
+
+ @property
+ defweight(self):
+"""Convenience access to the visibility weight array.
+
+ Equivalent to `self.flags['vis_weight']`.
+ """
+ returnself.flags["vis_weight"]
+
+ @property
+ definput_flags(self):
+"""Convenience access to the input flags dataset.
+
+ Equivalent to `self.flags['inputs']`.
+ """
+ returnself.flags["inputs"]
+
+ @property
+ defdataset_id(self):
+"""Access dataset id dataset in unicode format."""
+ dsid=memh5.ensure_unicode(self.flags["dataset_id"][:])
+ dsid.flags.writeable=False
+
+ returndsid
+
+ @property
+ defnprod(self):
+"""Length of the prod axis."""
+ returnlen(self.index_map["prod"])
+
+ @property
+ defprod(self):
+"""The correlation product axis as channel pairs."""
+ returnself.index_map["prod"]
+
+ @property
+ defnfreq(self):
+"""Length of the freq axis."""
+ returnlen(self.index_map["freq"])
+
+ @property
+ deffreq(self):
+"""The spectral frequency axis as bin centres in MHz."""
+ returnself.index_map["freq"]["centre"]
+
+ @property
+ defninput(self):
+ returnlen(self.index_map["input"])
+
+ @property
+ definput(self):
+ returnself.index_map["input"]
+
+ @property
+ defnstack(self):
+ returnlen(self.index_map["stack"])
+
+ @property
+ defstack(self):
+"""The correlation product axis as channel pairs."""
+ returnself.index_map["stack"]
+
+ @property
+ defprodstack(self):
+"""A pair of input indices representative of those in the stack.
+
+ Note, these are correctly conjugated on return, and so calculations
+ of the baseline and polarisation can be done without additionally
+ looking up the stack conjugation.
+ """
+ ifnotself.is_stacked:
+ returnself.prod
+
+ t=self.index_map["prod"][:][self.index_map["stack"]["prod"]]
+
+ prodmap=t.copy()
+ conj=self.stack["conjugate"]
+ prodmap["input_a"]=np.where(conj,t["input_b"],t["input_a"])
+ prodmap["input_b"]=np.where(conj,t["input_a"],t["input_b"])
+
+ returnprodmap
+
+ @property
+ defis_stacked(self):
+ return"stack"inself.index_mapandlen(self.stack)!=len(self.prod)
+
+ @classmethod
+ def_interpret_and_read(
+ cls,
+ acq_files,
+ start,
+ stop,
+ datasets,
+ out_group,
+ stack_sel,
+ prod_sel,
+ input_sel,
+ freq_sel,
+ apply_gain,
+ renormalize,
+ ):
+ # Selection defaults.
+ freq_sel=_ensure_1D_selection(freq_sel)
+ # If calculating the 'gain' dataset, ensure prerequisite datasets
+ # are loaded.
+ ifdatasetsisnotNoneand(
+ ("vis"indatasetsandapply_gain)or("gain"indatasets)
+ ):
+ datasets=tuple(datasets)+("gain","gain_exp","gain_coeff")
+ # Always load packet loss dataset if available, so we can normalized
+ # for it.
+ ifdatasetsisnotNone:
+ norm_dsets=[dfordindatasetsifre.match(ACQ_VIS_DATASETS,d)]
+ if"vis_weight"indatasets:
+ norm_dsets+=["vis_weight"]
+ iflen(norm_dsets):
+ datasets=tuple(datasets)+("flags/lost_packet_count",)
+
+ # Inspect the header of the first file for version information.
+ f=acq_files[0]
+ try:
+ archive_version=memh5.bytes_to_unicode(f.attrs["archive_version"])
+ exceptKeyError:
+ archive_version="1.0.0"
+
+ # Transform the dataset according to the version.
+ ifversiontuple(archive_version)<versiontuple("2.0.0"):
+ # Nothing to do for input_sel as there is not input axis.
+ ifinput_selisnotNone:
+ msg=(
+ "*input_sel* specified for archive version"
+ " 1.0 data which has no input axis."
+ )
+ raiseValueError(msg)
+ prod_sel=_ensure_1D_selection(prod_sel)
+ data=andata_from_acq1(
+ acq_files,start,stop,prod_sel,freq_sel,datasets,out_group
+ )
+ input_sel=_ensure_1D_selection(input_sel)
+ elifversiontuple(archive_version)>=versiontuple("2.0.0"):
+ data,input_sel=andata_from_archive2(
+ cls,
+ acq_files,
+ start,
+ stop,
+ stack_sel,
+ prod_sel,
+ input_sel,
+ freq_sel,
+ datasets,
+ out_group,
+ )
+
+ # Generate the correct index_map/input for older files
+ ifversiontuple(archive_version)<versiontuple("2.1.0"):
+ _remap_inputs(data)
+
+ # Insert the gain dataset if requested, or datasets is not specified
+ # For version 3.0.0 we don't need to do any of this
+ ifversiontuple(archive_version)<versiontuple("3.0.0")and(
+ datasetsisNoneor"gain"indatasets
+ ):
+ _insert_gains(data,input_sel)
+
+ # Remove the FPGA applied gains (need to invert them first).
+ ifapply_gainandany(
+ [re.match(ACQ_VIS_DATASETS,key)forkeyindata.datasets]
+ ):
+ fromch_utilimporttools
+
+ gain=data.gain[:]
+
+ # Create an array of safe-inverse gains.
+ gain_inv=tools.invert_no_zero(gain)
+
+ # Loop over datasets and apply inverse gains where appropriate
+ forkey,dsetindata.datasets.items():
+ if(
+ re.match(ACQ_VIS_DATASETS,key)
+ anddset.attrs["axis"][1]=="prod"
+ ):
+ tools.apply_gain(
+ dset[:],
+ gain_inv,
+ out=dset[:],
+ prod_map=data.index_map["prod"],
+ )
+
+ # Fix up wrapping of FPGA counts
+ ifversiontuple(archive_version)<versiontuple("2.4.0"):
+ _unwrap_fpga_counts(data)
+
+ # Renormalize for dropped packets
+ # Not needed for > 3.0
+ if(
+ versiontuple(archive_version)<versiontuple("3.0.0")
+ andrenormalize
+ and"lost_packet_count"indata.flags
+ ):
+ _renormalize(data)
+
+ returndata
+
+
+[docs]
+ @classmethod
+ deffrom_acq_h5(cls,acq_files,start=None,stop=None,**kwargs):
+"""Convert acquisition format hdf5 data to analysis data object.
+
+ This method overloads the one in BaseData.
+
+ Changed Jan. 22, 2016: input arguments are now ``(acq_files, start,
+ stop, **kwargs)`` instead of ``(acq_files, start, stop, prod_sel,
+ freq_sel, datasets, out_group)``.
+
+ Reads hdf5 data produced by the acquisition system and converts it to
+ analysis format in memory.
+
+ Parameters
+ ----------
+ acq_files : filename, `h5py.File` or list there-of or filename pattern
+ Files to convert from acquisition format to analysis format.
+ Filename patterns with wild cards (e.g. "foo*.h5") are supported.
+ start : integer, optional
+ What frame to start at in the full set of files.
+ stop : integer, optional
+ What frame to stop at in the full set of files.
+ stack_sel : valid numpy index
+ Used to select a subset of the stacked correlation products.
+ Only one of *stack_sel*, *prod_sel*, and *input_sel* may be
+ specified, with *prod_sel* preferred over *input_sel* and
+ *stack_sel* proferred over both.
+ :mod:`h5py` fancy indexing supported but to be used with caution
+ due to poor reading performance.
+ prod_sel : valid numpy index
+ Used to select a subset of correlation products.
+ Only one of *stack_sel*, *prod_sel*, and *input_sel* may be
+ specified, with *prod_sel* preferred over *input_sel* and
+ *stack_sel* proferred over both.
+ :mod:`h5py` fancy indexing supported but to be used with caution
+ due to poor reading performance.
+ input_sel : valid numpy index
+ Used to select a subset of correlator inputs.
+ Only one of *stack_sel*, *prod_sel*, and *input_sel* may be
+ specified, with *prod_sel* preferred over *input_sel* and
+ *stack_sel* proferred over both.
+ :mod:`h5py` fancy indexing supported but to be used with caution
+ due to poor reading performance.
+ freq_sel : valid numpy index
+ Used to select a subset of frequencies.
+ :mod:`h5py` fancy indexing supported but to be used with caution
+ due to poor reading performance.
+ datasets : list of strings
+ Names of datasets to include from acquisition files. Default is to
+ include all datasets found in the acquisition files.
+ out_group : `h5py.Group`, hdf5 filename or `memh5.Group`
+ Underlying hdf5 like container that will store the data for the
+ BaseData instance.
+ apply_gain : boolean, optional
+ Whether to apply the inverse gains to the visibility datasets.
+ renormalize : boolean, optional
+ Whether to renormalize for dropped packets.
+ distributed : boolean, optional
+ Load data into a distributed dataset.
+ comm : MPI.Comm
+ Communicator to distributed over. Use MPI.COMM_WORLD if not set.
+
+ Returns
+ -------
+ data : CorrData
+ Loaded data object.
+
+ Examples
+ --------
+
+ Suppose we have two acquisition format files (this test data is
+ included in the ch_util repository):
+
+ >>> import os
+ >>> import glob
+ >>> from . import test_andata
+ >>> os.chdir(test_andata.data_path)
+ >>> print(glob.glob('test_acq.h5*'))
+ ['test_acq.h5.0001', 'test_acq.h5.0002']
+
+ These can be converted into one big analysis format data object:
+
+ >>> data = CorrData.from_acq_h5('test_acq.h5*')
+ >>> print(data.vis.shape)
+ (1024, 36, 31)
+
+ If we only want a subset of the total frames (time bins) in these files
+ we can supply start and stop indices.
+
+ >>> data = CorrData.from_acq_h5('test_acq.h5*', start=5, stop=-3)
+ >>> print(data.vis.shape)
+ (1024, 36, 23)
+
+ If we want a subset of the correlation products or spectral
+ frequencies, specify the *prod_sel* or *freq_sel* respectively:
+
+ >>> data = CorrData.from_acq_h5(
+ ... 'test_acq.h5*',
+ ... prod_sel=[0, 8, 15, 21],
+ ... freq_sel=slice(5, 15),
+ ... )
+ >>> print(data.vis.shape)
+ (10, 4, 31)
+ >>> data = CorrData.from_acq_h5('test_acq.h5*', prod_sel=1,
+ ... freq_sel=slice(None, None, 10))
+ >>> print(data.vis.shape)
+ (103, 1, 31)
+
+ The underlying hdf5-like container that holds the *analysis format*
+ data can also be specified.
+
+ >>> group = memh5.MemGroup()
+ >>> data = CorrData.from_acq_h5('test_acq.h5*', out_group=group)
+ >>> print(group['vis'].shape)
+ (1024, 36, 31)
+ >>> group['vis'] is data.vis
+ True
+
+ """
+
+ stack_sel=kwargs.pop("stack_sel",None)
+ prod_sel=kwargs.pop("prod_sel",None)
+ input_sel=kwargs.pop("input_sel",None)
+ freq_sel=kwargs.pop("freq_sel",None)
+ datasets=kwargs.pop("datasets",None)
+ out_group=kwargs.pop("out_group",None)
+ apply_gain=kwargs.pop("apply_gain",True)
+ renormalize=kwargs.pop("renormalize",True)
+ distributed=kwargs.pop("distributed",False)
+ comm=kwargs.pop("comm",None)
+
+ ifkwargs:
+ msg="Received unknown keyword arguments {}."
+ raiseValueError(msg.format(kwargs.keys()))
+
+ # If want a distributed file, just pass straight off to a private method
+ ifdistributed:
+ returncls._from_acq_h5_distributed(
+ acq_files=acq_files,
+ start=start,
+ stop=stop,
+ datasets=datasets,
+ stack_sel=stack_sel,
+ prod_sel=prod_sel,
+ input_sel=input_sel,
+ freq_sel=freq_sel,
+ apply_gain=apply_gain,
+ renormalize=renormalize,
+ comm=comm,
+ )
+
+ returnsuper(CorrData,cls).from_acq_h5(
+ acq_files=acq_files,
+ start=start,
+ stop=stop,
+ datasets=datasets,
+ out_group=out_group,
+ stack_sel=stack_sel,
+ prod_sel=prod_sel,
+ input_sel=input_sel,
+ freq_sel=freq_sel,
+ apply_gain=apply_gain,
+ renormalize=renormalize,
+ )
+
+
+ @classmethod
+ def_from_acq_h5_distributed(
+ cls,
+ acq_files,
+ start,
+ stop,
+ stack_sel,
+ prod_sel,
+ input_sel,
+ freq_sel,
+ datasets,
+ apply_gain,
+ renormalize,
+ comm,
+ ):
+ frommpi4pyimportMPI
+ fromcaputimportmpiutil,mpiarray,memh5
+
+ # Turn into actual list of files
+ files=tod.ensure_file_list(acq_files)
+
+ # Construct communicator to use.
+ ifcommisNone:
+ comm=MPI.COMM_WORLD
+
+ # Determine the total number of frequencies
+ nfreq=None
+ ifcomm.rank==0:
+ withh5py.File(files[0],"r")asf:
+ nfreq=len(f["index_map/freq"][:])
+ nfreq=comm.bcast(nfreq,root=0)
+
+ # Calculate the global frequency selection
+ freq_sel=_ensure_1D_selection(freq_sel)
+ ifisinstance(freq_sel,slice):
+ freq_sel=list(range(*freq_sel.indices(nfreq)))
+ nfreq=len(freq_sel)
+
+ # Calculate the local frequency selection
+ n_local,f_start,f_end=mpiutil.split_local(nfreq)
+ local_freq_sel=_ensure_1D_selection(
+ _convert_to_slice(freq_sel[f_start:f_end])
+ )
+
+ # Load just the local part of the data.
+ local_data=super(CorrData,cls).from_acq_h5(
+ acq_files=acq_files,
+ start=start,
+ stop=stop,
+ datasets=datasets,
+ out_group=None,
+ stack_sel=stack_sel,
+ prod_sel=prod_sel,
+ input_sel=input_sel,
+ freq_sel=local_freq_sel,
+ apply_gain=apply_gain,
+ renormalize=renormalize,
+ )
+
+ # Datasets that we should convert into distribute ones
+ _DIST_DSETS=[
+ "vis",
+ "vis_flag",
+ "vis_weight",
+ "gain",
+ "gain_coeff",
+ "frac_lost",
+ "dataset_id",
+ "eval",
+ "evec",
+ "erms",
+ ]
+
+ # Initialise distributed container
+ data=CorrData(distributed=True,comm=comm)
+
+ # Copy over the attributes
+ memh5.copyattrs(
+ local_data.attrs,data.attrs,convert_strings=cls.convert_attribute_strings
+ )
+
+ # Iterate over the datasets and copy them over
+ forname,old_dsetinlocal_data.datasets.items():
+ # If this should be distributed, extract the sections and turn them into an MPIArray
+ ifnamein_DIST_DSETS:
+ array=mpiarray.MPIArray.wrap(old_dset._data,axis=0,comm=comm)
+ else:
+ # Otherwise just copy out the old dataset
+ array=old_dset[:]
+
+ # Create the new dataset and copy over attributes
+ new_dset=data.create_dataset(name,data=array)
+ memh5.copyattrs(
+ old_dset.attrs,
+ new_dset.attrs,
+ convert_strings=cls.convert_attribute_strings,
+ )
+
+ # Iterate over the flags and copy them over
+ forname,old_dsetinlocal_data.flags.items():
+ # If this should be distributed, extract the sections and turn them into an MPIArray
+ ifnamein_DIST_DSETS:
+ array=mpiarray.MPIArray.wrap(old_dset._data,axis=0,comm=comm)
+ else:
+ # Otherwise just copy out the old dataset
+ array=old_dset[:]
+
+ # Create the new dataset and copy over attributes
+ new_dset=data.create_flag(name,data=array)
+ memh5.copyattrs(
+ old_dset.attrs,
+ new_dset.attrs,
+ convert_strings=cls.convert_attribute_strings,
+ )
+
+ # Copy over index maps
+ forname,index_mapinlocal_data.index_map.items():
+ # Get reference to actual array
+ index_map=index_map[:]
+
+ # We need to explicitly stitch the frequency map back together
+ ifname=="freq":
+ # Gather all frequencies onto all nodes and stich together
+ freq_gather=comm.allgather(index_map)
+ index_map=np.concatenate(freq_gather)
+
+ # Create index map
+ data.create_index_map(name,index_map)
+ memh5.copyattrs(local_data.index_attrs[name],data.index_attrs[name])
+
+ # Copy over reverse maps
+ forname,reverse_mapinlocal_data.reverse_map.items():
+ # Get reference to actual array
+ reverse_map=reverse_map[:]
+
+ # Create index map
+ data.create_reverse_map(name,reverse_map)
+
+ returndata
+
+
+[docs]
+ @classmethod
+ deffrom_acq_h5_fast(cls,fname,comm=None,freq_sel=None,start=None,stop=None):
+"""Efficiently read a CorrData file in a distributed fashion.
+
+ This reads a single file from disk into a distributed container. In
+ contrast to to `CorrData.from_acq_h5` it is more restrictive,
+ allowing only contiguous slices of the frequency and time axes,
+ and no down selection of the input/product/stack axis.
+
+ Parameters
+ ----------
+ fname : str
+ File name to read. Only supports one file at a time.
+ comm : MPI.Comm, optional
+ MPI communicator to distribute over. By default this will
+ use `MPI.COMM_WORLD`.
+ freq_sel : slice, optional
+ A selection over the frequency axis. Only `slice` objects
+ are supported. If not set, read all frequencies.
+ start, stop : int, optional
+ Start and stop indexes of the time selection.
+
+ Returns
+ -------
+ data : andata.CorrData
+ The CorrData container.
+ """
+ frommpi4pyimportMPI
+ fromcaputimportmisc,mpiarray,memh5
+
+ ## Datasets to read, if it's not listed here, it's not read at all
+ # Datasets read by andata (should be small)
+ DSET_CORE=["flags/inputs","flags/frac_lost","flags/dataset_id"]
+ # Datasets read directly and then inserted after the fact
+ # (should have an input/product/stack axis, as axis=1)
+ DSETS_DIRECT=["vis","gain","flags/vis_weight"]
+
+ ifcommisNone:
+ comm=MPI.COMM_WORLD
+
+ # Check the frequency selection
+ iffreq_selisNone:
+ freq_sel=slice(None)
+ ifnotisinstance(freq_sel,slice):
+ raiseValueError("freq_sel must be a slice object, not %s"%repr(freq_sel))
+
+ # Create the time selection
+ time_sel=slice(start,stop)
+
+ # Read the core dataset directly
+ ad=cls.from_acq_h5(
+ fname,
+ datasets=DSET_CORE,
+ distributed=True,
+ comm=comm,
+ freq_sel=freq_sel,
+ start=start,
+ stop=stop,
+ )
+
+ archive_version=memh5.bytes_to_unicode(ad.attrs["archive_version"])
+ ifversiontuple(archive_version)<versiontuple("3.0.0"):
+ raiseValueError("Fast read not supported for files with version < 3.0.0")
+
+ # Specify the selection to read from the file
+ sel=(freq_sel,slice(None),time_sel)
+
+ withmisc.open_h5py_mpi(fname,"r",comm=comm)asfh:
+ fords_nameinDSETS_DIRECT:
+ ifds_namenotinfh:
+ continue
+
+ # Read dataset directly (distributed over input/product/stack axis) and
+ # add to container
+ arr=mpiarray.MPIArray.from_hdf5(
+ fh,ds_name,comm=comm,axis=1,sel=sel
+ )
+ arr=arr.redistribute(axis=0)
+ dset=ad.create_dataset(ds_name,data=arr,distributed=True)
+
+ # Copy over the attributes
+ memh5.copyattrs(
+ fh[ds_name].attrs,
+ dset.attrs,
+ convert_strings=cls.convert_attribute_strings,
+ )
+
+ returnad
+[docs]
+classHKData(BaseData):
+"""Subclass of :class:`BaseData` for housekeeping data."""
+
+ @property
+ defatmel(self):
+"""Get the ATMEL board that took these data.
+
+ Returns
+ -------
+ comp : :obj:`layout.component`
+ The ATMEL component that took these data.
+ """
+ try:
+ from.importlayout
+ exceptValueError:
+ from.importlayout
+
+ sn="ATMEGA"+"".join([str(i)foriinself.attrs["atmel_id"]])
+ returnlayout.component.get(sn=sn)
+
+ @property
+ defmux(self):
+"""Get the list of muxes in the data."""
+ try:
+ returnself._mux
+ exceptAttributeError:
+ self._mux=[]
+ fordummy,dinself.datasets.items():
+ self._mux.append(d.attrs["mux_address"][0])
+ self._mux=np.sort(self._mux)
+ returnself._mux
+
+ @property
+ defnmux(self):
+"""Get the number of muxes in the data."""
+ returnlen(self.mux)
+
+ def_find_mux(self,mux):
+ fordummy,dinself.datasets.items():
+ ifd.attrs["mux_address"]==mux:
+ returnd
+ raiseValueError("No dataset with mux = %d is present."%(mux))
+
+
+[docs]
+ defchan(self,mux=-1):
+"""Convenience access to the list of channels in a given mux.
+
+ Parameters
+ ----------
+ mux : int
+ A mux number. For housekeeping files with no multiplexing (e.g.,
+ FLA's), leave this as ``-1``.
+
+ Returns
+ -------
+ n : list
+ The channels numbers.
+
+ Raises
+ ------
+ :exc:`ValueError`
+ Raised if **mux** does not exist.
+ """
+ try:
+ self._chan
+ exceptAttributeError:
+ self._chan=dict()
+ try:
+ returnself._chan[mux]
+ exceptKeyError:
+ ds=self._find_mux(mux)
+ # chan_map = ds.attrs["axis"][0]
+ self._chan[mux]=list(self.index_map[ds.attrs["axis"][0]])
+ returnself._chan[mux]
+
+
+
+[docs]
+ defnchan(self,mux=-1):
+"""Convenience access to the number of channels in a given mux.
+
+ Parameters
+ ----------
+ mux : int
+ A mux number. For housekeeping files with no multiplexing (e.g.,
+ FLA's), leave this as ``-1``.
+
+ Returns
+ -------
+ n : int
+ The number of channels
+
+ Raises
+ ------
+ :exc:`ValueError`
+ Raised if **mux** does not exist.
+ """
+ returnlen(self.chan(mux))
+
+
+
+[docs]
+ deftod(self,chan,mux=-1):
+"""Convenience access to a single time-ordered datastream (TOD).
+
+ Parameters
+ ----------
+ chan : int
+ A channel number. (Generally, they should be in the range 0--7 for
+ non-multiplexed data and 0--15 for multiplexed data.)
+ mux : int
+ A mux number. For housekeeping files with no multiplexing (e.g.,
+ FLA's), leave this as ``-1``.
+
+ Returns
+ -------
+ tod : :obj:`numpy.array`
+ A 1D array of values for the requested channel/mux combination. Note
+ that a reference to the data in the dataset is returned; this method
+ does not make a copy.
+
+ Raises
+ ------
+ :exc:`ValueError`
+ Raised if one of **chan** or **mux** is not present in any dataset.
+ """
+ ds=self._find_mux(mux)
+ chan_map=ds.attrs["axis"][0]
+ try:
+ idx=list(self.index_map[chan_map]).index(chan)
+ exceptKeyError:
+ raiseValueError("No channel %d exists for mux %d."%(chan,mux))
+
+ # Return the data.
+ returnds[idx,:]
+
+
+ @classmethod
+ def_interpret_and_read(cls,acq_files,start,stop,datasets,out_group):
+ # Save a reference to the first file to get index map information for
+ # later.
+ f_first=acq_files[0]
+
+ # Define dataset filter to do the transpose.
+ defdset_filter(dataset):
+ name=path.split(dataset.name)[1]
+ match=False
+ forregexinHK_DATASET_NAMES:
+ ifre.match(re.compile(regex),name):
+ match=True
+ ifmatch:
+ # Do the transpose.
+ data=np.empty((len(dataset[0]),len(dataset)),dtype=dataset[0].dtype)
+ data=memh5.MemDatasetCommon.from_numpy_array(data)
+ foriinrange(len(dataset)):
+ forjinrange(len(dataset[i])):
+ data[j,i]=dataset[i][j]
+ memh5.copyattrs(
+ dataset.attrs,data.attrs,convert_strings=cls.convert_attribute_strings
+ )
+ data.attrs["axis"]=(dataset.attrs["axis"][1],"time")
+ returndata
+
+ andata_objs=[HKData(d)fordinacq_files]
+ data=concatenate(
+ andata_objs,
+ out_group=out_group,
+ start=start,
+ stop=stop,
+ datasets=datasets,
+ dataset_filter=dset_filter,
+ convert_attribute_strings=cls.convert_attribute_strings,
+ convert_dataset_strings=cls.convert_dataset_strings,
+ )
+
+ # Some index maps saved as attributes, so convert to datasets.
+ fork,vinf_first["index_map"].attrs.items():
+ data.create_index_map(k,v)
+ returndata
+
+
+[docs]
+ @classmethod
+ deffrom_acq_h5(
+ cls,acq_files,start=None,stop=None,datasets=None,out_group=None
+ ):
+"""Convert acquisition format hdf5 data to analysis data object.
+
+ This method overloads the one in BaseData.
+
+ Reads hdf5 data produced by the acquisition system and converts it to
+ analysis format in memory.
+
+ Parameters
+ ----------
+ acq_files : filename, `h5py.File` or list there-of or filename pattern
+ Files to convert from acquisition format to analysis format.
+ Filename patterns with wild cards (e.g. "foo*.h5") are supported.
+ start : integer, optional
+ What frame to start at in the full set of files.
+ stop : integer, optional
+ What frame to stop at in the full set of files.
+ datasets : list of strings
+ Names of datasets to include from acquisition files. Default is to
+ include all datasets found in the acquisition files.
+ out_group : `h5py.Group`, hdf5 filename or `memh5.Group`
+ Underlying hdf5 like container that will store the data for the
+ BaseData instance.
+
+ Examples
+ --------
+ Examples are analogous to those of :meth:`CorrData.from_acq_h5`.
+ """
+ returnsuper(HKData,cls).from_acq_h5(
+ acq_files=acq_files,
+ start=start,
+ stop=stop,
+ datasets=datasets,
+ out_group=out_group,
+ )
+
+
+
+
+
+[docs]
+classHKPData(memh5.MemDiskGroup):
+"""Subclass of :class:`BaseData` for housekeeping data."""
+
+ # Convert strings to/from unicode on load and save
+ convert_attribute_strings=True
+ convert_dataset_strings=True
+
+
+[docs]
+ @staticmethod
+ defmetrics(acq_files):
+"""Get the names of the metrics contained within the files.
+
+ Parameters
+ ----------
+ acq_files: list
+ List of acquisition filenames.
+
+ Returns
+ -------
+ metrics : list
+ """
+
+ importh5py
+
+ metric_names=set()
+
+ ifisinstance(acq_files,str):
+ acq_files=[acq_files]
+
+ forfnameinacq_files:
+ withh5py.File(fname,"r")asfh:
+ metric_names|=set(fh.keys())
+
+ returnmetric_names
+
+
+
+[docs]
+ @classmethod
+ deffrom_acq_h5(
+ cls,acq_files,start=None,stop=None,metrics=None,datasets=None,**kwargs
+ ):
+"""Load in the housekeeping files.
+
+ Parameters
+ ----------
+ acq_files : list
+ List of files to load.
+ start, stop : datetime or float, optional
+ Start and stop times for the range of data to load. Default is all.
+ metrics : list
+ Names of metrics to load. Default is all.
+ datasets : list
+ Synonym for metrics (the value of metrics will take precedence).
+
+
+ Returns
+ -------
+ data : HKPData
+ """
+
+ fromcaputimporttimeasctime
+
+ metrics=metricsifmetricsisnotNoneelsedatasets
+
+ if"mode"notinkwargs:
+ kwargs["mode"]="r"
+ if"ondisk"notinkwargs:
+ kwargs["ondisk"]=True
+
+ acq_files=[acq_files]ifisinstance(acq_files,str)elseacq_files
+ files=[
+ cls.from_file(
+ f,
+ convert_attribute_strings=cls.convert_attribute_strings,
+ convert_dataset_strings=cls.convert_dataset_strings,
+ **kwargs
+ )
+ forfinacq_files
+ ]
+
+ deffilter_time_range(dset):
+"""Trim dataset to the specified time range."""
+ data=dset[:]
+ time=data["time"]
+
+ mask=np.ones(time.shape,dtype=bool)
+
+ ifstartisnotNone:
+ tstart=ctime.ensure_unix(start)
+ mask[:]*=time>=tstart
+
+ ifstopisnotNone:
+ tstop=ctime.ensure_unix(stop)
+ mask[:]*=time<=tstop
+
+ returndata[mask]
+
+ deffilter_file(f):
+"""Filter a file's data down to the requested metrics
+ and time range.
+ """
+ metrics_to_copy=set(f.keys())
+
+ ifmetricsisnotNone:
+ metrics_to_copy=metrics_to_copy&set(metrics)
+
+ filtered_data={}
+ fordset_nameinmetrics_to_copy:
+ filtered_data[dset_name]=filter_time_range(f[dset_name])
+ returnfiltered_data
+
+ defget_full_dtype(dset_name,filtered_data):
+"""Returns a numpy.dtype object with the union of all columns
+ from all files. Also returns the total length of the data set
+ (metric) including all files.
+ """
+
+ length=0
+ all_columns=[]
+ all_types=[]
+ # review number of times and columns:
+ foriiinrange(len(filtered_data)):
+ # If this file has this data set:
+ ifdset_namenotinfiltered_data[ii]:
+ continue
+ # Increase the length of the data:
+ length+=len(filtered_data[ii][dset_name])
+ # Add 'time' and 'value' columns first:
+ if"time"notinall_columns:
+ all_columns.append("time")
+ all_types.append(filtered_data[ii][dset_name].dtype["time"])
+ if"value"notinall_columns:
+ all_columns.append("value")
+ all_types.append(filtered_data[ii][dset_name].dtype["value"])
+ # Add new column if any:
+ forcolinfiltered_data[ii][dset_name].dtype.names:
+ ifcolnotinall_columns:
+ all_columns.append(col)
+ all_types.append(filtered_data[ii][dset_name].dtype[col])
+
+ data_dtype=np.dtype(
+ [(all_columns[ii],all_types[ii])foriiinrange(len(all_columns))]
+ )
+
+ returndata_dtype,length
+
+ defget_full_attrs(dset_name,files):
+"""Creates a 'full_attrs' dictionary of all attributes and all
+ possible values they can take, from all the files, for a
+ particular data set (metric). Also returns an 'index_remap'
+ list of dictionaries to remap indices of values in different
+ files.
+ """
+
+ full_attrs={}# Dictionary of attributes
+ index_remap=[]# List of dictionaries (one per file)
+ forii,flinenumerate(files):
+ ifdset_namenotinfl:
+ continue
+ index_remap.append({})# List of dictionaries (one per file)
+ foratt,valuesinfl[dset_name].attrs.items():
+ # Reserve zeroeth entry for N/A
+ index_remap[ii][att]=np.zeros(len(values)+1,dtype=int)
+ ifattnotinfull_attrs:
+ full_attrs[att]=[]
+ foridx,valinenumerate(values):
+ ifvalnotinfull_attrs[att]:
+ full_attrs[att]=np.append(full_attrs[att],val)
+ # Index of idx'th val in full_attrs[att]:
+ new_idx=np.where(full_attrs[att]==val)[0][0]
+ # zero is for N/A:
+ index_remap[ii][att][idx+1]=new_idx+1
+
+ returnfull_attrs,index_remap
+
+ defget_full_data(length,data_dtype,index_remap,filtered_data,dset_name):
+"""Returns the full data matrix as a structured array. Values are
+ modified when necessary acording to 'index_remap' to correspond
+ to the final positions in the 'full_attrs'.
+ """
+
+ full_data=np.zeros(length,data_dtype)
+
+ curr_ent=0# Current entry we are in the full data file
+ foriiinrange(len(filtered_data)):
+ len_fl=len(filtered_data[ii][dset_name])
+ curr_slice=np.s_[curr_ent:curr_ent+len_fl]
+ ifdset_namenotinfiltered_data[ii]:
+ continue
+ forattindata_dtype.names:
+ # Length of this file:
+ ifattin["time","value"]:
+ # No need to remap values:
+ full_data[att][curr_slice]=filtered_data[ii][dset_name][att]
+ elifattinindex_remap[ii]:
+ # Needs remapping values
+ # (need to remove 1 beause indices are 1-based):
+ full_data[att][curr_slice]=index_remap[ii][att][
+ filtered_data[ii][dset_name][att]
+ ]
+ else:
+ # Column not in file. Fill with zeros:
+ full_data[att][curr_slice]=np.zeros(len_fl)
+ # Update current entry value:
+ curr_ent=curr_ent+len_fl
+
+ returnfull_data
+
+ hkp_data=cls()
+
+ filtered_data=[]
+ forflinfiles:
+ filtered_data.append(filter_file(fl))
+
+ fordset_nameinmetrics:
+ data_dtype,length=get_full_dtype(dset_name,filtered_data)
+
+ # Create the full dictionary of all attributes:
+ full_attrs,index_remap=get_full_attrs(dset_name,files)
+
+ # Populate the data here.( Need full attrs)
+ full_data=get_full_data(
+ length,data_dtype,index_remap,filtered_data,dset_name
+ )
+ new_dset=hkp_data.create_dataset(dset_name,data=full_data)
+
+ # Populate attrs
+ foratt,valuesinfull_attrs.items():
+ new_dset.attrs[att]=memh5.bytes_to_unicode(values)
+
+ returnhkp_data
+[docs]
+ defresample(self,metric_name,rule,how="mean",unstack=False,**kwargs):
+"""Resample the metric onto a regular grid of time.
+
+ This internally uses the Pandas resampling functionality so that
+ documentation is a useful reference. This will return the metric with
+ the labels as a series of multi-level columns.
+
+ Parameters
+ ----------
+ metric_name : str
+ Name of metric to resample.
+ rule : str
+ The set of times to resample onto (example '30S', '1Min', '2D'). See
+ the pandas docs for a full description.
+ how : str or callable, optional
+ How should we combine samples to regrid the data? This takes any
+ valid argument for the the pandas apply method. Useful options are
+ `'mean'`, `'sum'`, `'min'`, `'max'` and `'std'`.
+ unstack : bool, optional
+ Unstack the data, i.e. return with the labels as hierarchial columns.
+ kwargs
+ Any remaining kwargs are passed to the `pandas.DataFrame.resample`
+ method to give fine grained control of the resampling.
+
+ Returns
+ -------
+ df : pandas.DataFrame
+ A dataframe resampled onto a regular grid. Labels now appear as part
+ of multi-level columns.
+ """
+
+ df=self.select(metric_name)
+
+ group_columns=list(set(df.columns)-{"value"})
+
+ resampled_df=df.groupby(group_columns).resample(rule).apply(how)
+
+ ifunstack:
+ returnresampled_df.unstack(group_columns)
+ else:
+ returnresampled_df.reset_index(group_columns)
+
+
+
+
+
+[docs]
+classWeatherData(BaseData):
+"""Subclass of :class:`BaseData` for weather data."""
+
+ @property
+ deftime(self):
+"""Needs to be able to extrac times from both mingun_weather files
+ and chime_weather files.
+ """
+ if"time"inself.index_map:
+ returnself.index_map["time"]
+ else:
+ returnself.index_map["station_time_blockhouse"]
+
+ @property
+ deftemperature(self):
+"""For easy access to outside weather station temperature.
+ Needs to be able to extrac temperatures from both mingun_weather files
+ and chime_weather files.
+ """
+ if"blockhouse"inself.keys():
+ returnself["blockhouse"]["outTemp"]
+ else:
+ returnself["outTemp"]
+
+
+[docs]
+ defdataset_name_allowed(self,name):
+"""Permits datasets in the root and 'blockhouse' groups."""
+
+ parent_name,name=posixpath.split(name)
+ returnTrueifparent_name=="/"orparent_name=="/blockhouse"elseFalse
+
+
+
+[docs]
+ defgroup_name_allowed(self,name):
+"""Permits only the "blockhouse" group."""
+
+ returnTrueifname=="/blockhouse"elseFalse
+[docs]
+classGainFlagData(BaseData):
+"""Subclass of :class:`BaseData` for gain, digitalgain, and flag input acquisitions.
+
+ These acquisitions consist of a collection of updates to the real-time pipeline ordered
+ chronologically. In most cases the updates do not occur at a regular cadence.
+ The time that each update occured can be accessed via `self.index_map['update_time']`.
+ In addition, each update is given a unique update ID that can be accessed via
+ `self.datasets['update_id']` and can be searched using the `self.search_update_id` method.
+ """
+
+
+[docs]
+ defresample(self,dataset,timestamp,transpose=False):
+"""Return a dataset resampled at specific times.
+
+ Parameters
+ ----------
+ dataset : string
+ Name of the dataset to resample.
+ timestamp : `np.ndarray`
+ Unix timestamps.
+ transpose : bool
+ Tranpose the data such that time is the fastest varying axis.
+ By default time will be the slowest varying axis.
+
+ Returns
+ -------
+ data : np.ndarray
+ The dataset resampled at the desired times and transposed if requested.
+ """
+ index=self.search_update_time(timestamp)
+ dset=self.datasets[dataset][index]
+ iftranspose:
+ dset=np.moveaxis(dset,0,-1)
+
+ returndset
+
+
+
+[docs]
+ defsearch_update_time(self,timestamp):
+"""Find the index into the `update_time` axis that is valid for specific times.
+
+ For each time returns the most recent update the occured before that time.
+
+ Parameters
+ ----------
+ timestamp : `np.ndarray` of unix timestamp
+ Unix timestamps.
+
+ Returns
+ -------
+ index : `np.ndarray` of `dtype = int`
+ Index into the `update_time` axis that will yield values
+ that are valid for the requested timestamps.
+ """
+ timestamp=np.atleast_1d(timestamp)
+
+ ifnp.min(timestamp)<np.min(self.time):
+ raiseValueError(
+ "Cannot request timestamps before the earliest update_time."
+ )
+
+ dmax=np.max(timestamp)-np.max(self.time)
+ ifdmax>0.0:
+ msg=(
+ "Requested timestamps are after the latest update_time "
+ "by as much as %0.2f hours."%(dmax/3600.0,)
+ )
+ warnings.warn(msg)
+
+ index=np.digitize(timestamp,self.time,right=False)-1
+
+ returnindex
+
+
+
+[docs]
+ defsearch_update_id(self,pattern,is_regex=False):
+"""Find the index into the `update_time` axis corresponding to a particular `update_id`.
+
+ Parameters
+ ----------
+ pattern : str
+ The desired `update_id` or a glob pattern to search.
+ is_regex : bool
+ Set to True if `pattern` is a regular expression.
+
+ Returns
+ -------
+ index : `np.ndarray` of `dtype = int`
+ Index into the `update_time` axis that will yield all
+ updates whose `update_id` matches the requested pattern.
+ """
+ importfnmatch
+
+ ptn=patternifis_regexelsefnmatch.translate(pattern)
+ regex=re.compile(ptn)
+ index=np.array(
+ [iiforii,uidinenumerate(self.update_id[:])ifregex.match(uid)]
+ )
+ returnindex
+[docs]
+classFlagInputData(GainFlagData):
+"""Subclass of :class:`GainFlagData` for flaginput acquisitions."""
+
+ @property
+ defflag(self):
+"""Aliases the `flag` dataset."""
+ returnself.datasets["flag"]
+
+ @property
+ defsource_flags(self):
+"""Dictionary that allow look up of source flags based on source name."""
+ ifnothasattr(self,"_source_flags"):
+ out={}
+ forkk,keyinenumerate(self.index_map["source"]):
+ out[key]=self.datasets["source_flags"][:,kk,:]
+
+ self._source_flags=memh5.ro_dict(out)
+
+ returnself._source_flags
+
+
+[docs]
+ defget_source_index(self,source_name):
+"""Index into the `source` axis for a given source name."""
+ returnlist(self.index_map["source"]).index(source_name)
+
+
+
+
+
+[docs]
+classGainData(GainFlagData):
+"""Subclass of :class:`GainFlagData` for gain and digitalgain acquisitions."""
+
+ @property
+ deffreq(self):
+"""The spectral frequency axis as bin centres in MHz."""
+ returnself.index_map["freq"]["centre"]
+
+ @property
+ defnfreq(self):
+"""Number of frequency bins."""
+ returnlen(self.index_map["freq"])
+
+
+
+
+[docs]
+classCalibrationGainData(GainData):
+"""Subclass of :class:`GainData` for gain acquisitions."""
+
+ @property
+ defsource(self):
+"""Names of the sources of gains."""
+ returnself.index_map["source"]
+
+ @property
+ defnsource(self):
+"""Number of sources of gains."""
+ returnlen(self.index_map["source"])
+
+ @property
+ defgain(self):
+"""Aliases the `gain` dataset."""
+ returnself.datasets["gain"]
+
+ @property
+ defweight(self):
+"""Aliases the `weight` dataset."""
+ returnself.datasets["weight"]
+
+ @property
+ defsource_gains(self):
+"""Dictionary that allows look up of source gains based on source name."""
+ ifnothasattr(self,"_source_gains"):
+ out={}
+ forkk,keyinenumerate(self.index_map["source"]):
+ out[key]=self.datasets["source_gains"][:,kk,:]
+
+ self._source_gains=memh5.ro_dict(out)
+
+ returnself._source_gains
+
+ @property
+ defsource_weights(self):
+"""Dictionary that allows look up of source weights based on source name."""
+ ifnothasattr(self,"_source_weights"):
+ out={}
+ forkk,keyinenumerate(self.index_map["source"]):
+ out[key]=self.datasets["source_weights"][:,kk,:]
+
+ self._source_weights=memh5.ro_dict(out)
+
+ returnself._source_weights
+
+
+[docs]
+ defget_source_index(self,source_name):
+"""Index into the `source` axis for a given source name."""
+ returnlist(self.index_map["source"]).index(source_name)
+
+
+
+
+
+[docs]
+classDigitalGainData(GainData):
+"""Subclass of :class:`GainData` for digitalgain acquisitions."""
+
+ @property
+ defgain_coeff(self):
+"""The coefficient of the digital gain applied to the channelized data."""
+ returnself.datasets["gain_coeff"]
+
+ @property
+ defgain_exp(self):
+"""The exponent of the digital gain applied to the channelized data."""
+ returnself.datasets["gain_exp"]
+
+ @property
+ defcompute_time(self):
+"""Unix timestamp indicating when the digital gain was computed."""
+ returnself.datasets["compute_time"]
+
+ @property
+ defgain(self):
+"""The digital gain applied to the channelized data."""
+ returnself.datasets["gain_coeff"][:]*2.0**(
+ self.datasets["gain_exp"][:,np.newaxis,:]
+ )
+
+
+
+
+[docs]
+classBaseReader(tod.Reader):
+"""Provides high level reading of CHIME data.
+
+ You do not want to use this class, but rather one of its inherited classes
+ (:class:`CorrReader`, :class:`HKReader`, :class:`WeatherReader`).
+
+ Parses and stores meta-data from file headers allowing for the
+ interpretation and selection of the data without reading it all from disk.
+
+ Parameters
+ ----------
+ files : filename, `h5py.File` or list there-of or filename pattern
+ Files containing data. Filename patterns with wild cards (e.g.
+ "foo*.h5") are supported.
+ """
+
+ data_class=BaseData
+
+ def__init__(self,files):
+ # If files is a filename, or pattern, turn into list of files.
+ ifisinstance(files,str):
+ files=sorted(glob.glob(files))
+
+ self._data_empty=self.data_class.from_acq_h5(files,datasets=())
+
+ # Fetch all meta data.
+ time=self._data_empty.time
+ datasets=_get_dataset_names(files[0])
+
+ # Set the metadata attributes.
+ self._files=tuple(files)
+ self._time=time
+ self._datasets=datasets
+ # Set the default selections of the data.
+ self.time_sel=(0,len(self.time))
+ self.dataset_sel=datasets
+
+
+[docs]
+ defselect_time_range(self,start_time=None,stop_time=None):
+"""Sets :attr:`~Reader.time_sel` to include a time range.
+
+ The times from the samples selected will have bin centre timestamps
+ that are bracketed by the given *start_time* and *stop_time*.
+
+ Parameters
+ ----------
+ start_time : float or :class:`datetime.datetime`
+ If a float, this is a Unix/POSIX time. Affects the first element of
+ :attr:`~Reader.time_sel`. Default leaves it unchanged.
+ stop_time : float or :class:`datetime.datetime`
+ If a float, this is a Unix/POSIX time. Affects the second element
+ of :attr:`~Reader.time_sel`. Default leaves it unchanged.
+
+ """
+
+ super(BaseReader,self).select_time_range(
+ start_time=start_time,stop_time=stop_time
+ )
+
+
+
+[docs]
+ defread(self,out_group=None):
+"""Read the selected data.
+
+ Parameters
+ ----------
+ out_group : `h5py.Group`, hdf5 filename or `memh5.Group`
+ Underlying hdf5 like container that will store the data for the
+ BaseData instance.
+
+ Returns
+ -------
+ data : :class:`BaseData`
+ Data read from :attr:`~Reader.files` based on the selections given
+ in :attr:`~Reader.time_sel`, :attr:`~Reader.prod_sel`, and
+ :attr:`~Reader.freq_sel`.
+
+ """
+
+ returnself.data_class.from_acq_h5(
+ self.files,
+ start=self.time_sel[0],
+ stop=self.time_sel[1],
+ datasets=self.dataset_sel,
+ out_group=out_group,
+ )
+
+
+
+
+
+[docs]
+classCorrReader(BaseReader):
+"""Subclass of :class:`BaseReader` for correlator data."""
+
+ data_class=CorrData
+
+ def__init__(self,files):
+ super(CorrReader,self).__init__(files)
+ data_empty=self._data_empty
+ prod=data_empty.prod
+ freq=data_empty.index_map["freq"]
+ input=data_empty.index_map["input"]
+ self._input=input
+ self._prod=prod
+ self._freq=freq
+ self.prod_sel=None
+ self.input_sel=None
+ self.freq_sel=None
+ # Create apply_gain and renormalize attributes,
+ # which are passed to CorrData.from_acq_h5() when
+ # the read() method is called. This gives the
+ # user the ability to turn off apply_gain and
+ # renormalize when using Reader.
+ self.apply_gain=True
+ self.renormalize=True
+ self.distributed=False
+ # Insert virtual 'gain' dataset if required parent datasets are present.
+ # We could be more careful about this, but I think this will always
+ # work.
+ datasets=self._datasets
+ # if ('gain_coeff' in datasets and 'gain_exp' in datasets):
+ datasets+=("gain",)
+ self._datasets=datasets
+ self.dataset_sel=datasets
+
+ # Properties
+ # ----------
+
+ @property
+ defprod(self):
+"""Correlation products in data files."""
+ returnself._prod[:].copy()
+
+ @property
+ definput(self):
+"""Correlator inputs in data files."""
+ returnself._input[:].copy()
+
+ @property
+ deffreq(self):
+"""Spectral frequency bin centres in data files."""
+ returnself._freq[:].copy()
+
+ @property
+ defprod_sel(self):
+"""Which correlation products to read.
+
+ Returns
+ -------
+ prod_sel : 1D data selection
+ Valid numpy index for a 1D array, specifying what data to read
+ along the correlation product axis.
+
+ """
+ returnself._prod_sel
+
+ @prod_sel.setter
+ defprod_sel(self,value):
+ ifvalueisnotNone:
+ # Check to make sure this is a valid index for the product axis.
+ self.prod["input_a"][value]
+ ifself.input_selisnotNone:
+ msg=(
+ "*input_sel* is set and cannot specify both *prod_sel*"
+ " and *input_sel*."
+ )
+ raiseValueError(msg)
+ self._prod_sel=value
+
+ @property
+ definput_sel(self):
+"""Which correlator intputs to read.
+
+ Returns
+ -------
+ input_sel : 1D data selection
+ Valid numpy index for a 1D array, specifying what data to read
+ along the correlation product axis.
+
+ """
+ returnself._input_sel
+
+ @input_sel.setter
+ definput_sel(self,value):
+ ifvalueisnotNone:
+ # Check to make sure this is a valid index for the product axis.
+ self.input["chan_id"][value]
+ ifself.prod_selisnotNone:
+ msg=(
+ "*prod_sel* is set and cannot specify both *prod_sel*"
+ " and *input_sel*."
+ )
+ raiseValueError(msg)
+ self._input_sel=value
+
+ @property
+ deffreq_sel(self):
+"""Which frequencies to read.
+
+ Returns
+ -------
+ freq_sel : 1D data selection
+ Valid numpy index for a 1D array, specifying what data to read
+ along the frequency axis.
+
+ """
+ returnself._freq_sel
+
+ @freq_sel.setter
+ deffreq_sel(self,value):
+ ifvalueisnotNone:
+ # Check to make sure this is a valid index for the frequency axis.
+ self.freq["centre"][value]
+ self._freq_sel=value
+
+ # Data Selection Methods
+ # ----------------------
+
+
+[docs]
+ defselect_prod_pairs(self,pairs):
+"""Sets :attr:`~Reader.prod_sel` to include given product pairs.
+
+ Parameters
+ ----------
+ pairs : list of integer pairs
+ Input pairs to be included.
+
+ """
+
+ sel=[]
+ forinput_a,input_binpairs:
+ foriiinrange(len(self.prod)):
+ p_input_a,p_input_b=self.prod[ii]
+ if(input_a==p_input_aandinput_b==p_input_b)or(
+ input_a==p_input_bandinput_b==p_input_a
+ ):
+ sel.append(ii)
+ self.prod_sel=sel
+
+
+
+[docs]
+ defselect_prod_autos(self):
+"""Sets :attr:`~Reader.prod_sel` to only auto-correlations."""
+
+ sel=[]
+ forii,prodinenumerate(self.prod):
+ ifprod[0]==prod[1]:
+ sel.append(ii)
+ self.prod_sel=sel
+
+
+
+[docs]
+ defselect_prod_by_input(self,input):
+"""Sets :attr:`~Reader.prod_sel` to only products with given input.
+
+ Parameters
+ ----------
+ input : integer
+ Correlator input number. All correlation products with
+ this input as one of the pairs are selected.
+
+ """
+
+ sel=[]
+ forii,prodinenumerate(self.prod):
+ ifprod[0]==inputorprod[1]==input:
+ sel.append(ii)
+ self.prod_sel=sel
+
+
+
+[docs]
+ defselect_freq_range(self,freq_low=None,freq_high=None,freq_step=None):
+"""Sets :attr:`~Reader.freq_sel` to given physical frequency range.
+
+ Frequencies selected will have bin centres bracked by provided range.
+
+ Parameters
+ ----------
+ freq_low : float
+ Lower end of the frequency range in MHz. Default is the lower edge
+ of the band.
+ freq_high : float
+ Upper end of the frequency range in MHz. Default is the upper edge
+ of the band.
+ freq_step : float
+ How much bandwidth to skip over between samples in MHz. This value
+ is approximate. Default is to include all samples in given range.
+
+ """
+
+ freq=self.freq["centre"]
+ nfreq=len(freq)
+ iffreq_stepisNone:
+ step=1
+ else:
+ df=abs(np.mean(np.diff(freq)))
+ step=int(freq_step//df)
+ # Noting that frequencies are reverse ordered in datasets.
+ iffreq_lowisNone:
+ stop=nfreq
+ else:
+ stop=np.where(freq<freq_low)[0][0]
+ iffreq_highisNone:
+ start=0
+ else:
+ start=np.where(freq<freq_high)[0][0]
+ # Slight tweak to behaviour if step is not unity, lining up edge on
+ # freq_low instead of freq_high.
+ start+=(stop-start-1)%step
+ self.freq_sel=np.s_[start:stop:step]
+
+
+
+[docs]
+ defselect_freq_physical(self,frequencies):
+"""Sets :attr:`~Reader.freq_sel` to include given physical frequencies.
+
+ Parameters
+ ----------
+ frequencies : list of floats
+ Frequencies to select. Physical frequencies are matched to indices
+ on a best match basis.
+
+ """
+
+ freq_centre=self.freq["centre"]
+ freq_width=self.freq["width"]
+ frequencies=np.array(frequencies)
+ n_sel=len(frequencies)
+ diff_freq=abs(freq_centre-frequencies[:,None])
+ match_mask=diff_freq<freq_width/2
+ freq_inds=[]
+ foriiinrange(n_sel):
+ matches=np.where(match_mask[ii,:])
+ try:
+ first_match=matches[0][0]
+ exceptIndexError:
+ msg="No match for frequency %f MHz."%frequencies[ii]
+ raiseValueError(msg)
+ freq_inds.append(first_match)
+ self.freq_sel=freq_inds
+
+
+ # Data Reading
+ # ------------
+
+
+[docs]
+ defread(self,out_group=None):
+"""Read the selected data.
+
+ Parameters
+ ----------
+ out_group : `h5py.Group`, hdf5 filename or `memh5.Group`
+ Underlying hdf5 like container that will store the data for the
+ BaseData instance.
+
+ Returns
+ -------
+ data : :class:`BaseData`
+ Data read from :attr:`~Reader.files` based on the selections given
+ in :attr:`~Reader.time_sel`, :attr:`~Reader.prod_sel`, and
+ :attr:`~Reader.freq_sel`.
+
+ """
+
+ dsets=tuple(self.dataset_sel)
+
+ # Add in virtual gain dataset
+ # This is done in earlier now, in self.datasets.
+ # if ('gain_coeff' in dsets and 'gain_exp' in dsets):
+ # dsets += ('gain',)
+
+ returnCorrData.from_acq_h5(
+ self.files,
+ start=self.time_sel[0],
+ stop=self.time_sel[1],
+ prod_sel=self.prod_sel,
+ freq_sel=self.freq_sel,
+ input_sel=self.input_sel,
+ apply_gain=self.apply_gain,
+ renormalize=self.renormalize,
+ distributed=self.distributed,
+ datasets=dsets,
+ out_group=out_group,
+ )
+[docs]
+classHKReader(BaseReader):
+"""Subclass of :class:`BaseReader` for HK data."""
+
+ data_class=HKData
+
+
+
+
+[docs]
+classHKPReader(BaseReader):
+"""Subclass of :class:`BaseReader` for HKP data."""
+
+ data_class=HKPData
+
+
+
+
+[docs]
+classWeatherReader(BaseReader):
+"""Subclass of :class:`BaseReader` for weather data."""
+
+ data_class=WeatherData
+
+
+
+
+[docs]
+classFlagInputReader(BaseReader):
+"""Subclass of :class:`BaseReader` for input flag data."""
+
+ data_class=FlagInputData
+
+
+
+
+[docs]
+classCalibrationGainReader(BaseReader):
+"""Subclass of :class:`BaseReader` for calibration gain data."""
+
+ data_class=CalibrationGainData
+
+
+
+
+[docs]
+classDigitalGainReader(BaseReader):
+"""Subclass of :class:`BaseReader` for digital gain data."""
+
+ data_class=DigitalGainData
+
+
+
+
+[docs]
+classRawADCReader(BaseReader):
+"""Subclass of :class:`BaseReader` for raw ADC data."""
+
+ data_class=RawADCData
+
+
+
+
+[docs]
+classAnDataError(Exception):
+"""Exception raised when something unexpected happens with the data."""
+
+ pass
+
+
+
+# Functions
+# ---------
+
+# In caput now.
+concatenate=tod.concatenate
+
+
+
+[docs]
+defsubclass_from_obj(cls,obj):
+"""Pick a subclass of :class:`BaseData` based on an input object.
+
+ Parameters
+ ----------
+ cls : subclass of :class:`BaseData` (class, not an instance)
+ Default class to return.
+ obj : :class:`h5py.Group`, filename, :class:`memh5.Group` or
+ :class:`BaseData` object from which to determine the appropriate
+ subclass of :class:`AnData`.
+
+ """
+ # If obj is a filename, open it and recurse.
+ ifisinstance(obj,str):
+ withh5py.File(obj,"r")asf:
+ cls=subclass_from_obj(cls,f)
+ returncls
+
+ new_cls=cls
+ acquisition_type=None
+ try:
+ acquisition_type=obj.attrs["acquisition_type"]
+ except(AttributeError,KeyError):
+ pass
+ ifacquisition_type=="corr":
+ new_cls=CorrData
+ elifacquisition_type=="hk":
+ new_cls=HKData
+ elifacquisition_typeisNone:
+ ifisinstance(obj,BaseData):
+ new_cls=obj.__class__
+ returnnew_cls
+
+
+
+# Private Functions
+# -----------------
+
+# Utilities
+
+
+def_open_files(files,opened):
+"""Ensure that files are open, keeping a record of what was done.
+
+ The arguments are modified in-place instead of returned, so that partial
+ work is recorded in the event of an error.
+
+ """
+
+ forii,this_fileinenumerate(list(files)):
+ # Sort out how to get an open hdf5 file.
+ open_file,was_opened=memh5.get_h5py_File(this_file,mode="r")
+ opened[ii]=was_opened
+ files[ii]=open_file
+
+
+def_ensure_1D_selection(selection):
+ ifisinstance(selection,tuple):
+ iflen(selection)!=1:
+ msg="Wrong number of indices."
+ raiseValueError(msg)
+ selection=selection[0]
+ ifselectionisNone:
+ selection=np.s_[:]
+ elifhasattr(selection,"__iter__"):
+ selection=np.array(selection)
+ elifisinstance(selection,slice):
+ pass
+ elifnp.issubdtype(type(selection),np.integer):
+ selection=np.s_[selection:selection+1]
+ else:
+ raiseValueError("Cannont be converted to a 1D selection.")
+
+ ifisinstance(selection,np.ndarray):
+ ifselection.ndim!=1:
+ msg="Data selections may only be one dimensional."
+ raiseValueError(msg)
+ # The following is more efficient and solves h5py issue #425. Converts
+ # to integer selection.
+ iflen(selection)==1:
+ return_ensure_1D_selection(selection[0])
+ ifnp.issubdtype(selection.dtype,np.integer):
+ ifnp.any(np.diff(selection)<=0):
+ raiseValueError("h5py requires sorted non-duplicate selections.")
+ elifnotnp.issubdtype(selection.dtype,bool):
+ raiseValueError("Array selections must be integer or boolean type.")
+ elifnp.issubdtype(selection.dtype,bool):
+ # This is a workaround for h5py/h5py#1750
+ selection=selection.nonzero()[0]
+
+ returnselection
+
+
+def_convert_to_slice(selection):
+ ifhasattr(selection,"__iter__")andlen(selection)>1:
+ uniq_step=np.unique(np.diff(selection))
+
+ if(len(uniq_step)==1)anduniq_step[0]:
+ a=selection[0]
+ b=selection[-1]
+ b=b+(1-(b<a)*2)
+
+ selection=slice(a,b,uniq_step[0])
+
+ returnselection
+
+
+def_get_dataset_names(f):
+ f,toclose=memh5.get_h5py_File(f,mode="r")
+ try:
+ dataset_names=()
+ fornameinf.keys():
+ ifnotmemh5.is_group(f[name]):
+ dataset_names+=(name,)
+ if"blockhouse"infandmemh5.is_group(f["blockhouse"]):
+ # chime_weather datasets are inside group "blockhouse"
+ fornameinf["blockhouse"].keys():
+ ifnotmemh5.is_group(f["blockhouse"][name]):
+ dataset_names+=("blockhouse/"+name,)
+ if"flags"infandmemh5.is_group(f["flags"]):
+ fornameinf["flags"].keys():
+ ifnotmemh5.is_group(f["flags"][name]):
+ dataset_names+=("flags/"+name,)
+ finally:
+ iftoclose:
+ f.close()
+ returndataset_names
+
+
+def_resolve_stack_prod_input_sel(
+ stack_sel,stack_map,stack_rmap,prod_sel,prod_map,input_sel,input_map
+):
+ nsels=(stack_selisnotNone)+(prod_selisnotNone)+(input_selisnotNone)
+ ifnsels>1:
+ raiseValueError(
+ "Only one of *stack_sel*, *input_sel*, and *prod_sel* may be specified."
+ )
+
+ ifnsels==0:
+ stack_sel=_ensure_1D_selection(stack_sel)
+ prod_sel=_ensure_1D_selection(prod_sel)
+ input_sel=_ensure_1D_selection(input_sel)
+ else:
+ ifprod_selisnotNone:
+ prod_sel=_ensure_1D_selection(prod_sel)
+ # Choose inputs involved in selected products.
+ input_sel=_input_sel_from_prod_sel(prod_sel,prod_map)
+ stack_sel=_stack_sel_from_prod_sel(prod_sel,stack_rmap)
+ elifinput_selisnotNone:
+ input_sel=_ensure_1D_selection(input_sel)
+ prod_sel=_prod_sel_from_input_sel(input_sel,input_map,prod_map)
+ stack_sel=_stack_sel_from_prod_sel(prod_sel,stack_rmap)
+ else:# stack_sel
+ stack_sel=_ensure_1D_selection(stack_sel)
+ prod_sel=_prod_sel_from_stack_sel(stack_sel,stack_map,stack_rmap)
+ input_sel=_input_sel_from_prod_sel(prod_sel,prod_map)
+
+ # Now we need to rejig the index maps for the subsets of the inputs,
+ # prods.
+ stack_inds=np.arange(len(stack_map),dtype=int)[stack_sel]
+ # prod_inds = np.arange(len(prod_map), dtype=int)[prod_sel] # never used
+ input_inds=np.arange(len(input_map),dtype=int)[input_sel]
+
+ stack_rmap=stack_rmap[prod_sel]
+ stack_rmap["stack"]=_search_array(stack_inds,stack_rmap["stack"])
+
+ # Remake stack map from scratch, since prod referenced in current stack
+ # map may have dissapeared.
+ stack_map=np.empty(len(stack_inds),dtype=stack_map.dtype)
+ stack_map["prod"]=_search_array(
+ stack_rmap["stack"],np.arange(len(stack_inds))
+ )
+ stack_map["conjugate"]=stack_rmap["conjugate"][stack_map["prod"]]
+
+ prod_map=prod_map[prod_sel]
+ pa=_search_array(input_inds,prod_map["input_a"])
+ pb=_search_array(input_inds,prod_map["input_b"])
+ prod_map["input_a"]=pa
+ prod_map["input_b"]=pb
+ input_map=input_map[input_sel]
+ returnstack_sel,stack_map,stack_rmap,prod_sel,prod_map,input_sel,input_map
+
+
+def_npissorted(arr):
+ returnnp.all(np.diff>=0)
+
+
+def_search_array(a,v):
+"""Find the indeces in array `a` of values in array 'v'.
+
+ Use algorithm that presorts `a`, efficient if `v` is long.
+
+ """
+ a_sort_inds=np.argsort(a,kind="mergesort")
+ a_sorted=a[a_sort_inds]
+ indeces_in_sorted=np.searchsorted(a_sorted,v)
+ # Make sure values actually present.
+ ifnotnp.all(v==a_sorted[indeces_in_sorted]):
+ raiseValueError("Element in 'v' not in 'a'.")
+ returna_sort_inds[indeces_in_sorted]
+
+
+def_input_sel_from_prod_sel(prod_sel,prod_map):
+ prod_map=prod_map[prod_sel]
+ input_sel=[]
+ forp0,p1inprod_map:
+ input_sel.append(p0)
+ input_sel.append(p1)
+ # ensure_1D here deals with h5py issue #425.
+ input_sel=_ensure_1D_selection(sorted(list(set(input_sel))))
+ returninput_sel
+
+
+def_prod_sel_from_input_sel(input_sel,input_map,prod_map):
+ inputs=list(np.arange(len(input_map),dtype=int)[input_sel])
+ prod_sel=[]
+ forii,pinenumerate(prod_map):
+ ifp[0]ininputsandp[1]ininputs:
+ prod_sel.append(ii)
+ # ensure_1D here deals with h5py issue #425.
+ prod_sel=_ensure_1D_selection(prod_sel)
+ returnprod_sel
+
+
+def_stack_sel_from_prod_sel(prod_sel,stack_rmap):
+ stack_sel=stack_rmap["stack"][prod_sel]
+ stack_sel=_ensure_1D_selection(sorted(list(set(stack_sel))))
+ returnstack_sel
+
+
+def_prod_sel_from_stack_sel(stack_sel,stack_map,stack_rmap):
+ stack_inds=np.arange(len(stack_map))[stack_sel]
+ stack_rmap_sort_inds=np.argsort(stack_rmap["stack"],kind="mergesort")
+ stack_rmap_sorted=stack_rmap["stack"][stack_rmap_sort_inds]
+ left_indeces=np.searchsorted(stack_rmap_sorted,stack_inds,side="left")
+ right_indeces=np.searchsorted(stack_rmap_sorted,stack_inds,side="right")
+ prod_sel=[]
+ foriiinrange(len(stack_inds)):
+ prod_sel.append(stack_rmap_sort_inds[left_indeces[ii]:right_indeces[ii]])
+ prod_sel=np.concatenate(prod_sel)
+ prod_sel=_ensure_1D_selection(sorted(list(set(prod_sel))))
+ returnprod_sel
+
+
+defversiontuple(v):
+ returntuple(map(int,(v.split("."))))
+
+
+# Calculations from data.
+
+
+def_renormalize(data):
+"""Correct vis and vis_weight for lost packets."""
+ fromch_utilimporttools
+
+ # Determine the datasets that need to be renormalized
+ datasets_to_renormalize=[
+ keyforkeyindata.datasetsifre.match(ACQ_VIS_DATASETS,key)
+ ]
+
+ ifnotdatasets_to_renormalize:
+ return
+
+ # Determine if we will correct vis_weight in addition to vis.
+ adjust_weight="vis_weight"indata.flags
+
+ # Extract number of packets expected
+ n_packets_expected=data.attrs["gpu.gpu_intergration_period"][0]
+
+ # Loop over frequencies to limit memory usage
+ forffinrange(data.nfreq):
+ # Calculate the fraction of packets received
+ weight_factor=1.0-data.flags["lost_packet_count"][ff]/float(
+ n_packets_expected
+ )
+
+ # Multiply vis_weight by fraction of packets received
+ ifadjust_weight:
+ data.flags["vis_weight"][ff]=np.round(
+ data.flags["vis_weight"][ff]*weight_factor[None,:]
+ )
+
+ # Divide vis by fraction of packets received
+ weight_factor=tools.invert_no_zero(weight_factor)
+
+ forkeyindatasets_to_renormalize:
+ data.datasets[key][ff]*=weight_factor[None,:]
+
+
+def_unwrap_fpga_counts(data):
+"""Unwrap 32-bit FPGA counts in a CorrData object."""
+
+ importdatetime
+
+ time_map=data.index_map["time"][:]
+
+ # If FPGA counts are already 64-bit then we don't need to unwrap
+ iftime_map["fpga_count"].dtype==np.uint64:
+ return
+
+ # Try and fetch out required attributes, if they are not there (which
+ # happens in older files), fill in the usual values
+ try:
+ nfreq=data.attrs["n_freq"][0]
+ samp_freq_MHz=data.attrs["fpga.samp_freq"][0]
+ exceptKeyError:
+ nfreq=1024
+ samp_freq_MHz=800.0
+
+ # Calculate the length of an FPGA count and the time it takes to wrap
+ seconds_per_count=2.0*nfreq/(samp_freq_MHz*1e6)
+ wrap_time=2**32.0*seconds_per_count
+
+ # Estimate the FPGA initial zero time from the timestamp in the acquisition
+ # name, if the acq name is not there, or of the correct format just silently return
+ try:
+ acq_name=data.attrs["acquisition_name"]
+ acq_dt=datetime.datetime.strptime(acq_name[:16],"%Y%m%dT%H%M%SZ")
+ except(KeyError,ValueError):
+ return
+ acq_start=CorrData.convert_time(acq_dt)
+
+ # Calculate the time that the count last wrapped
+ last_wrap=time_map["ctime"]-time_map["fpga_count"]*seconds_per_count
+
+ # Use this and the FPGA zero time to calculate the total number of wraps
+ num_wraps=np.round((last_wrap-acq_start)/wrap_time).astype(np.uint64)
+
+ # Correct the FPGA counts by adding on the counts lost by wrapping
+ fpga_corrected=time_map["fpga_count"]+num_wraps*2**32
+
+ # Create an array to represent the new time dataset, and fill in the corrected values
+ _time_dtype=[("fpga_count",np.uint64),("ctime",np.float64)]
+ new_time_map=np.zeros(time_map.shape,dtype=_time_dtype)
+ new_time_map["fpga_count"]=fpga_corrected
+ new_time_map["ctime"]=time_map["ctime"]
+
+ # Replace the time input map
+ data.del_index_map("time")
+ data.create_index_map("time",new_time_map)
+
+
+def_timestamp_from_fpga_cpu(cpu_s,cpu_us,fpga_counts):
+ ntime=len(cpu_s)
+ timestamp=np.empty(ntime,dtype=np.float64)
+ timestamp[:]=cpu_s
+ ifcpu_usisnotNone:
+ timestamp+=cpu_us/1.0e6
+ # If we have the more precise fpga clock, use it. Use the above to
+ # calibrate.
+ iffpga_countsisnotNone:
+ timestamp_cpu=timestamp.copy()
+ # Find discontinuities in the fpga_counts from wrapping.
+ d_fpga_counts=np.diff(fpga_counts.astype(np.int64))
+ (edge_inds,)=np.where(d_fpga_counts!=np.median(d_fpga_counts))
+ edge_inds=np.concatenate(([0],edge_inds+1,[ntime]))
+ # Calculate a global slope.
+ slope_num=0
+ slope_den=0
+ foriiinrange(len(edge_inds)-1):
+ sl=np.s_[edge_inds[ii]:edge_inds[ii+1]]
+ mean_cpu=np.mean(timestamp_cpu[sl])
+ mean_fpga=np.mean(fpga_counts[sl])
+ diff_cpu=timestamp_cpu[sl]-mean_cpu
+ diff_fpga=fpga_counts[sl]-mean_fpga
+ slope_num+=np.sum(diff_cpu*diff_fpga)
+ slope_den+=np.sum(diff_fpga**2)
+ slope=slope_num/slope_den
+ # Calculate offset in each section.
+ foriiinrange(len(edge_inds)-1):
+ sl=np.s_[edge_inds[ii]:edge_inds[ii+1]]
+ mean_cpu=np.mean(timestamp_cpu[sl])
+ mean_fpga=np.mean(fpga_counts[sl])
+ offset=mean_cpu-slope*mean_fpga
+ # Apply fit.
+ timestamp[sl]=slope*fpga_counts[sl]+offset
+ # XXX
+ # The above provides integration ends, not centres. Fix:
+ # delta = np.median(np.diff(timestamp))
+ # timestamp -= abs(delta) / 2.
+ returntimestamp
+
+
+# IO for acquisition format 1.0
+
+
+def_copy_dataset_acq1(
+ dataset_name,acq_files,start,stop,out_data,prod_sel=None,freq_sel=None
+):
+ s_ind=0
+ ntime=stop-start
+ forii,acqinenumerate(acq_files):
+ acq_dataset=acq[dataset_name]
+ this_ntime=len(acq_dataset)
+ ifs_ind+this_ntime<startors_ind>=stop:
+ # No data from this file is included.
+ s_ind+=this_ntime
+ continue
+ # What data (time frames) are included in this file.
+ # out_slice = np.s_[max(0, s_ind - start):s_ind - start + this_ntime]
+ # acq_slice = np.s_[max(0, start - s_ind):min(this_ntime, stop - s_ind)]
+ acq_slice,out_slice=tod._get_in_out_slice(start,stop,s_ind,this_ntime)
+ # Split the fields of the dataset into separate datasets and reformat.
+ split_dsets,split_dsets_cal=_format_split_acq_dataset_acq1(
+ acq_dataset,acq_slice
+ )
+ ifdataset_name=="vis":
+ # Convert to 64 but complex.
+ ifset(split_dsets.keys())!={"imag","real"}:
+ msg=(
+ "Visibilities should have fields 'real' and 'imag'"
+ " and instead have %s."%str(list(split_dsets.keys()))
+ )
+ raiseValueError(msg)
+ vis_data=np.empty(split_dsets["real"].shape,dtype=np.complex64)
+ vis_data.real[:]=split_dsets["real"]
+ vis_data.imag[:]=split_dsets["imag"]
+
+ split_dsets={"":vis_data}
+ split_dsets_cal={}
+
+ forsplit_dset_name,split_dsetinsplit_dsets.items():
+ ifprod_selisnotNone:# prod_sel could be 0.
+ # Do this in two steps to get around shape matching.
+ split_dset=split_dset[freq_sel,:,:]
+ split_dset=split_dset[:,prod_sel,:]
+ ifsplit_dset_name:
+ full_name=dataset_name+"_"+split_dset_name
+ else:
+ full_name=dataset_name
+ ifstart>=s_ind:
+ # First file, initialize output dataset.
+ shape=split_dset.shape[:-1]+(ntime,)
+ ifsplit_dset_nameinsplit_dsets_cal:
+ attrs={"cal":split_dsets_cal[split_dset_name]}
+ else:
+ attrs={}
+ # Try to figure out the axis names.
+ ifprod_selisnotNone:
+ # The shape of the visibilities.
+ attrs["axis"]=("freq","prod","time")
+ else:
+ ndim=len(shape)
+ attrs["axis"]=("UNKNOWN",)*(ndim-1)+("time",)
+ ds=out_data.create_dataset(
+ full_name,dtype=split_dset.dtype,shape=shape
+ )
+
+ # Copy over attributes
+ fork,vinattrs.items():
+ ds.attrs[k]=v
+ # Finally copy the data over.
+ out_data.datasets[full_name][...,out_slice]=split_dset[:]
+ s_ind+=this_ntime
+
+
+def_check_files_acq1(files):
+"""Gets a list of open hdf5 file objects and checks their consistency.
+
+ Checks that they all have the same datasets and that all datasets have
+ consistent data types.
+
+ Essential arguments are modified in-place instead of using return values.
+ This keeps the lists as up to date as possible in the event that an
+ exception is raised within this function.
+
+ Non-essential information is returned such as the dtypes for all the
+ datasets.
+
+ """
+
+ first_file=True
+ forii,open_fileinenumerate(list(files)):
+ # Sort out how to get an open hdf5 file.
+ # Check that all files have the same datasets with the same dtypes
+ # and consistent shape.
+ # All datasets in the same file must be the same shape.
+ # Between files, all datasets with the same name must have the same
+ # dtype.
+ this_dtypes={}
+ first_dset=True
+ forkeyinopen_file.keys():
+ ifnotmemh5.is_group(open_file[key]):
+ this_dtypes[key]=open_file[key].dtype
+ iffirst_dset:
+ this_dset_shape=open_file[key].shape
+ first_dset=False
+ else:
+ ifopen_file[key].shape!=this_dset_shape:
+ msg="Datasets in a file do not all have same shape."
+ raiseValueError(msg)
+ iffirst_file:
+ dtypes=this_dtypes
+ first_file=False
+ else:
+ ifthis_dtypes!=dtypes:
+ msg="Files do not have compatible datasets."
+ raiseValueError(msg)
+ returndtypes
+
+
+def_get_header_info_acq1(h5_file):
+ # Right now only have to deal with one format. In the future will need to
+ # deal with all different kinds of data.
+ header_info=_data_attrs_from_acq_attrs_acq1(h5_file.attrs)
+ # Now need to calculate the time stamps.
+ timestamp_data=h5_file["timestamp"]
+ ifnotlen(timestamp_data):
+ msg="Acquisition file contains zero frames"
+ raiseAnDataError(msg)
+ time=np.empty(
+ len(timestamp_data),dtype=[("fpga_count","<u4"),("ctime","<f8")]
+ )
+ time_upper_edges=_timestamp_from_fpga_cpu(
+ timestamp_data["cpu_s"],timestamp_data["cpu_us"],timestamp_data["fpga_count"]
+ )
+ time_lower_edges=time_upper_edges-np.median(np.diff(time_upper_edges))
+ time["ctime"]=time_lower_edges
+ time["fpga_count"]=timestamp_data["fpga_count"]
+ header_info["time"]=time
+ datasets=[keyforkeyinh5_file.keys()ifnotmemh5.is_group(h5_file[key])]
+ header_info["datasets"]=tuple(datasets)
+ returnheader_info
+
+
+def_resolve_header_info_acq1(header_info):
+ first_info=header_info[0]
+ freq=first_info["freq"]
+ prod=first_info["prod"]
+ datasets=first_info["datasets"]
+ time_list=[first_info["time"]]
+ forinfoinheader_info[1:]:
+ ifnotnp.allclose(info["freq"]["width"],freq["width"]):
+ msg="Files do not have consistent frequency bin widths."
+ raiseValueError(msg)
+ ifnotnp.allclose(info["freq"]["centre"],freq["centre"]):
+ msg="Files do not have consistent frequency bin centres."
+ raiseValueError(msg)
+ ifnotnp.all(info["prod"]==prod):
+ msg="Files do not have consistent correlation products."
+ raiseValueError(msg)
+ ifnotnp.all(info["datasets"]==datasets):
+ msg="Files do not have consistent data sets."
+ raiseValueError(msg)
+ time_list.append(info["time"])
+ time=np.concatenate(time_list)
+ returntime,prod,freq,datasets
+
+
+def_get_files_frames_acq1(files,start,stop):
+"""Counts the number of frames in each file and sorts out which frames to
+ read."""
+
+ dataset_name="vis"# For now just base everything off of 'vis'.
+ n_times=[]
+ forthis_fileinfiles:
+ # Make sure the dataset is 1D.
+ iflen(this_file[dataset_name].shape)!=1:
+ raiseValueError("Expected 1D datasets.")
+ n_times.append(len(this_file[dataset_name]))
+ n_time_total=np.sum(n_times)
+ returntod._start_stop_inds(start,stop,n_time_total)
+
+
+def_format_split_acq_dataset_acq1(dataset,time_slice):
+"""Formats a dataset from a acq h5 file into a more easily handled array.
+
+ Completely reverses the order of all axes.
+
+ """
+
+ # Get shape information.
+ ntime=len(dataset)
+ ntime_out=len(np.arange(ntime)[time_slice])
+ # If each record is an array, then get that shape.
+ back_shape=dataset[0].shape
+ # The shape of the output array.
+ reversed_back_shape=list(back_shape)
+ reversed_back_shape.reverse()
+ out_shape=tuple(reversed_back_shape)+(ntime_out,)
+ # Check if there are multiple data fields in this dataset. If so they will
+ # each end up in their own separate arrays.
+ ifdataset[0].dtype.fieldsisNone:
+ dtype=dataset[0].dtype
+ out=np.empty(out_shape,dtype=dtype)
+ forjj,iiinenumerate(np.arange(ntime)[time_slice]):
+ # 1D case is trivial.
+ ifnotback_shape:
+ out[jj]=dataset[ii]
+ eliflen(back_shape)==1:
+ out[:,jj]=dataset[ii]
+ else:
+ raiseNotImplementedError("Not done yet.")
+ # Otherwise, loop over all dimensions except the last one.
+ it=np.nditer(dataset[ii][...,0],flags=["multi_index"],order="C")
+ whilenotit.finished:
+ it.iternext()
+ if"cal"indataset.attrs:
+ iflen(dataset.attrs["cal"])!=1:
+ msg="Mismatch between dataset and it's cal attribute."
+ raiseAttributeError(msg)
+ out_cal={"":dataset.attrs["cal"][0]}
+ else:
+ out_cal={}
+ return{"":out},out_cal
+ else:
+ fields=list(dataset[0].dtype.fields.keys())
+ # If there is a 'cal' attribute, make sure it's the right shape.
+ if"cal"indataset.attrs:
+ ifdataset.attrs["cal"].shape!=(1,):
+ msg="'cal' attribute has more than one element."
+ raiseAttributeError(msg)
+ iflen(list(dataset.attrs["cal"].dtype.fields.keys()))!=len(fields):
+ msg="'cal' attribute not compatible with dataset dtype."
+ raiseAttributeError(msg)
+ out={}
+ out_cal={}
+ # Figure out what fields there are and allocate memory.
+ forfieldinfields:
+ dtype=dataset[0][field].dtype
+ out_arr=np.empty(out_shape,dtype=dtype)
+ out[field]=out_arr
+ if"cal"indataset.attrs:
+ out_cal[field]=memh5.bytes_to_unicode(dataset.attrs["cal"][0][field])
+ forjj,iiinenumerate(np.arange(ntime)[time_slice]):
+ # Copy data for efficient read.
+ record=dataset[ii]# Copies to memory.
+ forfieldinfields:
+ ifnotback_shape:
+ out[field][jj]=record[field]
+ eliflen(back_shape)==1:
+ out[field][:,jj]=record[field][:]
+ else:
+ # Multidimensional, try to be more efficient.
+ it=np.nditer(record[...,0],flags=["multi_index"],order="C")
+ whilenotit.finished:
+ # Reverse the multiindex for the out array.
+ ind=it.multi_index+(slice(None),)
+ ind_rev=list(ind)
+ ind_rev.reverse()
+ ind_rev=tuple(ind_rev)+(jj,)
+ out[field][ind_rev]=record[field][ind]
+ it.iternext()
+ returnout,out_cal
+
+
+def_data_attrs_from_acq_attrs_acq1(acq_attrs):
+ # The frequency axis. In MHz.
+ samp_freq=float(acq_attrs["system_sampling_frequency"])/1e6
+ nfreq=int(acq_attrs["n_freq"])
+ freq_width=samp_freq/2/nfreq
+ freq_width_array=np.empty((nfreq,),dtype=np.float64)
+ freq_width_array[:]=freq_width
+ freq_centre=(
+ samp_freq-np.cumsum(freq_width_array)+freq_width
+ )# This offset gives the correct channels
+ freq=np.empty(nfreq,dtype=[("centre",np.float64),("width",np.float64)])
+ freq["centre"]=freq_centre
+ freq["width"]=freq_width
+ # The product axis.
+ prod_channels=acq_attrs["chan_indices"]
+ nprod=len(prod_channels)
+ prod=np.empty(nprod,dtype=[("input_a",np.int64),("input_b",np.int64)])
+ # This raises a warning for some data, where the col names aren't exactly
+ # 'input_a' and 'input_b'.
+ withwarnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ prod[:]=prod_channels
+ foriiinrange(nprod):
+ prod[ii][0]=prod_channels[ii][0]
+ prod[ii][1]=prod_channels[ii][1]
+ # Populate the output.
+ out={}
+ out["freq"]=freq
+ out["prod"]=prod
+ returnout
+
+
+def_get_index_map_from_acq1(acq_files,time_sel,prod_sel,freq_sel):
+ data_headers=[]
+ foracq_fileinacq_files:
+ data_headers.append(_get_header_info_acq1(acq_file))
+ time,prod,freq,tmp_dsets=_resolve_header_info_acq1(data_headers)
+ # Populate output.
+ out={}
+ out["time"]=time[time_sel[0]:time_sel[1]]
+ out["prod"]=prod[prod_sel]
+ out["freq"]=freq[freq_sel]
+ returnout
+
+
+defandata_from_acq1(acq_files,start,stop,prod_sel,freq_sel,datasets,out_group):
+ # First open all the files and collect necessary data for all of them.
+ dtypes=_check_files_acq1(acq_files)
+ # Figure how much of the total data to read.
+ start,stop=_get_files_frames_acq1(acq_files,start,stop)
+ # Initialize the output.
+ data=CorrData(out_group)
+ # Assume all meta-data are the same as in the first file and copy it
+ # over.
+ acq=acq_files[0]
+ data.add_history("acq",memh5.attrs2dict(acq.attrs))
+ data.history["acq"]["archive_version"]="1.0.0"
+ # Copy data attribute axis info.
+ index_map=_get_index_map_from_acq1(acq_files,(start,stop),prod_sel,freq_sel)
+ foraxis_name,axis_valuesinindex_map.items():
+ data.create_index_map(axis_name,axis_values)
+ # Set file format attributes.
+ data.attrs["instrument_name"]=(
+ "UNKNOWN"
+ if"instrument_name"notinacq.attrs
+ elseacq.attrs["instrument_name"]
+ )
+ data.attrs["acquisition_name"]="UNKNOWN"
+ data.attrs["acquisition_type"]="corr"
+ # Copy over the cal information if there is any.
+ if"cal"inacq:
+ memh5.deep_group_copy(
+ acq["cal"],
+ data._data["cal"],
+ convert_attribute_strings=CorrData.convert_attribute_strings,
+ convert_dataset_strings=CorrData.convert_dataset_strings,
+ )
+ # Now copy the datasets.
+ ifdatasetsisNone:
+ datasets=list(dtypes.keys())
+ # Start with the visibilities.
+ vis_shape=()
+ fordataset_nameindtypes.keys():
+ ifdataset_namenotindatasets:
+ continue
+ # msg = "No dataset named %s in Acq files." % dataset_name
+ # raise ValueError(msg)
+
+ ifdataset_nameinACQ_VIS_SHAPE_DATASETS:
+ # These datasets must all be the same shape.
+ ifnotvis_shape:
+ vis_shape=dtypes[dataset_name].shape
+ elifdtypes[dataset_name].shape!=vis_shapeorlen(vis_shape)!=2:
+ msg=(
+ "Expected the following datasets to be"
+ " identically shaped and 3D in Acq files: %s."
+ %str(ACQ_VIS_SHAPE_DATASETS)
+ )
+ raiseValueError(msg)
+ _copy_dataset_acq1(
+ dataset_name,acq_files,start,stop,data,prod_sel,freq_sel
+ )
+ else:
+ _copy_dataset_acq1(dataset_name,acq_files,start,stop,data)
+ returndata
+
+
+# IO for archive format 2.0
+
+
+defandata_from_archive2(
+ cls,
+ acq_files,
+ start,
+ stop,
+ stack_sel,
+ prod_sel,
+ input_sel,
+ freq_sel,
+ datasets,
+ out_group,
+):
+ # XXX For short term force to CorrData class. Will be fixed once archive
+ # files carry 'acquisition_type' attribute.
+ # andata_objs = [ cls(d) for d in acq_files ]
+ andata_objs=[CorrData(d)fordinacq_files]
+
+ # Resolve input and prod maps
+ first_imap=andata_objs[0].index_map
+ first_rmap=andata_objs[0].reverse_map
+
+ # Cannot use input/prod sel for stacked data
+ if"stack"infirst_imap:
+ ifinput_sel:
+ raiseValueError("Cannot give input_sel for a stacked dataset.")
+ ifprod_sel:
+ raiseValueError("Cannot give prod_sel for a stacked dataset.")
+
+ prod_map=first_imap["prod"][:].view(np.ndarray).copy()
+ input_map=first_imap["input"][:].view(np.ndarray).copy()
+ input_map=memh5.ensure_unicode(input_map)# Convert string entries to unicode
+ if"stack"infirst_imap:
+ stack_map=first_imap["stack"][:].view(np.ndarray).copy()
+ stack_rmap=first_rmap["stack"][:].view(np.ndarray).copy()
+ else:
+ # Unstacked so the stack and prod axes are essentially the same.
+ nprod=len(prod_map)
+ stack_map=np.empty(nprod,dtype=[("prod","<u4"),("conjugate","u1")])
+ stack_map["conjugate"][:]=0
+ stack_map["prod"]=np.arange(nprod)
+ stack_rmap=np.empty(nprod,dtype=[("stack","<u4"),("conjugate","u1")])
+ stack_rmap["conjugate"][:]=0
+ stack_rmap["stack"]=np.arange(nprod)
+ # Efficiently slice prod axis, not stack axis.
+ ifstack_selisnotNone:
+ prod_sel=stack_sel
+ stack_sel=None
+
+ (
+ stack_sel,
+ stack_map,
+ stack_rmap,
+ prod_sel,
+ prod_map,
+ input_sel,
+ input_map,
+ )=_resolve_stack_prod_input_sel(
+ stack_sel,stack_map,stack_rmap,prod_sel,prod_map,input_sel,input_map
+ )
+
+ # Define dataset filter to convert vis datatype.
+ defdset_filter(dataset,time_sel=None):
+ # For compatibility with older caput.
+ iftime_selisNone:
+ time_sel=slice(None)
+ # A lot of the logic here is that h5py can only deal with one
+ # *fancy* slice (that is 1 axis where the slice is an array).
+ # Note that *time_sel* is always a normal slice, so don't have to worry
+ # about it as much.
+ attrs=getattr(dataset,"attrs",{})
+ name=path.split(dataset.name)[-1]
+ # Special treatement for pure sub-array dtypes, which get
+ # modified by numpy to add dimensions when read.
+ dtype=dataset.dtype
+ ifdtype.kind=="V"andnotdtype.fieldsanddtype.shape:
+ field_name=str(name.split("/")[-1])
+ dtype=np.dtype([(field_name,dtype)])
+ shape=dataset.shape
+ # The datasets this effects are tiny, so just read them in.
+ dataset=dataset[:].view(dtype)
+ dataset.shape=shape
+
+ axis=attrs["axis"]
+ ifaxis[0]=="freq"andaxis[1]in("stack","prod","input"):
+ # For large datasets, take great pains to down-select as
+ # efficiently as possible.
+ ifaxis[1]=="stack":
+ msel=stack_sel
+ elifaxis[1]=="prod":
+ msel=prod_sel
+ else:
+ msel=input_sel
+ ifisinstance(msel,np.ndarray)andisinstance(freq_sel,np.ndarray):
+ nfsel=np.sum(freq_sel)iffreq_sel.dtype==boolelselen(freq_sel)
+ npsel=np.sum(msel)ifmsel.dtype==boolelselen(msel)
+ nfreq=len(andata_objs[0].index_map["freq"])
+ nprod=len(andata_objs[0].index_map["prod"])
+ frac_fsel=float(nfsel)/nfreq
+ frac_psel=float(npsel)/nprod
+
+ iffrac_psel<frac_fsel:
+ dataset=dataset[:,msel,time_sel][freq_sel,:,:]
+ else:
+ dataset=dataset[freq_sel,:,time_sel][:,msel,:]
+ else:
+ # At least one of *msel* and *freq_sel* is an
+ # integer or slice object and h5py can do the full read
+ # efficiently.
+ dataset=dataset[freq_sel,msel,time_sel]
+ else:
+ # Dynamically figure out the axis ordering.
+ axis=memh5.bytes_to_unicode(attrs["axis"])
+ ndim=len(dataset.shape)# h5py datasets don't have ndim.
+ if("freq"inaxisandisinstance(freq_sel,np.ndarray))+(
+ "stack"inaxisandisinstance(stack_sel,np.ndarray)
+ )+("prod"inaxisandisinstance(prod_sel,np.ndarray))+(
+ "input"inaxisandisinstance(input_sel,np.ndarray)
+ )>1:
+ # At least two array slices. Incrementally down select.
+ # First freq.
+ dataset_sel=[slice(None)]*ndim
+ foriiinrange(ndim):
+ ifaxis[ii]=="freq":
+ dataset_sel[ii]=freq_sel
+ # Assume the time is the fastest varying index
+ # and down select here.
+ dataset_sel[-1]=time_sel
+ dataset=dataset[tuple(dataset_sel)]
+ # And again for stack.
+ dataset_sel=[slice(None)]*ndim
+ foriiinrange(ndim):
+ ifattrs["axis"][ii]=="stack":
+ dataset_sel[ii]=stack_sel
+ dataset=dataset[tuple(dataset_sel)]
+ # And again for prod.
+ dataset_sel=[slice(None)]*ndim
+ foriiinrange(ndim):
+ ifaxis[ii]=="prod":
+ dataset_sel[ii]=prod_sel
+ dataset=dataset[tuple(dataset_sel)]
+ # And again for input.
+ dataset_sel=[slice(None)]*ndim
+ foriiinrange(ndim):
+ ifaxis[ii]=="input":
+ dataset_sel[ii]=input_sel
+ dataset=dataset[tuple(dataset_sel)]
+ else:
+ dataset_sel=[slice(None)]*ndim
+ foriiinrange(ndim):
+ ifaxis[ii]=="freq":
+ dataset_sel[ii]=freq_sel
+ elifaxis[ii]=="stack":
+ dataset_sel[ii]=stack_sel
+ elifaxis[ii]=="prod":
+ dataset_sel[ii]=prod_sel
+ elifaxis[ii]=="input":
+ dataset_sel[ii]=input_sel
+ elifaxis[ii]inCONCATENATION_AXES:
+ dataset_sel[ii]=time_sel
+ dataset=dataset[tuple(dataset_sel)]
+
+ # Change data type for the visibilities, if necessary.
+ ifre.match(ACQ_VIS_DATASETS,name)anddtype!=np.complex64:
+ data=dataset[:]
+ dataset=np.empty(dataset.shape,dtype=np.complex64)
+ dataset.real=data["r"]
+ dataset.imag=data["i"]
+
+ returndataset
+
+ # The actual read, file by file.
+ data=concatenate(
+ andata_objs,
+ out_group=out_group,
+ start=start,
+ stop=stop,
+ datasets=datasets,
+ dataset_filter=dset_filter,
+ convert_attribute_strings=cls.convert_attribute_strings,
+ convert_dataset_strings=cls.convert_dataset_strings,
+ )
+
+ # Andata (or memh5) should already do the right thing.
+ # Explicitly close up files
+ # for ad in andata_objs:
+ # ad.close()
+
+ # Rejig the index map according to prod_sel and freq_sel.
+ # Need to use numpy arrays to avoid weird cyclic reference issues.
+ # (https://github.com/numpy/numpy/issues/1601)
+ fmap=data.index_map["freq"][freq_sel].view(np.ndarray).copy()
+ # pmap = data.index_map['prod'][prod_sel].view(np.ndarray).copy()
+ # imap = data.index_map['input'][input_sel].view(np.ndarray).copy()
+ data.create_index_map("freq",fmap)
+ data.create_index_map("stack",stack_map)
+ data.create_reverse_map("stack",stack_rmap)
+ data.create_index_map("prod",prod_map)
+ data.create_index_map("input",input_map)
+ returndata,input_sel
+
+
+# Routines for re-mapping the index_map/input to match up the order that is
+# in the files, and the layout database
+
+
+def_generate_input_map(serials,chans=None):
+ # Generate an input map in the correct format. If chans is None, just
+ # number from 0 upwards, otherwise use the channel numbers specified.
+
+ # Define datatype of input map array
+ # TODO: Python 3 string issues
+ _imap_dtype=[
+ ("chan_id",np.int64),
+ ("correlator_input","U32"),
+ ]
+
+ # Add in channel numbers correctly
+ ifchansisNone:
+ chan_iter=enumerate(serials)
+ else:
+ chan_iter=list(zip(chans,serials))
+
+ imap=np.array(list(chan_iter),dtype=_imap_dtype)
+
+ returnimap
+
+
+def_get_versiontuple(afile):
+ if"acq"inafile.history:
+ archive_version=afile.history["acq"]["archive_version"]
+ else:
+ archive_version=afile.attrs["archive_version"]
+
+ archive_version=memh5.bytes_to_unicode(archive_version)
+
+ returnversiontuple(archive_version)
+
+
+def_remap_stone_abbot(afile):
+ # Generate an index_map/input for the old stone/abbot files
+
+ # Really old files do not have an adc_serial attribute
+ if"adc_serial"notinafile.history["acq"]:
+ warnings.warn("Super old file. Cannot tell difference between stone and abbot.")
+ serial=-1
+ else:
+ # Fetch and parse serial value
+ serial=int(afile.history["acq"]["adc_serial"])
+
+ # The serials are defined oddly in the files, use a dict to look them up
+ serial_map={1:"0003",33:"0033",-1:"????"}# Stone # Abbot # Unknown
+
+ # Construct new array of index_map
+ serial_pat="29821-0000-%s-C%%i"%serial_map[serial]
+ inputmap=_generate_input_map([serial_pat%ciforciinrange(8)])
+
+ # Copy out old index_map/input if it exists
+ if"input"inafile.index_map:
+ afile.create_index_map("input_orig",np.array(afile.index_map["input"]))
+ # del afile._data['index_map']._dict['input']
+ afile.del_index_map("input")
+
+ # Create new index map
+ afile.create_index_map("input",inputmap)
+
+ returnafile
+
+
+def_remap_blanchard(afile):
+ # Remap a blanchard correlator file
+
+ BPC_END=(
+ 1410586200.0# 2014/09/13 05:30 UTC ~ when blanchard was moved into the crate
+ )
+ last_time=afile.time[-1]
+
+ # Use time to check if blanchard was in the crate or not
+ iflast_time<BPC_END:
+ # Find list of channels and adc serial using different methods depending on the archive file version
+ if_get_versiontuple(afile)<versiontuple("2.0.0"):
+ # The older files have no index_map/input so we need to guess/construct it.
+ chanlist=list(range(16))
+ adc_serial=afile.history["acq"]["adc_serial"][0]
+
+ else:
+ # The newer archive files have the index map, and so we can just parse this
+ chanlist=afile.index_map["input"]["chan"]
+ adc_serial=afile.index_map["input"]["adc_serial"][0]
+
+ # Construct new array of index_map
+ serial_pat="29821-0000-%s-C%%02i"%adc_serial
+ inputmap=_generate_input_map([serial_pat%ciforciinchanlist])
+
+ else:
+ _remap_crate_corr(afile,0)
+ returnafile
+
+ # Copy out old index_map/input if it exists
+ if"input"inafile.index_map:
+ afile.create_index_map("input_orig",np.array(afile.index_map["input"]))
+ # del afile._data['index_map']._dict['input']
+ afile.del_index_map("input")
+
+ # Create new index map
+ afile.create_index_map("input",inputmap)
+
+ returnafile
+
+
+def_remap_first9ucrate(afile):
+ # Remap a first9ucrate file
+ if_get_versiontuple(afile)<versiontuple("2.0.0"):
+ warnings.warn("Remapping old format first9ucrate files is not supported.")
+ returnafile
+
+ # Remap ignoring the fact that there was firt9ucrate data in the old format
+ _remap_crate_corr(afile,15)
+
+ returnafile
+
+
+def_remap_slotX(afile):
+ # Remap a slotXX correlator file
+
+ # Figure out the slot number
+ inst_name=afile.attrs["instrument_name"]
+ slotnum=int(inst_name[4:])
+
+ _remap_crate_corr(afile,slotnum)
+
+ returnafile
+
+
+def_remap_crate_corr(afile,slot):
+ # Worker routine for remapping the new style files for blanchard, first9ucrate and slotX
+
+ if_get_versiontuple(afile)<versiontuple("2.0.0"):
+ raiseException("Only functions with archive 2.0.0 files.")
+
+ CRATE_CHANGE=1412640000.0# The crate serial changed over for layout 60
+ last_time=afile.time[-1]
+
+ iflast_time<CRATE_CHANGE:
+ crate_serial="K7BP16-0002"
+ else:
+ crate_serial="K7BP16-0004"
+
+ # Fetch and remap the channel list
+ chanlist=afile.index_map["input"]["chan"]
+ channel_remapping=np.array(
+ [12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3]
+ )# Channel order in new scheme
+ chanlist=channel_remapping[chanlist]
+
+ # The slot remapping function (i.e. C(c) from doclib/165/channel_standards)
+ slot_remapping=[
+ 80,
+ 16,
+ 64,
+ 0,
+ 208,
+ 144,
+ 192,
+ 128,
+ 240,
+ 176,
+ 224,
+ 160,
+ 112,
+ 48,
+ 96,
+ 32,
+ ]
+
+ # Create new list of serials
+ serial_pat=crate_serial+("%02i%%02i"%int(slot))
+ serials=[serial_pat%ciforciinchanlist]
+
+ # Create a list of channel ids (taking into account that they are
+ # meaningless for the old crate)
+ iflast_time>=CRATE_CHANGE:
+ chans=[slot_remapping[slot-1]+ciforciinchanlist]
+ else:
+ chans=chanlist
+
+ inputmap=_generate_input_map(serials,chans)
+
+ # Save and remove old index map
+ afile.create_index_map("input_orig",np.array(afile.index_map["input"]))
+ afile.del_index_map("input")
+
+ # Create new index map
+ afile.create_index_map("input",inputmap)
+
+ returnafile
+
+
+def_remap_inputs(afile):
+ # Master routine for remapping inputs. This tries to figure out which
+ # instrument took the data, and then dispatch to the right routine to
+ # generate the new index_map/input. This follows the logic in doclib:165
+
+ # Eventually the change will be made in the correlator software and we
+ # can stop remapping files after that time.
+
+ # NOTE: need to be careful where you use afile.attrs versus
+ # afile.history['acq'] for getting properties
+
+ last_time=afile.time[-1]
+ SA_END=1397088000.0# 2014/04/10 ~ last time stone and abbot were working
+
+ # h5py should return a byte string for the attribute and so we need to decode
+ # it
+ inst_name=memh5.bytes_to_unicode(afile.attrs.get("instrument_name",b""))
+ num_antenna=int(afile.history.get("acq",{}).get("n_antenna","-1"))
+
+ # Test if is abbot or stone
+ iflast_time<SA_ENDandnum_antenna==8:
+ # Relies upon old files having the acq history
+ _remap_stone_abbot(afile)
+
+ elifinst_name=="blanchard":
+ _remap_blanchard(afile)
+
+ elifinst_name=="first9ucrate":
+ _remap_first9ucrate(afile)
+
+ elifinst_name[:4]=="slot":
+ _remap_slotX(afile)
+
+ else:
+ warnings.warn("I don't know what this data is.")
+
+
+def_insert_gains(data,input_sel):
+ # Construct a full dataset for the gains and insert it into the CorrData
+ # object
+ # freq_sel is needed for selecting the relevant frequencies in old data
+
+ # Input_sel is only used for pre archive_version 2.2, where there is no way
+ # to know which header items to pull out.
+
+ # For old versions the gains are stored in the attributes and need to be
+ # extracted
+ if("archive_version"notindata.attrs)orversiontuple(
+ memh5.bytes_to_unicode(data.attrs["archive_version"])
+ )<versiontuple("2.2.0"):
+ # Hack to find the indices of the frequencies in the file
+ fc=data.index_map["freq"]["centre"]
+ fr=np.linspace(
+ 800,400.0,1024,endpoint=False
+ )# The should be the frequency channel
+
+ # Compare with a tolerance (< 1e-4). Broken out into loop so we can deal
+ # with the case where there are no matches
+ fsel=[]
+ forfreqinfc:
+ fi=np.argwhere(np.abs(fr-freq)<1e-4)
+
+ iflen(fi)==1:
+ fsel.append(fi[0,0])
+
+ # Initialise gains to one by default
+ gain=np.ones((data.nfreq,data.ninput),dtype=np.complex64)
+
+ try:
+ ninput_orig=data.attrs["number_of_antennas"]
+ exceptKeyError:
+ ninput_orig=data.history["acq"]["number_of_antennas"]
+
+ # In certain files this entry is a length-1 array, turn it into a scalar if it is not
+ ifisinstance(ninput_orig,np.ndarray):
+ ninput_orig=ninput_orig[0]
+
+ ifninput_orig<=16:
+ # For 16 channel or earlier data, each channel has a simple
+ # labelling for its gains
+ keylist=[
+ (channel,"antenna_scaler_gain"+str(channel))
+ forchannelinrange(ninput_orig)
+ ]
+ else:
+ # For 256 channel data this is more complicated
+
+ # Construct list of keys for all gain entries
+ keylist=[keyforkeyindata.attrs.keys()ifkey[:2]=="ID"]
+
+ # Extract the channel id from each key
+ chanid=[key.split("_")[1]forkeyinkeylist]
+
+ # Sort the keylist according to the channel ids, as the inputs
+ # should be sorted by channel id.
+ keylist=sorted(zip(chanid,keylist))
+ # Down select keylist based on input_sel.
+ input_sel_list=list(np.arange(ninput_orig,dtype=int)[input_sel])
+ keylist=[keylist[ii]foriiininput_sel_list]
+
+ iflen(fsel)!=data.nfreq:
+ warnings.warn(
+ "Could not match all frequency channels. Skipping gain calculation."
+ )
+ else:
+ # Iterate over the keys and extract the gains
+ forchan,keyinkeylist:
+ # Try and find gain entry
+ ifkeyindata.attrs:
+ g_data=data.attrs[key]
+ elifkeyindata.history["acq"]:
+ g_data=data.history["acq"][key]
+ else:
+ warnings.warn(
+ "Cannot find gain entry [%s] for channel %i"%(key,chan)
+ )
+ continue
+
+ # Unpack the gain values and construct the gain array
+ g_real,g_imag=g_data[1:-1:2],g_data[2:-1:2]
+ g_exp=g_data[-1]
+
+ g_full=(g_real+1.0j*g_imag)*2**g_exp
+
+ # Select frequencies that are loaded from the file
+ g_sel=g_full[fsel]
+
+ gain[:,input_sel_list.index(chan)]=g_sel
+
+ # Gain array must be specified for all times, repeat along the time axis
+ gain=np.tile(gain[:,:,np.newaxis],(1,1,data.ntime))
+
+ else:
+ gain=np.ones((data.nfreq,data.ninput,data.ntime),dtype=np.complex64)
+
+ # Check that the gain datasets have been loaded
+ if("gain_coeff"notindata.datasets)or("gain_exp"notindata.datasets):
+ warnings.warn(
+ "Required gain datasets not loaded from file (> v2.2.0), using unit gains."
+ )
+
+ else:
+ # Extract the gain datasets from the file
+ gain_exp=data.datasets["gain_exp"][:]
+ gain_coeff=data.datasets["gain_coeff"][:]
+
+ # Turn into a single array
+ ifgain_coeff.dtype==np.complex64:
+ gain*=gain_coeff
+ else:
+ gain.real[:]=gain_coeff["r"]
+ gain.imag[:]=gain_coeff["i"]
+ gain*=2**gain_exp[np.newaxis,:,:]
+
+ # Add gain dataset to object, and create axis attribute
+ gain_dset=data.create_dataset("gain",data=gain)
+ gain_dset.attrs["axis"]=np.array(["freq","input","time"])
+
+
+if__name__=="__main__":
+ importdoctest
+
+ doctest.testmod()
+
+"""
+Tools for point source calibration
+
+This module contains tools for performing point-source calibration.
+"""
+
+fromabcimportABCMeta,abstractmethod
+fromdatetimeimportdatetime
+importinspect
+importlogging
+fromtypingimportDict,Optional,Union
+
+importnumpyasnp
+importscipy.stats
+fromscipy.optimizeimportcurve_fit
+fromscipy.interpolateimportinterp1d
+fromscipy.linalgimportlstsq,inv
+
+fromcaputimportmemh5,timeasctime
+fromchimedbimportdatasetasds
+fromchimedb.dataset.utilsimportstate_id_of_type,unique_unmasked_entry
+fromch_utilimportephemeris,tools
+
+# Set up logging
+logger=logging.getLogger(__name__)
+logger.addHandler(logging.NullHandler())
+
+
+
+[docs]
+classFitTransit(object,metaclass=ABCMeta):
+"""Base class for fitting models to point source transits.
+
+ The `fit` method should be used to populate the `param`, `param_cov`, `chisq`,
+ and `ndof` attributes. The `predict` and `uncertainty` methods can then be used
+ to obtain the model prediction for the response and uncertainty on this quantity
+ at a given hour angle.
+
+ Attributes
+ ----------
+ param : np.ndarray[..., nparam]
+ Best-fit parameters.
+ param_cov : np.ndarray[..., nparam, nparam]
+ Covariance of the fit parameters.
+ chisq : np.ndarray[...]
+ Chi-squared of the fit.
+ ndof : np.ndarray[...]
+ Number of degrees of freedom.
+
+ Abstract Methods
+ ----------------
+ Any subclass of FitTransit must define these methods:
+ peak
+ _fit
+ _model
+ _jacobian
+ """
+
+ _tval={}
+ component=np.array(["complex"],dtype=np.string_)
+
+ def__init__(self,*args,**kwargs):
+"""Instantiates a FitTransit object.
+
+ Parameters
+ ----------
+ param : np.ndarray[..., nparam]
+ Best-fit parameters.
+ param_cov : np.ndarray[..., nparam, nparam]
+ Covariance of the fit parameters.
+ chisq : np.ndarray[..., ncomponent]
+ Chi-squared.
+ ndof : np.ndarray[..., ncomponent]
+ Number of degrees of freedom.
+ """
+ # Save keyword arguments as attributes
+ self.param=kwargs.pop("param",None)
+ self.param_cov=kwargs.pop("param_cov",None)
+ self.chisq=kwargs.pop("chisq",None)
+ self.ndof=kwargs.pop("ndof",None)
+ self.model_kwargs=kwargs
+
+
+[docs]
+ defpredict(self,ha,elementwise=False):
+"""Predict the point source response.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,] or float
+ The hour angle in degrees.
+ elementwise : bool
+ If False, then the model will be evaluated at the
+ requested hour angles for every set of parameters.
+ If True, then the model will be evaluated at a
+ separate hour angle for each set of parameters
+ (requires `ha.shape == self.N`).
+
+ Returns
+ -------
+ model : np.ndarray[..., nha] or float
+ Model for the point source response at the requested
+ hour angles. Complex valued.
+ """
+ withnp.errstate(all="ignore"):
+ mdl=self._model(ha,elementwise=elementwise)
+ returnnp.where(np.isfinite(mdl),mdl,0.0+0.0j)
+
+
+
+[docs]
+ defuncertainty(self,ha,alpha=0.32,elementwise=False):
+"""Predict the uncertainty on the point source response.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,] or float
+ The hour angle in degrees.
+ alpha : float
+ Confidence level given by 1 - alpha.
+ elementwise : bool
+ If False, then the uncertainty will be evaluated at
+ the requested hour angles for every set of parameters.
+ If True, then the uncertainty will be evaluated at a
+ separate hour angle for each set of parameters
+ (requires `ha.shape == self.N`).
+
+ Returns
+ -------
+ err : np.ndarray[..., nha]
+ Uncertainty on the point source response at the
+ requested hour angles.
+ """
+ x=np.atleast_1d(ha)
+ withnp.errstate(all="ignore"):
+ err=_propagate_uncertainty(
+ self._jacobian(x,elementwise=elementwise),
+ self.param_cov,
+ self.tval(alpha,self.ndof),
+ )
+ returnnp.squeeze(np.where(np.isfinite(err),err,0.0))
+
+
+
+[docs]
+ deffit(self,ha,resp,resp_err,width=5,absolute_sigma=False,**kwargs):
+"""Apply subclass defined `_fit` method to multiple transits.
+
+ This function can be used to fit the transit for multiple inputs
+ and frequencies. Populates the `param`, `param_cov`, `chisq`, and `ndof`
+ attributes.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,]
+ Hour angle in degrees.
+ resp : np.ndarray[..., nha]
+ Measured response to the point source. Complex valued.
+ resp_err : np.ndarray[..., nha]
+ Error on the measured response.
+ width : np.ndarray[...]
+ Initial guess at the width (sigma) of the transit in degrees.
+ absolute_sigma : bool
+ Set to True if the errors provided are absolute. Set to False if
+ the errors provided are relative, in which case the parameter covariance
+ will be scaled by the chi-squared per degree-of-freedom.
+ """
+ shp=resp.shape[:-1]
+ dtype=ha.dtype
+
+ ifnotnp.isscalar(width)and(width.shape!=shp):
+ ValueError("Keyword with must be scalar or have shape %s."%str(shp))
+
+ self.param=np.full(shp+(self.nparam,),np.nan,dtype=dtype)
+ self.param_cov=np.full(shp+(self.nparam,self.nparam),np.nan,dtype=dtype)
+ self.chisq=np.full(shp+(self.ncomponent,),np.nan,dtype=dtype)
+ self.ndof=np.full(shp+(self.ncomponent,),0,dtype=int)
+
+ withnp.errstate(all="ignore"):
+ forindinnp.ndindex(*shp):
+ wi=widthifnp.isscalar(width)elsewidth[ind[:width.ndim]]
+
+ err=resp_err[ind]
+ good=np.flatnonzero(err>0.0)
+
+ if(good.size//2)<=self.nparam:
+ continue
+
+ try:
+ param,param_cov,chisq,ndof=self._fit(
+ ha[good],
+ resp[ind][good],
+ err[good],
+ width=wi,
+ absolute_sigma=absolute_sigma,
+ **kwargs,
+ )
+ exceptExceptionaserror:
+ logger.debug("Index %s failed with error: %s"%(str(ind),error))
+ continue
+
+ self.param[ind]=param
+ self.param_cov[ind]=param_cov
+ self.chisq[ind]=chisq
+ self.ndof[ind]=ndof
+
+
+ @property
+ defparameter_names(self):
+"""
+ Array of strings containing the name of the fit parameters.
+
+ Returns
+ -------
+ parameter_names : np.ndarray[nparam,]
+ Names of the parameters.
+ """
+ returnnp.array(["param%d"%pforpinrange(self.nparam)],dtype=np.string_)
+
+ @property
+ defparam_corr(self):
+"""
+ Parameter correlation matrix.
+
+ Returns
+ -------
+ param_corr : np.ndarray[..., nparam, nparam]
+ Correlation of the fit parameters.
+ """
+ idiag=tools.invert_no_zero(
+ np.sqrt(np.diagonal(self.param_cov,axis1=-2,axis2=-1))
+ )
+ returnself.param_cov*idiag[...,np.newaxis,:]*idiag[...,np.newaxis]
+
+ @property
+ defN(self):
+"""
+ Number of independent transit fits contained in this object.
+
+ Returns
+ -------
+ N : tuple
+ Numpy-style shape indicating the number of
+ fits that the object contains. Is None
+ if the object contains a single fit.
+ """
+ ifself.paramisnotNone:
+ returnself.param.shape[:-1]orNone
+
+ @property
+ defnparam(self):
+"""
+ Number of parameters.
+
+ Returns
+ -------
+ nparam : int
+ Number of fit parameters.
+ """
+ returnself.param.shape[-1]
+
+ @property
+ defncomponent(self):
+"""
+ Number of components.
+
+ Returns
+ -------
+ ncomponent : int
+ Number of components (i.e, real and imag, amp and phase, complex) that have been fit.
+ """
+ returnself.component.size
+
+ def__getitem__(self,val):
+"""Instantiates a new TransitFit object containing some subset of the fits."""
+
+ ifself.NisNone:
+ raiseKeyError(
+ "Attempting to slice TransitFit object containing single fit."
+ )
+
+ returnself.__class__(
+ param=self.param[val],
+ param_cov=self.param_cov[val],
+ ndof=self.ndof[val],
+ chisq=self.chisq[val],
+ **self.model_kwargs,
+ )
+
+
+[docs]
+ @abstractmethod
+ defpeak(self):
+"""Calculate the peak of the transit.
+
+ Any subclass of FitTransit must define this method.
+ """
+ return
+
+
+ @abstractmethod
+ def_fit(self,ha,resp,resp_err,width=None,absolute_sigma=False):
+"""Fit data to the model.
+
+ Any subclass of FitTransit must define this method.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,]
+ Hour angle in degrees.
+ resp : np.ndarray[nha,]
+ Measured response to the point source. Complex valued.
+ resp_err : np.ndarray[nha,]
+ Error on the measured response.
+ width : np.ndarray
+ Initial guess at the width (sigma) of the transit in degrees.
+ absolute_sigma : bool
+ Set to True if the errors provided are absolute. Set to False if
+ the errors provided are relative, in which case the parameter covariance
+ will be scaled by the chi-squared per degree-of-freedom.
+
+ Returns
+ -------
+ param : np.ndarray[nparam,]
+ Best-fit model parameters.
+ param_cov : np.ndarray[nparam, nparam]
+ Covariance of the best-fit model parameters.
+ chisq : float
+ Chi-squared of the fit.
+ ndof : int
+ Number of degrees of freedom of the fit.
+ """
+ return
+
+ @abstractmethod
+ def_model(self,ha):
+"""Calculate the model for the point source response.
+
+ Any subclass of FitTransit must define this method.
+
+ Parameters
+ ----------
+ ha : np.ndarray
+ Hour angle in degrees.
+ """
+ return
+
+ @abstractmethod
+ def_jacobian(self,ha):
+"""Calculate the jacobian of the model for the point source response.
+
+ Any subclass of FitTransit must define this method.
+
+ Parameters
+ ----------
+ ha : np.ndarray
+ Hour angle in degrees.
+
+ Returns
+ -------
+ jac : np.ndarray[..., nparam, nha]
+ The jacobian defined as
+ jac[..., i, j] = d(model(ha)) / d(param[i]) evaluated at ha[j]
+ """
+ return
+
+
+[docs]
+ @classmethod
+ deftval(cls,alpha,ndof):
+"""Quantile of a standardized Student's t random variable.
+
+ This quantity is slow to compute. Past values will be cached
+ in a dictionary shared by all instances of the class.
+
+ Parameters
+ ----------
+ alpha : float
+ Calculate the quantile corresponding to the lower tail probability
+ 1 - alpha / 2.
+ ndof : np.ndarray or int
+ Number of degrees of freedom of the Student's t variable.
+
+ Returns
+ -------
+ tval : np.ndarray or float
+ Quantile of a standardized Student's t random variable.
+ """
+ prob=1.0-0.5*alpha
+
+ arr_ndof=np.atleast_1d(ndof)
+ tval=np.zeros(arr_ndof.shape,dtype=np.float32)
+
+ forind,ndinnp.ndenumerate(arr_ndof):
+ key=(int(100.0*prob),nd)
+ ifkeynotincls._tval:
+ cls._tval[key]=scipy.stats.t.ppf(prob,nd)
+ tval[ind]=cls._tval[key]
+
+ ifnp.isscalar(ndof):
+ tval=np.squeeze(tval)
+
+ returntval
+
+
+
+
+
+[docs]
+classFitPoly(FitTransit):
+"""Base class for fitting polynomials to point source transits.
+
+ Maps methods of np.polynomial to methods of the class for the
+ requested polynomial type.
+ """
+
+ def__init__(self,poly_type="standard",*args,**kwargs):
+"""Instantiates a FitPoly object.
+
+ Parameters
+ ----------
+ poly_type : str
+ Type of polynomial. Can be 'standard', 'hermite', or 'chebyshev'.
+ """
+ super(FitPoly,self).__init__(poly_type=poly_type,*args,**kwargs)
+
+ self._set_polynomial_model(poly_type)
+
+ def_set_polynomial_model(self,poly_type):
+"""Map methods of np.polynomial to methods of the class."""
+ ifpoly_type=="standard":
+ self._vander=np.polynomial.polynomial.polyvander
+ self._eval=np.polynomial.polynomial.polyval
+ self._deriv=np.polynomial.polynomial.polyder
+ self._root=np.polynomial.polynomial.polyroots
+ elifpoly_type=="hermite":
+ self._vander=np.polynomial.hermite.hermvander
+ self._eval=np.polynomial.hermite.hermval
+ self._deriv=np.polynomial.hermite.hermder
+ self._root=np.polynomial.hermite.hermroots
+ elifpoly_type=="chebyshev":
+ self._vander=np.polynomial.chebyshev.chebvander
+ self._eval=np.polynomial.chebyshev.chebval
+ self._deriv=np.polynomial.chebyshev.chebder
+ self._root=np.polynomial.chebyshev.chebroots
+ else:
+ raiseValueError(
+ "Do not recognize polynomial type %s."
+ "Options are 'standard', 'hermite', or 'chebyshev'."%poly_type
+ )
+
+ self.poly_type=poly_type
+
+ def_fast_eval(self,ha,param=None,elementwise=False):
+"""Evaluate the polynomial at the requested hour angle."""
+ ifparamisNone:
+ param=self.param
+
+ vander=self._vander(ha,param.shape[-1]-1)
+
+ ifelementwise:
+ out=np.sum(vander*param,axis=-1)
+ elifparam.ndim==1:
+ out=np.dot(vander,param)
+ else:
+ out=np.matmul(param,np.rollaxis(vander,-1))
+
+ returnnp.squeeze(out,axis=-1)ifnp.isscalar(ha)elseout
+
+
+
+
+[docs]
+classFitRealImag(FitTransit):
+"""Base class for fitting models to the real and imag component.
+
+ Assumes an independent fit to real and imaginary, and provides
+ methods for predicting the uncertainty on each.
+ """
+
+ component=np.array(["real","imag"],dtype=np.string_)
+
+
+[docs]
+ defuncertainty_real(self,ha,alpha=0.32,elementwise=False):
+"""Predicts the uncertainty on real component at given hour angle(s).
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,] or float
+ Hour angle in degrees.
+ alpha : float
+ Confidence level given by 1 - alpha.
+
+ Returns
+ -------
+ err : np.ndarray[..., nha] or float
+ Uncertainty on the real component.
+ """
+ x=np.atleast_1d(ha)
+ err=_propagate_uncertainty(
+ self._jacobian_real(x,elementwise=elementwise),
+ self.param_cov[...,:self.nparr,:self.nparr],
+ self.tval(alpha,self.ndofr),
+ )
+ returnnp.squeeze(err,axis=-1)ifnp.isscalar(ha)elseerr
+
+
+
+[docs]
+ defuncertainty_imag(self,ha,alpha=0.32,elementwise=False):
+"""Predicts the uncertainty on imag component at given hour angle(s).
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,] or float
+ Hour angle in degrees.
+ alpha : float
+ Confidence level given by 1 - alpha.
+
+ Returns
+ -------
+ err : np.ndarray[..., nha] or float
+ Uncertainty on the imag component.
+ """
+ x=np.atleast_1d(ha)
+ err=_propagate_uncertainty(
+ self._jacobian_imag(x,elementwise=elementwise),
+ self.param_cov[...,self.nparr:,self.nparr:],
+ self.tval(alpha,self.ndofi),
+ )
+ returnnp.squeeze(err,axis=-1)ifnp.isscalar(ha)elseerr
+
+
+
+[docs]
+ defuncertainty(self,ha,alpha=0.32,elementwise=False):
+"""Predicts the uncertainty on the response at given hour angle(s).
+
+ Returns the quadrature sum of the real and imag uncertainty.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,] or float
+ Hour angle in degrees.
+ alpha : float
+ Confidence level given by 1 - alpha.
+
+ Returns
+ -------
+ err : np.ndarray[..., nha] or float
+ Uncertainty on the response.
+ """
+ withnp.errstate(all="ignore"):
+ err=np.sqrt(
+ self.uncertainty_real(ha,alpha=alpha,elementwise=elementwise)**2
+ +self.uncertainty_imag(ha,alpha=alpha,elementwise=elementwise)**2
+ )
+ returnerr
+
+
+ def_jacobian(self,ha):
+ raiseNotImplementedError(
+ "Fits to real and imaginary are independent. "
+ "Use _jacobian_real and _jacobian_imag instead."
+ )
+
+ @abstractmethod
+ def_jacobian_real(self,ha):
+"""Calculate the jacobian of the model for the real component."""
+ return
+
+ @abstractmethod
+ def_jacobian_imag(self,ha):
+"""Calculate the jacobian of the model for the imag component."""
+ return
+
+ @property
+ defnparam(self):
+ returnself.nparr+self.npari
+
+
+
+
+[docs]
+classFitPolyRealPolyImag(FitPoly,FitRealImag):
+"""Class that enables separate fits of a polynomial to real and imag components.
+
+ Used to fit cross-polar response that is not well-described by the
+ FitPolyLogAmpPolyPhase used for co-polar response.
+ """
+
+ def__init__(self,poly_deg=5,even=False,odd=False,*args,**kwargs):
+"""Instantiates a FitPolyRealPolyImag object.
+
+ Parameters
+ ----------
+ poly_deg : int
+ Degree of the polynomial to fit to real and imaginary component.
+ """
+ ifevenandodd:
+ raiseRuntimeError("Cannot request both even AND odd.")
+
+ super(FitPolyRealPolyImag,self).__init__(
+ poly_deg=poly_deg,even=even,odd=odd,*args,**kwargs
+ )
+
+ self.poly_deg=poly_deg
+ self.even=even
+ self.odd=odd
+
+ ind=np.arange(self.poly_deg+1)
+ ifself.even:
+ self.coeff_index=np.flatnonzero((ind==0)|~(ind%2))
+
+ elifself.odd:
+ self.coeff_index=np.flatnonzero((ind==0)|(ind%2))
+
+ else:
+ self.coeff_index=ind
+
+ self.nparr=self.coeff_index.size
+ self.npari=self.nparr
+
+
+
+
+ def_fit(self,ha,resp,resp_err,absolute_sigma=False):
+"""Fit polynomial to real and imaginary component.
+
+ Use weighted least squares.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,]
+ Hour angle in degrees.
+ resp : np.ndarray[nha,]
+ Measured response to the point source. Complex valued.
+ resp_err : np.ndarray[nha,]
+ Error on the measured response.
+ absolute_sigma : bool
+ Set to True if the errors provided are absolute. Set to False if
+ the errors provided are relative, in which case the parameter covariance
+ will be scaled by the chi-squared per degree-of-freedom.
+
+ Returns
+ -------
+ param : np.ndarray[nparam,]
+ Best-fit model parameters.
+ param_cov : np.ndarray[nparam, nparam]
+ Covariance of the best-fit model parameters.
+ chisq : np.ndarray[2,]
+ Chi-squared of the fit to amplitude and phase.
+ ndof : np.ndarray[2,]
+ Number of degrees of freedom of the fit to amplitude and phase.
+ """
+ min_nfit=min(self.nparr,self.npari)+1
+
+ # Prepare amplitude data
+ amp=np.abs(resp)
+ w0=tools.invert_no_zero(resp_err)**2
+
+ # Only perform fit if there is enough data.
+ this_flag=(amp>0.0)&(w0>0.0)
+ ndata=int(np.sum(this_flag))
+ ifndata<min_nfit:
+ raiseRuntimeError("Number of data points less than number of parameters.")
+
+ wf=w0*this_flag.astype(np.float32)
+
+ # Compute real and imaginary component of complex response
+ yr=np.real(resp)
+ yi=np.imag(resp)
+
+ # Calculate vandermonde matrix
+ A=self.vander(ha)
+
+ # Compute parameter covariance
+ cov=inv(np.dot(A.T,wf[:,np.newaxis]*A))
+
+ # Compute best-fit coefficients
+ coeffr=np.dot(cov,np.dot(A.T,wf*yr))
+ coeffi=np.dot(cov,np.dot(A.T,wf*yi))
+
+ # Compute model estimate
+ mr=np.dot(A,coeffr)
+ mi=np.dot(A,coeffi)
+
+ # Compute chisq per degree of freedom
+ ndofr=ndata-self.nparr
+ ndofi=ndata-self.npari
+
+ ndof=np.array([ndofr,ndofi])
+ chisq=np.array([np.sum(wf*(yr-mr)**2),np.sum(wf*(yi-mi)**2)])
+
+ # Scale the parameter covariance by chisq per degree of freedom.
+ # Equivalent to using RMS of the residuals to set the absolute error
+ # on the measurements.
+ ifnotabsolute_sigma:
+ scale_factor=chisq*tools.invert_no_zero(ndof.astype(np.float32))
+ covr=cov*scale_factor[0]
+ covi=cov*scale_factor[1]
+ else:
+ covr=cov
+ covi=cov
+
+ param=np.concatenate((coeffr,coeffi))
+
+ param_cov=np.zeros((self.nparam,self.nparam),dtype=np.float32)
+ param_cov[:self.nparr,:self.nparr]=covr
+ param_cov[self.nparr:,self.nparr:]=covi
+
+ returnparam,param_cov,chisq,ndof
+
+ def_model(self,ha,elementwise=False):
+ real=self._fast_eval(
+ ha,self.param[...,:self.nparr],elementwise=elementwise
+ )
+ imag=self._fast_eval(
+ ha,self.param[...,self.nparr:],elementwise=elementwise
+ )
+
+ returnreal+1.0j*imag
+
+ def_jacobian_real(self,ha,elementwise=False):
+ jac=np.rollaxis(self.vander(ha),-1)
+ ifnotelementwiseandself.NisnotNone:
+ slc=(None,)*len(self.N)
+ jac=jac[slc]
+
+ returnjac
+
+ def_jacobian_imag(self,ha,elementwise=False):
+ jac=np.rollaxis(self.vander(ha),-1)
+ ifnotelementwiseandself.NisnotNone:
+ slc=(None,)*len(self.N)
+ jac=jac[slc]
+
+ returnjac
+
+ @property
+ defndofr(self):
+"""Number of degrees of freedom for the real fit."""
+ returnself.ndof[...,0]
+
+ @property
+ defndofi(self):
+"""Number of degrees of freedom for the imag fit."""
+ returnself.ndof[...,1]
+
+ @property
+ defparameter_names(self):
+"""Array of strings containing the name of the fit parameters."""
+ returnnp.array(
+ ["%s_poly_real_coeff%d"%(self.poly_type,p)forpinrange(self.nparr)]
+ +["%s_poly_imag_coeff%d"%(self.poly_type,p)forpinrange(self.npari)],
+ dtype=np.string_,
+ )
+
+
+[docs]
+ defpeak(self):
+"""Calculate the peak of the transit."""
+ logger.warning("The peak is not defined for this model.")
+ return
+
+
+
+
+
+[docs]
+classFitAmpPhase(FitTransit):
+"""Base class for fitting models to the amplitude and phase.
+
+ Assumes an independent fit to amplitude and phase, and provides
+ methods for predicting the uncertainty on each.
+ """
+
+ component=np.array(["amplitude","phase"],dtype=np.string_)
+
+
+[docs]
+ defuncertainty_amp(self,ha,alpha=0.32,elementwise=False):
+"""Predicts the uncertainty on amplitude at given hour angle(s).
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,] or float
+ Hour angle in degrees.
+ alpha : float
+ Confidence level given by 1 - alpha.
+
+ Returns
+ -------
+ err : np.ndarray[..., nha] or float
+ Uncertainty on the amplitude in fractional units.
+ """
+ x=np.atleast_1d(ha)
+ err=_propagate_uncertainty(
+ self._jacobian_amp(x,elementwise=elementwise),
+ self.param_cov[...,:self.npara,:self.npara],
+ self.tval(alpha,self.ndofa),
+ )
+ returnnp.squeeze(err,axis=-1)ifnp.isscalar(ha)elseerr
+
+
+
+[docs]
+ defuncertainty_phi(self,ha,alpha=0.32,elementwise=False):
+"""Predicts the uncertainty on phase at given hour angle(s).
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,] or float
+ Hour angle in degrees.
+ alpha : float
+ Confidence level given by 1 - alpha.
+
+ Returns
+ -------
+ err : np.ndarray[..., nha] or float
+ Uncertainty on the phase in radians.
+ """
+ x=np.atleast_1d(ha)
+ err=_propagate_uncertainty(
+ self._jacobian_phi(x,elementwise=elementwise),
+ self.param_cov[...,self.npara:,self.npara:],
+ self.tval(alpha,self.ndofp),
+ )
+ returnnp.squeeze(err,axis=-1)ifnp.isscalar(ha)elseerr
+
+
+
+[docs]
+ defuncertainty(self,ha,alpha=0.32,elementwise=False):
+"""Predicts the uncertainty on the response at given hour angle(s).
+
+ Returns the quadrature sum of the amplitude and phase uncertainty.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,] or float
+ Hour angle in degrees.
+ alpha : float
+ Confidence level given by 1 - alpha.
+
+ Returns
+ -------
+ err : np.ndarray[..., nha] or float
+ Uncertainty on the response.
+ """
+ withnp.errstate(all="ignore"):
+ err=np.abs(self._model(ha,elementwise=elementwise))*np.sqrt(
+ self.uncertainty_amp(ha,alpha=alpha,elementwise=elementwise)**2
+ +self.uncertainty_phi(ha,alpha=alpha,elementwise=elementwise)**2
+ )
+ returnerr
+
+
+ def_jacobian(self,ha):
+ raiseNotImplementedError(
+ "Fits to amplitude and phase are independent. "
+ "Use _jacobian_amp and _jacobian_phi instead."
+ )
+
+ @abstractmethod
+ def_jacobian_amp(self,ha):
+"""Calculate the jacobian of the model for the amplitude."""
+ return
+
+ @abstractmethod
+ def_jacobian_phi(self,ha):
+"""Calculate the jacobian of the model for the phase."""
+ return
+
+ @property
+ defnparam(self):
+ returnself.npara+self.nparp
+
+
+
+
+[docs]
+classFitPolyLogAmpPolyPhase(FitPoly,FitAmpPhase):
+"""Class that enables separate fits of a polynomial to log amplitude and phase."""
+
+ def__init__(self,poly_deg_amp=5,poly_deg_phi=5,*args,**kwargs):
+"""Instantiates a FitPolyLogAmpPolyPhase object.
+
+ Parameters
+ ----------
+ poly_deg_amp : int
+ Degree of the polynomial to fit to log amplitude.
+ poly_deg_phi : int
+ Degree of the polynomial to fit to phase.
+ """
+ super(FitPolyLogAmpPolyPhase,self).__init__(
+ poly_deg_amp=poly_deg_amp,poly_deg_phi=poly_deg_phi,*args,**kwargs
+ )
+
+ self.poly_deg_amp=poly_deg_amp
+ self.poly_deg_phi=poly_deg_phi
+
+ self.npara=poly_deg_amp+1
+ self.nparp=poly_deg_phi+1
+
+ def_fit(
+ self,
+ ha,
+ resp,
+ resp_err,
+ width=None,
+ absolute_sigma=False,
+ moving_window=0.3,
+ niter=5,
+ ):
+"""Fit polynomial to log amplitude and polynomial to phase.
+
+ Use weighted least squares. The initial errors on log amplitude
+ are set to `resp_err / abs(resp)`. If the niter parameter is greater than 1,
+ then those errors will be updated with `resp_err / model_amp`, where `model_amp`
+ is the best-fit model for the amplitude from the previous iteration. The errors
+ on the phase are set to `resp_err / model_amp` where `model_amp` is the best-fit
+ model for the amplitude from the log amplitude fit.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,]
+ Hour angle in degrees.
+ resp : np.ndarray[nha,]
+ Measured response to the point source. Complex valued.
+ resp_err : np.ndarray[nha,]
+ Error on the measured response.
+ width : float
+ Initial guess at the width (sigma) of the transit in degrees.
+ absolute_sigma : bool
+ Set to True if the errors provided are absolute. Set to False if
+ the errors provided are relative, in which case the parameter covariance
+ will be scaled by the chi-squared per degree-of-freedom.
+ niter : int
+ Number of iterations for the log amplitude fit.
+ moving_window : float
+ Only fit hour angles within +/- window * width from the peak.
+ Note that the peak location is updated with each iteration.
+ Set to None to fit all hour angles where resp_err > 0.0.
+
+ Returns
+ -------
+ param : np.ndarray[nparam,]
+ Best-fit model parameters.
+ param_cov : np.ndarray[nparam, nparam]
+ Covariance of the best-fit model parameters.
+ chisq : np.ndarray[2,]
+ Chi-squared of the fit to amplitude and phase.
+ ndof : np.ndarray[2,]
+ Number of degrees of freedom of the fit to amplitude and phase.
+ """
+ min_nfit=min(self.npara,self.nparp)+1
+
+ window=width*moving_windowif(widthandmoving_window)elseNone
+
+ # Prepare amplitude data
+ model_amp=np.abs(resp)
+ w0=tools.invert_no_zero(resp_err)**2
+
+ # Only perform fit if there is enough data.
+ this_flag=(model_amp>0.0)&(w0>0.0)
+ ndata=int(np.sum(this_flag))
+ ifndata<min_nfit:
+ raiseRuntimeError("Number of data points less than number of parameters.")
+
+ # Prepare amplitude data
+ ya=np.log(model_amp)
+
+ # Prepare phase data.
+ phi=np.angle(resp)
+ phi0=phi[np.argmin(np.abs(ha))]
+
+ yp=phi-phi0
+ yp+=(yp<-np.pi)*2*np.pi-(yp>np.pi)*2*np.pi
+ yp+=phi0
+
+ # Calculate vandermonde matrix
+ A=self._vander(ha,self.poly_deg_amp)
+ center=0.0
+
+ # Iterate to obtain model estimate for amplitude
+ forkkinrange(niter):
+ wk=w0*model_amp**2
+
+ ifwindowisnotNone:
+ ifkk>0:
+ center=self.peak(param=coeff)
+
+ ifnp.isnan(center):
+ raiseRuntimeError("No peak found.")
+
+ wk*=(np.abs(ha-center)<=window).astype(np.float64)
+
+ ndata=int(np.sum(wk>0.0))
+ ifndata<min_nfit:
+ raiseRuntimeError(
+ "Number of data points less than number of parameters."
+ )
+
+ C=np.dot(A.T,wk[:,np.newaxis]*A)
+ coeff=lstsq(C,np.dot(A.T,wk*ya))[0]
+
+ model_amp=np.exp(np.dot(A,coeff))
+
+ # Compute final value for amplitude
+ center=self.peak(param=coeff)
+
+ ifnp.isnan(center):
+ raiseRuntimeError("No peak found.")
+
+ wf=w0*model_amp**2
+ ifwindowisnotNone:
+ wf*=(np.abs(ha-center)<=window).astype(np.float64)
+
+ ndata=int(np.sum(wf>0.0))
+ ifndata<min_nfit:
+ raiseRuntimeError(
+ "Number of data points less than number of parameters."
+ )
+
+ cova=inv(np.dot(A.T,wf[:,np.newaxis]*A))
+ coeffa=np.dot(cova,np.dot(A.T,wf*ya))
+
+ mamp=np.dot(A,coeffa)
+
+ # Compute final value for phase
+ A=self._vander(ha,self.poly_deg_phi)
+
+ covp=inv(np.dot(A.T,wf[:,np.newaxis]*A))
+ coeffp=np.dot(covp,np.dot(A.T,wf*yp))
+
+ mphi=np.dot(A,coeffp)
+
+ # Compute chisq per degree of freedom
+ ndofa=ndata-self.npara
+ ndofp=ndata-self.nparp
+
+ ndof=np.array([ndofa,ndofp])
+ chisq=np.array([np.sum(wf*(ya-mamp)**2),np.sum(wf*(yp-mphi)**2)])
+
+ # Scale the parameter covariance by chisq per degree of freedom.
+ # Equivalent to using RMS of the residuals to set the absolute error
+ # on the measurements.
+ ifnotabsolute_sigma:
+ scale_factor=chisq*tools.invert_no_zero(ndof.astype(np.float32))
+ cova*=scale_factor[0]
+ covp*=scale_factor[1]
+
+ param=np.concatenate((coeffa,coeffp))
+
+ param_cov=np.zeros((self.nparam,self.nparam),dtype=np.float32)
+ param_cov[:self.npara,:self.npara]=cova
+ param_cov[self.npara:,self.npara:]=covp
+
+ returnparam,param_cov,chisq,ndof
+
+
+[docs]
+ defpeak(self,param=None):
+"""Find the peak of the transit.
+
+ Parameters
+ ----------
+ param : np.ndarray[..., nparam]
+ Coefficients of the polynomial model for log amplitude.
+ Defaults to `self.param`.
+
+ Returns
+ -------
+ peak : np.ndarray[...]
+ Location of the maximum amplitude in degrees hour angle.
+ If the polynomial does not have a maximum, then NaN is returned.
+ """
+ ifparamisNone:
+ param=self.param
+
+ der1=self._deriv(param[...,:self.npara],m=1,axis=-1)
+ der2=self._deriv(param[...,:self.npara],m=2,axis=-1)
+
+ shp=der1.shape[:-1]
+ peak=np.full(shp,np.nan,dtype=der1.dtype)
+
+ forindinnp.ndindex(*shp):
+ ider1=der1[ind]
+
+ ifnp.any(~np.isfinite(ider1)):
+ continue
+
+ root=self._root(ider1)
+ xmax=np.real(
+ [
+ rr
+ forrrinroot
+ if(rr.imag==0)and(self._eval(rr,der2[ind])<0.0)
+ ]
+ )
+
+ peak[ind]=xmax[np.argmin(np.abs(xmax))]ifxmax.size>0elsenp.nan
+
+ returnpeak
+
+
+ def_model(self,ha,elementwise=False):
+ amp=self._fast_eval(
+ ha,self.param[...,:self.npara],elementwise=elementwise
+ )
+ phi=self._fast_eval(
+ ha,self.param[...,self.npara:],elementwise=elementwise
+ )
+
+ returnnp.exp(amp)*(np.cos(phi)+1.0j*np.sin(phi))
+
+ def_jacobian_amp(self,ha,elementwise=False):
+ jac=self._vander(ha,self.poly_deg_amp)
+ ifnotelementwise:
+ jac=np.rollaxis(jac,-1)
+ ifself.NisnotNone:
+ slc=(None,)*len(self.N)
+ jac=jac[slc]
+
+ returnjac
+
+ def_jacobian_phi(self,ha,elementwise=False):
+ jac=self._vander(ha,self.poly_deg_phi)
+ ifnotelementwise:
+ jac=np.rollaxis(jac,-1)
+ ifself.NisnotNone:
+ slc=(None,)*len(self.N)
+ jac=jac[slc]
+
+ returnjac
+
+ @property
+ defndofa(self):
+"""
+ Number of degrees of freedom for the amplitude fit.
+
+ Returns
+ -------
+ ndofa : np.ndarray[...]
+ Number of degrees of freedom of the amplitude fit.
+ """
+ returnself.ndof[...,0]
+
+ @property
+ defndofp(self):
+"""
+ Number of degrees of freedom for the phase fit.
+
+ Returns
+ -------
+ ndofp : np.ndarray[...]
+ Number of degrees of freedom of the phase fit.
+ """
+ returnself.ndof[...,1]
+
+ @property
+ defparameter_names(self):
+"""Array of strings containing the name of the fit parameters."""
+ returnnp.array(
+ ["%s_poly_amp_coeff%d"%(self.poly_type,p)forpinrange(self.npara)]
+ +["%s_poly_phi_coeff%d"%(self.poly_type,p)forpinrange(self.nparp)],
+ dtype=np.string_,
+ )
+
+
+
+
+[docs]
+classFitGaussAmpPolyPhase(FitPoly,FitAmpPhase):
+"""Class that enables fits of a gaussian to amplitude and a polynomial to phase."""
+
+ component=np.array(["complex"],dtype=np.string_)
+ npara=3
+
+ def__init__(self,poly_deg_phi=5,*args,**kwargs):
+"""Instantiates a FitGaussAmpPolyPhase object.
+
+ Parameters
+ ----------
+ poly_deg_phi : int
+ Degree of the polynomial to fit to phase.
+ """
+ super(FitGaussAmpPolyPhase,self).__init__(
+ poly_deg_phi=poly_deg_phi,*args,**kwargs
+ )
+
+ self.poly_deg_phi=poly_deg_phi
+ self.nparp=poly_deg_phi+1
+
+ def_fit(self,ha,resp,resp_err,width=5,absolute_sigma=False,param0=None):
+"""Fit gaussian to amplitude and polynomial to phase.
+
+ Uses non-linear least squares (`scipy.optimize.curve_fit`) to
+ fit the model to the complex valued data.
+
+ Parameters
+ ----------
+ ha : np.ndarray[nha,]
+ Hour angle in degrees.
+ resp : np.ndarray[nha,]
+ Measured response to the point source. Complex valued.
+ resp_err : np.ndarray[nha,]
+ Error on the measured response.
+ width : float
+ Initial guess at the width (sigma) of the transit in degrees.
+ absolute_sigma : bool
+ Set to True if the errors provided are absolute. Set to False if
+ the errors provided are relative, in which case the parameter covariance
+ will be scaled by the chi-squared per degree-of-freedom.
+ param0 : np.ndarray[nparam,]
+ Initial guess at the parameters for the Levenberg-Marquardt algorithm.
+ If these are not provided, then this function will make reasonable guesses.
+
+ Returns
+ -------
+ param : np.ndarray[nparam,]
+ Best-fit model parameters.
+ param_cov : np.ndarray[nparam, nparam]
+ Covariance of the best-fit model parameters.
+ chisq : float
+ Chi-squared of the fit.
+ ndof : int
+ Number of degrees of freedom of the fit.
+ """
+ ifha.size<(min(self.npara,self.nparp)+1):
+ raiseRuntimeError("Number of data points less than number of parameters.")
+
+ # We will fit the complex data. Break n-element complex array y(x)
+ # into 2n-element real array [Re{y(x)}, Im{y(x)}] for fit.
+ x=np.tile(ha,2)
+ y=np.concatenate((resp.real,resp.imag))
+ err=np.tile(resp_err,2)
+
+ # Initial estimate of parameter values:
+ # [peak_amplitude, centroid, fwhm, phi_0, phi_1, phi_2, ...]
+ ifparam0isNone:
+ param0=[np.max(np.nan_to_num(np.abs(resp))),0.0,2.355*width]
+ param0.append(np.median(np.nan_to_num(np.angle(resp,deg=True))))
+ param0+=[0.0]*(self.nparp-1)
+ param0=np.array(param0)
+
+ # Perform the fit.
+ param,param_cov=curve_fit(
+ self._get_fit_func(),
+ x,
+ y,
+ sigma=err,
+ p0=param0,
+ absolute_sigma=absolute_sigma,
+ jac=self._get_fit_jac(),
+ )
+
+ chisq=np.sum(
+ (
+ np.abs(resp-self._model(ha,param=param))
+ *tools.invert_no_zero(resp_err)
+ )
+ **2
+ )
+ ndof=y.size-self.nparam
+
+ returnparam,param_cov,chisq,ndof
+
+
+[docs]
+ defpeak(self):
+"""Return the peak of the transit.
+
+ Returns
+ -------
+ peak : float
+ Centroid of the gaussian fit to amplitude.
+ """
+ returnself.param[...,1]
+
+
+ def_get_fit_func(self):
+"""Generates a function that can be used by `curve_fit` to compute the model."""
+
+ deffit_func(x,*param):
+"""Function used by `curve_fit` to compute the model.
+
+ Parameters
+ ----------
+ x : np.ndarray[2 * nha,]
+ Hour angle in degrees replicated twice for the real
+ and imaginary components, i.e., `x = np.concatenate((ha, ha))`.
+ *param : floats
+ Parameters of the model.
+
+ Returns
+ -------
+ model : np.ndarray[2 * nha,]
+ Model for the complex valued point source response,
+ packaged as `np.concatenate((model.real, model.imag))`.
+ """
+ peak_amplitude,centroid,fwhm=param[:3]
+ poly_coeff=param[3:]
+
+ nreal=len(x)//2
+ xr=x[:nreal]
+
+ dxr=_correct_phase_wrap(xr-centroid)
+
+ model_amp=peak_amplitude*np.exp(-4.0*np.log(2.0)*(dxr/fwhm)**2)
+ model_phase=self._eval(xr,poly_coeff)
+
+ model=np.concatenate(
+ (model_amp*np.cos(model_phase),model_amp*np.sin(model_phase))
+ )
+
+ returnmodel
+
+ returnfit_func
+
+ def_get_fit_jac(self):
+"""Generates a function that can be used by `curve_fit` to compute jacobian of the model."""
+
+ deffit_jac(x,*param):
+"""Function used by `curve_fit` to compute the jacobian.
+
+ Parameters
+ ----------
+ x : np.ndarray[2 * nha,]
+ Hour angle in degrees. Replicated twice for the real
+ and imaginary components, i.e., `x = np.concatenate((ha, ha))`.
+ *param : float
+ Parameters of the model.
+
+ Returns
+ -------
+ jac : np.ndarray[2 * nha, nparam]
+ The jacobian defined as
+ jac[i, j] = d(model(ha)) / d(param[j]) evaluated at ha[i]
+ """
+
+ peak_amplitude,centroid,fwhm=param[:3]
+ poly_coeff=param[3:]
+
+ nparam=len(param)
+ nx=len(x)
+ nreal=nx//2
+
+ jac=np.empty((nx,nparam),dtype=x.dtype)
+
+ dx=_correct_phase_wrap(x-centroid)
+
+ dxr=dx[:nreal]
+ xr=x[:nreal]
+
+ model_amp=peak_amplitude*np.exp(-4.0*np.log(2.0)*(dxr/fwhm)**2)
+ model_phase=self._eval(xr,poly_coeff)
+ model=np.concatenate(
+ (model_amp*np.cos(model_phase),model_amp*np.sin(model_phase))
+ )
+
+ dmodel_dphase=np.concatenate((-model[nreal:],model[:nreal]))
+
+ jac[:,0]=tools.invert_no_zero(peak_amplitude)*model
+ jac[:,1]=8.0*np.log(2.0)*dx*tools.invert_no_zero(fwhm)**2*model
+ jac[:,2]=(
+ 8.0*np.log(2.0)*dx**2*tools.invert_no_zero(fwhm)**3*model
+ )
+ jac[:,3:]=(
+ self._vander(x,self.poly_deg_phi)*dmodel_dphase[:,np.newaxis]
+ )
+
+ returnjac
+
+ returnfit_jac
+
+ def_model(self,ha,param=None,elementwise=False):
+ ifparamisNone:
+ param=self.param
+
+ # Evaluate phase
+ model_phase=self._fast_eval(
+ ha,param[...,self.npara:],elementwise=elementwise
+ )
+
+ # Evaluate amplitude
+ amp_param=param[...,:self.npara]
+ ndim1=amp_param.ndim
+ ifnotelementwiseand(ndim1>1)andnotnp.isscalar(ha):
+ ndim2=ha.ndim
+ amp_param=amp_param[(slice(None),)*ndim1+(None,)*ndim2]
+ ha=ha[(None,)*(ndim1-1)+(slice(None),)*ndim2]
+
+ slc=(slice(None),)*(ndim1-1)
+ peak_amplitude=amp_param[slc+(0,)]
+ centroid=amp_param[slc+(1,)]
+ fwhm=amp_param[slc+(2,)]
+
+ dha=_correct_phase_wrap(ha-centroid)
+
+ model_amp=peak_amplitude*np.exp(-4.0*np.log(2.0)*(dha/fwhm)**2)
+
+ # Return complex valued quantity
+ returnmodel_amp*(np.cos(model_phase)+1.0j*np.sin(model_phase))
+
+ def_jacobian_amp(self,ha,elementwise=False):
+ amp_param=self.param[...,:self.npara]
+
+ shp=amp_param.shape
+ ndim1=amp_param.ndim
+
+ ifnotelementwise:
+ shp=shp+ha.shape
+
+ ifndim1>1:
+ ndim2=ha.ndim
+ amp_param=amp_param[(slice(None),)*ndim1+(None,)*ndim2]
+ ha=ha[(None,)*(ndim1-1)+(slice(None),)*ndim2]
+
+ slc=(slice(None),)*(ndim1-1)
+ peak_amplitude=amp_param[slc+(0,)]
+ centroid=amp_param[slc+(1,)]
+ fwhm=amp_param[slc+(2,)]
+
+ dha=_correct_phase_wrap(ha-centroid)
+
+ jac=np.zeros(shp,dtype=ha.dtype)
+ jac[slc+(0,)]=tools.invert_no_zero(peak_amplitude)
+ jac[slc+(1,)]=8.0*np.log(2.0)*dha*tools.invert_no_zero(fwhm)**2
+ jac[slc+(2,)]=8.0*np.log(2.0)*dha**2*tools.invert_no_zero(fwhm)**3
+
+ returnjac
+
+ def_jacobian_phi(self,ha,elementwise=False):
+ jac=self._vander(ha,self.poly_deg_phi)
+ ifnotelementwise:
+ jac=np.rollaxis(jac,-1)
+ ifself.NisnotNone:
+ slc=(None,)*len(self.N)
+ jac=jac[slc]
+
+ returnjac
+
+ @property
+ defparameter_names(self):
+"""Array of strings containing the name of the fit parameters."""
+ returnnp.array(
+ ["peak_amplitude","centroid","fwhm"]
+ +["%s_poly_phi_coeff%d"%(self.poly_type,p)forpinrange(self.nparp)],
+ dtype=np.string_,
+ )
+
+ @property
+ defndofa(self):
+"""
+ Number of degrees of freedom for the amplitude fit.
+
+ Returns
+ -------
+ ndofa : np.ndarray[...]
+ Number of degrees of freedom of the amplitude fit.
+ """
+ returnself.ndof[...,0]
+
+ @property
+ defndofp(self):
+"""
+ Number of degrees of freedom for the phase fit.
+
+ Returns
+ -------
+ ndofp : np.ndarray[...]
+ Number of degrees of freedom of the phase fit.
+ """
+ returnself.ndof[...,0]
+
+
+
+def_propagate_uncertainty(jac,cov,tval):
+"""Propagate uncertainty on parameters to uncertainty on model prediction.
+
+ Parameters
+ ----------
+ jac : np.ndarray[..., nparam] (elementwise) or np.ndarray[..., nparam, nha]
+ The jacobian defined as
+ jac[..., i, j] = d(model(ha)) / d(param[i]) evaluated at ha[j]
+ cov : [..., nparam, nparam]
+ Covariance of model parameters.
+ tval : np.ndarray[...]
+ Quantile of a standardized Student's t random variable.
+ The 1-sigma uncertainties will be scaled by this value.
+
+ Returns
+ -------
+ err : np.ndarray[...] (elementwise) or np.ndarray[..., nha]
+ Uncertainty on the model.
+ """
+ ifjac.ndim==cov.ndim:
+ # Corresponds to non-elementwise analysis
+ df2=np.sum(jac*np.matmul(cov,jac),axis=-2)
+ else:
+ # Corresponds to elementwise analysis
+ df2=np.sum(jac*np.sum(cov*jac[...,np.newaxis],axis=-1),axis=-1)
+
+ # Expand the tval array so that it can be broadcast against
+ # the sum squared error df2
+ add_dim=df2.ndim-tval.ndim
+ ifadd_dim>0:
+ tval=tval[(np.s_[...],)+(None,)*add_dim]
+
+ returntval*np.sqrt(df2)
+
+
+def_correct_phase_wrap(ha):
+"""Ensure hour angle is between -180 and 180 degrees.
+
+ Parameters
+ ----------
+ ha : np.ndarray or float
+ Hour angle in degrees.
+
+ Returns
+ -------
+ out : same as ha
+ Hour angle between -180 and 180 degrees.
+ """
+ return((ha+180.0)%360.0)-180.0
+
+
+
+[docs]
+deffit_point_source_map(
+ ra,
+ dec,
+ submap,
+ rms=None,
+ dirty_beam=None,
+ real_map=False,
+ freq=600.0,
+ ra0=None,
+ dec0=None,
+):
+"""Fits a map of a point source to a model.
+
+ Parameters
+ ----------
+ ra : np.ndarray[nra, ]
+ Transit right ascension.
+ dec : np.ndarray[ndec, ]
+ Transit declination.
+ submap : np.ndarray[..., nra, ndec]
+ Region of the ringmap around the point source.
+ rms : np.ndarray[..., nra]
+ RMS error on the map.
+ flag : np.ndarray[..., nra, ndec]
+ Boolean array that indicates which pixels to fit.
+ dirty_beam : np.ndarray[..., nra, ndec] or [ra, dec, dirty_beam]
+ Fourier transform of the weighting function used to create
+ the map. If input, then the interpolated dirty beam will be used
+ as the model for the point source response in the declination direction.
+ Can either be an array that is the same size as submap, or a list/tuple
+ of length 3 that contains [ra, dec, dirty_beam] since the shape of the
+ dirty beam is likely to be larger than the shape of the subregion of the
+ map, at least in the declination direction.
+
+ Returns
+ -------
+ param_name : np.ndarray[nparam, ]
+ Names of the parameters.
+ param : np.ndarray[..., nparam]
+ Best-fit parameters for each item.
+ param_cov: np.ndarray[..., nparam, nparam]
+ Parameter covariance for each item.
+ """
+
+ el=_dec_to_el(dec)
+
+ # Check if dirty beam was input
+ do_dirty=(dirty_beamisnotNone)and(
+ (len(dirty_beam)==3)or(dirty_beam.shape==submap.shape)
+ )
+ ifdo_dirty:
+ ifreal_map:
+ model=func_real_dirty_gauss
+ else:
+ model=func_dirty_gauss
+
+ # Get parameter names through inspection
+ param_name=inspect.getargspec(model(None)).args[1:]
+
+ # Define dimensions of the dirty beam
+ iflen(dirty_beam)!=3:
+ db_ra,db_dec,db=submap.ra,submap.dec,dirty_beam
+ else:
+ db_ra,db_dec,db=dirty_beam
+
+ db_el=_dec_to_el(db_dec)
+
+ # Define dimensions of the submap
+ coord=[ra,el]
+
+ else:
+ model=func_2d_gauss
+ param_name=inspect.getargspec(model).args[1:]
+
+ # Create 1d vectors that span the (ra, dec) grid
+ coord=[ra,dec]
+
+ # Extract parameter names from function
+ nparam=len(param_name)
+
+ # Examine dimensions of input data
+ dims=submap.shape
+ ndims=len(dims)
+
+ # If we are performing a single fit, then we need to recast shape to allow iteration
+ ifndims==2:
+ submap=submap[np.newaxis,...]
+ ifdo_dirty:
+ db=db[np.newaxis,...]
+ ifrmsisnotNone:
+ rms=rms[np.newaxis,...]
+
+ dims=submap.shape
+
+ dims=dims[0:-2]
+
+ # Create arrays to hold best-fit parameters and
+ # parameter covariance. Initialize to NaN.
+ param=np.full(dims+(nparam,),np.nan,dtype=np.float64)
+ param_cov=np.full(dims+(nparam,nparam),np.nan,dtype=np.float64)
+ resid_rms=np.full(dims,np.nan,dtype=np.float64)
+
+ # Iterate over dimensions
+ forindexinnp.ndindex(*dims):
+ # Extract the RMS for this index. In the process,
+ # check for data flagged as bad (rms == 0.0).
+ ifrmsisnotNone:
+ good_ra=rms[index]>0.0
+ this_rms=np.tile(
+ rms[index][good_ra,np.newaxis],[1,submap.shape[-1]]
+ ).ravel()
+ else:
+ good_ra=np.ones(submap.shape[-2],dtype=bool)
+ this_rms=None
+
+ ifnp.sum(good_ra)<=nparam:
+ continue
+
+ # Extract map
+ this_submap=submap[index][good_ra,:].ravel()
+ this_coord=[coord[0][good_ra],coord[1]]
+
+ # Specify initial estimates of parameter and parameter boundaries
+ ifra0isNone:
+ ra0=np.median(ra)
+ ifdec0isNone:
+ dec0=_el_to_dec(np.median(el))
+ offset0=np.median(np.nan_to_num(this_submap))
+ peak0=np.max(np.nan_to_num(this_submap))
+
+ p0_dict={
+ "peak_amplitude":peak0,
+ "centroid_x":ra0,
+ "centroid_y":dec0,
+ "fwhm_x":2.0,
+ "fwhm_y":2.0,
+ "offset":offset0,
+ "fringe_rate":22.0*freq*1e6/3e8,
+ }
+
+ lb_dict={
+ "peak_amplitude":0.0,
+ "centroid_x":ra0-1.5,
+ "centroid_y":dec0-0.75,
+ "fwhm_x":0.5,
+ "fwhm_y":0.5,
+ "offset":offset0-2.0*np.abs(offset0),
+ "fringe_rate":-200.0,
+ }
+
+ ub_dict={
+ "peak_amplitude":1.5*peak0,
+ "centroid_x":ra0+1.5,
+ "centroid_y":dec0+0.75,
+ "fwhm_x":6.0,
+ "fwhm_y":6.0,
+ "offset":offset0+2.0*np.abs(offset0),
+ "fringe_rate":200.0,
+ }
+
+ p0=np.array([p0_dict[key]forkeyinparam_name])
+
+ bounds=(
+ np.array([lb_dict[key]forkeyinparam_name]),
+ np.array([ub_dict[key]forkeyinparam_name]),
+ )
+
+ # Define model
+ ifdo_dirty:
+ fdirty=interp1d(
+ db_el,
+ db[index][good_ra,:],
+ axis=-1,
+ copy=False,
+ kind="cubic",
+ bounds_error=False,
+ fill_value=0.0,
+ )
+ this_model=model(fdirty)
+ else:
+ this_model=model
+
+ # Perform the fit. If there is an error,
+ # then we leave parameter values as NaN.
+ try:
+ popt,pcov=curve_fit(
+ this_model,
+ this_coord,
+ this_submap,
+ p0=p0,
+ sigma=this_rms,
+ absolute_sigma=True,
+ )# , bounds=bounds)
+ exceptExceptionaserror:
+ print(
+ "index %s: %s"
+ %("("+", ".join(["%d"%iiforiiinindex])+")",error)
+ )
+ continue
+
+ # Save the results
+ param[index]=popt
+ param_cov[index]=pcov
+
+ # Calculate RMS of the residuals
+ resid=this_submap-this_model(this_coord,*popt)
+ resid_rms[index]=1.4826*np.median(np.abs(resid-np.median(resid)))
+
+ # If this is a single fit, then remove singleton dimension
+ ifndims==2:
+ param=param[0]
+ param_cov=param_cov[0]
+ resid_rms=resid_rms[0]
+ submap=submap[0]
+ ifdo_dirty:
+ db=db[0]
+
+ # Return the best-fit parameters and parameter covariance
+ returnparam_name,param,param_cov,resid_rms
+
+
+
+
+[docs]
+deffunc_2d_gauss(
+ coord,peak_amplitude,centroid_x,centroid_y,fwhm_x,fwhm_y,offset
+):
+"""Returns a parameteric model for the map of a point source,
+ consisting of a 2-dimensional gaussian.
+
+ Parameters
+ ----------
+ coord : (ra, dec)
+ Tuple containing the right ascension and declination. These should be
+ coordinate vectors of length nra and ndec, respectively.
+ peak_amplitude : float
+ Model parameter. Normalization of the gaussian.
+ centroid_x : float
+ Model parameter. Centroid of the gaussian in degrees in the
+ right ascension direction.
+ centroid_y : float
+ Model parameter. Centroid of the gaussian in degrees in the
+ declination direction.
+ fwhm_x : float
+ Model parameter. Full width at half maximum of the gaussian
+ in degrees in the right ascension direction.
+ fwhm_y : float
+ Model parameter. Full width at half maximum of the gaussian
+ in degrees in the declination direction.
+ offset : float
+ Model parameter. Constant background value of the map.
+
+ Returns
+ -------
+ model : np.ndarray[nra*ndec]
+ Model prediction for the map of the point source.
+ """
+ x,y=coord
+
+ model=(
+ peak_amplitude
+ *np.exp(-4.0*np.log(2.0)*((x[:,np.newaxis]-centroid_x)/fwhm_x)**2)
+ *np.exp(-4.0*np.log(2.0)*((y[np.newaxis,:]-centroid_y)/fwhm_y)**2)
+ )+offset
+
+ returnmodel.ravel()
+
+
+
+
+[docs]
+deffunc_2d_sinc_gauss(
+ coord,peak_amplitude,centroid_x,centroid_y,fwhm_x,fwhm_y,offset
+):
+"""Returns a parameteric model for the map of a point source,
+ consisting of a sinc function along the declination direction
+ and gaussian along the right ascension direction.
+
+ Parameters
+ ----------
+ coord : (ra, dec)
+ Tuple containing the right ascension and declination. These should be
+ coordinate vectors of length nra and ndec, respectively.
+ peak_amplitude : float
+ Model parameter. Normalization of the gaussian.
+ centroid_x : float
+ Model parameter. Centroid of the gaussian in degrees in the
+ right ascension direction.
+ centroid_y : float
+ Model parameter. Centroid of the sinc function in degrees in the
+ declination direction.
+ fwhm_x : float
+ Model parameter. Full width at half maximum of the gaussian
+ in degrees in the right ascension direction.
+ fwhm_y : float
+ Model parameter. Full width at half maximum of the sinc function
+ in degrees in the declination direction.
+ offset : float
+ Model parameter. Constant background value of the map.
+
+ Returns
+ -------
+ model : np.ndarray[nra*ndec]
+ Model prediction for the map of the point source.
+ """
+ x,y=coord
+
+ model=(
+ peak_amplitude
+ *np.exp(-4.0*np.log(2.0)*((x[:,np.newaxis]-centroid_x)/fwhm_x)**2)
+ *np.sinc(1.2075*(y[np.newaxis,:]-centroid_y)/fwhm_y)
+ )+offset
+
+ returnmodel.ravel()
+
+
+
+
+[docs]
+deffunc_dirty_gauss(dirty_beam):
+"""Returns a parameteric model for the map of a point source,
+ consisting of the interpolated dirty beam along the y-axis
+ and a gaussian along the x-axis.
+
+ This function is a wrapper that defines the interpolated
+ dirty beam.
+
+ Parameters
+ ----------
+ dirty_beam : scipy.interpolate.interp1d
+ Interpolation function that takes as an argument el = sin(za)
+ and outputs an np.ndarray[nel, nra] that represents the dirty
+ beam evaluated at the same right ascension as the map.
+
+ Returns
+ -------
+ dirty_gauss : np.ndarray[nra*ndec]
+ Model prediction for the map of the point source.
+ """
+
+ defdirty_gauss(coord,peak_amplitude,centroid_x,centroid_y,fwhm_x,offset):
+"""Returns a parameteric model for the map of a point source,
+ consisting of the interpolated dirty beam along the y-axis
+ and a gaussian along the x-axis.
+
+ Parameter
+ ---------
+ coord : [ra, dec]
+ Tuple containing the right ascension and declination. These should be
+ coordinate vectors of length nra and ndec, respectively.
+ peak_amplitude : float
+ Model parameter. Normalization of the gaussian
+ in the right ascension direction.
+ centroid_x : float
+ Model parameter. Centroid of the gaussian in degrees in the
+ right ascension direction.
+ centroid_y : float
+ Model parameter. Centroid of the dirty beam in degrees in the
+ declination direction.
+ fwhm_x : float
+ Model parameter. Full width at half maximum of the gaussian
+ in degrees in the right ascension direction.
+ offset : float
+ Model parameter. Constant background value of the map.
+
+ Returns
+ -------
+ model : np.ndarray[nra*ndec]
+ Model prediction for the map of the point source.
+ """
+
+ x,y=coord
+
+ model=(
+ peak_amplitude
+ *np.exp(
+ -4.0*np.log(2.0)*((x[:,np.newaxis]-centroid_x)/fwhm_x)**2
+ )
+ *dirty_beam(y-_dec_to_el(centroid_y))
+ )+offset
+
+ returnmodel.ravel()
+
+ returndirty_gauss
+
+
+
+
+[docs]
+deffunc_real_dirty_gauss(dirty_beam):
+"""Returns a parameteric model for the map of a point source,
+ consisting of the interpolated dirty beam along the y-axis
+ and a sinusoid with gaussian envelope along the x-axis.
+
+ This function is a wrapper that defines the interpolated
+ dirty beam.
+
+ Parameters
+ ----------
+ dirty_beam : scipy.interpolate.interp1d
+ Interpolation function that takes as an argument el = sin(za)
+ and outputs an np.ndarray[nel, nra] that represents the dirty
+ beam evaluated at the same right ascension as the map.
+
+ Returns
+ -------
+ real_dirty_gauss : np.ndarray[nra*ndec]
+ Model prediction for the map of the point source.
+ """
+
+ defreal_dirty_gauss(
+ coord,peak_amplitude,centroid_x,centroid_y,fwhm_x,offset,fringe_rate
+ ):
+"""Returns a parameteric model for the map of a point source,
+ consisting of the interpolated dirty beam along the y-axis
+ and a sinusoid with gaussian envelope along the x-axis.
+
+ Parameter
+ ---------
+ coord : [ra, dec]
+ Tuple containing the right ascension and declination, each
+ of which is coordinate vectors of length nra and ndec, respectively.
+ peak_amplitude : float
+ Model parameter. Normalization of the gaussian
+ in the right ascension direction.
+ centroid_x : float
+ Model parameter. Centroid of the gaussian in degrees in the
+ right ascension direction.
+ centroid_y : float
+ Model parameter. Centroid of the dirty beam in degrees in the
+ declination direction.
+ fwhm_x : float
+ Model parameter. Full width at half maximum of the gaussian
+ in degrees in the right ascension direction.
+ offset : float
+ Model parameter. Constant background value of the map.
+ fringe_rate : float
+ Model parameter. Frequency of the sinusoid.
+
+ Returns
+ -------
+ model : np.ndarray[nra*ndec]
+ Model prediction for the map of the point source.
+ """
+
+ x,y=coord
+
+ model=(
+ peak_amplitude
+ *np.exp(
+ -4.0*np.log(2.0)*((x[:,np.newaxis]-centroid_x)/fwhm_x)**2
+ )
+ *dirty_beam(y-_dec_to_el(centroid_y))
+ )+offset
+
+ phase=np.exp(
+ 2.0j
+ *np.pi
+ *np.cos(np.radians(centroid_y))
+ *np.sin(-np.radians(x-centroid_x))
+ *fringe_rate
+ )
+
+ return(model*phase[:,np.newaxis]).real.ravel()
+
+ returnreal_dirty_gauss
+
+
+
+
+[docs]
+defguess_fwhm(freq,pol="X",dec=None,sigma=False,voltage=False,seconds=False):
+"""Provide rough estimate of the FWHM of the CHIME primary beam pattern.
+
+ It uses a linear fit to the median FWHM(nu) over all feeds of a given
+ polarization for CygA transits. CasA and TauA transits also showed
+ good agreement with this relationship.
+
+ Parameters
+ ----------
+ freq : float or np.ndarray
+ Frequency in MHz.
+ pol : string or bool
+ Polarization, can be 'X'/'E' or 'Y'/'S'
+ dec : float
+ Declination of the source in radians. If this quantity
+ is input, then the FWHM is divided by cos(dec) to account
+ for the increased rate at which a source rotates across
+ the sky. Default is do not correct for this effect.
+ sigma : bool
+ Return the standard deviation instead of the FWHM.
+ Default is to return the FWHM.
+ voltage : bool
+ Return the value for a voltage beam, otherwise returns
+ value for a power beam.
+ seconds : bool
+ Convert to elapsed time in units of seconds.
+ Otherwise returns in units of degrees on the sky.
+
+ Returns
+ -------
+ fwhm : float or np.ndarray
+ Rough estimate of the FWHM (or standard deviation if sigma=True).
+ """
+ # Define linear coefficients based on polarization
+ if(pol=="Y")or(pol=="S"):
+ coeff=[1.226e-06,-0.004097,3.790]
+ else:
+ coeff=[7.896e-07,-0.003226,3.717]
+
+ # Estimate standard deviation
+ sig=np.polyval(coeff,freq)
+
+ # Divide by declination to convert to degrees hour angle
+ ifdecisnotNone:
+ sig/=np.cos(dec)
+
+ # If requested, convert to seconds
+ ifseconds:
+ earth_rotation_rate=360.0/(24.0*3600.0)
+ sig/=earth_rotation_rate
+
+ # If requested, convert to width of voltage beam
+ ifvoltage:
+ sig*=np.sqrt(2)
+
+ # If sigma not explicitely requested, then convert to FWHM
+ ifnotsigma:
+ sig*=2.35482
+
+ returnsig
+
+
+
+
+[docs]
+defestimate_directional_scale(z,c=2.1):
+"""Calculate robust, direction dependent estimate of scale.
+
+ Parameters
+ ----------
+ z: np.ndarray
+ 1D array containing the data.
+ c: float
+ Cutoff in number of MAD. Data points whose absolute value is
+ larger than c * MAD from the median are saturated at the
+ maximum value in the estimator.
+
+ Returns
+ -------
+ zmed : float
+ The median value of z.
+ sa : float
+ Estimate of scale for z <= zmed.
+ sb : float
+ Estimate of scale for z > zmed.
+ """
+ zmed=np.median(z)
+
+ x=z-zmed
+
+ xa=x[x<=0.0]
+ xb=x[x>=0.0]
+
+ defhuber_rho(dx,c=2.1):
+ num=float(dx.size)
+
+ s0=1.4826*np.median(np.abs(dx))
+
+ dx_sig0=dx*tools.invert_no_zero(s0)
+
+ rho=(dx_sig0/c)**2
+ rho[rho>1.0]=1.0
+
+ return1.54*s0*np.sqrt(2.0*np.sum(rho)/num)
+
+ sa=huber_rho(xa,c=c)
+ sb=huber_rho(xb,c=c)
+
+ returnzmed,sa,sb
+
+
+
+
+[docs]
+deffit_histogram(
+ arr,
+ bins="auto",
+ rng=None,
+ no_weight=False,
+ test_normal=False,
+ return_histogram=False,
+):
+"""
+ Fit a gaussian to a histogram of the data.
+
+ Parameters
+ ----------
+ arr : np.ndarray
+ 1D array containing the data. Arrays with more than one dimension are flattened.
+ bins : int or sequence of scalars or str
+ - If `bins` is an int, it defines the number of equal-width bins in `rng`.
+ - If `bins` is a sequence, it defines a monotonically increasing array of bin edges,
+ including the rightmost edge, allowing for non-uniform bin widths.
+ - If `bins` is a string, it defines a method for computing the bins.
+ rng : (float, float)
+ The lower and upper range of the bins. If not provided, then the range spans
+ the minimum to maximum value of `arr`.
+ no_weight : bool
+ Give equal weighting to each histogram bin. Otherwise use proper weights based
+ on number of counts observed in each bin.
+ test_normal : bool
+ Apply the Shapiro-Wilk and Anderson-Darling tests for normality to the data.
+ return_histogram : bool
+ Return the histogram. Otherwise return only the best fit parameters and test statistics.
+
+ Returns
+ -------
+ results: dict
+ Dictionary containing the following fields:
+ indmin : int
+ Only bins whose index is greater than indmin were included in the fit.
+ indmax : int
+ Only bins whose index is less than indmax were included in the fit.
+ xmin : float
+ The data value corresponding to the centre of the `indmin` bin.
+ xmax : float
+ The data value corresponding to the centre of the `indmax` bin.
+ par: [float, float, float]
+ The parameters of the fit, ordered as [peak, mu, sigma].
+ chisq: float
+ The chi-squared of the fit.
+ ndof : int
+ The number of degrees of freedom of the fit.
+ pte : float
+ The probability to observe the chi-squared of the fit.
+
+ If `return_histogram` is True, then `results` will also contain the following fields:
+
+ bin_centre : np.ndarray
+ The bin centre of the histogram.
+ bin_count : np.ndarray
+ The bin counts of the histogram.
+
+ If `test_normal` is True, then `results` will also contain the following fields:
+
+ shapiro : dict
+ stat : float
+ The Shapiro-Wilk test statistic.
+ pte : float
+ The probability to observe `stat` if the data were drawn from a gaussian.
+ anderson : dict
+ stat : float
+ The Anderson-Darling test statistic.
+ critical : list of float
+ The critical values of the test statistic.
+ alpha : list of float
+ The significance levels corresponding to each critical value.
+ past : list of bool
+ Boolean indicating if the data passes the test for each critical value.
+ """
+ # Make sure the data is 1D
+ data=np.ravel(arr)
+
+ # Histogram the data
+ count,xbin=np.histogram(data,bins=bins,range=rng)
+ cbin=0.5*(xbin[0:-1]+xbin[1:])
+
+ cbin=cbin.astype(np.float64)
+ count=count.astype(np.float64)
+
+ # Form initial guess at parameter values using median and MAD
+ nparams=3
+ par0=np.zeros(nparams,dtype=np.float64)
+ par0[0]=np.max(count)
+ par0[1]=np.median(data)
+ par0[2]=1.48625*np.median(np.abs(data-par0[1]))
+
+ # Find the first zero points on either side of the median
+ cont=True
+ indmin=np.argmin(np.abs(cbin-par0[1]))
+ whilecont:
+ indmin-=1
+ cont=(count[indmin]>0.0)and(indmin>0)
+ indmin+=count[indmin]==0.0
+
+ cont=True
+ indmax=np.argmin(np.abs(cbin-par0[1]))
+ whilecont:
+ indmax+=1
+ cont=(count[indmax]>0.0)and(indmax<(len(count)-1))
+ indmax-=count[indmax]==0.0
+
+ # Restrict range of fit to between zero points
+ x=cbin[indmin:indmax+1]
+ y=count[indmin:indmax+1]
+ yerr=np.sqrt(y*(1.0-y/np.sum(y)))
+
+ sigma=Noneifno_weightelseyerr
+
+ # Require positive values of amp and sigma
+ bnd=(np.array([0.0,-np.inf,0.0]),np.array([np.inf,np.inf,np.inf]))
+
+ # Define the fitting function
+ defgauss(x,peak,mu,sigma):
+ returnpeak*np.exp(-((x-mu)**2)/(2.0*sigma**2))
+
+ # Perform the fit
+ par,var_par=curve_fit(
+ gauss,
+ cbin[indmin:indmax+1],
+ count[indmin:indmax+1],
+ p0=par0,
+ sigma=sigma,
+ absolute_sigma=(notno_weight),
+ bounds=bnd,
+ method="trf",
+ )
+
+ # Calculate quality of fit
+ chisq=np.sum(((y-gauss(x,*par))/yerr)**2)
+ ndof=np.size(y)-nparams
+ pte=1.0-scipy.stats.chi2.cdf(chisq,ndof)
+
+ # Store results in dictionary
+ results_dict={}
+ results_dict["indmin"]=indmin
+ results_dict["indmax"]=indmax
+ results_dict["xmin"]=cbin[indmin]
+ results_dict["xmax"]=cbin[indmax]
+ results_dict["par"]=par
+ results_dict["chisq"]=chisq
+ results_dict["ndof"]=ndof
+ results_dict["pte"]=pte
+
+ ifreturn_histogram:
+ results_dict["bin_centre"]=cbin
+ results_dict["bin_count"]=count
+
+ # If requested, test normality of the main distribution
+ iftest_normal:
+ flag=(data>cbin[indmin])&(data<cbin[indmax])
+ shap_stat,shap_pte=scipy.stats.shapiro(data[flag])
+
+ results_dict["shapiro"]={}
+ results_dict["shapiro"]["stat"]=shap_stat
+ results_dict["shapiro"]["pte"]=shap_pte
+
+ ander_stat,ander_crit,ander_signif=scipy.stats.anderson(
+ data[flag],dist="norm"
+ )
+
+ results_dict["anderson"]={}
+ results_dict["anderson"]["stat"]=ander_stat
+ results_dict["anderson"]["critical"]=ander_crit
+ results_dict["anderson"]["alpha"]=ander_signif
+ results_dict["anderson"]["pass"]=ander_stat<ander_crit
+
+ # Return dictionary
+ returnresults_dict
+[docs]
+defflag_outliers(raw,flag,window=25,nsigma=5.0):
+"""Flag outliers with respect to rolling median.
+
+ Parameters
+ ----------
+ raw : np.ndarray[nsample,]
+ Raw data sampled at fixed rate. Use the `flag` parameter to indicate missing
+ or invalid data.
+ flag : np.ndarray[nsample,]
+ Boolean array where True indicates valid data and False indicates invalid data.
+ window : int
+ Window size (in number of samples) used to determine local median.
+ nsigma : float
+ Data is considered an outlier if it is greater than this number of median absolute
+ deviations away from the local median.
+ Returns
+ -------
+ not_outlier : np.ndarray[nsample,]
+ Boolean array where True indicates valid data and False indicates data that is
+ either an outlier or had flag = True.
+ """
+ # Make sure we have an even window size
+ ifwindow%2:
+ window+=1
+
+ hwidth=window//2-1
+
+ nraw=raw.size
+ dtype=raw.dtype
+
+ # Replace flagged samples with nan
+ good=np.flatnonzero(flag)
+
+ data=np.full((nraw,),np.nan,dtype=dtype)
+ data[good]=raw[good]
+
+ # Expand the edges
+ expanded_data=np.concatenate(
+ (
+ np.full((hwidth,),np.nan,dtype=dtype),
+ data,
+ np.full((hwidth+1,),np.nan,dtype=dtype),
+ )
+ )
+
+ # Apply median filter
+ smooth=np.nanmedian(_sliding_window(expanded_data,window),axis=-1)
+
+ # Calculate RMS of residual
+ resid=np.abs(data-smooth)
+
+ rwidth=9*window
+ hrwidth=rwidth//2-1
+
+ expanded_resid=np.concatenate(
+ (
+ np.full((hrwidth,),np.nan,dtype=dtype),
+ resid,
+ np.full((hrwidth+1,),np.nan,dtype=dtype),
+ )
+ )
+
+ sig=1.4826*np.nanmedian(_sliding_window(expanded_resid,rwidth),axis=-1)
+
+ not_outlier=resid<(nsigma*sig)
+
+ returnnot_outlier
+
+
+
+
+[docs]
+definterpolate_gain(freq,gain,weight,flag=None,length_scale=30.0):
+"""Replace gain at flagged frequencies with interpolated values.
+
+ Uses a gaussian process regression to perform the interpolation
+ with a Matern function describing the covariance between frequencies.
+
+ Parameters
+ ----------
+ freq : np.ndarray[nfreq,]
+ Frequencies in MHz.
+ gain : np.ndarray[nfreq, ninput]
+ Complex gain for each input and frequency.
+ weight : np.ndarray[nfreq, ninput]
+ Uncertainty on the complex gain, expressed as inverse variance.
+ flag : np.ndarray[nfreq, ninput]
+ Boolean array indicating the good (True) and bad (False) gains.
+ If not provided, then it will be determined by evaluating `weight > 0.0`.
+ length_scale : float
+ Correlation length in frequency in MHz.
+
+ Returns
+ -------
+ interp_gain : np.ndarray[nfreq, ninput]
+ For frequencies with `flag = True`, this will be equal to gain. For frequencies with
+ `flag = False`, this will be an interpolation of the gains with `flag = True`.
+ interp_weight : np.ndarray[nfreq, ninput]
+ For frequencies with `flag = True`, this will be equal to weight. For frequencies with
+ `flag = False`, this will be the expected uncertainty on the interpolation.
+ """
+ fromsklearnimportgaussian_process
+ fromsklearn.gaussian_process.kernelsimportMatern,ConstantKernel
+
+ ifflagisNone:
+ flag=weight>0.0
+
+ nfreq,ninput=gain.shape
+
+ iscomplex=np.any(np.iscomplex(gain))
+
+ interp_gain=gain.copy()
+ interp_weight=weight.copy()
+
+ alpha=tools.invert_no_zero(weight)
+
+ x=freq.reshape(-1,1)
+
+ foriiinrange(ninput):
+ train=np.flatnonzero(flag[:,ii])
+ test=np.flatnonzero(~flag[:,ii])
+
+ iftrain.size>0:
+ xtest=x[test,:]
+
+ xtrain=x[train,:]
+ ifiscomplex:
+ ytrain=np.hstack(
+ (gain[train,ii,np.newaxis].real,gain[train,ii,np.newaxis].imag)
+ )
+ else:
+ ytrain=gain[train,ii,np.newaxis].real
+
+ # Mean subtract
+ ytrain_mu=np.mean(ytrain,axis=0,keepdims=True)
+ ytrain=ytrain-ytrain_mu
+
+ # Get initial estimate of variance
+ var=0.5*np.sum(
+ (
+ 1.4826
+ *np.median(
+ np.abs(ytrain-np.median(ytrain,axis=0,keepdims=True)),
+ axis=0,
+ )
+ )
+ **2
+ )
+
+ # Define kernel
+ kernel=ConstantKernel(
+ constant_value=var,constant_value_bounds=(0.01*var,100.0*var)
+ )*Matern(length_scale=length_scale,length_scale_bounds="fixed",nu=1.5)
+
+ # Regress against non-flagged data
+ gp=gaussian_process.GaussianProcessRegressor(
+ kernel=kernel,alpha=alpha[train,ii]
+ )
+
+ gp.fit(xtrain,ytrain)
+
+ # Predict error
+ ypred,err_ypred=gp.predict(xtest,return_std=True)
+
+ # When the gains are not complex, ypred will have a single dimension for
+ # sklearn version 1.1.2, but will have a second dimension of length 1 for
+ # earlier versions. The line below ensures consistent behavior.
+ ifypred.ndim==1:
+ ypred=ypred[:,np.newaxis]
+
+ interp_gain[test,ii]=ypred[:,0]+ytrain_mu[:,0]
+ ifiscomplex:
+ interp_gain[test,ii]+=1.0j*(ypred[:,1]+ytrain_mu[:,1])
+
+ # When the gains are complex, err_ypred will have a second dimension
+ # of length 2 for sklearn version 1.1.2, but will have a single dimension
+ # for earlier versions. The line below ensures consistent behavior.
+ iferr_ypred.ndim>1:
+ err_ypred=np.sqrt(np.sum(err_ypred**2,axis=-1)/err_ypred.shape[-1])
+
+ interp_weight[test,ii]=tools.invert_no_zero(err_ypred**2)
+
+ else:
+ # No valid data
+ interp_gain[:,ii]=0.0+0.0j
+ interp_weight[:,ii]=0.0
+
+ returninterp_gain,interp_weight
+
+
+
+
+[docs]
+definterpolate_gain_quiet(*args,**kwargs):
+"""Call `interpolate_gain` with `ConvergenceWarnings` silenced.
+
+ Accepts and passes all arguments and keyword arguments for `interpolate_gain`.
+ """
+ importwarnings
+ fromsklearn.exceptionsimportConvergenceWarning
+
+ withwarnings.catch_warnings():
+ warnings.filterwarnings("ignore",category=ConvergenceWarning,module="sklearn")
+ results=interpolate_gain(*args,**kwargs)
+
+ returnresults
+
+
+
+
+[docs]
+defthermal_amplitude(delta_T,freq):
+"""Computes the amplitude gain correction given a (set of) temperature
+ difference and a (set of) frequency based on the thermal model.
+
+ Parameters
+ ----------
+ delta_T : float or array of foats
+ Temperature difference (T - T_0) for which to find a gain correction.
+ freq : float or array of foats
+ Frequencies in MHz
+
+ Returns
+ -------
+ g : float or array of floats
+ Gain amplitude corrections. Multiply by data
+ to correct it.
+ """
+ m_params=[-4.28268629e-09,8.39576400e-06,-2.00612389e-03]
+ m=np.polyval(m_params,freq)
+
+ return1.0+m*delta_T
+
+
+
+def_el_to_dec(el):
+"""Convert from el = sin(zenith angle) to declination in degrees."""
+
+ returnnp.degrees(np.arcsin(el))+ephemeris.CHIMELATITUDE
+
+
+def_dec_to_el(dec):
+"""Convert from declination in degrees to el = sin(zenith angle)."""
+
+ returnnp.sin(np.radians(dec-ephemeris.CHIMELATITUDE))
+
+
+
+[docs]
+defget_reference_times_file(
+ times:np.ndarray,
+ cal_file:memh5.MemGroup,
+ logger:Optional[logging.Logger]=None,
+)->Dict[str,np.ndarray]:
+"""For a given set of times determine when and how they were calibrated.
+
+ This uses the pre-calculated calibration time reference files.
+
+ Parameters
+ ----------
+ times
+ Unix times of data points to be calibrated as floats.
+ cal_file
+ memh5 container which containes the reference times for calibration source
+ transits.
+ logger
+ A logging object to use for messages. If not provided, use a module level
+ logger.
+
+ Returns
+ -------
+ reftime_result : dict
+ A dictionary containing four entries:
+
+ - reftime: Unix time of same length as `times`. Reference times of transit of the
+ source used to calibrate the data at each time in `times`. Returns `NaN` for
+ times without a reference.
+ - reftime_prev: The Unix time of the previous gain update. Only set for time
+ samples that need to be interpolated, otherwise `NaN`.
+ - interp_start: The Unix time of the start of the interpolation period. Only
+ set for time samples that need to be interpolated, otherwise `NaN`.
+ - interp_stop: The Unix time of the end of the interpolation period. Only
+ set for time samples that need to be interpolated, otherwise `NaN`.
+ """
+
+ ifloggerisNone:
+ logger=logging.getLogger(__name__)
+
+ # Data from calibration file.
+ is_restart=cal_file["is_restart"][:]
+ tref=cal_file["tref"][:]
+ tstart=cal_file["tstart"][:]
+ tend=cal_file["tend"][:]
+ # Length of calibration file and of data points
+ n_cal_file=len(tstart)
+ ntimes=len(times)
+
+ # Len of times, indices in cal_file.
+ last_start_index=np.searchsorted(tstart,times,side="right")-1
+ # Len of times, indices in cal_file.
+ last_end_index=np.searchsorted(tend,times,side="right")-1
+ # Check for times before first update or after last update.
+ too_early=last_start_index<0
+ n_too_early=np.sum(too_early)
+ ifn_too_early>0:
+ msg=(
+ "{0} out of {1} time entries have no reference update."
+ +"Cannot correct gains for those entries."
+ )
+ logger.warning(msg.format(n_too_early,ntimes))
+ # Fot times after the last update, I cannot be sure the calibration is valid
+ # (could be that the cal file is incomplete. To be conservative, raise warning.)
+ too_late=(last_start_index>=(n_cal_file-1))&(
+ last_end_index>=(n_cal_file-1)
+ )
+ n_too_late=np.sum(too_late)
+ ifn_too_late>0:
+ msg=(
+ "{0} out of {1} time entries are beyond calibration file time values."
+ +"Cannot correct gains for those entries."
+ )
+ logger.warning(msg.format(n_too_late,ntimes))
+
+ # Array to contain reference times for each entry.
+ # NaN for entries with no reference time.
+ reftime=np.full(ntimes,np.nan,dtype=np.float64)
+ # Array to hold reftimes of previous updates
+ # (for entries that need interpolation).
+ reftime_prev=np.full(ntimes,np.nan,dtype=np.float64)
+ # Arrays to hold start and stop times of gain transition
+ # (for entries that need interpolation).
+ interp_start=np.full(ntimes,np.nan,dtype=np.float64)
+ interp_stop=np.full(ntimes,np.nan,dtype=np.float64)
+
+ # Acquisition restart. We load an old gain.
+ acqrestart=is_restart[last_start_index]==1
+ reftime[acqrestart]=tref[last_start_index][acqrestart]
+
+ # FPGA restart. Data not calibrated.
+ # There shouldn't be any time points here. Raise a warning if there are.
+ fpga_restart=is_restart[last_start_index]==2
+ n_fpga_restart=np.sum(fpga_restart)
+ ifn_fpga_restart>0:
+ msg=(
+ "{0} out of {1} time entries are after an FPGA restart but before the "
+ +"next kotekan restart. Cannot correct gains for those entries."
+ )
+ logger.warning(msg.format(n_fpga_restart,ntimes))
+
+ # This is a gain update
+ gainupdate=is_restart[last_start_index]==0
+
+ # This is the simplest case. Last update was a gain update and
+ # it is finished. No need to interpolate.
+ calrange=(last_start_index==last_end_index)&gainupdate
+ reftime[calrange]=tref[last_start_index][calrange]
+
+ # The next cases might need interpolation. Last update was a gain
+ # update and it is *NOT* finished. Update is in transition.
+ gaintrans=last_start_index==(last_end_index+1)
+
+ # This update is in gain transition and previous update was an
+ # FPGA restart. Just use new gain, no interpolation.
+ prev_is_fpga=is_restart[last_start_index-1]==2
+ prev_is_fpga=prev_is_fpga&gaintrans&gainupdate
+ reftime[prev_is_fpga]=tref[last_start_index][prev_is_fpga]
+
+ # The next two cases need interpolation of gain corrections.
+ # It's not possible to correct interpolated gains because the
+ # products have been stacked. Just interpolate the gain
+ # corrections to avoide a sharp transition.
+
+ # This update is in gain transition and previous update was a
+ # Kotekan restart. Need to interpolate gain corrections.
+ prev_is_kotekan=is_restart[last_start_index-1]==1
+ to_interpolate=prev_is_kotekan&gaintrans&gainupdate
+
+ # This update is in gain transition and previous update was a
+ # gain update. Need to interpolate.
+ prev_is_gain=is_restart[last_start_index-1]==0
+ to_interpolate=to_interpolate|(prev_is_gain&gaintrans&gainupdate)
+
+ # Reference time of this update
+ reftime[to_interpolate]=tref[last_start_index][to_interpolate]
+ # Reference time of previous update
+ reftime_prev[to_interpolate]=tref[last_start_index-1][to_interpolate]
+ # Start and stop times of gain transition.
+ interp_start[to_interpolate]=tstart[last_start_index][to_interpolate]
+ interp_stop[to_interpolate]=tend[last_start_index][to_interpolate]
+
+ # For times too early or too late, don't correct gain.
+ # This might mean we don't correct gains right after the last update
+ # that could in principle be corrected. But there is no way to know
+ # If the calibration file is up-to-date and the last update applies
+ # to all entries that come after it.
+ reftime[too_early|too_late]=np.nan
+
+ # Test for un-identified NaNs
+ known_bad_times=(too_early)|(too_late)|(fpga_restart)
+ n_bad_times=np.sum(~np.isfinite(reftime[~known_bad_times]))
+ ifn_bad_times>0:
+ msg=(
+ "{0} out of {1} time entries don't have a reference calibration time "
+ +"without an identifiable cause. Cannot correct gains for those entries."
+ )
+ logger.warning(msg.format(n_bad_times,ntimes))
+
+ # Bundle result in dictionary
+ result={
+ "reftime":reftime,
+ "reftime_prev":reftime_prev,
+ "interp_start":interp_start,
+ "interp_stop":interp_stop,
+ }
+
+ returnresult
+
+
+
+
+[docs]
+defget_reference_times_dataset_id(
+ times:np.ndarray,
+ dataset_ids:np.ndarray,
+ logger:Optional[logging.Logger]=None,
+)->Dict[str,Union[np.ndarray,Dict]]:
+"""Calculate the relevant calibration reference times from the dataset IDs.
+
+ .. warning::
+ Dataset IDs before 2020/10/10 are corrupt so this routine won't work.
+
+ Parameters
+ ----------
+ times
+ Unix times of data points to be calibrated as floats.
+ dataset_ids
+ The dataset IDs as an array of strings.
+ logger
+ A logging object to use for messages. If not provided, use a module level
+ logger.
+
+ Returns
+ -------
+ reftime_result
+ A dictionary containing the results. See `get_reference_times_file` for a
+ description of the contents.
+ """
+ ifloggerisNone:
+ logger=logging.getLogger(__name__)
+
+ # Dataset IDs before this date are untrustworthy
+ ds_start=ephemeris.datetime_to_unix(datetime(2020,11,1))
+ if(times<ds_start).any():
+ raiseValueError(
+ "Dataset IDs before 2020/11/01 are corrupt, so this method won't work. "
+ f"You passed in a time as early as {ctime.unix_to_datetime(times.min())}."
+ )
+
+ # The CHIME calibration sources
+ _source_dict={
+ "cyga":ephemeris.CygA,
+ "casa":ephemeris.CasA,
+ "taua":ephemeris.TauA,
+ "vira":ephemeris.VirA,
+ }
+
+ # Get the set of gain IDs for each time stamp
+ gain_ids=state_id_of_type(dataset_ids,"gains")
+ collapsed_ids=unique_unmasked_entry(gain_ids,axis=0)
+ unique_gains_ids=np.unique(collapsed_ids.compressed())
+
+ gain_info_dict={}
+
+ # For each gain update extract all the relevant information
+ forstate_idinunique_gains_ids:
+ d={}
+ gain_info_dict[state_id]=d
+
+ # Extract the update ID
+ update_id=ds.DatasetState.from_id(state_id).data["data"]["update_id"]
+
+ # Parse the ID for the required information
+ split_id=update_id.split("_")
+ # After restart we sometimes have only a timing update without a source
+ # reference. These aren't valid for our purposes here, and can be distinguished
+ # at the update_id doesn't contain source information, and is thus shorter
+ d["valid"]=any([srcinsplit_idforsrcin_source_dict.keys()])
+ d["interpolated"]="transition"insplit_id
+ # If it's not a valid update we shouldn't try to extract everything else
+ ifnotd["valid"]:
+ continue
+
+ d["gen_time"]=ctime.datetime_to_unix(ctime.timestr_to_datetime(split_id[1]))
+ d["source_name"]=split_id[2].lower()
+
+ # Calculate the source transit time, and sanity check it
+ source=_source_dict[d["source_name"]]
+ d["source_transit"]=ephemeris.transit_times(
+ source,d["gen_time"]-24*3600.0
+ )
+ cal_diff_hours=(d["gen_time"]-d["source_transit"])/3600
+ ifcal_diff_hours>3:
+ logger.warn(
+ f"Transit time ({ctime.unix_to_datetime(d['source_transit'])}) "
+ f"for source {d['source_name']} was a surprisingly long time "
+ f"before the gain update time ({cal_diff_hours} hours)."
+ )
+
+ # Array to store the extracted times in
+ reftime=np.zeros(len(collapsed_ids),dtype=np.float64)
+ reftime_prev=np.zeros(len(collapsed_ids),dtype=np.float64)
+ interp_start=np.zeros(len(collapsed_ids),dtype=np.float64)
+ interp_stop=np.zeros(len(collapsed_ids),dtype=np.float64)
+
+ # Iterate forward through the updates, setting transit times, and keeping track of
+ # the last valid update. This is used to set the previous source transit and the
+ # interpolation start time for all blended updates
+ last_valid_non_interpolated=None
+ last_non_interpolated=None
+ forii,state_idinenumerate(collapsed_ids):
+ valid_id=notnp.ma.is_masked(state_id)
+ update=gain_info_dict[state_id]ifvalid_idelse{}
+ valid=valid_idandupdate["valid"]
+
+ ifvalid:
+ reftime[ii]=update["source_transit"]
+ eliflast_valid_non_interpolatedisnotNone:
+ reftime[ii]=reftime[last_valid_non_interpolated]
+ else:
+ reftime[ii]=np.nan
+
+ ifvalidandupdate["interpolated"]andlast_valid_non_interpolatedisnotNone:
+ reftime_prev[ii]=reftime[last_valid_non_interpolated]
+ interp_start[ii]=times[last_non_interpolated]
+ else:
+ reftime_prev[ii]=np.nan
+ interp_start[ii]=np.nan
+
+ ifvalidandnotupdate["interpolated"]:
+ last_valid_non_interpolated=ii
+ ifvalid_idandnotupdate["interpolated"]:
+ last_non_interpolated=ii
+ # To identify the end of the interpolation periods we need to iterate
+ # backwards in time. As before we need to keep track of the last valid update
+ # we see, and then we set the interpolation end in the same manner.
+ last_non_interpolated=None
+ forii,state_idinlist(enumerate(collapsed_ids))[::-1]:
+ valid_id=notnp.ma.is_masked(state_id)
+ update=gain_info_dict[state_id]ifvalid_idelse{}
+ valid=valid_idandupdate.get("valid",False)
+
+ ifvalidandupdate["interpolated"]andlast_non_interpolatedisnotNone:
+ interp_stop[ii]=times[last_non_interpolated]
+ else:
+ interp_stop[ii]=np.nan
+
+ ifvalid_idandnotupdate["interpolated"]:
+ last_non_interpolated=ii
+
+ return{
+ "reftime":reftime,
+ "reftime_prev":reftime_prev,
+ "interp_start":interp_start,
+ "interp_stop":interp_stop,
+ "update_info":gain_info_dict,
+ }
+[docs]
+ defparams_ft(self,tm,vis,dec,x0_shift=5.0):
+"""Extract relevant parameters from source transit
+ visibility in two steps:
+ 1) FFT visibility
+ 2) Fit a gaussian to the transform
+
+ Parameters
+ ----------
+ tm : array-like
+ Independent variable (time)
+ trace : array-like
+ Dependent variable (visibility)
+ freq : float
+ Frenquency of the visibility trace, in MHz.
+ dec : float
+ Declination of source. Used for initial guess of
+ gaussian width. Defaults to CygA declination: 0.71
+
+ Returns
+ -------
+ popt : array of float
+ List with optimal parameters: [A,mu,sig2]
+ pcov : array of float
+ Covariance matrix for optimal parameters.
+ For details see documentation on numpy.curve_fit
+
+ """
+ fromscipy.optimizeimportcurve_fit
+
+ freqs=self.freqs
+ prods=self.prods
+
+ # Gaussian function for fit:
+ defgaus(x,A,mu,sig2):
+ returnA*np.exp(-((x-mu)**2)/(2.0*sig2))
+
+ # FFT:
+ # TODO: add check to see if length is multiple of 2
+ Nt=len(tm)
+ dt=tm[1]-tm[0]
+ ft=np.fft.fft(vis,axis=2)
+ fr=np.fft.fftfreq(Nt,dt)
+ # Re-order frequencies:
+ ft_ord=np.concatenate(
+ (ft[...,Nt//2+Nt%2:],ft[...,:Nt//2+Nt%2]),axis=2
+ )
+ ft_ord=abs(ft_ord)
+ fr_ord=np.concatenate((fr[Nt//2+Nt%2:],fr[:Nt//2+Nt%2]))
+
+ # Gaussian fits:
+ # Initial guesses:
+ # distx_0 = self.bslns0[:,0] # Should all be either 0 or +-22 for Pathfinder
+ x0_shift=5.0
+ distx_0=self.bslns0[:,0]+x0_shift# Shift to test robustness
+ mu0=(
+ -2.0
+ *np.pi
+ *freqs[:,np.newaxis]
+ *1e6
+ *distx_0[np.newaxis,:]
+ *np.sin(np.pi/2.0-dec)
+ /(3e8*SD)
+ )
+ ctr_idx=np.argmin(
+ abs(fr_ord[np.newaxis,np.newaxis,:]-mu0[...,np.newaxis]),axis=2
+ )
+ A0=np.array(
+ [
+ [ft_ord[ii,jj,ctr_idx[ii,jj]]forjjinrange(self.Npr)]
+ foriiinrange(self.Nfr)
+ ]
+ )
+ # 1 deg => dt = 1*(pi/180)*(24*3600/2*pi) = 240s
+ sigsqr0=1.0/(4.0*np.pi**2*(240.0*np.cos(dec))**2)
+ p0=np.array(
+ [
+ [[A0[ii,jj],mu0[ii,jj],sigsqr0]forjjinrange(self.Npr)]
+ foriiinrange(self.Nfr)
+ ]
+ )
+ # Perform fit:
+ # TODO: there must be a way to do the fits without the for-loops
+ prms=np.zeros((self.Nfr,self.Npr,3))
+ foriiinrange(self.Nfr):
+ forjjinrange(self.Npr):
+ try:
+ popt,pcov=curve_fit(gaus,fr_ord,ft_ord[ii,jj,:],p0[ii,jj])
+ prms[ii,jj]=np.array(popt)
+ # TODO: look for the right exception:
+ except:
+ # TODO: Use masked arrays instead of None?
+ prms[ii,jj]=[None]*3
+
+ returnprms
+
+
+ # TODO: change to 'get_yparams'
+ defgetparams_ft(self):
+""" """
+ # TODO: Add test to eliminate bad fits!
+ self.ft_prms1=self.params_ft(self.tm1,self.vis1,self.dec1)
+ ifself.source2isnotNone:
+ self.ft_prms2=self.params_ft(self.tm2,self.vis2,self.dec2)
+ else:
+ self.ft_prms2=None
+
+ returnself.ft_prms1,self.ft_prms2
+
+ # TODO: change all occurences of 'get_xdist' to 'xdists'
+ # to make it more consistent
+
+
+
+ defdata_quality(self):
+""" """
+ ifself.pass_xd1isNone:
+ ifself.source2isnotNone:
+ self.xdist_test(
+ self.c_xdists1,self.c_xdists2
+ )# Assigns self.pass_xd1, self.pass_xd2
+ else:
+ # Slightly higher tolerance since it uses rotated dists
+ self.xdist_test(self.xdists1,tol=2.5)
+
+ ifself.pass_cont1isNone:
+ self.continuity_test()# Assigns self.pass_cont1 and self.pass_cont2
+
+ gpxd1,gfxd1=self.good_prod_freq(self.pass_xd1)
+ gpc1,gfc1=self.good_prod_freq(self.pass_cont1)
+
+ ifself.source2isnotNone:
+ gpxd2,gfxd2=self.good_prod_freq(self.pass_xd2)
+ gpc2,gfc2=self.good_prod_freq(self.pass_cont2)
+
+ self.good_prods=np.logical_and(
+ np.logical_or(gpc1,gpc2),np.logical_or(gpxd1,gpxd2)
+ )# good prods
+ self.good_freqs=np.logical_and(
+ np.logical_or(gfc1,gfc2),np.logical_or(gfxd1,gfxd2)
+ )# good freqs
+
+ # TODO: Delete these conservative estimates?
+ self.good_prods_cons=np.logical_and(
+ np.logical_and(gpc1,gpc2),np.logical_and(gpxd1,gpxd2)
+ )# Conservative good prods
+ self.good_freqs_cons=np.logical_and(
+ np.logical_and(gfc1,gfc2),np.logical_and(gfxd1,gfxd2)
+ )# Conservative good freqs
+ else:
+ self.good_prods=np.logical_and(gpc1,gpxd1)# good prods
+ self.good_freqs=np.logical_and(gfc1,gfxd1)# good freqs
+
+ ifself.bsiptsisnotNone:
+ self.set_good_ipts(self.bsipts)# good_prods to good_ipts
+
+ defsingle_source_test(self):
+""" """
+ self.getparams_ft()
+ self.xdists1=self.get_xdist(self.ft_prms1,self.dec1)
+
+ self.data_quality()
+
+ defget_dists(self):
+""" """
+ # Get x distances in Earth coords (EW)
+ self.getparams_ft()
+ self.xdists1=self.get_xdist(self.ft_prms1,self.dec1)
+ self.xdists2=self.get_xdist(self.ft_prms2,self.dec2)
+ # Preliminary test for bad freqs (needed for ydists):
+ ifself.pass_cont1isNone:
+ self.continuity_test()# Assigns self.pass_cont1 and self.pass_cont2
+ gpc1,gfc1=self.good_prod_freq(self.pass_cont1)
+ gpc2,gfc2=self.good_prod_freq(self.pass_cont2)
+ gf=np.logical_and(gfc1,gfc2)# Preliminary good freqs
+ # Get y distances in cylinder coordinates (NS rotated by 2 deg)
+ self.getphases_tr()
+ self.c_ydists=self.get_c_ydist(good_freqs=gf)
+ # Transform between Earth and cylinder coords
+ self.c_xdists1=(
+ self.xdists1+self.c_ydists[np.newaxis,:]*np.sin(CR)
+ )/np.cos(CR)
+ self.c_xdists2=(
+ self.xdists2+self.c_ydists[np.newaxis,:]*np.sin(CR)
+ )/np.cos(CR)
+ self.ydists1=(
+ self.xdists1+self.c_ydists[np.newaxis,:]*np.sin(CR)
+ )*np.tan(CR)+self.c_ydists[np.newaxis,:]*np.cos(CR)
+ self.ydists2=(
+ self.xdists2+self.c_ydists[np.newaxis,:]*np.sin(CR)
+ )*np.tan(CR)+self.c_ydists[np.newaxis,:]*np.cos(CR)
+
+ self.dists_computed=True
+
+ self.data_quality()
+
+ returnself.c_xdists1,self.c_xdists2,self.c_ydists
+
+
+[docs]
+ defset_good_ipts(self,base_ipts):
+"""Good_prods to good_ipts"""
+ inp_list=[inptforinptinself.inputs]# Full input list
+ self.good_ipts=np.zeros(self.inputs.shape,dtype=bool)
+ forii,inprdinenumerate(self.inprds):
+ ifinprd[0]notinbase_ipts:
+ self.good_ipts[inp_list.index(inprd[0])]=self.good_prods[ii]
+ ifinprd[1]notinbase_ipts:
+ self.good_ipts[inp_list.index(inprd[1])]=self.good_prods[ii]
+ if(inprd[0]inbase_ipts)and(inprd[1]inbase_ipts):
+ self.good_ipts[inp_list.index(inprd[0])]=self.good_prods[ii]
+ self.good_ipts[inp_list.index(inprd[1])]=self.good_prods[ii]
+ # To make sure base inputs are tagged good:
+ forbsipinbase_ipts:
+ self.good_ipts[inp_list.index(bsip)]=True
+
+
+ defsolv_pos(self,dists,base_ipt):
+""" """
+ fromscipy.linalgimportsvd
+
+ # Matrix defining order of subtraction for baseline distances
+ M=np.zeros((self.Npr,self.Nip-1))
+ # Remove base_ipt as its position will be set to zero
+ sht_inp_list=[inptforinptinself.inputsifinpt!=base_ipt]
+ forii,inprdinenumerate(self.inprds):
+ ifinprd[0]!=base_ipt:
+ M[ii,sht_inp_list.index(inprd[0])]=1.0
+ ifinprd[1]!=base_ipt:
+ M[ii,sht_inp_list.index(inprd[1])]=-1.0
+ U,s,Vh=svd(M)
+ # TODO: add test for small s values to zero. Check existing code for that.
+ # Pseudo-inverse:
+ psd_inv=np.dot(np.transpose(Vh)*(1.0/s)[np.newaxis,:],np.transpose(U))
+ # Positions:
+ pstns=np.dot(psd_inv,dists)
+ # Add position of base_input
+ inp_list=[inptforinptinself.inputs]# Full input list
+ bs_inpt_idx=inp_list.index(base_ipt)# Original index of base_ipt
+ pstns=np.insert(pstns,bs_inpt_idx,0.0)
+
+ returnpstns
+
+ defget_postns(self):
+""" """
+ self.c_xd1=np.nanmedian(self.c_xdists1[self.good_freqs],axis=0)
+ self.c_xd2=np.nanmedian(self.c_xdists2[self.good_freqs],axis=0)
+ # Solve positions:
+ self.c_y=self.solv_pos(self.c_ydists,self.bsipts[0])
+ self.c_x1=self.solv_pos(self.c_xd1,self.bsipts[0])
+ self.c_x2=self.solv_pos(self.c_xd2,self.bsipts[0])
+ self.expy=self.solv_pos(self.c_bslns0[:,1],self.bsipts[0])
+ self.expx=self.solv_pos(self.c_bslns0[:,0],self.bsipts[0])
+
+ returnself.c_x1,self.c_x2,self.c_y
+
+ defxdist_test(self,xds1,xds2=None,tol=2.0):
+""" """
+
+ defget_centre(xdists,tol):
+"""Returns the median (across frequencies) of NS separation dists for each
+ baseline if this median is withing *tol* of a multiple of 22 meters. Else,
+ returns the multiple of 22 meters closest to this median (up to 3*22=66 meters)
+ """
+ xmeds=np.nanmedian(xdists,axis=0)
+ cylseps=np.arange(-1,2)*22.0ifself.PATHelsenp.arange(-3,4)*22.0
+ devs=abs(xmeds[:,np.newaxis]-cylseps[np.newaxis,:])
+ devmins=devs.min(axis=1)
+ centres=np.array(
+ [
+ (
+ xmeds[ii]# return median
+ ifdevmins[ii]<tol# if reasonable
+ elsecylseps[np.argmin(devs[ii])]
+ )# or use closest value
+ foriiinrange(devmins.size)
+ ]
+ )
+
+ returncentres
+
+ xcentre1=get_centre(xds1,tol)
+ xerr1=abs(xds1-xcentre1[np.newaxis,:])
+ self.pass_xd1=xerr1<tol
+
+ ifxds2isnotNone:
+ xcentre2=get_centre(xds2,tol)
+ xerr2=abs(xds2-xcentre2[np.newaxis,:])
+ self.pass_xd2=xerr2<tol
+ else:
+ self.pass_xd2=None
+
+ returnself.pass_xd1,self.pass_xd2
+
+
+[docs]
+ defcontinuity_test(self,tol=0.2,knl=5):
+"""Call only if freqs are adjacent.
+ Uses xdists (Earth coords) instead of c_xdists (cylinder coords)
+ to allow for calling before ydists are computed. Doesn't make any
+ difference for this test. Results are used in computing y_dists.
+ """
+ fromscipy.signalimportmedfilt
+
+ clean_xdists1=medfilt(self.xdists1,kernel_size=[knl,1])
+ diffs1=abs(self.xdists1-clean_xdists1)
+ self.pass_cont1=diffs1<tol
+
+ ifself.source2isnotNone:
+ clean_xdists2=medfilt(self.xdists2,kernel_size=[knl,1])
+ diffs2=abs(self.xdists2-clean_xdists2)
+ self.pass_cont2=diffs2<tol
+ else:
+ self.pass_cont2=None
+
+ returnself.pass_cont1,self.pass_cont2
+
+
+
+[docs]
+ defgood_prod_freq(
+ self,pass_rst,tol_ch1=0.3,tol_ch2=0.7,tol_fr1=0.6,tol_fr2=0.7
+ ):
+"""Tries to determine overall bad products and overall bad frequencies
+ from a test_pass result.
+ """
+
+ # First iteration:
+ chans_score=np.sum(pass_rst,axis=0)/float(pass_rst.shape[0])
+ freqs_score=np.sum(pass_rst,axis=1)/float(pass_rst.shape[1])
+ good_chans=chans_score>tol_ch1
+ good_freqs=freqs_score>tol_fr1
+ # Second Iteration:
+ pass_gch=pass_rst[:,np.where(good_chans)[0]]# Only good channels
+ pass_gfr=pass_rst[np.where(good_freqs)[0],:]# Only good freqs
+ chans_score=np.sum(pass_gfr,axis=0)/float(pass_gfr.shape[0])
+ freqs_score=np.sum(pass_gch,axis=1)/float(pass_gch.shape[1])
+ good_chans=chans_score>tol_ch2
+ good_freqs=freqs_score>tol_fr2
+
+ returngood_chans,good_freqs
+
+
+
+
+
+[docs]
+classChanMonitor(object):
+"""This class provides the user interface to FeedLocator.
+
+ It initializes instances of FeedLocator (normally one per polarization)
+ and returns results combined lists of results (good channels and positions,
+ agreement/disagreement with the layout database, etc.)
+
+ Feed locator should not
+ have to sepparate the visibilities in data to run the test on and data not to run the
+ test on. ChanMonitor should make the sepparation and provide FeedLocator with the right
+ data cube to test.
+
+ Parameters
+ ----------
+ t1 [t2] : Initial [final] time for the test period. If t2 not provided it is
+ set to 1 sideral day after t1
+ freq_sel
+ prod_sel
+ """
+
+ def__init__(
+ self,
+ t1,
+ t2=None,
+ freq_sel=None,
+ prod_sel=None,
+ bswp1=26,
+ bswp2=90,
+ bsep1=154,
+ bsep2=218,
+ ):
+"""Here t1 and t2 have to be unix time (floats)"""
+ self.t1=t1
+ ift2isNone:
+ self.t2=self.t1+SD
+ else:
+ self.t2=t2
+
+ self.acq_list=None
+ self.night_acq_list=None
+
+ self.finder=None
+ self.night_finder=None
+
+ self.source1=None
+ self.source2=None
+
+ # if prod_sel is not None:
+ self.prod_sel=prod_sel
+ # if freq_sel is not None:
+ self.freq_sel=freq_sel
+
+ self.dat1=None
+ self.dat2=None
+ self.tm1=None
+ self.tm2=None
+
+ self.freqs=None
+ self.prods=None
+ self.input_map=None
+ self.inputs=None
+
+ self.corr_inputs=None
+ self.pwds=None
+ self.pstns=None
+ self.p1_idx,self.p2_idx=None,None
+
+ self.bswp1=bswp1
+ self.bsep1=bsep1
+ self.bswp2=bswp2
+ self.bsep2=bsep2
+
+
+
+
+ # TODO: this is kind of silly right now.
+ # If it is initialized from data, I should use the data given
+ # or not allow for that possibility.
+
+[docs]
+ @classmethod
+ deffromdata(cls,data,freq_sel=None,prod_sel=None):
+"""Initialize class from andata object"""
+ t1=data.time[0]
+ t2=data.time[-1]
+ returncls(t1,t2,freq_sel=freq_sel,prod_sel=prod_sel)
+[docs]
+ defset_metadata(self,tms,input_map):
+"""Sets self.corr_inputs, self.pwds, self.pstns, self.p1_idx, self.p2_idx"""
+ fromch_utilimporttools
+
+ # Get CHIME ON channels:
+ half_time=ephemeris.unix_to_datetime(tms[int(len(tms)//2)])
+ corr_inputs=tools.get_correlator_inputs(half_time)
+ self.corr_inputs=tools.reorder_correlator_inputs(input_map,corr_inputs)
+ pwds=tools.is_chime_on(self.corr_inputs)# Which inputs are CHIME ON antennas
+ self.pwds=np.array(pwds,dtype=bool)
+ # Get cylinders and polarizations
+ self.pstns,self.p1_idx,self.p2_idx=self.get_pos_pol(
+ self.corr_inputs,self.pwds
+ )
+
+
+ defdetermine_bad_gpu_nodes(self,data,frac_time_on=0.7):
+ node_on=np.any(data.vis[:].real!=0.0,axis=1)
+
+ self.gpu_node_flag=np.sum(node_on,axis=1)>frac_time_on*node_on.shape[1]
+
+ defget_prod_sel(self,data):
+""" """
+ fromch_utilimporttools
+
+ input_map=data.input
+ tms=data.time
+ half_time=ephemeris.unix_to_datetime(tms[int(len(tms)//2)])
+ corr_inputs=tools.get_correlator_inputs(half_time)
+ corr_inputs=tools.reorder_correlator_inputs(input_map,corr_inputs)
+ pwds=tools.is_chime_on(corr_inputs)# Which inputs are CHIME ON antennas
+
+ wchp1,wchp2,echp1,echp2=self.get_cyl_pol(corr_inputs,pwds)
+
+ # Ensure base channels are CHIME and ON
+ whilenotpwds[np.where(input_map["chan_id"]==self.bswp1)[0][0]]:
+ self.bswp1+=1
+ whilenotpwds[np.where(input_map["chan_id"]==self.bswp2)[0][0]]:
+ self.bswp2+=1
+ whilenotpwds[np.where(input_map["chan_id"]==self.bsep1)[0][0]]:
+ self.bsep1+=1
+ whilenotpwds[np.where(input_map["chan_id"]==self.bsep2)[0][0]]:
+ self.bsep2+=1
+
+ prod_sel=[]
+ forii,prodinenumerate(data.prod):
+ add_prod=False
+ add_prod=add_prodor(
+ (prod[0]==self.bswp1andprod[1]inechp1)
+ or(prod[1]==self.bswp1andprod[0]inechp1)
+ )
+ add_prod=add_prodor(
+ (prod[0]==self.bswp2andprod[1]inechp2)
+ or(prod[1]==self.bswp2andprod[0]inechp2)
+ )
+ add_prod=add_prodor(
+ (prod[0]==self.bsep1andprod[1]inwchp1)
+ or(prod[1]==self.bsep1andprod[0]inwchp1)
+ )
+ add_prod=add_prodor(
+ (prod[0]==self.bsep2andprod[1]inwchp2)
+ or(prod[1]==self.bsep2andprod[0]inwchp2)
+ )
+
+ ifadd_prod:
+ prod_sel.append(ii)
+
+ prod_sel.sort()
+
+ returnprod_sel,pwds
+
+ defget_data(self):
+""" """
+ fromch_utilimportni_utils
+
+ self.set_acq_list()
+ src_cndts=self.get_src_cndts()
+
+ forsrcinsrc_cndts:
+ results_list=self.get_results(src)
+ iflen(results_list)!=0:
+ ifself.source1isNone:
+ # Get prod_sel if not given:
+ ifself.prod_selisNone:
+ # Load data with a single frequency to get prod_sel
+ dat=results_list[0].as_loaded_data(freq_sel=[0])
+ self.prod_sel,pwds=self.get_prod_sel(dat)
+ # Load data:
+ self.source1=src
+ self.dat1=results_list[0].as_loaded_data(
+ prod_sel=self.prod_sel,freq_sel=self.freq_sel
+ )
+ # TODO: correct process_synced_data to not crash when no NS
+ try:
+ self.dat1=ni_utils.process_synced_data(self.dat1)
+ except:
+ pass
+ self.freqs=self.dat1.freq
+ self.prods=self.dat1.prod
+ self.input_map=self.dat1.input
+ self.inputs=self.input_map["chan_id"]
+ self.tm1=self.dat1.time
+ # Set metadata (corr_inputs, pstns, polarizatins, etc...
+ self.set_metadata(self.tm1,self.input_map)
+
+ # Determine what frequencies are bad
+ # due to gpu nodes that are down
+ self.determine_bad_gpu_nodes(self.dat1)
+
+ # TODO: get corr_inputs for dat2 as well and compare to dat1
+ elifself.source2isNone:
+ self.source2=src
+ self.dat2=results_list[0].as_loaded_data(
+ prod_sel=self.prod_sel,freq_sel=self.freq_sel
+ )
+ # TODO: correct process_synced_data to not crash when no NS
+ try:
+ self.dat2=ni_utils.process_synced_data(self.dat2)
+ except:
+ pass
+ self.tm2=self.dat2.time
+ break
+
+ returnself.source1,self.source2
+
+
+[docs]
+ defget_results(self,src,tdelt=2800):
+"""If self.finder exists, then it takes a deep copy of this object,
+ further restricts the time range to include only src transits,
+ and then queries the database to obtain a list of the acquisitions.
+ If self.finder does not exist, then it creates a finder object,
+ restricts the time range to include only src transits between
+ self.t1 and self.t2, and then queries the database to obtain a list
+ of the acquisitions.
+ """
+
+ ifself.finderisnotNone:
+ f=copy.deepcopy(self.finder)
+ else:
+ f=finder.Finder(node_spoof=_DEFAULT_NODE_SPOOF)
+ f.filter_acqs((data_index.ArchiveInst.name=="pathfinder"))
+ f.only_corr()
+ f.set_time_range(self.t1,self.t2)
+
+ f.include_transits(src,time_delta=tdelt)
+
+ returnf.get_results()
+
+
+
+[docs]
+ defset_acq_list(self):
+"""This method sets four attributes. The first two attributes
+ are 'night_finder' and 'night_acq_list', which are the
+ finder object and list of acquisitions that
+ contain all night time data between self.t1 and self.t2.
+ The second two attributes are 'finder' and 'acq_list',
+ which are the finder object and list of acquisitions
+ that contain all data beween self.t1 and self.t2 with the
+ sunrise, sun transit, and sunset removed.
+ """
+
+ # Create a Finder object and focus on time range
+ f=finder.Finder(node_spoof=_DEFAULT_NODE_SPOOF)
+ f.filter_acqs((data_index.ArchiveInst.name=="pathfinder"))
+ f.only_corr()
+ f.set_time_range(self.t1,self.t2)
+
+ # Create a list of acquisitions that only contain data collected at night
+ f_night=copy.deepcopy(f)
+ f_night.exclude_daytime()
+
+ self.night_finder=f_night
+ self.night_acq_list=f_night.get_results()
+
+ # Create a list of acquisitions that flag out sunrise, sun transit, and sunset
+ mm=ephemeris.unix_to_datetime(self.t1).month
+ dd=ephemeris.unix_to_datetime(self.t1).day
+ mm=mm+float(dd)/30.0
+
+ fct=3.0
+ tol1=(np.arctan((mm-3.0)*fct)+np.pi/2.0)*10500.0/np.pi+1500.0
+ tol2=(np.pi/2.0-np.arctan((mm-11.0)*fct))*10500.0/np.pi+1500.0
+ ttol=np.minimum(tol1,tol2)
+
+ fct=5.0
+ tol1=(np.arctan((mm-4.0)*fct)+np.pi/2.0)*2100.0/np.pi+6000.0
+ tol2=(np.pi/2.0-np.arctan((mm-10.0)*fct))*2100.0/np.pi+6000.0
+ rstol=np.minimum(tol1,tol2)
+
+ f.exclude_sun(time_delta=ttol,time_delta_rise_set=rstol)
+
+ self.finder=f
+ self.acq_list=f.get_results()
+
+
+
+[docs]
+ defget_sunfree_srcs(self,srcs=None):
+"""This method uses the attributes 'night_acq_list' and
+ 'acq_list' to determine the srcs that transit
+ in the available data. If these attributes do not
+ exist, then the method 'set_acq_list' is called.
+ If srcs is not specified, then it defaults to the
+ brightest four radio point sources in the sky:
+ CygA, CasA, TauA, and VirA.
+ """
+
+ ifself.acq_listisNone:
+ self.set_acq_list()
+
+ ifsrcsisNone:
+ srcs=[ephemeris.CygA,ephemeris.CasA,ephemeris.TauA,ephemeris.VirA]
+ Ns=len(srcs)
+
+ clr=[False]*Ns
+ ntt=[False]*Ns# night transit
+
+ forii,srcinenumerate(srcs):
+ night_transit=np.array([])
+ foracqinself.night_acq_list:
+ night_transit=np.append(
+ night_transit,ephemeris.transit_times(src,*acq[1])
+ )
+
+ ifnight_transit.size:
+ ntt[ii]=True
+
+ ifsrc.namein["CygA","CasA"]:
+ transit=np.array([])
+ foracqinself.acq_list:
+ transit=np.append(transit,ephemeris.transit_times(src,*acq[1]))
+
+ iftransit.size:
+ clr[ii]=True
+
+ else:
+ clr[ii]=ntt[ii]
+
+ returnclr,ntt,srcs
+
+
+
+[docs]
+ defsingle_source_check(self):
+"""Assumes self.source1 is NOT None"""
+ Nipts=len(self.inputs)
+ self.good_ipts=np.zeros(Nipts,dtype=bool)
+ self.good_freqs=None
+
+ iflen(self.p1_idx)>0:
+ self.init_feedloc_p1()# Initiate FeedLocator
+ self.flp1.single_source_test()
+ self.good_freqs=self.flp1.good_freqs
+ good_frac=self.get_res_sing_src(self.flp1)
+ ifgood_frac<0.6:
+ msg="""
+WARNING!
+Less than 60% of P1 channels turned out good.
+This may be due to a poor choice of base channel.
+Consider re-running the test with different
+bswp1 and bsep1 arguments
+"""
+ print(msg)
+
+ iflen(self.p2_idx)>0:
+ self.init_feedloc_p2()# Initiate FeedLocator
+ self.flp2.single_source_test()
+ good_frac=self.get_res_sing_src(self.flp2)
+ ifgood_frac<0.6:
+ msg="""
+WARNING!
+Less than 60% of P2 channels turned out good.
+This may be due to a poor choice of base channel.
+Consider re-running the test with different
+bswp2 and bsep2 arguments
+"""
+ print(msg)
+
+ ifself.good_freqsisNone:
+ self.good_freqs=self.flp2.good_freqs
+ else:
+ self.good_freqs=np.logical_or(self.good_freqs,self.flp2.good_freqs)
+
+ self.results_summary()
+
+
+ deffull_check(self):
+""" """
+ ifself.source1isNone:
+ self.get_data()
+ ifself.source2isNone:
+ ifself.source1isNone:
+ raiseRuntimeError("No sources available.")
+ else:
+ self.single_source_check()
+ else:
+ Nipts=len(self.inputs)
+ self.good_ipts=np.zeros(Nipts,dtype=bool)
+ self.postns=np.zeros((Nipts,2))
+ self.expostns=np.zeros((Nipts,2))
+ self.good_freqs=None
+ iflen(self.p1_idx)>0:
+ self.init_feedloc_p1()# Initiate FeedLocator
+ self.flp1.get_dists()# Run tests
+ self.flp1.get_postns()# Solve for positions
+
+ self.good_freqs=self.flp1.good_freqs
+ good_frac=self.get_test_res(self.flp1)
+ ifgood_frac<0.6:
+ msg="""
+WARNING!
+Less than 60% of P1 channels turned out good.
+This may be due to a poor choice of base channel.
+Consider re-running the test with different
+bswp1 and bsep1 arguments
+"""
+ print(msg)
+
+ iflen(self.p2_idx)>0:
+ self.init_feedloc_p2()# Initiate FeedLocator
+ self.flp2.get_dists()# Run tests
+ self.flp2.get_postns()# Solve for positions
+
+ ifself.good_freqsisNone:
+ self.good_freqs=self.flp2.good_freqs
+ else:
+ self.good_freqs=np.logical_or(
+ self.good_freqs,self.flp2.good_freqs
+ )
+ good_frac=self.get_test_res(self.flp2)
+ ifgood_frac<0.6:
+ msg="""
+WARNING!
+Less than 60% of P2 channels turned out good.
+This may be due to a poor choice of base channel.
+Consider re-running the test with different
+bswp2 and bsep2 arguments
+"""
+ print(msg)
+
+ self.results_summary()
+
+ defresults_summary(self):
+""" """
+ self.bad_ipts=self.input_map[np.logical_not(self.good_ipts)]
+ self.deemed_bad_but_good=self.input_map[
+ np.logical_and(np.logical_not(self.pwds),self.good_ipts)
+ ]
+ self.bad_not_accounted=self.input_map[
+ np.logical_and(self.pwds,np.logical_not(self.good_ipts))
+ ]
+ ifself.source2isnotNone:
+ # TODO: maybe use only x-position. Y is too erratic...
+ self.pos_err=np.sum((self.postns-self.expostns)**2,axis=1)**0.5
+ self.wrong_position=self.input_map[self.pos_err>1.0]
+
+ defget_test_res(self,fl):
+""" """
+ forii,iptinenumerate(self.inputs):
+ forjj,fl_iptinenumerate(fl.inputs):
+ iffl_ipt==ipt:
+ self.good_ipts[ii]=fl.good_ipts[jj]
+ # TODO: add some treatment for c_x2 (mean? test diff?)
+ self.postns[ii][0]=fl.c_x1[jj]
+ self.postns[ii][1]=fl.c_y[jj]
+ self.expostns[ii][0]=fl.expx[jj]
+ self.expostns[ii][1]=fl.expy[jj]
+
+ good_frac=float(np.sum(fl.good_ipts))/float(fl.good_ipts.size)
+ returngood_frac
+
+ defget_res_sing_src(self,fl):
+""" """
+ forii,iptinenumerate(self.inputs):
+ forjj,fl_iptinenumerate(fl.inputs):
+ iffl_ipt==ipt:
+ self.good_ipts[ii]=fl.good_ipts[jj]
+
+ good_frac=float(np.sum(fl.good_ipts))/float(fl.good_ipts.size)
+ returngood_frac
+[docs]
+defgood_channels(
+ data,
+ gain_tol=10.0,
+ noise_tol=2.0,
+ fit_tol=0.02,
+ test_freq=0,
+ noise_synced=None,
+ inputs=None,
+ res_plot=False,
+ verbose=True,
+):
+"""Test data for misbehaving channels.
+
+ Three tests are performed:
+
+ 1. Excessively high digital gains,
+ 2. Compliance of noise to the radiometer equation and
+ 3. Goodness of fit to a template Tsky.
+
+ See `Doclib:235
+ <https://bao.phas.ubc.ca/doc/cgi-bin/general/documents/display?Id=235>`_
+ file 'data_quality.pdf' for details on how the filters and tolerances work.
+
+ Parameters
+ ----------
+ data : ch_util.andata.CorrData object
+ Data to run test on.
+ If andata object contains cross-correlations,
+ test is performed on auto-correlations only.
+ gain_tol : float
+ Tolerance for digital gains filter. Flag channels whose
+ digital gain fractional absolute deviation
+ is above 'gain_tol' (default is 10.)
+ noise_tol : float
+ Tolerance for radiometer noise filter. Flag channels whose
+ noise rms is higher then 'noise_tol' times the expected
+ from the radiometer equation. (default = 2.)
+ fit_tol : float
+ Tolerance for the fit-to-Tsky filter. Flag channels whose
+ fractional rms for the 'gain' fit parameter is above
+ 'fit_tol' (default = 0.02)
+ test_freq : integer
+ Index of frequency to test. Default is 0.
+ noise_synced : boolean
+ Use this to force the code to call (or not call)
+ ni_utils.process_synced_data(). If not given,
+ the code will determine if syncronized noise injection was on.
+ For acquisitions newer then 20150626T200540Z_pathfinder_corr,
+ noise injection info is written in the attributes. For older
+ acquisitions the function _check_ni() is called to determine
+ if noise injection is On.
+ inputs : list of CorrInputs, optional
+ List of CorrInput objects describing the channels in this
+ dataset. This is optional, if not set (default), then it will
+ look the data up in the database. This option just allows
+ control of the database accesses.
+ res_plot : boolean, optional
+ If True, a plot with all the tested channels and the
+ Tsky fits is generated. File naming is
+ `plot_fit_{timestamp}.pdf`
+ verbose : boolean, optional
+ Print out useful output as the tests are run.
+
+ Returns
+ -------
+ good_gains : list of int
+ 1. for channels that pass the gains filter, 0. otherwise.
+ good_noise : list of int
+ 1. for channels that pass the noise filter, 0. otherwise.
+ good_fit : list of int
+ 1. for channels that pass the fit-to-Tsky filter,
+ 0. otherwise.
+ test_chans : list of int
+ A list of the channels tested in the same order as they
+ appear in all the other lists returned
+
+ Examples
+ --------
+
+ Run test on frequency index 3. data is an andata object:
+
+ >>> good_gains, good_noise, good_fit, test_chans = good_channels(data,test_freq=3)
+
+ And to create a plot of the results:
+
+ >>> good_gains, good_noise, good_fit, test_chans = good_channels(data,test_freq=3,res_plot=True)
+
+ """
+
+ ifverbose:
+ print("Running data quality test.")
+
+ # Determine if data has a gated visibility:
+ is_gated_format="gated_vis0"indata
+
+ # Get number of samples during an integration period:
+ if"gpu.gpu_intergration_period"indata.attrs:
+ # From attributes:
+ n_samp=data.attrs["gpu.gpu_intergration_period"][0]
+ else:
+ # Or from integration time and bandwidth:
+ t_step=(
+ data.index_map["time"]["ctime"][1]-data.index_map["time"]["ctime"][0]
+ )# Integration time
+ bwdth=data.index_map["freq"][0][1]*1e6# Bandwidth comes in MHz
+ n_samp=t_step*bwdth
+
+ # Processing noise synced data, if noise_synced != False:
+ ifnoise_synced==False:
+ pass
+ elifnoise_synced==True:
+ ifis_gated_format:
+ # If data is gated, ignore noise_synced argument:
+ msg=(
+ "Warning: noise_synced=True argument given "
+ +"but data seems to be gated.\n"
+ +"Ignoring noise_synced argument"
+ )
+ print(msg)
+ else:
+ # Process noise synced data:
+ data=ni_utils.process_synced_data(data)
+ elifnoise_synced==None:
+ # If noise_synced is not given, try to read ni_enable from data:
+ try:
+ # Newer data have a noise-injection flag
+ ni_enable=data.attrs["fpga.ni_enable"][0].astype(bool)
+ except:
+ # If no info is found, run function to determine ni_enable:
+ ni_enable=_check_ni(data,test_freq)
+ # If noise injection is enabled and data is not gated:
+ ifni_enableandnotis_gated_format:
+ # Process noise synced data:
+ data=ni_utils.process_synced_data(data)
+
+ # Read full product array in data:
+ prod_array_full=data.index_map["prod"]
+ # Get indices for auto-corrs:
+ autos_index,autos_chan=_get_autos_index(prod_array_full)
+ # Select auto-corrs and test_freq only:
+ visi=np.array([data.vis[test_freq,jj,:]forjjinautos_index])
+ chan_array=np.array([chanforchaninautos_chan])
+ tmstp=data.index_map["time"]["ctime"]
+
+ # Remove non-chime channels (Noise source, RFI, 26m...):
+ visi,test_chans=_cut_non_chime(data,visi,chan_array,inputs)
+
+ # Digital gains test:
+ if"gain"indata:
+ ifverbose:
+ print("Testing quality of digital gains")
+
+ good_gains=_gains_test(data,test_freq,test_chans,tol=gain_tol)
+ else:
+ ifverbose:
+ msg=(
+ "Could not obtain digital gains information from data. "
+ +"Ignoring gains test."
+ )
+ print(msg)
+
+ good_gains=None
+
+ # Radiometer noise test:
+ ifverbose:
+ print("Testing noise levels")
+
+ good_noise,rnt=_noise_test(visi,tmstp,n_samp,tol=noise_tol)
+
+ # Standard channels to fit for Tsky:
+ if(good_gainsisnotNone)and(good_noiseisnotNone):
+ # Use channels that pass both tests
+ stand_chans=good_gains*good_noise
+ elif(good_gainsisNone)and(good_noiseisNone):
+ # If gains and noise tests are missing, run fit on all channels:
+ stand_chans=[1.0]*len(test_chans)
+ else:
+ ifgood_gainsisnotNone:
+ # If only gains test was run
+ stand_chans=good_gains
+ ifgood_noiseisnotNone:
+ # If only noise test was run
+ stand_chans=good_noise
+
+ # Median filter visibilities for fit test:
+ cut_vis=_median_filter(visi)
+
+ # Cut sun transit from visibilities:
+ ifverbose:
+ print("Cutting Sun transist from visibilities")
+ cut_vis,cut_tmstp=_cut_sun_transit(cut_vis,tmstp)
+
+ # Only run fit test if there are enough good channels
+ # and enogh time around Sun transits:
+ ifnp.sum(stand_chans)>50andlen(cut_tmstp)>100:
+ # Getting template visibility (most typical visibility):
+ ifverbose:
+ print("Getting template visibility")
+ gn,Ts=_get_template(cut_vis,stand_chans)
+
+ # Fit template to visibilities:
+ ifverbose:
+ print("Fitting template to visibilities")
+ good_fit,popt,perr,sky=_fit_template(Ts,cut_vis,tol=fit_tol)
+
+ # Create plot with results, if res_plot is True:
+ ifres_plot:
+ print("Generating plots")
+ _create_plot(
+ visi,
+ tmstp,
+ cut_tmstp,
+ sky,
+ popt,
+ test_chans,
+ good_gains,
+ good_noise,
+ good_fit,
+ )
+ else:
+ ifverbose:
+ ifnotnp.sum(stand_chans)>50:
+ print("Not enough channels for fit test.")
+ ifnotlen(cut_tmstp)>100:
+ print("Not enough time around Sun transit.")
+ print("Skipping template fit test.")
+ good_fit=None
+
+ ifverbose:
+ # Computing some statistics to the filter:
+ Nact,Nnoisy,Ngains,Nfit,Nbad=_stats_print(
+ good_noise,good_gains,good_fit,test_chans
+ )
+
+ print("Finished running data quality test.")
+
+ returngood_gains,good_noise,good_fit,test_chans
+
+
+
+def_check_ni(data,test_freq=0):
+"""This is a quick and dirt function to determine if
+ noise injection was ON or OFF for acquisitions
+ older then ctime = 1435349183, when noise injection
+ info started to be written to the h5py files
+
+ Parameters
+ ----------
+ data : andata.CorrData
+ Data to check for noise injection.
+ test_freq : int
+ frequency bin within data, to be run the test on
+
+ Returns
+ -------
+ ni_on : boolean
+ True if noise injection is On, False otherwise.
+ """
+
+ visi=data.vis[test_freq].real
+ # Divide visibility in even and odd time bins
+ ifvisi.shape[1]%2==0:
+ v_even=visi[:,0::2]
+ else:
+ v_even=visi[:,0:-1:2]# v_even and v_odd have same length
+ v_odd=visi[:,1::2]
+
+ # Average difference ON-OFF. Should be the same as Off-Off
+ # if noise injection is Off.
+ diff_on_off=np.mean(abs(v_even-v_odd))
+
+ # Divide odd visibility again in odd and even
+ ifv_odd.shape[1]%2==0:
+ v_1=v_odd[:,0::2]
+ else:
+ v_1=v_odd[:,0:-1:2]
+ v_2=v_odd[:,1::2]
+
+ # Average difference OFF-OFF.
+ diff_off_off=np.mean(abs(v_1-v_2))
+
+ # Ratio of differences. Sould be close to 1
+ # if noise injection is off.
+ ratio=diff_on_off/diff_off_off
+
+ ifratio>3.0:
+ ni_on=True
+ else:
+ ni_on=False
+
+ returnni_on
+
+
+def_get_autos_index(prod_array):
+"""Obtain auto-correlation indices from the 'prod' index map
+ returned by andata.
+ """
+ autos_index,autos_chan=[],[]
+ foriiinrange(len(prod_array)):
+ ifprod_array[ii][0]==prod_array[ii][1]:
+ autos_index.append(ii)
+ autos_chan.append(prod_array[ii][0])
+
+ returnautos_index,autos_chan
+
+
+def_get_prod_array(path):
+"""Function to get visibility product array from file path
+
+ Useful when desired file is known but not the time span, so that
+ finder and as_reader are not useful. Or when file is not known
+ to alpenhorn
+
+ Parameters:
+ ***********
+ path : string, path to file
+
+ Returns:
+ ********
+ prod_array : array-like, the visibility products.
+ """
+
+ # If given list of files, use first one:
+ ifisinstance(path,list):
+ path=path[0]
+
+ # Get file with single time, single frequency:
+ data_aux=andata.AnData.from_acq_h5(path,start=0,stop=1,freq_sel=0)
+
+ returndata_aux.index_map["prod"]
+
+
+def_cut_non_chime(data,visi,chan_array,inputs=None):
+"""
+ Remove non CHIME channels (noise injection, RFI antenna,
+ 26m, etc...) from visibility. Also remove channels marked
+ as powered-off in layout DB.
+ """
+
+ # Map of channels to corr. inputs:
+ input_map=data.input
+ tmstp=data.index_map["time"]["ctime"]# time stamp
+ # Datetime halfway through data:
+ half_time=ch_eph.unix_to_datetime(tmstp[int(len(tmstp)//2)])
+ # Get information on correlator inputs, if not already supplied
+ ifinputsisNone:
+ inputs=tools.get_correlator_inputs(half_time)
+ # Reorder inputs to have sema order as input map (and data)
+ inputs=tools.reorder_correlator_inputs(input_map,inputs)
+ # Get noise source channel index:
+
+ # Test if inputs are attached to CHIME antenna and powered on:
+ pwds=tools.is_chime_on(inputs)
+
+ foriiinrange(len(inputs)):
+ # if ( (not tools.is_chime(inputs[ii]))
+ if(notpwds[ii])and(iiinchan_array):
+ # Remove non-CHIME-on channels from visibility matrix...
+ idx=np.where(chan_array==ii)[0][0]# index of channel
+ visi=np.delete(visi,idx,axis=0)
+ # ...and from product array:
+ chan_array=np.delete(chan_array,idx,axis=0)
+
+ returnvisi,chan_array
+
+
+def_noise_test(visi,tmstp,n_samp,tol):
+"""Calls radiom_noise to obtain radiometer statistics
+ and aplies the noise tolerance to get a list of
+ channels that pass the radiometer noise test
+ """
+ Nchans=visi.shape[0]
+ # Array to hold radiom noise fractions
+ rnt=np.full((Nchans),np.nan)
+
+ # Cut daytime from visibility:
+ visi_night,tmstp_night=_cut_daytime(visi,tmstp)
+
+ run_noise_test=True
+ iftmstp_nightisNone:
+ # All data is in day-time
+ run_noise_test=False
+ elif(notisinstance(tmstp_night,list))and(len(tmstp_night)<20):
+ # To little night-time:
+ run_noise_test=False
+
+ ifnotrun_noise_test:
+ msg="Not enough night-time for noise test. Ignoring noise test."
+ print(msg)
+ good_noise=None
+ rnt=None
+ else:
+ # Run noise test
+ foriiinrange(Nchans):
+ # If multiple nights are present, result is a list:
+ ifisinstance(tmstp_night,list):
+ rnt_aux=[]# rnt parameter for each night
+ forjjinrange(len(visi_night)):
+ rnt_array,rnt_med,rnt_max,rnt_min=_radiom_noise(
+ visi_night[jj][ii,:].real,n_samp
+ )
+ rnt_aux.append(rnt_med)
+ # Use median of rnt's as parameter:
+ rnt[ii]=np.median(rnt_aux)
+ else:
+ rnt_array,rnt_med,rnt_max,rnt_min=_radiom_noise(
+ visi_night[ii,:].real,n_samp
+ )
+ # Use median of rnt's as parameter:
+ rnt[ii]=rnt_med
+
+ # List of good noise channels (Initialized with all True):
+ good_noise=np.ones((Nchans))
+ # Test noise against tolerance and isnan, isinf:
+ foriiinrange(Nchans):
+ is_nan_inf=np.isnan(rnt[ii])ornp.isinf(rnt[ii])
+ ifis_nan_inforrnt[ii]>tol:
+ good_noise[ii]=0.0
+ returngood_noise,rnt
+
+
+def_radiom_noise(trace,n_samp,wind=100):
+"""Generates radiometer noise test statistics"""
+
+ # If window is < the length, use length of trace:
+ wind=min(len(trace),wind)
+
+ # Window has to be even in length:
+ ifwind%2==1:
+ wind=wind-1
+
+ # Separate trace in windows:
+ t_w=[trace[ii*wind:(ii+1)*wind]foriiinrange(int(len(trace)//wind))]
+
+ # Estimate total Temp by median of each window:
+ T=[np.median(entry)forentryint_w]
+
+ # Subtract even - odd bins to get rid of general trends in data:
+ t_s=[
+ [t_w[ii][jj]-t_w[ii][jj+1]forjjinrange(0,int(wind),2)]
+ foriiinrange(len(t_w))
+ ]
+
+ # RMS of each window:
+ # Use MAD to estimate RMS. More robust against RFI/correlator spikes.
+ # sqrt(2) factor is due to my subtracting even - odd time bins.
+ # 1.4826 factor is to go from MAD to RMS of a normal distribution:
+ # rms = [ np.std(entry)/np.sqrt(2) for entry in t_s ] # Using MAD to estimate rms for now
+ rms=[
+ np.median([np.abs(entry[ii]-np.median(entry))foriiinrange(len(entry))])
+ *1.4826
+ /np.sqrt(2)
+ forentryint_s
+ ]
+
+ # Radiometer equation proporcionality factor:
+ r_fact=(0.5*n_samp)**0.5
+ # Radiometer noise factor (should be ~1):
+ rnt=[rms[ii]*r_fact/(T[ii])foriiinrange(len(rms))]
+
+ rnt_med=np.median(rnt)
+ rnt_max=np.max(rnt)
+ rnt_min=np.min(rnt)
+
+ returnrnt,rnt_med,rnt_max,rnt_min
+
+
+def_cut_daytime(visi,tmstp):
+"""Returns visibilities with night time only.
+ Returns an array if a single night is present.
+ Returns a list of arrays if multiple nights are present.
+ """
+
+ tstp=tmstp[1]-tmstp[0]# Get time step
+
+ risings=ch_eph.solar_rising(tmstp[0],tmstp[-1])
+ settings=ch_eph.solar_setting(tmstp[0],tmstp[-1])
+
+ iflen(risings)==0andlen(settings)==0:
+ next_rising=ch_eph.solar_rising(tmstp[-1])
+ next_setting=ch_eph.solar_setting(tmstp[-1])
+
+ ifnext_setting<next_rising:
+ # All data is in daylight time
+ cut_vis=None
+ cut_tmstp=None
+ else:
+ # All data is in night time
+ cut_vis=np.copy(visi)
+ cut_tmstp=tmstp
+
+ eliflen(settings)==0:# Only one rising:
+ sr=risings[0]
+ # Find time bin index closest to solar rising:
+ idx=np.argmin(np.abs(tmstp-sr))
+
+ # Determine time limits to cut:
+ # (20 min after setting and before rising, if within range)
+ cut_low=max(0,idx-int(20.0*60.0/tstp))# lower limit of time cut
+
+ # Cut daylight times:
+ cut_vis=np.copy(visi[:,:cut_low])
+ cut_tmstp=tmstp[:cut_low]
+
+ eliflen(risings)==0:# Only one setting:
+ ss=settings[0]
+ # Find time bin index closest to solar setting:
+ idx=np.argmin(np.abs(tmstp-ss))
+
+ # Determine time limits to cut:
+ # (20 min after setting and before rising, if within range)
+ cut_up=min(
+ len(tmstp),idx+int(20.0*60.0/tstp)
+ )# upper limit of time to cut
+
+ # Cut daylight times:
+ cut_vis=np.copy(visi[:,cut_up:])
+ cut_tmstp=tmstp[cut_up:]
+
+ else:
+ cut_pairs=[]
+ ifrisings[0]>settings[0]:
+ cut_pairs.append([tmstp[0],settings[0]])
+ foriiinrange(1,len(settings)):
+ cut_pairs.append([risings[ii-1],settings[ii]])
+ iflen(risings)==len(settings):
+ cut_pairs.append([risings[-1],tmstp[-1]])
+ else:
+ foriiinrange(len(settings)):
+ cut_pairs.append([risings[ii],settings[ii]])
+ iflen(risings)>len(settings):
+ cut_pairs.append([risings[-1],tmstp[-1]])
+
+ cut_tmstp=[]
+ cut_vis=[]
+ tmstp_remain=tmstp
+ vis_remain=np.copy(visi)
+
+ forcpincut_pairs:
+ # Find time bin index closest to cuts:
+ idx_low=np.argmin(np.abs(tmstp_remain-cp[0]))
+ idx_up=np.argmin(np.abs(tmstp_remain-cp[1]))
+
+ # Determine time limits to cut:
+ # (20 min after setting and before rising, if within range)
+ cut_low=max(
+ 0,idx_low-int(20.0*60.0/tstp)
+ )# lower limit of time cut
+ cut_up=min(
+ len(tmstp_remain),idx_up+int(20.0*60.0/tstp)
+ )# upper limit of time to cut
+
+ iflen(tmstp_remain[:cut_low])>0:
+ cut_vis.append(vis_remain[:,:cut_low])
+ cut_tmstp.append(
+ tmstp_remain[:cut_low]
+ )# Append times before rising to cut_tmstp
+ vis_remain=vis_remain[:,cut_up:]
+ tmstp_remain=tmstp_remain[
+ cut_up:
+ ]# Use times after setting for further cuts
+ iflen(tmstp_remain)>0:
+ # If there is a bit of night data in the end, append it:
+ cut_tmstp.append(tmstp_remain)
+ cut_vis.append(vis_remain)
+
+ returncut_vis,cut_tmstp
+
+
+def_gains_test(data,test_freq,test_chans,tol):
+"""Test channels for excessive digital gains."""
+
+ input_map=[entry[0]forentryindata.input]
+
+ # Get gains:
+ # (only gains of channels being tested)
+ gains=abs(
+ np.array(
+ [data.gain[test_freq,input_map.index(chan),0]forchanintest_chans]
+ )
+ )
+
+ g_med=np.median(gains)# median
+ g_devs=[abs(entry-g_med)forentryingains]# deviations from median
+ g_mad=np.median(g_devs)# MAD is insensitive to outlier deviations
+ g_frac_devs=[dev/g_madfordeving_devs]# Fractional deviations
+
+ Nchans=len(gains)# Number of channels
+
+ good_gains=np.ones(Nchans)# Good gains initialized to ones
+
+ foriiinrange(Nchans):
+ ifg_frac_devs[ii]>tol:# Tolerance for gain deviations
+ good_gains[ii]=0.0
+
+ returngood_gains
+
+
+def_stats_print(good_noise,good_gains,good_fit,test_chans):
+"""Generate a simple set of statistics for the test
+ and print them to screen.
+ """
+ print("\nFilter statistics:")
+
+ good_chans=[1]*len(test_chans)
+
+ Nact=len(test_chans)# Number of active channels
+ ifgood_noiseisnotNone:
+ Nnoisy=Nact-int(np.sum(good_noise))
+ print(
+ "Noisy channels: {0} out of {1} active channels ({2:2.1f}%)".format(
+ Nnoisy,Nact,Nnoisy*100/Nact
+ )
+ )
+ good_chans=good_chans*good_noise
+ else:
+ Nnoisy=None
+ ifgood_gainsisnotNone:
+ Ngains=Nact-int(np.sum(good_gains))
+ print(
+ "High digital gains: {0} out of {1} active channels ({2:2.1f}%)".format(
+ Ngains,Nact,Ngains*100/Nact
+ )
+ )
+ good_chans=good_chans*good_gains
+ else:
+ Ngains=None
+ ifgood_fitisnotNone:
+ Nfit=Nact-int(np.sum(good_fit))
+ print(
+ "Bad fit to T_sky: {0} out of {1} active channels ({2:2.1f}%)".format(
+ Nfit,Nact,Nfit*100/Nact
+ )
+ )
+ good_chans=good_chans*good_fit
+ else:
+ Nfit=None
+
+ # Obtain total number of bad channels:
+
+ ifnot((good_noiseisNone)and(good_gainsisNone)and(good_fitisNone)):
+ Nbad=Nact-int(np.sum(good_chans))
+ print(
+ "Overall bad: {0} out of {1} active channels ({2:2.1f}%)\n".format(
+ Nbad,Nact,Nbad*100/Nact
+ )
+ )
+ else:
+ Nbad=None
+
+ returnNact,Nnoisy,Ngains,Nfit,Nbad
+
+
+def_cut_sun_transit(cut_vis,tmstp,tcut=120.0):
+"""Cut sun transit times from visibilities.
+
+ Parameters
+ ----------
+ cut_vis : numpy 2D array
+ visibilities to cut (prod,time).
+ tmstp : numpy 1D array
+ time stamps (u-time)
+ tcut : float
+ time (in minutes) to cut on both sides of Sun transit.
+
+ """
+
+ # Start looking for transits tcut minutes before start time:
+ st_time=tmstp[0]-tcut*60.0
+ # Stop looking for transits tcut minutes after end time:
+ end_time=tmstp[-1]+tcut*60.0
+
+ # Find Sun transits between start time and end time:
+ sun_trans=ch_eph.solar_transit(st_time,end_time)
+
+ cut_tmstp=tmstp# Time stamps to be cut
+ tstp=tmstp[1]-tmstp[0]# Get time step
+ forstinsun_trans:
+ # Find time bin index closest to solar transit:
+ idx=np.argmin(np.abs(cut_tmstp-st))
+
+ # Determine time limits to cut:
+ # (tcut min on both sides of solar transit, if within range)
+ # lower limit of time cut:
+ cut_low=max(0,idx-int(tcut*60.0/tstp))
+ # upper limit of time cut:
+ cut_up=min(len(cut_tmstp),idx+int(tcut*60.0/tstp))
+
+ # Cut times of solar transit:
+ cut_vis=np.concatenate((cut_vis[:,:cut_low],cut_vis[:,cut_up:]),axis=1)
+ cut_tmstp=np.concatenate((cut_tmstp[:cut_low],cut_tmstp[cut_up:]))
+
+ returncut_vis,cut_tmstp
+
+
+def_median_filter(visi,ks=3):
+"""Median filter visibilities for fit test."""
+ fromscipy.signalimportmedfilt
+
+ # Median filter visibilities:
+ cut_vis=np.array(
+ [medfilt(visi[jj,:].real,kernel_size=ks)forjjinrange(visi.shape[0])]
+ )
+ returncut_vis
+
+
+def_get_template(cut_vis_full,stand_chans):
+"""Obtain template visibility through an SVD.
+ This template will be compared to the actual
+ visibilities in _fit_template.
+ """
+
+ # Full copy of visibilities without sun:
+ cut_vis=np.copy(cut_vis_full)
+
+ # Cut out noisy and bad-gain channels:
+ cut_vis=np.array(
+ [cut_vis[jj,:]forjjinrange(cut_vis.shape[0])ifstand_chans[jj]]
+ )
+
+ Nchans=cut_vis.shape[0]# Number of channels after cut
+
+ # Perform a first cut of the most outlying visibilities:
+ # Remove the offset of the visibilities (aprox T_receiver):
+ vis_test=np.array(
+ [cut_vis[jj,:]-np.min(cut_vis[jj,:])forjjinrange(Nchans)]
+ )
+ # Normalize visibilities:
+ vis_test=np.array(
+ [
+ vis_test[jj,:]/(np.max(vis_test[jj,:])-np.min(vis_test[jj,:]))
+ forjjinrange(vis_test.shape[0])
+ ]
+ )
+ medn=np.median(vis_test,axis=0)# Median visibility across channels
+ devs=[np.sum(abs(entry-medn))forentryinvis_test]# Deviations sumed in time
+ # Number of channels that can be ignored due
+ # to excessive deviations:
+ Ncut=10
+ # Find the channels with largest deviations:
+ indices=list(range(len(devs)))
+ del_ind=[]
+ fornninrange(Ncut):
+ max_ind=np.argmax(devs)
+ del_ind.append(indices[max_ind])
+ devs=np.delete(devs,max_ind)
+ indices=np.delete(indices,max_ind)
+ # Cut-out channels with largest deviations:
+ cut_vis=np.array(
+ [cut_vis[jj,:]forjjinrange(len(cut_vis))ifnot(jjindel_ind)]
+ )
+
+ Nchans=cut_vis.shape[0]# Number of channels after cut
+
+ # Find most typical channel within each frequency:
+ # Model: visi = gn*(Ts + Tr) = gn*Ts + gTr
+ # Where:
+ # gn : gains
+ # Ts : Sky temperature
+ # Tr : reciver temperature
+ # gTr = gn * Tr
+
+ # Determine first guess for receiver temperature * gain: Tr * gn = gTr
+ # (lower value of low-pass filtered visibility)
+ gTr=np.array([np.min(cut_vis[jj,:])forjjinrange(Nchans)])
+ # Matrix Vs is the visibilities minus the guessed receiver temperature * gn
+ # For the exact gTr it should be: Vs = visi - gTr = gn * Ts
+ Vs=np.array([cut_vis[jj,:]-gTr[jj]forjjinrange(Nchans)])
+
+ ss=1.0
+ ss_diff=1.0
+ tries=0
+ whiless_diff>0.01orss<3.0:
+ # SVD on Vs:
+ U,s,V=np.linalg.svd(Vs,full_matrices=False)
+
+ ss_diff=abs(ss-s[0]/s[1])
+ ss=s[0]/s[1]
+ # print 'Ratio of first to second singular value: ', ss
+
+ # Updated gains, sky temp, sky visibility and
+ # gTr approximations:
+ gn=U[:,0]*s[0]# Only first singular value
+ Ts=V[0,:]# Only first singular value
+ Vs=np.outer(gn,Ts)# Outer product
+ # New gTr is visi minus spprox sky vis, averaged over time:
+ gTr=np.mean((cut_vis-Vs),axis=1)
+ Vs=np.array([cut_vis[jj]-gTr[jj]forjjinrange(Nchans)])
+
+ tries+=1
+ iftries==100:
+ msg=(
+ "SVD search for Tsky at freq {0} did NOT converge.\n"
+ +"Bad channels list might not be accurate."
+ )
+ print(msg)
+ break
+
+ # Solution could have negative gains and negative T sky:
+ ifnp.sum(gn)<0.0:# Most gains are < 0 means sign is wrong
+ # if np.all(Ts < 0) and np.all(gn < 0):
+ Ts=Ts*(-1.0)
+ gn=gn*(-1.0)
+
+ returngn,Ts
+
+
+def_fit_template(Ts,cut_vis,tol):
+"""Fits template visibility to actual ones
+ to identify bad channels.
+ """
+
+ fromscipy.optimizeimportcurve_fit
+
+ classTemplate(object):
+ def__init__(self,tmplt):
+ self.tmplt=tmplt
+
+ deffit_func(self,t,gn,Tr):
+ returngn*self.tmplt[t]+Tr# Template*gains + receiver temper.
+
+ sky=Template(Ts)
+
+ # Amplitude of template used in first guesses:
+ amp_t=np.max(Ts)-np.min(Ts)
+ min_t=np.min(Ts)
+
+ popt,perr=[],[]
+ forchaninrange(cut_vis.shape[0]):
+ # First guesses:
+ amp=np.max(cut_vis[chan])-np.min(cut_vis[chan])
+ gn0=amp/amp_t# gain as fraction of amplitudes
+ Tr0=np.min(cut_vis[chan])-min_t*gn0
+ p0=[gn0,Tr0]# First guesses at parameters
+
+ # Make the fit:
+ xdata=list(range(len(cut_vis[chan])))
+ popt_aux,pcov=curve_fit(sky.fit_func,xdata,cut_vis[chan],p0)
+ perr_aux=np.sqrt(np.diag(pcov))# Standard deviation of parameters
+
+ popt.append(popt_aux)
+ perr.append(perr_aux)
+
+ Nchans=cut_vis.shape[0]
+ good_fit=np.ones(Nchans)
+ foriiinrange(Nchans):
+ neg_gain=popt[ii][0]<0.0# Negative gains fail the test
+ ifneg_gainor(abs(perr[ii][0]/popt[ii][0])>tol):
+ good_fit[ii]=0.0
+
+ returngood_fit,popt,perr,sky
+
+
+def_create_plot(
+ visi,tmstp,cut_tmstp,sky,popt,test_chans,good_gains,good_noise,good_fit
+):
+"""Creates plot of the visibilities and the fits
+ with labels for those that fail the tests
+ """
+ importmatplotlib
+
+ matplotlib.use("PDF")
+ importmatplotlib.pyplotasplt
+ importtime
+
+ # Visibilities to plot:
+ visi1=visi# Raw data
+ tmstp1=tmstp# Raw data
+ visi2=np.array(
+ [
+ [sky.fit_func(tt,popt[ii][0],popt[ii][1])forttinrange(len(cut_tmstp))]
+ foriiinrange(len(popt))
+ ]
+ )
+ tmstp2=cut_tmstp
+
+ # For title, use start time stamp:
+ title="Good channels result for {0}".format(
+ ch_eph.unix_to_datetime(tmstp1[0]).date()
+ )
+
+ # I need to know the slot for each channel:
+ defget_slot(channel):
+ slot_array=[4,2,16,14,3,1,15,13,8,6,12,10,7,5,11,9]
+ returnslot_array[int(channel)//16]
+
+ fig=plt.figure(figsize=(8,64))
+ fig.suptitle(title,fontsize=16)
+
+ if(tmstp1[-1]-tmstp1[0])/(24.0*3600.0)>3.0:
+ # Days since starting time
+ # Notice: same starting time for both
+ time_pl1=(tmstp1-tmstp1[0])/(3600*24)
+ time_pl2=(tmstp2-tmstp1[0])/(3600*24)
+ time_unit="days"
+ else:
+ # Hours since starting time
+ time_pl1=(tmstp1-tmstp1[0])/(3600)
+ time_pl2=(tmstp2-tmstp1[0])/(3600)
+ time_unit="hours"
+
+ foriiinrange(len(visi1)):
+ chan=test_chans[ii]
+
+ # Determine position in subplot:
+ ifchan<64:
+ pos=chan*4+1
+ elifchan<128:
+ pos=(chan-64)*4+2
+ elifchan<192:
+ pos=(chan-128)*4+3
+ elifchan<256:
+ pos=(chan-192)*4+4
+
+ # Create subplot:
+ plt.subplot(64,4,pos)
+
+ lab=""
+ # Or print standard label:
+ ifgood_gainsisnotNone:
+ ifnotgood_gains[ii]:
+ lab=lab+"bad gains | "
+ ifgood_noiseisnotNone:
+ ifnotgood_noise[ii]:
+ lab=lab+"noisy | "
+ ifnotgood_fit[ii]:
+ lab=lab+"bad fit"
+
+ iflab!="":
+ plt.plot([],[],"1.0",label=lab)
+ plt.legend(loc="best",prop={"size":6})
+
+ trace_pl1=visi1[ii,:].real
+ plt.plot(time_pl1,trace_pl1,"b-")
+
+ trace_pl2=visi2[ii,:].real
+ plt.plot(time_pl2,trace_pl2,"r-")
+
+ tm_brd=(time_pl1[-1]-time_pl1[0])/10.0
+ plt.xlim(time_pl1[0]-tm_brd,time_pl1[-1]+tm_brd)
+
+ # Determine limits of plots:
+ med=np.median(trace_pl1)
+ mad=np.median([abs(entry-med)forentryintrace_pl1])
+ plt.ylim(med-7.0*mad,med+7.0*mad)
+
+ # labels:
+ plt.ylabel("Ch{0} (Sl.{1})".format(chan,get_slot(chan)),fontsize=8)
+
+ # Hide numbering:
+ frame=plt.gca()
+ frame.axes.get_yaxis().set_ticks([])
+ if(chan!=63)and(chan!=127)and(chan!=191)and(chan!=255):
+ # Remove x-axis, except on bottom plots:
+ frame.axes.get_xaxis().set_ticks([])
+ else:
+ # Change size of numbers in x axis:
+ frame.tick_params(axis="both",which="major",labelsize=10)
+ frame.tick_params(axis="both",which="minor",labelsize=8)
+ ifchan==127:
+ # Put x-labels on bottom plots:
+ iftime_unit=="days":
+ plt.xlabel(
+ "Time (days since {0} UTC)".format(
+ ch_eph.unix_to_datetime(tmstp1[0])
+ ),
+ fontsize=10,
+ )
+ else:
+ plt.xlabel(
+ "Time (hours since {0} UTC)".format(
+ ch_eph.unix_to_datetime(tmstp1[0])
+ ),
+ fontsize=10,
+ )
+
+ ifchan==0:
+ plt.title("West cyl. P1(N-S)",fontsize=12)
+ elifchan==64:
+ plt.title("West cyl. P2(E-W)",fontsize=12)
+ elifchan==128:
+ plt.title("East cyl. P1(N-S)",fontsize=12)
+ elifchan==192:
+ plt.title("East cyl. P2(E-W)",fontsize=12)
+
+ filename="plot_fit_{0}.pdf".format(int(time.time()))
+ plt.savefig(filename)
+ plt.close()
+ print("Finished creating plot. File name: {0}".format(filename))
+
+"""
+Ephemeris routines
+
+The precession of the Earth's axis gives noticeable shifts in object
+positions over the life time of CHIME. To minimise the effects of this we
+need to be careful and consistent with our ephemeris calculations.
+Historically Right Ascension has been given with respect to the Vernal
+Equinox which has a significant (and unnecessary) precession in the origin of
+the RA axis. To avoid this we use the new Celestial Intermediate Reference
+System which does not suffer from this issue.
+
+Practically this means that when calculating RA, DEC coordinates for a source
+position at a *given time* you must be careful to obtain CIRS coordinates
+(and not equinox based ones). Internally using `ephemeris.object_coords` does
+exactly that for you, so for any lookup of coordinates you should use that on
+your requested body.
+
+Note that the actual coordinate positions of sources must be specified using
+RA, DEC coordinates in ICRS (which is roughly equivalent to J2000). The
+purpose of object_coords is to transform into new RA, DEC coordinates taking
+into account the precession and nutation of the Earth's polar axis since
+then.
+
+These kind of coordinate issues are tricky, confusing and hard to debug years
+later, so if you're unsure you are recommended to seek some advice.
+
+Constants
+=========
+
+:const:`CHIMELATITUDE`
+ CHIME's latitude [degrees].
+:const:`CHIMELONGITUDE`
+ CHIME's longitude [degrees].
+:const:`CHIMEALTITUDE`
+ CHIME's altitude [metres].
+:const:`SIDEREAL_S`
+ Number of SI seconds in a sidereal second [s/sidereal s]. You probably want
+ STELLAR_S instead.
+:const:`STELLAR_S`
+ Number of SI seconds in a stellar second [s/stellar s].
+:const:`CasA`
+ :class:`skyfield.starlib.Star` representing Cassiopeia A.
+:const:`CygA`
+ :class:`skyfield.starlib.Star` representing Cygnus A.
+:const:`TauA`
+ :class:`skyfield.starlib.Star` representing Taurus A.
+:const:`VirA`
+ :class:`skyfield.starlib.Star` representing Virgo A.
+
+
+Telescope Instances
+===================
+
+- :const:`chime`
+
+
+Ephemeris Functions
+===================
+
+- :py:meth:`skyfield_star_from_ra_dec`
+- :py:meth:`transit_times`
+- :py:meth:`solar_transit`
+- :py:meth:`lunar_transit`
+- :py:meth:`setting_times`
+- :py:meth:`solar_setting`
+- :py:meth:`lunar_setting`
+- :py:meth:`rising_times`
+- :py:meth:`solar_rising`
+- :py:meth:`lunar_rising`
+- :py:meth:`_is_skyfield_obj`
+- :py:meth:`peak_RA`
+- :py:meth:`get_source_dictionary`
+- :py:meth:`lsa`
+
+
+Time Utilities
+==============
+
+- :py:meth:`ensure_unix`
+- :py:meth:`chime_local_datetime`
+- :py:meth:`unix_to_datetime`
+- :py:meth:`datetime_to_unix`
+- :py:meth:`datetime_to_timestr`
+- :py:meth:`timestr_to_datetime`
+- :py:meth:`unix_to_skyfield_time`
+- :py:meth:`skyfield_time_to_unix`
+- :py:meth:`time_of_day`
+- :py:meth:`csd`
+- :py:meth:`csd_to_unix`
+- :py:meth:`unix_to_csd`
+- :py:meth:`parse_date`
+
+
+Miscellaneous Utilities
+=======================
+
+- :py:meth:`galt_pointing_model_ha`
+- :py:meth:`galt_pointing_model_dec`
+- :py:meth:`sphdist`
+"""
+
+fromdatetimeimportdatetime
+fromnumpy.core.multiarrayimportunravel_index
+
+# NOTE: Load Skyfield API but be sure to use skyfield_wrapper for loading data
+importskyfield.api
+
+importnumpyasnp
+
+fromcaput.timeimport(
+ unix_to_datetime,
+ datetime_to_unix,
+ datetime_to_timestr,
+ timestr_to_datetime,
+ leap_seconds_between,
+ time_of_day,
+ Observer,
+ unix_to_skyfield_time,
+ skyfield_time_to_unix,
+ skyfield_star_from_ra_dec,
+ skyfield_wrapper,
+ ensure_unix,
+ SIDEREAL_S,
+ STELLAR_S,
+)
+
+# Calvin derived the horizontal position of the center of the focal lines...
+# ...and the elevation of the focal line from survey coordinates:
+# All altitudes given in meters above sea level
+CHIMELATITUDE=49.3207092194
+CHIMELONGITUDE=-119.6236774310
+CHIMEALTITUDE=555.372
+
+# Calvin also positioned the GBO/TONE Outrigger similarly.
+# GBO/TONE Outrigger
+TONELATITUDE=38.4292962636
+TONELONGITUDE=-79.8451625395
+TONEALTITUDE=810.000
+
+# Rough position for outriggers.
+# These will be updated as positioning gets refined.
+# https://bao.chimenet.ca/doc/documents/1727
+KKOLATITUDE=49.41905
+KKOLONGITUDE=-120.5253
+KKOALTITUDE=835
+
+# Aliases for backwards compatibility
+PCOLATITUDE=KKOLATITUDE
+PCOLONGITUDE=KKOLONGITUDE
+PCOALTITUDE=KKOALTITUDE
+
+GBOLATITUDE=38.436122
+GBOLONGITUDE=-79.827922
+GBOALTITUDE=2710/3.28084
+
+HCOLATITUDE=40.8171082
+HCOLONGITUDE=-121.4689584
+HCOALTITUDE=3346/3.28084
+
+# Create the Observer instances for CHIME and outriggers
+chime=Observer(
+ lon=CHIMELONGITUDE,
+ lat=CHIMELATITUDE,
+ alt=CHIMEALTITUDE,
+ lsd_start=datetime(2013,11,15),
+)
+
+tone=Observer(
+ lon=TONELONGITUDE,
+ lat=TONELATITUDE,
+ alt=TONEALTITUDE,
+ lsd_start=datetime(2013,11,15),
+)
+
+kko=Observer(
+ lon=KKOLONGITUDE,
+ lat=KKOLATITUDE,
+ alt=KKOALTITUDE,
+ lsd_start=datetime(2013,11,15),
+)
+
+gbo=Observer(
+ lon=GBOLONGITUDE,
+ lat=GBOLATITUDE,
+ alt=GBOALTITUDE,
+ lsd_start=datetime(2013,11,15),
+)
+
+hco=Observer(
+ lon=HCOLONGITUDE,
+ lat=HCOLATITUDE,
+ alt=HCOALTITUDE,
+ lsd_start=datetime(2013,11,15),
+)
+
+
+def_get_chime():
+ importwarnings
+
+ warnings.warn("Use `ephemeris.chime` instead.",DeprecationWarning)
+ returnchime
+
+
+
+[docs]
+defgalt_pointing_model_ha(
+ ha_in,dec_in,a=[-5.872,-0.5292,5.458,-0.076,-0.707,0.0,0.0]
+):
+"""Calculate pointing correction in hour angle for the Galt Telescope
+ See description of the pointing model by Lewis Knee CHIME document library
+ 754 https://bao.chimenet.ca/doc/documents/754
+
+ Parameters
+ ----------
+ ha, dec : Skyfield Angle objects
+ Target hour angle and declination
+
+ a : list of floats
+ List of coefficients (in arcmin) for the pointing model
+ (NOTE: it is very unlikely that a user will want to change these
+ from the defaults, which are taken from the pointing model as of
+ 2019-2-15)
+
+ Returns
+ -------
+ Skyfield Angle object
+ Angular offset in hour angle
+ """
+
+ fromskyfield.positionlibimportAngle
+
+ ha=ha_in.radians
+ dec=dec_in.radians
+
+ # hour angle pointing correction in arcmin
+ delta_ha_cos_dec=(
+ a[0]
+ +a[1]*np.sin(dec)
+ +a[2]*np.cos(dec)
+ +a[3]*np.sin(ha)*np.sin(dec)
+ +a[4]*np.cos(ha)*np.sin(dec)
+ +a[5]*np.sin(ha)*np.cos(dec)
+ +a[6]*np.cos(ha)*np.cos(dec)
+ )
+
+ returnAngle(degrees=(delta_ha_cos_dec/np.cos(dec))/60.0)
+
+
+
+
+[docs]
+defgalt_pointing_model_dec(
+ ha_in,dec_in,b=[1.081,0.707,-0.076,0.0,0.0,0.0,0.0]
+):
+"""Calculate pointing correction in declination for the Galt Telescope
+ See description of the pointing model by Lewis Knee CHIME document library
+ 754 https://bao.chimenet.ca/doc/documents/754
+
+ Parameters
+ ----------
+ ha, dec : Skyfield Angle objects
+ Target hour angle and declination
+
+ b : list of floats
+ List of coefficients (in arcmin) for the pointing model
+ (NOTE: it is very unlikely that a user will want to change these
+ from the defaults, which are taken from the pointing model as of
+ 2019-2-15)
+
+ Returns
+ -------
+ Skyfield Angle object
+ Angular offset in hour angle
+ """
+
+ fromskyfield.positionlibimportAngle
+
+ ha=ha_in.radians
+ dec=dec_in.radians
+
+ # declination pointing correction in arcmin
+ delta_dec=(
+ b[0]
+ +b[1]*np.sin(ha)
+ +b[2]*np.cos(ha)
+ +b[3]*np.sin(dec)
+ +b[4]*np.cos(dec)
+ +b[5]*np.sin(dec)*np.cos(ha)
+ +b[6]*np.sin(dec)*np.sin(ha)
+ )
+
+ returnAngle(degrees=delta_dec/60.0)
+
+
+
+
+[docs]
+defparse_date(datestring):
+"""Convert date string to a datetime object.
+
+ Parameters
+ ----------
+ datestring : string
+ Date as YYYYMMDD-AAA, where AAA is one of [UTC, PST, PDT]
+
+ Returns
+ -------
+ date : datetime
+ A python datetime object in UTC.
+ """
+ fromdatetimeimportdatetime,timedelta
+ importre
+
+ rm=re.match("([0-9]{8})-([A-Z]{3})",datestring)
+ ifrmisNone:
+ msg=(
+ "Wrong format for datestring: {0}.".format(datestring)
+ +"\nShould be YYYYMMDD-AAA, "
+ +"where AAA is one of [UTC,PST,PDT]"
+ )
+ raiseValueError(msg)
+
+ datestring=rm.group(1)
+ tzoffset=0.0
+ tz=rm.group(2)
+
+ tzs={"PDT":-7.0,"PST":-8.0,"EDT":-4.0,"EST":-5.0,"UTC":0.0}
+
+ iftzisnotNone:
+ try:
+ tzoffset=tzs[tz.upper()]
+ exceptKeyError:
+ print("Time zone {} not known. Known time zones:".format(tz))
+ forkey,valueintzs.items():
+ print(key,value)
+ print("Using UTC{:+.1f}.".format(tzoffset))
+
+ returndatetime.strptime(datestring,"%Y%m%d")-timedelta(hours=tzoffset)
+
+
+
+
+[docs]
+defutc_lst_to_mjd(datestring,lst,obs=chime):
+"""Convert datetime string and LST to corresponding modified Julian Day
+
+ Parameters
+ ----------
+ datestring : string
+ Date as YYYYMMDD-AAA, where AAA is one of [UTC, PST, PDT]
+ lst : float
+ Local sidereal time at DRAO (CHIME) in decimal hours
+ obs : caput.Observer object
+
+ Returns
+ -------
+ mjd : float
+ Modified Julian Date corresponding to the given time.
+ """
+ return(
+ unix_to_skyfield_time(
+ obs.lsa_to_unix(lst*360/24,datetime_to_unix(parse_date(datestring)))
+ ).tt
+ -2400000.5
+ )
+
+
+
+
+[docs]
+defsphdist(long1,lat1,long2,lat2):
+"""
+ Return the angular distance between two coordinates.
+
+ Parameters
+ ----------
+
+ long1, lat1 : Skyfield Angle objects
+ longitude and latitude of the first coordinate. Each should be the
+ same length; can be one or longer.
+
+ long2, lat2 : Skyfield Angle objects
+ longitude and latitude of the second coordinate. Each should be the
+ same length. If long1, lat1 have length longer than 1, long2 and
+ lat2 should either have the same length as coordinate 1 or length 1.
+
+ Returns
+ -------
+ dist : Skyfield Angle object
+ angle between the two coordinates
+ """
+ fromskyfield.positionlibimportAngle
+
+ dsinb=np.sin((lat1.radians-lat2.radians)/2.0)**2
+
+ dsinl=(
+ np.cos(lat1.radians)
+ *np.cos(lat2.radians)
+ *(np.sin((long1.radians-long2.radians)/2.0))**2
+ )
+
+ dist=np.arcsin(np.sqrt(dsinl+dsinb))
+
+ returnAngle(radians=2*dist)
+
+
+
+
+[docs]
+defsolar_transit(start_time,end_time=None,obs=chime):
+"""Find the Solar transits between two times for CHIME.
+
+ Parameters
+ ----------
+ start_time : float (UNIX time) or datetime
+ Start time to find transits.
+ end_time : float (UNIX time) or datetime, optional
+ End time for finding transits. If `None` default, search for 24 hours
+ after start time.
+
+ Returns
+ -------
+ transit_times : array_like
+ Array of transit times (in UNIX time).
+
+ """
+
+ planets=skyfield_wrapper.ephemeris
+ sun=planets["sun"]
+ returnobs.transit_times(sun,start_time,end_time)
+
+
+
+
+[docs]
+deflunar_transit(start_time,end_time=None,obs=chime):
+"""Find the Lunar transits between two times for CHIME.
+
+ Parameters
+ ----------
+ start_time : float (UNIX time) or datetime
+ Start time to find transits.
+ end_time : float (UNIX time) or datetime, optional
+ End time for finding transits. If `None` default, search for 24 hours
+ after start time.
+
+ Returns
+ -------
+ transit_times : array_like
+ Array of transit times (in UNIX time).
+
+ """
+
+ planets=skyfield_wrapper.ephemeris
+ moon=planets["moon"]
+ returnobs.transit_times(moon,start_time,end_time)
+
+
+
+# Create CHIME specific versions of various calls.
+lsa_to_unix=chime.lsa_to_unix
+unix_to_lsa=chime.unix_to_lsa
+unix_to_csd=chime.unix_to_lsd
+csd_to_unix=chime.lsd_to_unix
+csd=unix_to_csd
+lsa=unix_to_lsa
+transit_times=chime.transit_times
+setting_times=chime.set_times
+rising_times=chime.rise_times
+CSD_ZERO=chime.lsd_start_day
+
+
+
+[docs]
+deftransit_RA(time):
+"""No longer supported. Use `lsa` instead."""
+ raiseNotImplementedError(
+ "No longer supported. Use the better defined `lsa` instead."
+ )
+
+
+
+
+[docs]
+defchime_local_datetime(*args):
+"""Create a :class:`datetime.datetime` object in Canada/Pacific timezone.
+
+ Parameters
+ ----------
+ *args
+ Any valid arguments to the constructor of :class:`datetime.datetime`
+ except *tzinfo*. Local date and time at CHIME.
+
+ Returns
+ -------
+ dt : :class:`datetime.datetime`
+ Timezone naive date and time but converted to UTC.
+
+ """
+
+ frompytzimporttimezone
+
+ tz=timezone("Canada/Pacific")
+ dt_naive=datetime(*args)
+ ifdt_naive.tzinfo:
+ msg="Time zone should not be supplied."
+ raiseValueError(msg)
+ dt_aware=tz.localize(dt_naive)
+ returndt_aware.replace(tzinfo=None)-dt_aware.utcoffset()
+
+
+
+
+[docs]
+defsolar_setting(start_time,end_time=None,obs=chime):
+"""Find the Solar settings between two times for CHIME.
+
+ Parameters
+ ----------
+ start_time : float (UNIX time) or datetime
+ Start time to find settings.
+ end_time : float (UNIX time) or datetime, optional
+ End time for finding settings. If `None` default, search for 24 hours
+ after start time.
+
+ Returns
+ -------
+ setting_times : array_like
+ Array of setting times (in UNIX time).
+
+ """
+
+ planets=skyfield_wrapper.ephemeris
+ sun=planets["sun"]
+ # Use 0.6 degrees for the angular diameter of the Sun to be conservative:
+ returnobs.set_times(sun,start_time,end_time,diameter=0.6)
+
+
+
+
+[docs]
+deflunar_setting(start_time,end_time=None,obs=chime):
+"""Find the Lunar settings between two times for CHIME.
+
+ Parameters
+ ----------
+ start_time : float (UNIX time) or datetime
+ Start time to find settings.
+ end_time : float (UNIX time) or datetime, optional
+ End time for finding settings. If `None` default, search for 24 hours
+ after start time.
+
+ Returns
+ -------
+ setting_times : array_like
+ Array of setting times (in UNIX time).
+
+ """
+
+ planets=skyfield_wrapper.ephemeris
+ moon=planets["moon"]
+ # Use 0.6 degrees for the angular diameter of the Moon to be conservative:
+ returnobs.set_times(moon,start_time,end_time,diameter=0.6)
+
+
+
+
+[docs]
+defsolar_rising(start_time,end_time=None,obs=chime):
+"""Find the Solar risings between two times for CHIME.
+
+ Parameters
+ ----------
+ start_time : float (UNIX time) or datetime
+ Start time to find risings.
+ end_time : float (UNIX time) or datetime, optional
+ End time for finding risings. If `None` default, search for 24 hours
+ after start time.
+
+ Returns
+ -------
+ rising_times : array_like
+ Array of rising times (in UNIX time).
+
+ """
+
+ planets=skyfield_wrapper.ephemeris
+ sun=planets["sun"]
+ # Use 0.6 degrees for the angular diameter of the Sun to be conservative:
+ returnobs.rise_times(sun,start_time,end_time,diameter=0.6)
+
+
+
+
+[docs]
+deflunar_rising(start_time,end_time=None,obs=chime):
+"""Find the Lunar risings between two times for CHIME.
+
+ Parameters
+ ----------
+ start_time : float (UNIX time) or datetime
+ Start time to find risings.
+ end_time : float (UNIX time) or datetime, optional
+ End time for finding risings. If `None` default, search for 24 hours after
+ start time.
+
+ Returns
+ -------
+ rising_times : array_like
+ Array of rising times (in UNIX time).
+
+ """
+
+ planets=skyfield_wrapper.ephemeris
+ moon=planets["moon"]
+ # Use 0.6 degrees for the angular diameter of the Moon to be conservative:
+ returnobs.rise_times(moon,start_time,end_time,diameter=0.6)
+[docs]
+defStar_cirs(ra,dec,epoch):
+"""Wrapper for skyfield.api.star that creates a position given CIRS
+ coordinates observed from CHIME
+
+ Parameters
+ ----------
+ ra, dec : skyfield.api.Angle
+ RA and dec of the source in CIRS coordinates
+ epoch : skyfield.api.Time
+ Time of the observation
+
+ Returns
+ -------
+ body : skyfield.api.Star
+ Star object in ICRS coordinates
+ """
+
+ fromskyfield.apiimportStar
+
+ returncirs_radec(Star(ra=ra,dec=dec,epoch=epoch))
+
+
+
+
+[docs]
+defcirs_radec(body,date=None,deg=False,obs=chime):
+"""Converts a Skyfield body in CIRS coordinates at a given epoch to
+ ICRS coordinates observed from CHIME
+
+ Parameters
+ ----------
+ body : skyfield.api.Star
+ Skyfield Star object with positions in CIRS coordinates.
+
+ Returns
+ -------
+ new_body : skyfield.api.Star
+ Skyfield Star object with positions in ICRS coordinates
+ """
+
+ fromskyfield.positionlibimportAngle
+ fromskyfield.apiimportStar
+
+ ts=skyfield_wrapper.timescale
+
+ epoch=ts.tt_jd(np.median(body.epoch))
+
+ pos=obs.skyfield_obs().at(epoch).observe(body)
+
+ # Matrix CT transforms from CIRS to ICRF (https://rhodesmill.org/skyfield/time.html)
+ r_au,dec,ra=skyfield.functions.to_polar(
+ np.einsum("ij...,j...->i...",epoch.CT,pos.position.au)
+ )
+
+ returnStar(
+ ra=Angle(radians=ra,preference="hours"),dec=Angle(radians=dec),epoch=epoch
+ )
+
+
+
+
+[docs]
+defobject_coords(body,date=None,deg=False,obs=chime):
+"""Calculates the RA and DEC of the source.
+
+ Gives the ICRS coordinates if no date is given (=J2000), or if a date is
+ specified gives the CIRS coordinates at that epoch.
+
+ This also returns the *apparent* position, including abberation and
+ deflection by gravitational lensing. This shifts the positions by up to
+ 20 arcseconds.
+
+ Parameters
+ ----------
+ body : skyfield source
+ skyfield.starlib.Star or skyfield.vectorlib.VectorSum or
+ skyfield.jpllib.ChebyshevPosition body representing the source.
+ date : float
+ Unix time at which to determine ra of source If None, use Jan 01
+ 2000.
+ deg : bool
+ Return RA ascension in degrees if True, radians if false (default).
+ obs : `caput.time.Observer`
+ An observer instance to use. If not supplied use `chime`. For many
+ calculations changing from this default will make little difference.
+
+ Returns
+ -------
+ ra, dec: float
+ Position of the source.
+ """
+
+ ifdateisNone:# No date, get ICRS coords
+ ifisinstance(body,skyfield.starlib.Star):
+ ra,dec=body.ra.radians,body.dec.radians
+ else:
+ raiseValueError(
+ "Body is not fixed, cannot calculate coordinates without a date."
+ )
+
+ else:# Calculate CIRS position with all corrections
+ date=unix_to_skyfield_time(date)
+ radec=obs.skyfield_obs().at(date).observe(body).apparent().cirs_radec(date)
+
+ ra,dec=radec[0].radians,radec[1].radians
+
+ # If requested, convert to degrees
+ ifdeg:
+ ra=np.degrees(ra)
+ dec=np.degrees(dec)
+
+ # Return
+ returnra,dec
+
+
+
+
+[docs]
+defpeak_RA(body,date=None,deg=False):
+"""Calculates the RA where a source is expected to peak in the beam.
+ Note that this is not the same as the RA where the source is at
+ transit, since the pathfinder is rotated with respect to north.
+
+ Parameters
+ ----------
+ body : ephem.FixedBody
+ skyfield.starlib.Star or skyfield.vectorlib.VectorSum or
+ skyfield.jpllib.ChebyshevPosition or Ephemeris body
+ representing the source.
+ date : float
+ Unix time at which to determine ra of source
+ If None, use Jan 01 2000.
+ Ignored if body is not a skyfield object
+ deg : bool
+ Return RA ascension in degrees if True,
+ radians if false (default).
+
+ Returns
+ -------
+ peak_ra : float
+ RA when the transiting source peaks.
+ """
+
+ _PF_ROT=np.radians(1.986)# Pathfinder rotation from north.
+ _PF_LAT=np.radians(CHIMELATITUDE)# Latitude of pathfinder
+
+ # Extract RA and dec of object
+ ra,dec=object_coords(body,date=date)
+
+ # Estimate the RA at which the transiting source peaks
+ ra=ra+np.tan(_PF_ROT)*(dec-_PF_LAT)/np.cos(_PF_LAT)
+
+ # If requested, convert to degrees
+ ifdeg:
+ ra=np.degrees(ra)
+
+ # Return
+ returnra
+
+
+
+
+[docs]
+defget_source_dictionary(*args):
+"""Returns a dictionary containing :class:`skyfield.starlib.Star`
+ objects for common radio point sources. This is useful for
+ obtaining the skyfield representation of a source from a string
+ containing its name.
+
+ Parameters
+ ----------
+ catalog_name : str
+ Name of the catalog. This must be the basename of the json file
+ in the `ch_util/catalogs` directory. Can take multiple catalogs,
+ with the first catalog favoured for any overlapping sources.
+
+ Returns
+ -------
+ src_dict : dictionary
+ Format is {'SOURCE_NAME': :class:`skyfield.starlib.Star`, ...}
+
+ """
+
+ importos
+ importjson
+
+ src_dict={}
+ forcatalog_nameinreversed(args):
+ path_to_catalog=os.path.join(
+ os.path.dirname(__file__),
+ "catalogs",
+ os.path.splitext(catalog_name)[0]+".json",
+ )
+
+ withopen(path_to_catalog,"r")ashandler:
+ catalog=json.load(handler)
+
+ forname,infoincatalog.items():
+ src_dict[name]=skyfield_star_from_ra_dec(info["ra"],info["dec"],name)
+
+ returnsrc_dict
+
+
+
+# Common radio point sources
+source_dictionary=get_source_dictionary(
+ "primary_calibrators_perley2016",
+ "specfind_v2_5Jy_vollmer2009",
+ "atnf_psrcat",
+ "hfb_target_list",
+)
+
+#: :class:`skyfield.starlib.Star` representing Cassiopeia A.
+CasA=source_dictionary["CAS_A"]
+
+#: :class:`skyfield.starlib.Star` representing Cygnus A.
+CygA=source_dictionary["CYG_A"]
+
+#: :class:`skyfield.starlib.Star` representing Taurus A.
+TauA=source_dictionary["TAU_A"]
+
+#: :class:`skyfield.starlib.Star` representing Virgo A.
+VirA=source_dictionary["VIR_A"]
+
+
+if__name__=="__main__":
+ importdoctest
+
+ doctest.testmod()
+
+"""
+Data Index Searcher for CHIME
+
+Search routines for locating data withing the CHIME data index.
+
+Data tables
+===========
+
+- :py:class:`DataFlag`
+- :py:class:`DataFlagType`
+
+
+Exceptions
+==========
+
+- :py:class:`DataFlagged`
+
+
+High Level Index Searcher
+=========================
+
+- :py:class:`Finder`
+- :py:class:`DataIntervalList`
+- :py:class:`BaseDataInterval`
+- :py:class:`CorrDataInterval`
+- :py:class:`HKDataInterval`
+- :py:class:`WeatherDataInterval`
+- :py:class:`FlagInputDataInterval`
+- :py:class:`CalibrationGainDataInterval`
+- :py:class:`DigitalGainDataInterval`
+
+
+Routines
+========
+
+- :py:meth:`connect_database`
+- :py:meth:`files_in_range`
+
+"""
+
+importlogging
+importos
+fromosimportpath
+importtime
+importsocket
+importpeeweeaspw
+importre
+
+importchimedb.coreasdb
+importchimedb.data_indexasdi
+from.importlayout,ephemeris
+
+fromchimedb.dataflagimportDataFlagType,DataFlag
+
+from.holographyimportHolographySource,HolographyObservation
+
+# Module Constants
+# ================
+
+GF_REJECT="gf_reject"
+GF_RAISE="gf_raise"
+GF_WARN="gf_warn"
+GF_ACCEPT="gf_accept"
+
+
+# Initializing connection to database.
+# ====================================
+
+from._db_tablesimportconnect_peewee_tablesasconnect_database
+
+
+# High level interface to the data index
+# ======================================
+
+# The following are the info tables that we use to join over when using the
+# finder.
+_acq_info_table=[di.CorrAcqInfo,di.HKAcqInfo,di.RawadcAcqInfo]
+
+# Import list of tables that have a ``start_time`` and ``end_time``
+# field: they are necessary to do any time-based search.
+fromchimedb.data_index.ormimportfile_info_table
+
+
+
+[docs]
+classFinder(object):
+"""High level searching of the CHIME data index.
+
+ This class gives a convenient way to search and filter data acquisitions
+ as well as time ranges of data within acquisitions. Search results
+ constitute a list of files within an acquisition as well as a time range for
+ the data within these files. Convenient methods are provided for loading
+ the precise time range of constituting a search result.
+
+ This is intended to make the most common types of searches of CHIME data as
+ convenient as possible. However for very complex searches, it may be
+ necessary to resort to the lower level interface.
+
+ Searching the index
+ ===================
+
+ There are four ways that a search can be modified which may be combined in
+ any way.
+
+ #. You can restrict the types of acquisition that are under
+ consideration, using methods whose names begin with ``only_``.
+ In this way, one can consider only, say, housekeeping acquisitions.
+ #. The second is to adjust the total time range under consideration.
+ This is achieved by assigning to :attr:`~Finder.time_range` or calling
+ methods beginning with ``set_time_range_``. The total time range affects
+ acquisitions under consideration as well as the data time ranges within
+ the acquisitions. Subsequent changes to the total time range under
+ consideration may only become more restrictive.
+ #. The data index may also be filtered by acquisition using methods whose
+ names begin with ``filter_acqs``. Again subsequent filtering are always
+ combined to become more restrictive. The attribute :attr:`~Finder.acqs`
+ lists the acquisitions currently included in the search for convenience
+ when searching interactively.
+ #. Time intervals within acquisitions are added using methods with names
+ beginning with ``include_``. Time intervals are defined in the
+ :attr:`~Finder.time_intervals` attribute, and are inclusive (you can
+ add as many as you want).
+ #. Finally, upon calling :meth:``get_results`` or :meth:``get_results_acq``,
+ one can pass an arbitrary condition on individual files, thereby
+ returning only a subset of files from each acquisition.
+
+ Getting results
+ ===============
+
+ Results of the search can be retrieved using methods whose names begin with
+ ``get_results`` An individual search result is constituted of a list of file
+ names and a time interval within these files. These can easily loaded into
+ memory using helper functions (see :class:`BaseDataInterval` and
+ :class:`DataIntervalList`).
+
+ Parameters
+ ----------
+ acqs : list of :class:`chimedb.data_index.ArchiveAcq` objects
+ Acquisitions to initially include in data search. Default is to search
+ all acquisitions.
+ node_spoof : dictionary
+ Normally, the DB will be queried to find which nodes are mounted on your
+ host. If you are on a machine that is cross-mounted, though, you can
+ enter a dictionary of "node_name": "mnt_root" pairs, specifying the
+ nodes to search and where they are mounted on your host.
+
+ Examples
+ --------
+
+ To find all the correlator data between two times.
+
+ >>> from ch_util import finder
+ >>> from datetime import datetime
+ >>> f = finder.Finder()
+ >>> f.only_corr()
+ >>> f.set_time_range(datetime(2014,02,24), datetime(2014,02,25))
+ >>> f.print_results_summary()
+ interval | acquisition | offset from start (s) | length (s) | N files
+ 1 | 20140219T145849Z_abbot_corr | 378053.1 | 86400.0 | 25
+ 2 | 20140224T051212Z_stone_corr | 0.0 | 67653.9 | 19
+ Total 154053.858720 seconds of data.
+
+ Search for transits of a given source.
+
+ >>> from ch_util import ephemeris
+ >>> f.include_transits(ephemeris.CasA, time_delta=3600)
+ >>> f.print_results_summary()
+ interval | acquisition | offset from start (s) | length (s) | N files
+ 1 | 20140219T145849Z_abbot_corr | 452087.2 | 3600.0 | 2
+ 2 | 20140224T051212Z_stone_corr | 55288.0 | 3600.0 | 2
+ Total 7200.000000 seconds of data.
+
+ To read the data,
+
+ >>> from ch_util import andata
+ >>> results_list = f.get_results()
+ >>> # Pick result number 1
+ >>> result = results_list[0]
+ >>> # Pick product number 0 (autocorrelation)
+ >>> data = result.as_loaded_data(prod_sel=0)
+ >>> print data.vis.shape
+ (1024, 1, 360)
+
+ More intricate filters on the acquisitions are possible.
+
+ >>> import chimedb.data_index as di
+ >>> f = finder.Finder()
+ >>> # Find ALL 10ms cadence data correlated by 'stone' with 8 channels.
+ >>> f.filter_acqs((di.CorrAcqInfo.integration < 0.011)
+ ... & (di.CorrAcqInfo.integration > 0.009)
+ ... & (di.CorrAcqInfo.nfreq == 1024)
+ ... & (di.CorrAcqInfo.nprod == 36)
+ ... & (di.ArchiveInst.name == 'stone'))
+ >>> f.print_results_summary()
+ interval | acquisition | offset from start (s) | length (s) | N files
+ 1 | 20140211T020307Z_stone_corr | 0.0 | 391.8 | 108
+ 2 | 20140128T135105Z_stone_corr | 0.0 | 4165.2 | 104
+ 3 | 20131208T070336Z_stone_corr | 0.0 | 1429.8 | 377
+ 4 | 20140212T014603Z_stone_corr | 0.0 | 2424.4 | 660
+ 5 | 20131210T060233Z_stone_corr | 0.0 | 1875.3 | 511
+ 6 | 20140210T021023Z_stone_corr | 0.0 | 874.1 | 240
+ Total 11160.663510 seconds of data.
+
+ Here is an example that uses node spoofing and also filters files within
+ acquisitions to include only LNA housekeeping files:
+
+ >>> f = finder.Finder(node_spoof = {"gong" : "/mnt/gong/archive",
+ "suzu" : "/mnt/suzu/hk_data"})
+ >>> f.only_hk()
+ >>> f.set_time_range(datetime(2014, 9, 1), datetime(2014, 10, 10))
+ >>> f.print_results_summary()
+ # | acquisition |start (s)| len (s) |files | MB
+ 0 | 20140830T005410Z_ben_hk | 169549 | 419873 | 47 | 2093
+ 1 | 20140905T203905Z_ben_hk | 0 | 16969 | 2 | 0
+ 2 | 20140908T153116Z_ben_hk | 0 | 1116260 | 56 | 4
+ 3 | 20141009T222415Z_ben_hk | 0 | 5745 | 2 | 0
+ >>> res = f.get_results(file_condition = (di.HKFileInfo.atmel_name == "LNA"))
+ >>> for r in res:
+ ... print "No. files: %d" % (len(r[0]))
+ No. files: 8
+ No. files: 1
+ No. files: 19
+ No. files: 1
+ >>> data = res[0].as_loaded_data()
+ >>> for m in data.mux:
+ ... print "Mux %d: %s", (m, data.chan(m))
+ Mux 0: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
+ Mux 1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
+ Mux 2: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
+ >>> print "Here are the raw data for Mux 1, Channel 14:", data.tod(14, 1)
+ Here are the raw data for Mux 1, Channel 14: [ 1744.19091797 1766.34472656 1771.03356934 ..., 1928.61279297 1938.90075684 1945.53491211]
+
+ In the above example, the restriction to LNA housekeeping could also have
+ been accomplished with the convenience method :meth:`Finder.set_hk_input`:
+
+ >>> f.set_hk_input("LNA")
+ >>> res = f.get_results()
+
+ """
+
+ # Constructors and setup
+ # ----------------------
+
+ def__init__(self,acqs=(),node_spoof=None):
+ importcopy
+
+ # Which nodes do we have available?
+ host=socket.gethostname().split(".")[0]
+ self._my_node=[]
+ self._node_spoof=node_spoof
+
+ connect_database()
+
+ ifnotnode_spoof:
+ fornin(
+ di.StorageNode.select()
+ .where(di.StorageNode.host==host)
+ .where(di.StorageNode.active)
+ ):
+ self._my_node.append(n)
+ else:
+ forkey,valinnode_spoof.items():
+ self._my_node.append(di.StorageNode.get(name=key))
+
+ ifnotlen(self._my_node):
+ raiseRuntimeError(
+ "No nodes found. Perhaps you need to pass a 'node_spoof' parameter?"
+ )
+
+ # Get list of join tables. We make a copy because the user may alter
+ # this later through the only_XXX() methods.
+ self._acq_info=copy.copy(_acq_info_table)
+ self._file_info=copy.copy(file_info_table)
+
+ ifacqs:
+ pass
+ else:
+ acqs=di.ArchiveAcq.select()
+ foriinself._acq_info:
+ acqs.join(i)
+ self._acqs=list(acqs)
+ self._time_range=(ephemeris.CSD_ZERO,time.time())
+ self._time_intervals=None
+ self._time_exclusions=[]
+ self._atmel_restrict=None
+ self.min_interval=240.0
+ self._gf_mode={"comment":GF_ACCEPT,"warning":GF_WARN,"severe":GF_REJECT}
+ self._data_flag_types=[]
+ # The following line cuts any acquisitions with no files.
+ # self.filter_acqs_by_files(True)
+ # This is very similar to the above line, but takes ~.5s instead of
+ # 12s.
+ acq_ids=[acq.idforacqinself.acqs]
+ ifnotacq_ids:
+ # Nothing to do.
+ return
+ condition=(
+ (di.ArchiveAcq.id<<acq_ids)
+ &(di.ArchiveFileCopy.node<<self._my_node)
+ &(di.ArchiveFileCopy.has_file=="Y")
+ )
+ selection=di.ArchiveAcq.select().join(di.ArchiveFile).join(di.ArchiveFileCopy)
+ self._acqs=list(selection.where(condition).group_by(di.ArchiveAcq))
+
+
+[docs]
+ @classmethod
+ defoffline(cls,acqs=()):
+"""Initialize :class:`~Finder` when not working on a storage node.
+
+ Normally only data that is available on the present host is searched,
+ and as such :class:`~Finder` can't be used to browse the index when you
+ don't have access to the acctual data. Initializing using this method
+ spoofs the 'gong' and 'niedermayer' storage nodes (which should have a
+ full copy of the archive) such that the data index can be search the
+ full archive.
+
+ """
+
+ node_spoof={}
+ # for n in di.StorageNode.select():
+ # node_spoof[n.name] = ''
+ # I think all the data live on at lease one of these -KM.
+ node_spoof["gong"]=""
+ node_spoof["niedermayer"]=""
+ returncls(acqs,node_spoof=node_spoof)
+
+
+ # Filters on the index
+ # --------------------
+
+ @property
+ defacqs(self):
+"""Acquisitions remaining in this search.
+
+ Returns
+ -------
+ acqs : list of :class:`chimedb.data_index.ArchiveAcq` objects
+
+ """
+
+ returnlist(self._acqs)
+
+ @property
+ deftime_range(self):
+"""Time range to be included in search.
+
+ Data files and acquisitions that do not overlap with this range are
+ excluded. Assigning to this is equivalent to calling
+ :meth:`~Finder.set_time_range`.
+
+ Returns
+ -------
+ time_range : tuple of 2 floats
+ Unix/POSIX beginning and end of the time range.
+
+ """
+
+ returnself._time_range
+
+ @property
+ deftime_intervals(self):
+"""Periods in time to be included.
+
+ Periods are combined with `OR` unless list is empty, in which case no
+ filtering is performed.
+
+ Returns
+ -------
+ time_intervals : list of pairs of floats
+ Each entry is the Unix/POSIX beginning and end of the time interval
+ to be included.
+
+ """
+
+ ifself._time_intervalsisNone:
+ return[self.time_range]
+ else:
+ returnlist(self._time_intervals)
+
+ def_append_time_interval(self,interval):
+ ifself._time_intervalsisNone:
+ time_intervals=[]
+ else:
+ time_intervals=self._time_intervals
+ time_intervals.append(interval)
+ self._time_intervals=time_intervals
+
+ @property
+ deftime_exclusions(self):
+"""Periods in time to be excluded.
+
+ Returns
+ -------
+ time_exclusions : list of pairs of floats
+ Each entry is the Unix/POSIX beginning and end of the time interval
+ to be excluded.
+
+ """
+
+ returnlist(self._time_exclusions)
+
+ def_append_time_exclusion(self,interval):
+ self._time_exclusions.append(interval)
+
+ @property
+ defmin_interval(self):
+"""Minimum length of a block of data to be considered.
+
+ This can be set to any number. The default is 240 seconds.
+
+ Returns
+ -------
+ min_interval : float
+ Length of time in seconds.
+
+ """
+
+ returnself._min_interval
+
+ @min_interval.setter
+ defmin_interval(self,value):
+ self._min_interval=float(value)
+
+ @property
+ defglobal_flag_mode(self):
+"""Global flag behaviour mode.
+
+ Defines how global flags are treated when finding data. There are three
+ severities of global flag: comment, warning, and severe. There are
+ four possible behaviours when a search result overlaps a global flag,
+ represented by module constants:
+
+ :GF_REJECT: Reject any data overlapping flag silently.
+ :GF_RAISE: Raise an exception when retrieving data intervals.
+ :GF_WARN: Send a warning when retrieving data intervals but proceed.
+ :GF_ACCEPT: Accept the data silently, ignoring the flag.
+
+ The behaviour for all three severities is represented by a dictionary.
+ If no mode is set, then the default behaviour is
+ `{'comment' : GF_ACCEPT, 'warning' : GF_WARN, 'severe' : GF_REJECT}`.
+
+ This is modified using :meth:`Finder.update_global_flag_mode`.
+
+ Returns
+ -------
+ global_flag_mode : dictionary with keys 'comment', 'warning', 'severe'.
+ Specifies finder behaviour.
+
+ """
+
+ returndict(self._gf_mode)
+
+ @property
+ defdata_flag_types(self):
+"""Types of DataFlag to exclude from results."""
+ returnself._data_flag_types
+
+ # Setting up filters on the data
+ # ------------------------------
+
+
+[docs]
+ defupdate_global_flag_mode(self,comment=None,warning=None,severe=None):
+"""Update :attr:`Finder.global_flag_mode`, the global flag mode.
+
+ Parameters
+ ----------
+ comment : One of *GF_REJECT*, *GF_RAISE*, *GF_WARN*, or *GF_ACCEPT*.
+ warning : One of *GF_REJECT*, *GF_RAISE*, *GF_WARN*, or *GF_ACCEPT*.
+ severe : One of *GF_REJECT*, *GF_RAISE*, *GF_WARN*, or *GF_ACCEPT*.
+
+ """
+
+ ifcomment:
+ _validate_gf_value(comment)
+ self._gf_mode["comment"]=comment
+ ifwarning:
+ _validate_gf_value(warning)
+ self._gf_mode["warning"]=warning
+ ifsevere:
+ _validate_gf_value(severe)
+ self._gf_mode["severe"]=severe
+
+
+
+[docs]
+ defaccept_all_global_flags(self):
+"""Set global flag behaviour to accept all data."""
+
+ self.update_global_flag_mode(
+ comment=GF_ACCEPT,warning=GF_ACCEPT,severe=GF_ACCEPT
+ )
+
+
+
+[docs]
+ defonly_corr(self):
+"""Only include correlator acquisitions in this search."""
+ self._acq_info=[di.CorrAcqInfo]
+ self._file_info=[di.CorrFileInfo]
+ self.filter_acqs(True)
+
+
+
+[docs]
+ defonly_hk(self):
+"""Only include housekeeping acquisitions in this search."""
+ self._acq_info=[di.HKAcqInfo]
+ self._file_info=[di.HKFileInfo]
+ self.filter_acqs(True)
+
+
+
+[docs]
+ defonly_rawadc(self):
+"""Only include raw ADC acquisitions in this search."""
+ self._acq_info=[di.RawadcAcqInfo]
+ self._file_info=[di.RawadcFileInfo]
+ self.filter_acqs(True)
+
+
+
+[docs]
+ defonly_hfb(self):
+"""Only include HFB acquisitions in this search."""
+ self._acq_info=[di.HFBAcqInfo]
+ self._file_info=[di.HFBFileInfo]
+ self.filter_acqs(True)
+
+
+
+[docs]
+ defonly_weather(self):
+"""Only include weather acquisitions in this search."""
+ self._acq_info=[]
+ self._file_info=[di.WeatherFileInfo]
+ self.filter_acqs(di.AcqType.name=="weather")
+
+
+
+[docs]
+ defonly_chime_weather(self):
+"""Only include chime weather acquisitions in this search.
+ This excludes the old format mingun-weather."""
+ self._acq_info=[]
+ self._file_info=[di.WeatherFileInfo]
+ self.filter_acqs(di.AcqType.name=="weather")
+ self.filter_acqs(di.ArchiveInst.name=="chime")
+
+
+
+[docs]
+ defonly_hkp(self):
+"""Only include Prometheus housekeeping data in this search"""
+ self._acq_info=[]
+ self._file_info=[di.HKPFileInfo]
+ self.filter_acqs(di.AcqType.name=="hkp")
+
+
+
+[docs]
+ defonly_digitalgain(self):
+"""Only include digital gain data in this search"""
+ self._acq_info=[]
+ self._file_info=[di.DigitalGainFileInfo]
+ self.filter_acqs(di.AcqType.name=="digitalgain")
+
+
+
+[docs]
+ defonly_gain(self):
+"""Only include calibration gain data in this search"""
+ self._acq_info=[]
+ self._file_info=[di.CalibrationGainFileInfo]
+ self.filter_acqs(di.AcqType.name=="gain")
+
+
+
+[docs]
+ defonly_flaginput(self):
+"""Only include input flag data in this search"""
+ self._acq_info=[]
+ self._file_info=[di.FlagInputFileInfo]
+ self.filter_acqs(di.AcqType.name=="flaginput")
+
+
+
+[docs]
+ deffilter_acqs(self,condition):
+"""Filter the acquisitions included in this search.
+
+ Parameters
+ ----------
+ condition : :mod:`peewee` comparison
+ Condition on any on :class:`chimedb.data_index.ArchiveAcq` or any
+ class joined to :class:`chimedb.data_index.ArchiveAcq`: using the
+ syntax from the :mod:`peewee` module [1]_.
+
+ Examples
+ --------
+
+ >>> from ch_util import finder
+ >>> import chimedb.data_index as di
+ >>> f = finder.Finder()
+ >>> f.filter_acqs(di.ArchiveInst.name == 'stone')
+ >>> f.filter_acqs((di.AcqType == 'corr') & (di.CorrAcqInfo.nprod == 36))
+
+ See Also
+ --------
+
+ :meth:`Finder.filter_acqs_by_files`
+
+
+ References
+ ----------
+
+ .. [1] http://peewee.readthedocs.org/en/latest/peewee/querying.html
+
+ """
+
+ # Get the acquisitions currently included.
+ acq_ids=[acq.idforacqinself.acqs]
+ ifnotacq_ids:
+ # Nothing to do.
+ return
+ # From these, only include those meeting the new condition.
+ # XXX simpler?
+ condition=(di.ArchiveAcq.id<<acq_ids)&condition
+
+ selection=di.ArchiveAcq.select().join(di.AcqType)
+ foriinself._acq_info:
+ selection=selection.switch(di.ArchiveAcq).join(i,pw.JOIN.LEFT_OUTER)
+ selection=selection.switch(di.ArchiveAcq).join(di.ArchiveInst)
+ self._acqs=list(selection.where(condition).group_by(di.ArchiveAcq))
+
+
+
+[docs]
+ deffilter_acqs_by_files(self,condition):
+"""Filter the acquisitions by the properties of its files.
+
+ Because each acquisition has many files, this filter should be
+ significantly slower than :meth:`Finder.filter_acqs`.
+
+ Parameters
+ ----------
+ condition : :mod:`peewee` comparison
+ Condition on any on :class:`chimedb.data_index.ArchiveAcq`,
+ :class:`chimedb.data_index.ArchiveFile` or any class joined to
+ :class:`chimedb.data_index.ArchiveFile` using the syntax from the
+ :mod:`peewee` module [2]_.
+
+ See Also
+ --------
+
+ :meth:`Finder.filter_acqs`
+
+ Examples
+ --------
+
+ References
+ ----------
+
+ .. [2] http://peewee.readthedocs.org/en/latest/peewee/querying.html
+
+ """
+ # Get the acquisitions currently included.
+ acq_ids=[acq.idforacqinself.acqs]
+ ifnotacq_ids:
+ # Nothing to do.
+ return
+ condition=(
+ (di.ArchiveAcq.id<<acq_ids)
+ &(di.ArchiveFileCopy.node<<self._my_node)
+ &(di.ArchiveFileCopy.has_file=="Y")
+ &(condition)
+ )
+ selection=di.ArchiveAcq.select().join(di.ArchiveFile).join(di.ArchiveFileCopy)
+ info_cond=False
+ foriinself._file_info:
+ selection=selection.switch(di.ArchiveFile).join(
+ i,join_type=pw.JOIN.LEFT_OUTER
+ )
+ # The following ensures that at least _one_ of the info tables is
+ # joined.
+ info_cond|=~(i.start_time>>None)
+ self._acqs=list(
+ selection.where(condition&info_cond).group_by(di.ArchiveAcq)
+ )
+
+
+
+[docs]
+ defset_time_range(self,start_time=None,end_time=None):
+"""Restrict the time range of the search.
+
+ This method updates the :attr:`~Index.time_range` property and also
+ excludes any acquisitions that do not overlap with the new range. This
+ method always narrows the time range under consideration, never expands
+ it.
+
+ Parameters
+ ----------
+ start_time : float or :class:`datetime.datetime`
+ Unix/POSIX time or UTC start of desired time range. Optional.
+ end_time : float or :class:`datetime.datetime`
+ Unix/POSIX time or UTC end of desired time range. Optional.
+
+ """
+ # Update `self.time_range`.
+ ifstart_timeisNone:
+ start_time=0
+ ifend_timeisNone:
+ end_time=time.time()
+ start_time=ephemeris.ensure_unix(start_time)
+ end_time=ephemeris.ensure_unix(end_time)
+ old_start_time,old_end_time=self.time_range
+ start_time=max(start_time,old_start_time)
+ end_time=min(end_time,old_end_time)
+ ifstart_time>=end_time:
+ msg="No time spanned by search. start=%s, stop=%s"
+ msg=msg%(start_time,end_time)
+ raiseValueError(msg)
+
+ # Delete any acquisitions that do not overlap with the new range.
+ cond=True
+ foriinself._file_info:
+ cond&=(i.start_time>>None)|(
+ (i.start_time<end_time)&(i.finish_time>start_time)
+ )
+ self.filter_acqs_by_files(cond)
+
+ ifnotself._time_intervalsisNone:
+ time_intervals=_trim_intervals_range(
+ self.time_intervals,(start_time,end_time)
+ )
+ self._time_intervals=time_intervals
+ time_exclusions=_trim_intervals_range(
+ self.time_exclusions,(start_time,end_time)
+ )
+ self._time_exclusions=time_exclusions
+ self._time_range=(start_time,end_time)
+
+
+
+[docs]
+ defset_time_range_global_flag(self,flag):
+"""Set time range to correspond to a global flag.
+
+ Parameters
+ ----------
+ flag : integer or string
+ Global flag ID or name, e.g. "run_pass1_a", or 11292.
+
+ Notes
+ -----
+
+ Global flag ID numbers, names, and descriptions are listed at
+ http://bao.phas.ubc.ca/layout/event.php?filt_event_type_id=7
+
+ """
+
+ start_time,end_time=_get_global_flag_times_by_name_event_id(flag)
+ self.set_time_range(start_time,end_time)
+
+
+
+[docs]
+ defset_time_range_season(self,year=None,season=None):
+"""Set the time range by as specific part of a given year.
+
+ NOT YET IMPLEMENTED
+
+ Parameters
+ ----------
+ year : integer
+ Calender year
+ season : string
+ Month name (3 letter abbreviations are acceptable) or one of
+ 'winter', 'spring', 'summer', or 'fall'.
+
+ """
+ raiseNotImplementedError()
+[docs]
+ defexclude_time_interval(self,start_time,end_time):
+"""Exclude a time interval.
+
+ Examples
+ --------
+
+ >>> from ch_util import finder
+ >>> from datetime import datetime
+ >>> f = finder.Finder()
+ >>> f.set_time_range(datetime(2014,04,04), datetime(2014,04,14))
+ >>> # f.print_results_summary() will show all the files in this time range
+ >>> # Now want to exclude all data from 04, 10 to 04, 11
+ >>> f.exclude_time_interval(datetime(2014,04,10),datetime(2014,04,11))
+ >>> f.print_results_summary()
+ interval | acquisition | offset from start (s) | length (s) | N files
+ 1 | 20140330T102505Z_abbot_corr | 394484.2 | 231900.8 | 65
+ 2 | 20140403T152314Z_blanchard_corr | 30988.4 | 309649.3 | 86
+ 3 | 20140408T222844Z_abbot_corr | 0.0 | 75589.3 | 21
+ 4 | 20140409T184530Z_blanchard_corr | 0.0 | 3795.0 | 2
+ 5 | 20140409T165603Z_blanchard_corr | 0.0 | 4952.7 | 2
+ 6 | 20140411T003404Z_blanchard_corr | 0.0 | 161606.5 | 45
+ 7 | 20140411T000920Z_blanchard_corr | 0.0 | 1080.4 | 36
+ 8 | 20140413T002319Z_blanchard_corr | 0.0 | 84981.7 | 24
+ Total 873555.739000 seconds of data.
+ """
+
+ interval=self._format_time_interval(start_time,end_time)
+ ifinterval:
+ self._append_time_exclusion(interval)
+
+
+
+[docs]
+ definclude_global_flag(self,flag):
+"""Update :attr:`time_intervals` to include a global flag.
+
+ Parameters
+ ----------
+ flag : integer or string
+ Global flag ID or name, e.g. "run_pass1_a", or 11292.
+
+ Notes
+ -----
+
+ Global flag ID numbers, names, and descriptions are listed at
+ http://bao.phas.ubc.ca/layout/event.php?filt_event_type_id=7
+
+ """
+
+ start_time,end_time=_get_global_flag_times_by_name_event_id(flag)
+ self.include_time_interval(start_time,end_time)
+
+
+
+[docs]
+ defexclude_global_flag(self,flag):
+"""Update :attr:`time_intervals` to exclude a global flag.
+
+ Parameters
+ ----------
+ flag : integer or string
+ Global flag ID or name, e.g. "run_pass1_a", or 65.
+
+ See Also
+ --------
+
+ Look under :meth:`include_global_flag` for a very similar example.
+
+ Notes
+ -----
+
+ Global flag ID numbers, names, and descriptions are listed at
+ http://bao.phas.ubc.ca/layout/event.php?filt_event_type_id=7
+
+ """
+
+ start_time,end_time=_get_global_flag_times_by_name_event_id(flag)
+ self.exclude_time_interval(start_time,end_time)
+
+
+
+[docs]
+ defexclude_data_flag_type(self,flag_type):
+"""Exclude times that overlap with DataFlags of this type.
+
+ Parameters
+ ----------
+ flag_type : string or list of string
+ Name of DataFlagType(s) to exclude from results, e.g. "rain".
+ """
+
+ ifisinstance(flag_type,list):
+ self.data_flag_types.extend(flag_type)
+ else:
+ self.data_flag_types.append(flag_type)
+[docs]
+ defexclude_RA_interval(self,start_RA,end_RA):
+"""Add time intervals to exclude passings of given right RA
+ intervals
+
+ Parameters
+ ----------
+ start_RA : float
+ Starting right ascension in degrees.
+ end_RA : float
+ Ending right ascension in degrees.
+
+ Examples
+ --------
+ Look under include_RA_interval for very similar example.
+
+ """
+ from.importephemeris
+
+ delta_RA=(end_RA-start_RA)%360
+ mid_RA=(start_RA+delta_RA/2.0)%360
+ time_delta=delta_RA*4*60.0*ephemeris.SIDEREAL_S
+ self.exclude_transits(mid_RA,time_delta=time_delta)
+
+
+
+[docs]
+ definclude_transits(self,body,time_delta=None):
+"""Add time intervals to include transits for given celestial body.
+
+ Parameters
+ ----------
+ body : :class:`ephem.Body` or float
+ Transiting celestial body. If a float, interpret as a right
+ ascension in degrees.
+ time_delta : float
+ Total amount of time to include surrounding the transit in
+ seconds. Default is to use twice the value of
+ :attr:`~Finder.min_interval`.
+
+ Examples
+ --------
+ >>> from ch_util import (finder, ephemeris)
+ >>> from datetime import datetime
+ >>> f = finder.Finder()
+ >>> f.set_time_range(datetime(2014,02,20), datetime(2014,02,22))
+ >>> f.include_transits(ephemeris.CasA, time_delta=3600)
+ >>> f.print_results_summary()
+ interval | acquisition | offset from start (s) | length (s) | N files
+ 1 | 20140219T145849Z_abbot_corr | 107430.9 | 3600.0 | 2
+ 2 | 20140219T145849Z_abbot_corr | 193595.0 | 3600.0 | 2
+ 3 | 20140220T213252Z_stone_corr | 0.0 | 990.2 | 1
+ 4 | 20140220T213252Z_stone_corr | 83554.3 | 3600.0 | 2
+ Total 11790.181012 seconds of data.
+
+ """
+
+ ifnottime_delta:
+ time_delta=self.min_interval*2
+ ttimes=ephemeris.transit_times(body,*self.time_range)
+ forttimeinttimes:
+ self.include_time_interval(
+ ttime-time_delta/2.0,ttime+time_delta/2.0
+ )
+
+
+
+[docs]
+ definclude_26m_obs(self,source,require_quality=True):
+"""Add time intervals to include 26m observations of a source.
+
+ Parameters
+ ----------
+ source : string
+ Source observed. Has to match name on database exactly.
+ require_quality : bool (default: True)
+ Require the quality flag to be zero (ie that the 26 m
+ pointing is trustworthy) or None
+
+ Examples
+ --------
+ >>> from ch_util import (finder, ephemeris)
+ >>> from datetime import datetime
+ >>> f = finder.Finder()
+ >>> f.only_corr()
+ >>> f.set_time_range(datetime(2017,8,1,10), datetime(2017,8,2))
+ >>> f.filter_acqs((di.ArchiveInst.name == 'pathfinder'))
+ >>> f.include_26m_obs('CasA')
+ >>> f.print_results_summary()
+ # | acquisition |start (s)| len (s) |files | MB
+ 0 | 20170801T063349Z_pathfinder_corr | 12337 | 11350 | 2 | 153499
+ 1 | 20170801T131035Z_pathfinder_corr | 0 | 6922 | 1 | 75911
+ Total 18271 seconds, 229410 MB of data.
+
+ """
+
+ connect_database()
+ sources=HolographySource.select()
+ sources=sources.where(HolographySource.name==source)
+ iflen(sources)==0:
+ msg=(
+ "No sources found in the database that match: {0}\n".format(source)
+ +"Returning full time range"
+ )
+ logging.warning(msg)
+ obs=(
+ HolographyObservation.select()
+ .join(HolographySource)
+ .where(HolographyObservation.source<<sources)
+ )
+ ifrequire_quality:
+ obs=obs.select().where(
+ (HolographyObservation.quality_flag==0)
+ |(HolographyObservation.quality_flag==None)
+ )
+
+ found_obs=False
+ forobinobs:
+ in_range=(self.time_range[1]>ob.start_time)and(
+ self.time_range[0]<ob.finish_time
+ )
+ ifin_range:
+ found_obs=True
+ self.include_time_interval(ob.start_time,ob.finish_time)
+ ifnotfound_obs:
+ msg=(
+ "No observation of the source ({0}) was found within the time range.\n".format(
+ source
+ )
+ +"Returning full time range"
+ )
+ logging.warning(msg)
+
+
+
+[docs]
+ defexclude_transits(self,body,time_delta):
+"""Add time intervals to exclude transits for given celestial body.
+
+ Parameters
+ ----------
+ body : :class:`ephem.Body` or float
+ Transiting celestial body. If a float, interpret as a right
+ ascension in degrees.
+ time_delta : float
+ Total amount of time to include surrounding the transit in
+ seconds. Default is to use twice the value of
+ :attr:`~Finder.min_interval`.
+
+ Examples
+ --------
+ >>> from ch_util import finder
+ >>> from datetime import datetime
+ >>> f = finder.Finder()
+ >>> f.set_time_range(datetime(2014,02,20), datetime(2014,02,22))
+ >>> import ephem
+ >>> f.exclude_transits(ephem.Sun(), time_delta=43200)
+ >>> f.print_results_summary()
+ interval | acquisition | offset from start (s) | length (s) | N files
+ 1 | 20140219T145849Z_abbot_corr | 32453.1 | 51128.4 | 15
+ 2 | 20140219T145849Z_abbot_corr | 126781.5 | 43193.0 | 13
+ 3 | 20140219T145523Z_stone_corr | 32662.5 | 18126.9 | 6
+ 4 | 20140220T213252Z_stone_corr | 16740.8 | 43193.0 | 13
+ Total 155641.231275 seconds of data.
+
+ """
+
+ ifnottime_delta:
+ time_delta=self.min_interval*2
+ ttimes=ephemeris.transit_times(body,*self.time_range)
+ forttimeinttimes:
+ self.exclude_time_interval(
+ ttime-time_delta/2.0,ttime+time_delta/2.0
+ )
+
+
+
+[docs]
+ defexclude_daytime(self):
+"""Add time intervals to exclude all day time data."""
+
+ rise_times=ephemeris.solar_rising(
+ self.time_range[0]-24*3600.0,self.time_range[1]
+ )
+
+ forrise_timeinrise_times:
+ set_time=ephemeris.solar_setting(rise_time)
+ self.exclude_time_interval(rise_time,set_time)
+
+
+
+[docs]
+ defexclude_nighttime(self):
+"""Add time intervals to exclude all night time data."""
+
+ set_times=ephemeris.solar_setting(
+ self.time_range[0]-24*3600.0,self.time_range[1]
+ )
+
+ forset_timeinset_times:
+ rise_time=ephemeris.solar_rising(set_time)
+ self.exclude_time_interval(set_time,rise_time)
+
+
+
+[docs]
+ defexclude_sun(self,time_delta=4000.0,time_delta_rise_set=4000.0):
+"""Add time intervals to exclude sunrise, sunset, and sun transit.
+
+ Parameters
+ ----------
+ time_delta : float
+ Total amount of time to exclude surrounding the sun transit in
+ seconds. Default is to use 4000.0 seconds.
+ time_delta_rise_set : float
+ Total amount of time to exclude after sunrise and before sunset
+ in seconds. Default is to use 4000.0 seconds.
+ """
+
+ # Sunrise
+ rise_times=ephemeris.solar_rising(
+ self.time_range[0]-time_delta_rise_set,self.time_range[1]
+ )
+ forrise_timeinrise_times:
+ self.exclude_time_interval(rise_time,rise_time+time_delta_rise_set)
+
+ # Sunset
+ set_times=ephemeris.solar_setting(
+ self.time_range[0],self.time_range[1]+time_delta_rise_set
+ )
+ forset_timeinset_times:
+ self.exclude_time_interval(set_time-time_delta_rise_set,set_time)
+
+ # Sun transit
+ transit_times=ephemeris.solar_transit(
+ self.time_range[0]-time_delta/2.0,self.time_range[1]+time_delta/2.0
+ )
+ fortransit_timeintransit_times:
+ self.exclude_time_interval(
+ transit_time-time_delta/2.0,transit_time+time_delta/2.0
+ )
+
+
+
+[docs]
+ defset_hk_input(self,name):
+"""Restrict files to only one HK input type.
+
+ This is a shortcut for specifying
+ ``file_condition = (chimedb.data_index.HKFileInfo.atmel_name == name)``
+ in :meth:`get_results_acq`. Instead, one can simply call this function
+ with **name** as, e.g., "LNA", "FLA", and calls to
+ :meth:`get_results_acq` will be appropriately restricted.
+
+ Parameters
+ ----------
+ name : str
+ The name of the housekeeping input.
+ """
+ self._atmel_restrict=di.HKFileInfo.atmel_name==name
+
+
+
+[docs]
+ defget_results_acq(self,acq_ind,file_condition=None):
+"""Get search results restricted to a given acquisition.
+
+ Parameters
+ ----------
+ acq_ind : int
+ Index of :attr:`Finder.acqs` for the desired acquisition.
+ file_condition : :mod:`peewee` comparison
+ Any additional condition for filtering the files within the
+ acquisition. In general, this should be a filter on one of the file
+ information tables, e.g., :class:`CorrFileInfo`.
+
+ Returns
+ -------
+ interval_list : :class:`DataIntervalList`
+ Search results.
+
+ """
+ acq=self.acqs[acq_ind]
+ acq_start=acq.start_time
+ acq_finish=acq.finish_time
+ time_intervals=_trim_intervals_range(
+ self.time_intervals,(acq_start,acq_finish),self.min_interval
+ )
+ time_intervals=_trim_intervals_exclusions(
+ time_intervals,self.time_exclusions,self.min_interval
+ )
+ # Deal with all global flags.
+ forseverity,modeinself.global_flag_mode.items():
+ ifmodeisGF_ACCEPT:
+ # Do nothing.
+ continue
+ else:
+ # Need to actually get the flags.
+ global_flags=layout.global_flags_between(
+ acq_start,acq_finish,severity
+ )
+ global_flag_names=[gf.nameforgfinglobal_flags]
+ flag_times=[]
+ forfinglobal_flags:
+ start,stop=layout.get_global_flag_times(f.id)
+ ifstopisNone:
+ stop=time.time()
+ start=ephemeris.ensure_unix(start)
+ stop=ephemeris.ensure_unix(stop)
+ flag_times.append((start,stop))
+ overlap=_check_intervals_overlap(time_intervals,flag_times)
+ ifmodeisGF_WARN:
+ ifoverlap:
+ msg=(
+ "Global flag with severity '%s' present in data"
+ " search results and warning requested."
+ " Global flag name: %s"
+ %(severity,global_flag_names[overlap[1]])
+ )
+ logging.warning(msg)
+ elifmodeisGF_RAISE:
+ ifoverlap:
+ msg=(
+ "Global flag with severity '%s' present in data"
+ " search results and exception requested."
+ " Global flag name: %s"
+ %(severity,global_flag_names[overlap[1]])
+ )
+ raiseDataFlagged(msg)
+ elifmodeisGF_REJECT:
+ ifoverlap:
+ time_intervals=_trim_intervals_exclusions(
+ time_intervals,flag_times,self.min_interval
+ )
+ else:
+ raiseRuntimeError("Finder has invalid global_flag_mode.")
+ # Do the same for Data flags
+ iflen(self.data_flag_types)>0:
+ df_types=[t.namefortinDataFlagType.select()]
+ fordftinself.data_flag_types:
+ ifnotdftindf_types:
+ raiseRuntimeError("Could not find data flag type {}.".format(dft))
+ flag_times=[]
+ forfinDataFlag.select().where(
+ DataFlag.type==DataFlagType.get(name=dft)
+ ):
+ start,stop=f.start_time,f.finish_time
+ ifstopisNone:
+ stop=time.time()
+ start=ephemeris.ensure_unix(start)
+ stop=ephemeris.ensure_unix(stop)
+ flag_times.append((start,stop))
+ overlap=_check_intervals_overlap(time_intervals,flag_times)
+ ifoverlap:
+ time_intervals=_trim_intervals_exclusions(
+ time_intervals,flag_times,self.min_interval
+ )
+ data_intervals=[]
+ ifself._atmel_restrict:
+ iffile_condition:
+ file_condition&=self._atmel_restrict
+ else:
+ file_condition=self._atmel_restrict
+ fortime_intervalintime_intervals:
+ filenames=files_in_range(
+ acq.id,
+ time_interval[0],
+ time_interval[1],
+ self._my_node,
+ file_condition,
+ self._node_spoof,
+ )
+ filenames=sorted(filenames)
+
+ tup=(filenames,time_interval)
+ ifacq.type==di.AcqType.corr():
+ data_intervals.append(CorrDataInterval(tup))
+ elifacq.type==di.AcqType.hk():
+ data_intervals.append(HKDataInterval(tup))
+ elifacq.type==di.AcqType.weather():
+ data_intervals.append(WeatherDataInterval(tup))
+ elifacq.type==di.AcqType.flaginput():
+ data_intervals.append(FlagInputDataInterval(tup))
+ elifacq.type==di.AcqType.gain():
+ data_intervals.append(CalibrationGainDataInterval(tup))
+ elifacq.type==di.AcqType.digitalgain():
+ data_intervals.append(DigitalGainDataInterval(tup))
+ else:
+ data_intervals.append(BaseDataInterval(tup))
+
+ returnDataIntervalList(data_intervals)
+
+
+
+[docs]
+ defget_results(self,file_condition=None):
+"""Get all search results.
+
+ Parameters
+ ----------
+ file_condition : :mod:`peewee` comparison
+ Any additional condition for filtering the files within the
+ acquisition. In general, this should be a filter on one of the file
+ information tables, e.g., chimedb.data_index.CorrFileInfo.
+
+ Returns
+ -------
+ interval_list : :class:`DataIntervalList`
+ Search results.
+ cond : :mod:`peewee` comparison
+ Any extra filters, particularly filters on individual files.
+
+ """
+
+ intervals=[]
+ foriiinrange(len(self.acqs)):
+ intervals+=self.get_results_acq(ii,file_condition)
+ returnDataIntervalList(intervals)
+
+
+
+[docs]
+ defprint_acq_info(self):
+"""Print the acquisitions included in this search and thier properties.
+
+ This method is convenient when searching the data index interactively
+ and you want to see what acquisitions remain after applying filters or
+ restricting the time range.
+
+ See Also
+ --------
+ :meth:`Finder.print_results_summary`
+
+ """
+
+ from.importephemeris
+
+ print("acquisition | name | start | length (hrs) | N files")
+ row_proto="%4d | %-36s | %s | %7.2f | %4d"
+ forii,acqinenumerate(self.acqs):
+ start=acq.start_time
+ finish=acq.finish_time
+ length=(finish-start)/3600.0
+ start=ephemeris.unix_to_datetime(start)
+ start=start.strftime("%Y-%m-%d %H:%M")
+ name=acq.name
+ n_files=acq.n_timed_files
+ print(row_proto%(ii,name,start,length,n_files))
+[docs]
+classDataIntervalList(list):
+"""A list of data index search results.
+
+ Just a normal python list of :class:`DataInterval`-derived objects with
+ some helper methods. Instances are created by calls to
+ :meth:`Finder.get_results`.
+ """
+
+
+[docs]
+ defiter_reader(self):
+"""Iterate over data intervals converting to :class:`andata.Reader`.
+
+ Returns
+ -------
+ reader_iterator
+ Iterator over data intervals as :class:`andata.Reader` instances.
+
+ """
+
+ fordata_intervalinself:
+ yielddata_interval.as_reader()
+
+
+
+[docs]
+ defiter_loaded_data(self,**kwargs):
+"""Iterate over data intervals loading as :class:`andata.AnData`.
+
+ Parameters
+ ----------
+ **kwargs : argument list
+ Pass any parameters accepted by the
+ :class:`BaseDataInverval`-derived class that you are using.
+
+ Returns
+ -------
+ loaded_data_iterator
+ Iterator over data intervals loaded into memory as
+ :class:`andata.BaseData`-derived instances.
+
+ Examples
+ --------
+
+ Use this method to loop over data loaded into memory.
+
+ >>> for data in interval_list.iter_loaded_data():
+ ... pass
+
+ Data is loaded into memory on each iteration. To immediately load all
+ data into memory, initialize a list using the iterator:
+
+ >>> loaded_data_list = list(interval_list.iter_loaded_data())
+
+ """
+
+ fordata_intervalinself:
+ yielddata_interval.as_loaded_data(**kwargs)
+
+
+
+
+
+[docs]
+classBaseDataInterval(tuple):
+"""A single data index search result.
+
+ Just a normal python tuple with some helper methods. Instances are created
+ by calls to :meth:`Finder.get_results`.
+
+ A data interval as two elements: a list of filenames and a time range within
+ those files.
+
+ You should generally only use the classes derived from this one (i.e.,
+ :class:`CorrDataInterval`, etc.)
+ """
+
+ @property
+ def_reader_class(self):
+ # only dynamic imports of andata allowed in this module.
+ from.importandata
+
+ returnandata.BaseReader
+
+
+[docs]
+ defas_reader(self):
+"""Get data interval as an :class:`andata.Reader` instance.
+
+ The :class:`andata.Reader` is initialized with the filename list part
+ of the data interval then the time range part of the data interval is
+ used as an arguments to :meth:`andata.Reader.select_time_range`.
+
+ Returns
+ -------
+ reader : :class:`andata.Reader`
+
+ """
+ rc=self._reader_class
+ reader=rc(self[0])
+ reader.select_time_range(self[1][0],self[1][1])
+ returnreader
+
+
+
+[docs]
+ defas_loaded_data(self,**kwargs):
+"""Load data interval to memory as an :class:`andata.AnData` instance.
+
+ Parameters
+ ----------
+ datasets : list of strings
+ Passed on to :meth:`andata.AnData.from_acq_h5`
+
+ Returns
+ -------
+ data : :class:`andata.AnData`
+ Data interval loaded into memory.
+
+ """
+ reader=self.as_reader()
+ fork,vinkwargs.items():
+ ifvisnotNone:
+ setattr(reader,k,v)
+ data=reader.read()
+ returndata
+
+
+
+
+
+[docs]
+classCorrDataInterval(BaseDataInterval):
+"""Derived class from :class:`BaseDataInterval` for correlator data."""
+
+ @property
+ def_reader_class(self):
+ # only dynamic imports of andata allowed in this module.
+ from.importandata
+
+ returnandata.CorrReader
+
+
+[docs]
+ defas_loaded_data(self,prod_sel=None,freq_sel=None,datasets=None):
+"""Load data interval to memory as an :class:`andata.CorrData` instance
+
+ Parameters
+ ----------
+ prod_sel : valid numpy index
+ Passed on to :meth:`andata.CorrData.from_acq_h5`
+ freq_sel : valid numpy index
+ Passed on to :meth:`andata.CorrData.from_acq_h5`
+ datasets : list of strings
+ Passed on to :meth:`andata.CorrData.from_acq_h5`
+
+ Returns
+ -------
+ data : :class:`andata.CorrData`
+ Data interval loaded into memory.
+
+ """
+ returnsuper(CorrDataInterval,self).as_loaded_data(
+ prod_sel=prod_sel,freq_sel=freq_sel,datasets=datasets
+ )
+[docs]
+classHKDataInterval(BaseDataInterval):
+"""Derived class from :class:`BaseDataInterval` for housekeeping data."""
+
+ @property
+ def_reader_class(self):
+ # only dynamic imports of andata allowed in this module.
+ from.importandata
+
+ returnandata.HKReader
+
+
+
+
+[docs]
+classWeatherDataInterval(BaseDataInterval):
+"""Derived class from :class:`BaseDataInterval` for weather data."""
+
+ @property
+ def_reader_class(self):
+ # only dynamic imports of andata allowed in this module.
+ from.importandata
+
+ returnandata.WeatherReader
+
+
+
+
+[docs]
+classFlagInputDataInterval(BaseDataInterval):
+"""Derived class from :class:`BaseDataInterval` for flag input data."""
+
+ @property
+ def_reader_class(self):
+ # only dynamic imports of andata allowed in this module.
+ from.importandata
+
+ returnandata.FlagInputReader
+
+
+
+
+[docs]
+classDigitalGainDataInterval(BaseDataInterval):
+"""Derived class from :class:`BaseDataInterval` for digital gain data."""
+
+ @property
+ def_reader_class(self):
+ # only dynamic imports of andata allowed in this module.
+ from.importandata
+
+ returnandata.DigitalGainReader
+
+
+
+
+[docs]
+classCalibrationGainDataInterval(BaseDataInterval):
+"""Derived class from :class:`BaseDataInterval` for calibration gain data."""
+
+ @property
+ def_reader_class(self):
+ # only dynamic imports of andata allowed in this module.
+ from.importandata
+
+ returnandata.CalibrationGainReader
+
+
+
+# Query routines
+# ==============
+
+
+
+[docs]
+deffiles_in_range(
+ acq,start_time,end_time,node_list,extra_cond=None,node_spoof=None
+):
+"""Get files for a given acquisition within a time range.
+
+ Parameters
+ ----------
+ acq : string or int
+ Which acquisition, by its name or id key.
+ start_time : float
+ POSIX/Unix time for the start or time range.
+ end_time : float
+ POSIX/Unix time for the end or time range.
+ node_list : list of `chimedb.data_index.StorageNode` objects
+ Only return files residing on the given nodes.
+ extra_cond : :mod:`peewee` comparison
+ Any additional expression for filtering files.
+
+ Returns
+ -------
+ file_names : list of strings
+ List of filenames, including the full path.
+
+ """
+
+ ifisinstance(acq,str):
+ acq_name=acq
+ acq=di.ArchiveAcq.get(di.ArchiveAcq.name==acq).acq
+ else:
+ acq_name=di.ArchiveAcq.get(di.ArchiveAcq.id==acq).name
+
+ cond=(
+ (di.ArchiveFile.acq==acq)
+ &(di.ArchiveFileCopy.node<<node_list)
+ &(di.ArchiveFileCopy.has_file=="Y")
+ )
+ info_cond=False
+ foriinfile_info_table:
+ info_cond|=(i.finish_time>=start_time)&(i.start_time<=end_time)
+
+ ifextra_cond:
+ cond&=extra_cond
+
+ query=(
+ di.ArchiveFileCopy.select(
+ di.ArchiveFileCopy.node,
+ di.ArchiveFile.name,
+ di.StorageNode.root,
+ di.StorageNode.name.alias("node_name"),
+ )
+ .join(di.StorageNode)
+ .switch(di.ArchiveFileCopy)
+ .join(di.ArchiveFile)
+ )
+ foriinfile_info_table:
+ query=query.switch(di.ArchiveFile).join(i,join_type=pw.JOIN.LEFT_OUTER)
+ query=query.where(cond&info_cond).objects()
+
+ ifnotnode_spoof:
+ return[path.join(af.root,acq_name,af.name)forafinquery]
+ else:
+ return[path.join(node_spoof[af.node_name],acq_name,af.name)forafinquery]
+
+
+
+# Exceptions
+# ==========
+
+# This is the base CHIMEdb exception
+fromchimedb.core.exceptionsimportCHIMEdbError
+
+
+
+[docs]
+classDataFlagged(CHIMEdbError):
+"""Raised when data is affected by a global flag."""
+"""
+Catalog the measured flux densities of astronomical sources
+
+This module contains tools for cataloging astronomical sources
+and predicting their flux density at radio frequencies based on
+previous measurements.
+"""
+
+fromabcimportABCMeta,abstractmethod
+importos
+importfnmatch
+importinspect
+importwarnings
+
+fromcollectionsimportOrderedDict
+importjson
+importpickle
+
+importnumpyasnp
+importbase64
+importdatetime
+importtime
+
+fromcaputimportmisc
+from.importephemeris
+
+# Define nominal frequency. Sources in catalog are ordered according to
+# their predicted flux density at this frequency. Also acts as default
+# pivot point in spectral fits.
+FREQ_NOMINAL=600.0
+
+# Define the source collections that should be loaded when this module is imported.
+DIR_COLLECTIONS=os.path.join(os.path.dirname(__file__),"catalogs")
+DEFAULT_COLLECTIONS=[
+ os.path.join(DIR_COLLECTIONS,"primary_calibrators_perley2016.json"),
+ os.path.join(DIR_COLLECTIONS,"specfind_v2_5Jy_vollmer2009.json"),
+]
+
+
+# ==================================================================================
+
+[docs]
+classFitSpectrum(object,metaclass=ABCMeta):
+"""A base class for modeling and fitting spectra. Any spectral model
+ used by FluxCatalog should be derived from this class.
+
+ The `fit` method should be used to populate the `param`, `param_cov`, and `stats`
+ attributes. The `predict` and `uncertainty` methods can then be used to obtain
+ the flux density and uncertainty at arbitrary frequencies.
+
+ Attributes
+ ----------
+ param : np.ndarray[nparam, ]
+ Best-fit parameters.
+ param_cov : np.ndarray[nparam, nparam]
+ Covariance of the fit parameters.
+ stats : dict
+ Dictionary that contains statistics related to the fit.
+ Must include 'chisq' and 'ndof'.
+
+ Abstract Methods
+ ----------------
+ Any subclass of FitSpectrum must define these methods:
+ fit
+ _get_x
+ _fit_func
+ _deriv_fit_func
+ """
+
+ def__init__(self,param=None,param_cov=None,stats=None):
+"""Instantiates a FitSpectrum object."""
+
+ self.param=param
+ self.param_cov=param_cov
+ self.stats=stats
+
+
+[docs]
+ defpredict(self,freq):
+"""Predicts the flux density at a particular frequency."""
+
+ x=self._get_x(freq)
+
+ returnself._fit_func(x,*self.param)
+
+
+
+[docs]
+ defuncertainty(self,freq,alpha=0.32):
+"""Predicts the uncertainty on the flux density at a
+ particular frequency.
+ """
+
+ fromscipy.statsimportt
+
+ prob=1.0-alpha/2.0
+ tval=t.ppf(prob,self.stats["ndof"])
+ nparam=len(self.param)
+
+ x=self._get_x(freq)
+
+ dfdp=self._deriv_fit_func(x,*self.param)
+
+ ifhasattr(x,"__iter__"):
+ df2=np.zeros(len(x),dtype=np.float64)
+ else:
+ df2=0.0
+
+ foriiinrange(nparam):
+ forjjinrange(nparam):
+ df2+=dfdp[ii]*dfdp[jj]*self.param_cov[ii][jj]
+
+ returntval*np.sqrt(df2)
+[docs]
+classCurvedPowerLaw(FitSpectrum):
+"""
+ Class to fit a spectrum to a polynomial in log-log space, given by
+
+ .. math::
+ \\ln{S} = a_{0} + a_{1} \\ln{\\nu'} + a_{2} \\ln{\\nu'}^2 + a_{3} \\ln{\\nu'}^3 + \\dots
+
+ where :math:`S` is the flux density, :math:`\\nu'` is the (normalized) frequency,
+ and :math:`a_{i}` are the fit parameters.
+
+ Parameters
+ ----------
+ nparam : int
+ Number of parameters. This sets the order of the polynomial.
+ Default is 2 (powerlaw).
+ freq_pivot : float
+ The pivot frequency :math:`\\nu' = \\nu / freq_pivot`.
+ Default is :py:const:`FREQ_NOMINAL`.
+ """
+
+ def__init__(self,freq_pivot=FREQ_NOMINAL,nparam=2,*args,**kwargs):
+"""Instantiates a CurvedPowerLaw object"""
+
+ super(CurvedPowerLaw,self).__init__(*args,**kwargs)
+
+ # Set the additional model kwargs
+ self.freq_pivot=freq_pivot
+
+ ifself.paramisnotNone:
+ self.nparam=len(self.param)
+ else:
+ self.nparam=nparam
+
+ deffit(self,freq,flux,eflux,flag=None):
+ ifflagisNone:
+ flag=np.ones(len(freq),dtype=bool)
+
+ # Make sure we have enough measurements
+ ifnp.sum(flag)>=self.nparam:
+ # Apply flag
+ fit_freq=freq[flag]
+ fit_flux=flux[flag]
+ fit_eflux=eflux[flag]
+
+ # Convert to log space
+ x=self._get_x(fit_freq)
+ y=np.log(fit_flux)
+
+ # Vandermonde matrix
+ A=self._vandermonde(x,self.nparam)
+
+ # Data covariance matrix
+ C=np.diag((fit_eflux/fit_flux)**2.0)
+
+ # Parameter estimate and covariance
+ param_cov=np.linalg.inv(np.dot(A.T,np.linalg.solve(C,A)))
+
+ param=np.dot(param_cov,np.dot(A.T,np.linalg.solve(C,y)))
+
+ # Compute residuals
+ resid=y-np.dot(A,param)
+
+ # Change the overall normalization to linear units
+ param[0]=np.exp(param[0])
+
+ foriiinrange(self.nparam):
+ forjjinrange(self.nparam):
+ param_cov[ii,jj]*=(param[0]ifii==0else1.0)*(
+ param[0]ifjj==0else1.0
+ )
+
+ # Save parameter estimate and covariance to instance
+ self.param=param.tolist()
+ self.param_cov=param_cov.tolist()
+
+ # Calculate statistics
+ ifnotisinstance(self.stats,dict):
+ self.stats={}
+ self.stats["ndof"]=len(x)-self.nparam
+ self.stats["chisq"]=np.sum(resid**2/np.diag(C))
+
+ # Return results
+ returnself.param,self.param_cov,self.stats
+
+ def_get_x(self,freq):
+ returnnp.log(freq/self.freq_pivot)
+
+ @staticmethod
+ def_vandermonde(x,nparam):
+ returnnp.vstack(tuple([x**rankforrankinrange(nparam)])).T
+
+ @staticmethod
+ def_fit_func(x,*param):
+ returnparam[0]*np.exp(
+ np.sum(
+ [par*x**(rank+1)forrank,parinenumerate(param[1:])],axis=0
+ )
+ )
+
+ @staticmethod
+ def_deriv_fit_func(x,*param):
+ z=param[0]*np.exp(
+ np.sum(
+ [par*x**(rank+1)forrank,parinenumerate(param[1:])],axis=0
+ )
+ )
+
+ dfdp=np.array([z*x**rankforrankinrange(len(param))])
+ dfdp[0]/=param[0]
+
+ returndfdp
+
+
+
+
+[docs]
+classMetaFluxCatalog(type):
+"""Metaclass for FluxCatalog. Defines magic methods
+ for the class that can act on and provice access to the
+ catalog of all astronomical sources.
+ """
+
+ def__str__(self):
+ returnself.string()
+
+ def__iter__(self):
+ returnself.iter()
+
+ def__reversed__(self):
+ returnself.reversed()
+
+ def__len__(self):
+ returnself.len()
+
+ def__getitem__(self,key):
+ returnself.get(key)
+
+ def__contains__(self,item):
+ try:
+ obj=self.get(item)
+ exceptKeyError:
+ obj=None
+
+ returnobjisnotNone
+
+
+
+
+[docs]
+classFluxCatalog(object,metaclass=MetaFluxCatalog):
+"""
+ Class for cataloging astronomical sources and predicting
+ their flux density at radio frequencies based on spectral fits
+ to previous measurements.
+
+ Class methods act upon and provide access to the catalog of
+ all sources. Instance methods act upon and provide access
+ to individual sources. All instances are stored in an
+ internal class dictionary.
+
+ Attributes
+ ----------
+ fields : list
+ List of attributes that are read-from and written-to the
+ JSON catalog files.
+ model_lookup : dict
+ Dictionary that provides access to the various models that
+ can be fit to the spectrum. These models should be
+ subclasses of FitSpectrum.
+ """
+
+ fields=[
+ "ra",
+ "dec",
+ "alternate_names",
+ "model",
+ "model_kwargs",
+ "stats",
+ "param",
+ "param_cov",
+ "measurements",
+ ]
+
+ model_lookup={"CurvedPowerLaw":CurvedPowerLaw}
+
+ _entries={}
+ _collections={}
+ _alternate_name_lookup={}
+
+ def__init__(
+ self,
+ name,
+ ra=None,
+ dec=None,
+ alternate_names=[],
+ model="CurvedPowerLaw",
+ model_kwargs=None,
+ stats=None,
+ param=None,
+ param_cov=None,
+ measurements=None,
+ overwrite=0,
+ ):
+"""
+ Instantiates a FluxCatalog object for an astronomical source.
+
+ Parameters
+ ----------
+ name : string
+ Name of the source. The convention for the source name is to
+ use the MAIN_ID in the SIMBAD database in all uppercase letters
+ with spaces replaced by underscores.
+
+ ra : float
+ Right Ascension in degrees.
+
+ dec : float
+ Declination in degrees.
+
+ alternate_names : list of strings
+ Alternate names for the source. Ideally should include all alternate names
+ present in the SIMBAD database using the naming convention specified above.
+
+ model : string
+ Name of FitSpectrum subclass.
+
+ model_kwargs : dict
+ Dictionary containing keywords required by the model.
+
+ stats : dict
+ Dictionary containing statistics from model fit.
+
+ param : list, length nparam
+ Best-fit parameters.
+
+ param_cov : 2D-list, size nparam x nparam
+ Estimate of covariance of fit parameters.
+
+ measurements : 2D-list, size nmeas x 7
+ List of measurements of the form:
+ [freq, flux, eflux, flag, catalog, epoch, citation].
+ Should use the add_measurement method to populate this list.
+
+ overwrite : int between 0 and 2
+ Action to take in the event that this source is already in the catalog:
+ - 0 - Return the existing entry.
+ - 1 - Add the measurements to the existing entry.
+ - 2 - Overwrite the existing entry.
+ Default is 0.
+ """
+
+ # The name argument is a unique identifier into the catalog.
+ # Check if there is already a source in the catalog with the
+ # input name. If there is, then the behavior is set by the
+ # overwrite argument.
+ if(overwrite<2)and(nameinFluxCatalog):
+ # Return existing entry
+ print("%s already has an entry in catalog."%name,end=" ")
+ ifoverwrite==0:
+ print("Returning existing entry.")
+ self=FluxCatalog[name]
+
+ # Add any measurements to existing entry
+ elifoverwrite==1:
+ print("Adding measurements to existing entry.")
+ self=FluxCatalog[name]
+ ifmeasurementsisnotNone:
+ self.add_measurement(*measurements)
+ self.fit_model()
+
+ else:
+ # Create new instance for this source.
+ self.name=format_source_name(name)
+
+ # Initialize object attributes
+ # Basic info:
+ self.ra=ra
+ self.dec=dec
+
+ self.alternate_names=[
+ format_source_name(aname)foranamein_ensure_list(alternate_names)
+ ]
+
+ # Measurements:
+ self.measurements=measurements
+
+ # Best-fit model:
+ self.model=model
+ self.param=param
+ self.param_cov=param_cov
+ self.stats=stats
+ self.model_kwargs=model_kwargs
+ ifself.model_kwargsisNone:
+ self.model_kwargs={}
+
+ # Create model object
+ self._model=self.model_lookup[self.model](
+ param=self.param,
+ param_cov=self.param_cov,
+ stats=self.stats,
+ **self.model_kwargs
+ )
+
+ # Populate the kwargs that were used
+ arg_list=misc.getfullargspec(self.model_lookup[self.model].__init__)
+ iflen(arg_list.args)>1:
+ keys=arg_list.args[1:]
+ forkeyinkeys:
+ ifhasattr(self._model,key):
+ self.model_kwargs[key]=getattr(self._model,key)
+
+ ifnotself.model_kwargs:
+ self.model_kwargs=None
+
+ # Save to class dictionary
+ self._entries[self.name]=self
+
+ # Add alternate names to class dictionary so they can be searched quickly
+ foralt_nameinself.alternate_names:
+ ifalt_nameinself._alternate_name_lookup:
+ warnings.warn(
+ "The alternate name %s is already held by the source %s."
+ %(alt_name,self._alternate_name_lookup[alt_name])
+ )
+ else:
+ self._alternate_name_lookup[alt_name]=self.name
+
+
+[docs]
+ defadd_measurement(
+ self,freq,flux,eflux,flag=True,catalog=None,epoch=None,citation=None
+ ):
+"""Add entries to the list of measurements. Each argument/keyword
+ can be a list of items with length equal to 'len(flux)', or
+ alternatively a single item in which case the same value is used
+ for all measurements.
+
+ Parameters
+ ----------
+ freq : float, list of floats
+ Frequency in MHz.
+
+ flux : float, list of floats
+ Flux density in Jansky.
+
+ eflux : float, list of floats
+ Uncertainty on flux density in Jansky.
+
+ flag : bool, list of bool
+ If True, use this measurement in model fit.
+ Default is True.
+
+ catalog : string or None, list of strings or Nones
+ Name of the catalog from which this measurement originates.
+ Default is None.
+
+ epoch : float or None, list of floats or Nones
+ Year when this measurement was taken.
+ Default is None.
+
+ citation : string or None, list of strings or Nones
+ Citation where this measurement can be found
+ (e.g., 'Baars et al. (1977)').
+ Default is None.
+
+ """
+
+ # Ensure that all of the inputs are lists
+ # of the same length as flux
+ flux=_ensure_list(flux)
+ nmeas=len(flux)
+
+ freq=_ensure_list(freq,nmeas)
+ eflux=_ensure_list(eflux,nmeas)
+ flag=_ensure_list(flag,nmeas)
+ catalog=_ensure_list(catalog,nmeas)
+ epoch=_ensure_list(epoch,nmeas)
+ citation=_ensure_list(citation,nmeas)
+
+ # Store as list
+ meas=[
+ [
+ freq[mm],
+ flux[mm],
+ eflux[mm],
+ flag[mm],
+ catalog[mm],
+ epoch[mm],
+ citation[mm],
+ ]
+ formminrange(nmeas)
+ ]
+
+ # Add measurements to internal list
+ ifself.measurementsisNone:
+ self.measurements=meas
+ else:
+ self.measurements+=meas
+
+ # Sort internal list by frequency
+ isort=np.argsort(self.freq)
+ self.measurements=[self.measurements[mm]formminisort]
+
+
+
+[docs]
+ deffit_model(self):
+"""Fit the measurements stored in the 'measurements' attribute with the
+ spectral model specified in the 'model' attribute. This populates the
+ 'param', 'param_cov', and 'stats' attributes.
+ """
+
+ arg_list=misc.getfullargspec(self._model.fit).args[1:]
+
+ args=[self.freq[self.flag],self.flux[self.flag],self.eflux[self.flag]]
+
+ if(self.epochisnotNone)and("epoch"inarg_list):
+ args.append(self.epoch[self.flag])
+
+ self.param,self.param_cov,self.stats=self._model.fit(*args)
+
+
+
+[docs]
+ defplot(self,legend=True,catalog=True,residuals=False):
+"""Plot the measurements, best-fit model, and confidence interval.
+
+ Parameters
+ ----------
+ legend : bool
+ Show legend. Default is True.
+
+ catalog : bool
+ If True, then label and color code the measurements according to
+ their catalog. If False, then label and color code the measurements
+ according to their citation. Default is True.
+
+ residuals : bool
+ Plot the residuals instead of the measurements and best-fit model.
+ Default is False.
+ """
+
+ importmatplotlib
+ importmatplotlib.pyplotasplt
+
+ # Define plot parameters
+ colors=["blue","darkorchid","m","plum","mediumvioletred","palevioletred"]
+ markers=["o","*","s","p","^"]
+ sizes=[10,12,12,12,12]
+
+ font={"family":"sans-serif","weight":"normal","size":16}
+
+ plt.rc("font",**font)
+
+ nplot=500
+
+ # Plot the model fit and uncertainty
+ xrng=[np.floor(np.log10(self.freq.min())),np.ceil(np.log10(self.freq.max()))]
+ xrng=[min(xrng[0],2.0),max(xrng[1],3.0)]
+
+ fplot=np.logspace(*xrng,num=nplot)
+
+ xrng=[10.0**xxforxxinxrng]
+
+ ifresiduals:
+ flux_hat=self.predict_flux(self.freq)
+ flux=(self.flux-flux_hat)/flux_hat
+ eflux=self.eflux/flux_hat
+ model=np.zeros_like(fplot)
+ delta=self.predict_uncertainty(fplot)/self.predict_flux(fplot)
+ ylbl="Residuals: "+r"$(S - \hat{S}) / \hat{S}$"
+ yrng=[-0.50,0.50]
+ else:
+ flux=self.flux
+ eflux=self.eflux
+ model=self.predict_flux(fplot)
+ delta=self.predict_uncertainty(fplot)
+ ylbl="Flux Density [Jy]"
+ yrng=[model.min(),model.max()]
+
+ plt.fill_between(
+ fplot,model-delta,model+delta,facecolor="darkgray",alpha=0.3
+ )
+ plt.plot(
+ fplot,
+ model-delta,
+ fplot,
+ model+delta,
+ color="black",
+ linestyle="-",
+ linewidth=0.5,
+ )
+
+ plt.plot(
+ fplot,model,color="black",linestyle="-",linewidth=1.0,label=self.model
+ )
+
+ # Plot the measurements
+ ifcatalog:
+ cat_uniq=list(set(self.catalog))
+ else:
+ cat_uniq=list(set(self.citation))
+
+ # Loop over catalogs/citations
+ forii,catinenumerate(cat_uniq):
+ ifcatalog:
+ pind=np.array([cc==catforccinself.catalog])
+ else:
+ pind=np.array([cc==catforccinself.citation])
+
+ ifcatisNone:
+ pcol="black"
+ pmrk="o"
+ psz=10
+ lbl="Meas."
+ else:
+ pcol=colors[ii%len(colors)]
+ pmrk=markers[ii//len(colors)]
+ psz=sizes[ii//len(colors)]
+ lbl=cat
+
+ plt.errorbar(
+ self.freq[pind],
+ flux[pind],
+ yerr=eflux[pind],
+ color=pcol,
+ marker=pmrk,
+ markersize=psz,
+ linestyle="None",
+ label=lbl,
+ )
+
+ # Set log axis
+ ax=plt.gca()
+ ax.set_xscale("log")
+ ifnotresiduals:
+ ax.set_yscale("log")
+ plt.xlim(xrng)
+ plt.ylim(yrng)
+
+ plt.grid(b=True,which="both")
+
+ # Plot lines denoting CHIME band
+ plt.axvspan(400.0,800.0,color="green",alpha=0.1)
+
+ # Create a legend
+ iflegend:
+ plt.legend(loc="lower left",numpoints=1,prop=font)
+
+ # Set labels
+ plt.xlabel("Frequency [MHz]")
+ plt.ylabel(ylbl)
+
+ # Create block with statistics
+ ifnotresiduals:
+ txt=r"$\chi^2 = %0.2f$ $(%d)$"%(self.stats["chisq"],self.stats["ndof"])
+
+ plt.text(
+ 0.95,
+ 0.95,
+ txt,
+ horizontalalignment="right",
+ verticalalignment="top",
+ transform=ax.transAxes,
+ )
+
+ # Create title
+ ttl=self.name.replace("_"," ")
+ plt.title(ttl)
+
+
+
+[docs]
+ defpredict_flux(self,freq,epoch=None):
+"""Predict the flux density of the source at a particular
+ frequency and epoch.
+
+ Parameters
+ ----------
+ freq : float, np.array of floats
+ Frequency in MHz.
+
+ epoch : float, np.array of floats
+ Year. Defaults to current year.
+
+ Returns
+ -------
+ flux : float, np.array of floats
+ Flux density in Jansky.
+
+ """
+
+ arg_list=misc.getfullargspec(self._model.predict).args[1:]
+
+ if(epochisnotNone)and("epoch"inarg_list):
+ args=[freq,epoch]
+ else:
+ args=[freq]
+
+ flux=self._model.predict(*args)
+
+ returnflux
+
+
+
+[docs]
+ defpredict_uncertainty(self,freq,epoch=None):
+"""Calculate the uncertainty in the estimate of the flux density
+ of the source at a particular frequency and epoch.
+
+ Parameters
+ ----------
+ freq : float, np.array of floats
+ Frequency in MHz.
+
+ epoch : float, np.array of floats
+ Year. Defaults to current year.
+
+ Returns
+ -------
+ flux_uncertainty : float, np.array of floats
+ Uncertainty on the flux density in Jansky.
+
+ """
+
+ arg_list=misc.getfullargspec(self._model.uncertainty).args[1:]
+
+ if(epochisnotNone)and("epoch"inarg_list):
+ args=[freq,epoch]
+ else:
+ args=[freq]
+
+ flux_uncertainty=self._model.uncertainty(*args)
+
+ returnflux_uncertainty
+
+
+
+[docs]
+ defto_dict(self):
+"""Returns an ordered dictionary containing attributes
+ for this instance object. Used to dump the information
+ stored in the instance object to a file.
+
+ Returns
+ -------
+ flux_body_dict : dict
+ Dictionary containing all attributes listed in
+ the 'fields' class attribute.
+ """
+
+ flux_body_dict=OrderedDict()
+
+ forkeyinself.fields:
+ ifhasattr(self,key)and(getattr(self,key)isnotNone):
+ flux_body_dict[key]=getattr(self,key)
+
+ returnflux_body_dict
+
+
+ def__str__(self):
+"""Returns a string containing basic information about the source.
+ Called by the print statement.
+ """
+ source_string=(
+ "{0:<25.25s}{1:>6.2f}{2:>6.2f}{3:>6d}{4:^15.1f}{5:^15.1f}".format(
+ self.name,
+ self.ra,
+ self.dec,
+ len(self),
+ self.predict_flux(FREQ_NOMINAL),
+ 100.0
+ *self.predict_uncertainty(FREQ_NOMINAL)
+ /self.predict_flux(FREQ_NOMINAL),
+ )
+ )
+
+ returnsource_string
+
+ def__len__(self):
+"""Returns the number of measurements of the source."""
+ returnlen(self.measurements)ifself.measurementsisnotNoneelse0
+
+
+
+
+ @property
+ defskyfield(self):
+"""Skyfield star representation :class:`skyfield.starlib.Star`
+ for the source.
+ """
+ returnephemeris.skyfield_star_from_ra_dec(self.ra,self.dec,self.name)
+
+ @property
+ deffreq(self):
+"""Frequency of measurements in MHz."""
+ returnnp.array([meas[0]formeasinself.measurements])
+
+ @property
+ defflux(self):
+"""Flux measurements in Jansky."""
+ returnnp.array([meas[1]formeasinself.measurements])
+
+ @property
+ defeflux(self):
+"""Error on the flux measurements in Jansky."""
+ returnnp.array([meas[2]formeasinself.measurements])
+
+ @property
+ defflag(self):
+"""Boolean flag indicating what measurements are used
+ in the spectral fit.
+ """
+ returnnp.array([meas[3]formeasinself.measurements])
+
+ @property
+ defcatalog(self):
+"""Catalog from which each measurement originates."""
+ returnnp.array([meas[4]formeasinself.measurements])
+
+ @property
+ defepoch(self):
+"""Year that each measurement occured."""
+ returnnp.array([meas[5]formeasinself.measurements])
+
+ @property
+ defcitation(self):
+"""Citation where more information on each measurement
+ can be found.
+ """
+ returnnp.array([meas[6]formeasinself.measurements])
+
+ @property
+ def_sort_id(self):
+"""Sources in the catalog are ordered according to this
+ property. Currently use the predicted flux at FREQ_NOMINAL
+ in descending order.
+ """
+ # Multipy by -1 so that we will
+ # sort from higher to lower flux
+ return-self.predict_flux(
+ FREQ_NOMINAL,epoch=get_epoch(datetime.datetime.now())
+ )
+
+ # =============================================================
+ # Class methods that act on the entire catalog
+ # =============================================================
+
+
+[docs]
+ @classmethod
+ defstring(cls):
+"""Print basic information about the sources in the catalog."""
+
+ catalog_string=[]
+
+ # Print the header
+ hdr="{0:<25s}{1:^6s}{2:^6s}{3:>6s}{4:^15s}{5:^15s}".format(
+ "Name","RA","Dec","Nmeas","Flux","Error"
+ )
+
+ units="{0:<25s}{1:^6s}{2:^6s}{3:>6s}{4:^15s}{5:^15s}".format(
+ "",
+ "[deg]",
+ "[deg]",
+ "",
+ "@%d MHz [Jy]"%FREQ_NOMINAL,
+ "@%d MHz [%%]"%FREQ_NOMINAL,
+ )
+
+ catalog_string.append("".join(["-"]*max(len(hdr),len(units))))
+ catalog_string.append(hdr)
+ catalog_string.append(units)
+ catalog_string.append("".join(["-"]*max(len(hdr),len(units))))
+
+ # Loop over sorted entries and print
+ forkeyincls.sort():
+ catalog_string.append(cls[key].__str__())
+
+ return"\n".join(catalog_string)
+
+
+
+[docs]
+ @classmethod
+ deffrom_dict(cls,name,flux_body_dict):
+"""Instantiates a FluxCatalog object for an astronomical source
+ from a dictionary of kwargs. Used when loading sources from a
+ JSON catalog file.
+
+ Parameters
+ ----------
+ name : str
+ Name of the astronomical source.
+
+ flux_body_dict : dict
+ Dictionary containing some or all of the keyword arguments
+ listed in the __init__ function for this class.
+
+ Returns
+ -------
+ obj : FluxCatalog instance
+ Object that can be used to predict the flux of this source,
+ plot flux measurements, etc.
+
+ """
+
+ arg_list=misc.getfullargspec(cls.__init__).args[2:]
+
+ kwargs={
+ field:flux_body_dict[field]
+ forfieldinarg_list
+ iffieldinflux_body_dict
+ }
+
+ returncls(name,**kwargs)
+
+
+
+[docs]
+ @classmethod
+ defget(cls,key):
+"""Searches the catalog for a source. First checks against the
+ 'name' of each entry, then checks against the 'alternate_names'
+ of each entry.
+
+ Parameters
+ ----------
+ key : str
+ Name of the astronomical source.
+
+ Returns
+ -------
+ obj : FluxCatalog instance
+ Object that can be used to predict the flux of this source,
+ plot flux measurements, etc.
+
+ """
+
+ # Check that key is a string
+ ifnotisinstance(key,str):
+ raiseTypeError("Provide source name as string.")
+
+ fkey=format_source_name(key)
+
+ # First check names
+ obj=cls._entries.get(fkey,None)
+
+ # Next check alternate names
+ ifobjisNone:
+ afkey=cls._alternate_name_lookup.get(fkey,None)
+ ifafkeyisnotNone:
+ obj=cls._entries.get(afkey)
+
+ # Check if the object was found
+ ifobjisNone:
+ raiseKeyError("%s was not found."%fkey)
+
+ # Return the body corresponding to this source
+ returnobj
+
+
+
+[docs]
+ @classmethod
+ defdelete(cls,source_name):
+"""Deletes a source from the catalog.
+
+ Parameters
+ ----------
+ source_name : str
+ Name of the astronomical source.
+
+ """
+
+ try:
+ obj=cls.get(source_name)
+ exceptKeyError:
+ key=None
+ else:
+ key=obj.name
+
+ ifkeyisnotNone:
+ obj=cls._entries.pop(key)
+
+ forakeyinobj.alternate_names:
+ cls._alternate_name_lookup.pop(akey,None)
+
+ delobj
+
+
+
+[docs]
+ @classmethod
+ defsort(cls):
+"""Sorts the entries in the catalog by their flux density
+ at FREQ_NOMINAL in descending order.
+
+ Returns
+ -------
+ names : list of str
+ List of source names in correct order.
+
+ """
+
+ keys=[]
+ forname,bodyincls._entries.items():
+ keys.append((body._sort_id,name))
+
+ keys.sort()
+
+ return[key[1]forkeyinkeys]
+
+
+
+[docs]
+ @classmethod
+ defkeys(cls):
+"""Alias for sort.
+
+ Returns
+ -------
+ names : list of str
+ List of source names in correct order.
+
+ """
+ returncls.sort()
+
+
+
+[docs]
+ @classmethod
+ defiter(cls):
+"""Iterates through the sources in the catalog.
+
+ Returns
+ -------
+ it : iterator
+ Provides the name of each source in the catalog
+ in the order specified by the 'sort' class method.
+
+ """
+ returniter(cls.sort())
+
+
+
+[docs]
+ @classmethod
+ defreversed(cls):
+"""Iterates through the sources in the catalog
+ in reverse order.
+
+ Returns
+ -------
+ it : iterator
+ Provides the name of each source in the catalog
+ in the reverse order as that specified by the
+ 'sort' class method.
+
+ """
+ returnreversed(cls.sort())
+
+
+
+[docs]
+ @classmethod
+ defiteritems(cls):
+"""Iterates through the sources in the catalog.
+
+ Returns
+ -------
+ it : iterator
+ Provides (name, object) for each source in the catalog
+ in the order specified by the 'sort' class method.
+
+ """
+ returniter([(key,cls._entries[key])forkeyincls.sort()])
+
+
+
+[docs]
+ @classmethod
+ deflen(cls):
+"""Number of sources in the catalog.
+
+ Returns
+ -------
+ N : int
+
+ """
+ returnlen(cls._entries)
+
+
+
+[docs]
+ @classmethod
+ defavailable_collections(cls):
+"""Search the local directory for potential collections that
+ can be loaded.
+
+ Returns
+ -------
+ collections : list of (str, [str, ...])
+ List containing a tuple for each collection. The tuple contains
+ the filename of the collection (str) and the sources it contains
+ (list of str).
+
+ """
+
+ # Determine the directory where this class is located
+ current_file=inspect.getfile(cls.__class__)
+ current_dir=os.path.abspath(os.path.dirname(os.path.dirname(current_file)))
+
+ # Search this directory recursively for JSON files.
+ # Load each one that is found into a dictionary and
+ # return the number of sources and source names.
+ matches=[]
+ forroot,dirnames,filenamesinos.walk(current_dir):
+ forfilenameinfnmatch.filter(filenames,"*.json")+fnmatch.filter(
+ filenames,"*.pickle"
+ ):
+ full_path=os.path.join(root,filename)
+
+ # Read into dictionary
+ withopen(full_path,"r")asfp:
+ collection_dict=json.load(fp,object_hook=json_numpy_obj_hook)
+
+ # Append (path, number of sources, source names) to list
+ matches.append((full_path,list(collection_dict.keys())))
+
+ # Return matches
+ returnmatches
+
+
+
+[docs]
+ @classmethod
+ defprint_available_collections(cls,verbose=False):
+"""Print information about the available collections.
+
+ Parameters
+ ----------
+ verbose : bool
+ If True, then print all source names in addition to the names
+ of the files and number of sources. Default is False.
+ """
+ forccincls.available_collections():
+ _print_collection_summary(*cc,verbose=verbose)
+
+
+
+[docs]
+ @classmethod
+ defloaded_collections(cls):
+"""Return the collections that have been loaded.
+
+ Returns
+ -------
+ collections : list of (str, [str, ...])
+ List containing a tuple for each collection. The tuple contains
+ the filename of the collection (str) and the sources it contains
+ (list of str).
+ """
+ returnlist(cls._collections.items())
+
+
+
+[docs]
+ @classmethod
+ defprint_loaded_collections(cls,verbose=False):
+"""Print information about the collection that have been loaded.
+
+ Parameters
+ ----------
+ verbose : bool
+ If True, then print all source names in addition to the names
+ of the files and number of sources. Default is False.
+ """
+ forcat,sourcesincls._collections.items():
+ _print_collection_summary(cat,sources,verbose=verbose)
+[docs]
+ @classmethod
+ defdump(cls,filename):
+"""Dumps the contents of the catalog to a file.
+
+ Parameters
+ ----------
+ filename : str
+ Valid path name. Should have .json or .pickle extension.
+
+ """
+
+ # Parse filename
+ filename=os.path.expandvars(os.path.expanduser(filename))
+ path=os.path.abspath(os.path.dirname(filename))
+ ext=os.path.splitext(filename)[1]
+
+ ifextnotin[".pickle",".json"]:
+ raiseValueError("Do not recognize '%s' extension."%ext)
+
+ try:
+ os.makedirs(path)
+ exceptOSError:
+ ifnotos.path.isdir(path):
+ raise
+
+ # Sort based on _sort_id
+ keys=cls.sort()
+
+ # Place a dictionary with the information
+ # stored in each object into an OrderedDict
+ output=OrderedDict()
+ forkeyinkeys:
+ output[key]=cls._entries[key].to_dict()
+
+ # Dump this dictionary to file
+ withopen(filename,"w")asfp:
+ ifext==".json":
+ json.dump(output,fp,cls=NumpyEncoder,indent=4)
+ elifext==".pickle":
+ pickle.dump(output,fp)
+
+
+
+[docs]
+ @classmethod
+ defload(cls,filename,overwrite=0,set_globals=False,verbose=False):
+"""
+ Load the contents of a file into the catalog.
+
+ Parameters
+ ----------
+ filename : str
+ Valid path name. Should have .json or .pickle extension.
+ overwrite : int between 0 and 2
+ Action to take in the event that this source is already in the catalog:
+ - 0 - Return the existing entry.
+ - 1 - Add any measurements to the existing entry.
+ - 2 - Overwrite the existing entry.
+ Default is 0.
+ set_globals : bool
+ If True, this creates a variable in the global space
+ for each source in the file. Default is False.
+ verbose : bool
+ If True, print some basic info about the contents of
+ the file as it is loaded. Default is False.
+ """
+
+ # Parse filename
+ # Define collection name as basename of file without extension
+ filename=os.path.expandvars(os.path.expanduser(filename))
+ collection_name,ext=os.path.splitext(os.path.basename(filename))
+
+ # Check if the file actually exists and has the correct extension
+ ifnotos.path.isfile(filename):
+ raiseValueError("%s does not exist."%filename)
+
+ ifextnotin[".pickle",".json"]:
+ raiseValueError("Do not recognize '%s' extension."%ext)
+
+ # Load contents of file into a dictionary
+ withopen(filename,"r")asfp:
+ ifext==".json":
+ collection_dict=json.load(fp,object_hook=json_numpy_obj_hook)
+ elifext==".pickle":
+ collection_dict=pickle.load(fp)
+
+ # Add this to the list of files
+ cls._collections[collection_name]=list(collection_dict.keys())
+
+ # If requested, print some basic info about the collection
+ ifverbose:
+ _print_collection_summary(cls._collections[collection_name])
+
+ # Loop through dictionary and add each source to the catalog
+ forkey,valueincollection_dict.items():
+ # Add overwrite keyword
+ value["overwrite"]=overwrite
+
+ # Create object for this source
+ obj=cls.from_dict(key,value)
+
+ # If requested, create a variable in the global space
+ # containing the object for this source.
+ ifset_globals:
+ varkey=varname(key)
+ globals()[varkey]=obj
+
+
+
+
+defget_epoch(date):
+ defsinceEpoch(date):# returns seconds since epoch
+ returntime.mktime(date.timetuple())
+
+ year=date.year
+ startOfThisYear=datetime.datetime(year=year,month=1,day=1)
+ startOfNextYear=datetime.datetime(year=year+1,month=1,day=1)
+
+ yearElapsed=sinceEpoch(date)-sinceEpoch(startOfThisYear)
+ yearDuration=sinceEpoch(startOfNextYear)-sinceEpoch(startOfThisYear)
+ fraction=yearElapsed/yearDuration
+
+ returndate.year+fraction
+
+
+defvarname(name):
+ varname=name.replace(" ","_")
+
+ ifvarname[0].isdigit():
+ varname="_"+varname
+
+ returnvarname
+
+
+defformat_source_name(input_name):
+ # Address some common naming conventions.
+ ifinput_name.startswith("NAME "):
+ # SIMBAD prefixes common source names with 'NAME '.
+ # Remove this.
+ output_name=input_name[5:]
+
+ elifnotany(char.isdigit()forcharininput_name):
+ # We have been using PascalCase to denote common source names.
+ # Convert from CygA, HerA, PerB ---> Cyg A, Her A, Per B.
+ output_name=input_name[0]
+ foriiinrange(1,len(input_name)):
+ ifinput_name[ii-1].islower()andinput_name[ii].isupper():
+ output_name+=" "+input_name[ii]
+ else:
+ output_name+=input_name[ii]
+
+ else:
+ # No funny business with the input_name in this case.
+ output_name=input_name
+
+ # Remove multiple spaces. Replace single spaces with underscores.
+ output_name="_".join(output_name.split())
+
+ # Put the name in all uppercase.
+ output_name=output_name.upper()
+
+ # Return properly formatted name
+ returnoutput_name
+
+
+
+[docs]
+ defdefault(self,obj):
+"""If input object is an ndarray it will be converted into a dict
+ holding dtype, shape and the data, base64 encoded.
+ """
+ ifisinstance(obj,np.ndarray):
+ ifobj.flags["C_CONTIGUOUS"]:
+ obj_data=obj.data
+ else:
+ cont_obj=np.ascontiguousarray(obj)
+ assertcont_obj.flags["C_CONTIGUOUS"]
+ obj_data=cont_obj.data
+ data_b64=base64.b64encode(obj_data)
+ returndict(__ndarray__=data_b64,dtype=str(obj.dtype),shape=obj.shape)
+ # Let the base class default method raise the TypeError
+ returnjson.JSONEncoder(self,obj)
+
+
+
+
+
+[docs]
+defjson_numpy_obj_hook(dct):
+"""Decodes a previously encoded numpy ndarray with proper shape and dtype.
+
+ :param dct: (dict) json encoded ndarray
+ :return: (ndarray) if input was an encoded ndarray
+ """
+ ifisinstance(dct,dict)and"__ndarray__"indct:
+ data=base64.b64decode(dct["__ndarray__"])
+ returnnp.frombuffer(data,dct["dtype"]).reshape(dct["shape"])
+ returndct
+
+
+
+def_print_collection_summary(collection_name,source_names,verbose=True):
+"""This prints out information about a collection of sources
+ in a standardized way.
+
+ Parameters
+ ----------
+ collection_name : str
+ Name of the collection.
+
+ source_names : list of str
+ Names of the sources in the collection.
+
+ verbose : bool
+ If true, then print out all of the source names.
+ """
+
+ ncol=4
+ nsrc=len(source_names)
+
+ # Create a header containing the collection name and number of sources
+ header=collection_name+" (%d Sources)"%nsrc
+ print(header)
+
+ # Print the sources contained in this collection
+ ifverbose:
+ # Seperator
+ print("".join(["-"]*len(header)))
+
+ # Source names
+ foriiinrange(0,nsrc,ncol):
+ jj=min(ii+ncol,nsrc)
+ print((" ".join(["%-25s"]*(jj-ii)))%tuple(source_names[ii:jj]))
+
+ # Space
+ print("")
+
+
+def_ensure_list(obj,num=None):
+ ifhasattr(obj,"__iter__")andnotisinstance(obj,str):
+ nnum=len(obj)
+ if(numisnotNone)and(nnum!=num):
+ raiseValueError("Input list has wrong size.")
+ else:
+ ifnumisnotNone:
+ obj=[obj]*num
+ else:
+ obj=[obj]
+
+ returnobj
+
+
+# Load the default collections
+forcolinDEFAULT_COLLECTIONS:
+ FluxCatalog.load(col)
+
+"""
+Holography observation tables.
+
+This module defines the tables:
+
+- :py:class:`HolographyObservation`
+- :py:class:`HolographySource`
+
+and the constants:
+
+- :py:const:`QUALITY_GOOD`
+- :py:const:`QUALITY_OFFSOURCE`
+- :py:const:`ONSOURCE_DIST_TO_FLAG`
+
+"""
+
+importos
+importwarnings
+importzipfile
+importnumpyasnp
+importpeeweeaspw
+fromchimedb.core.ormimportbase_model
+
+fromch_utilimportephemeris
+
+# Global variables and constants.
+# ================================
+
+QUALITY_GOOD=0
+QUALITY_OFFSOURCE=1
+QUALITY_BADGATING=2
+QUALITY_NOISEOFF=4
+ONSOURCE_DIST_TO_FLAG=0.1
+
+# Tables in the for tracking Holography observations
+# ==================================================
+
+
+
+[docs]
+classHolographySource(base_model):
+"""A peewee model for the Holography sources.
+
+ Attributes
+ ----------
+ name : str
+ Unique name for the source. Be careful to avoid duplicates.
+ ra, dec : float
+ ICRS co-ordinates of the source.
+ """
+
+ name=pw.CharField(max_length=128,unique=True)
+ ra=pw.FloatField()
+ dec=pw.FloatField()
+
+
+
+
+[docs]
+classHolographyObservation(base_model):
+"""
+ A peewee model for the holographic observations.
+
+ Attributes
+ ----------
+ source : foreign key
+ The source that we were observing.
+ start_time, finish_time : float
+ Start and end times of the source observation (as UNIX times).
+ notes : str
+ Any free form notes about the observation.
+ """
+
+ source=pw.ForeignKeyField(HolographySource,backref="observations")
+ start_time=pw.DoubleField()
+ finish_time=pw.DoubleField()
+
+ quality_flag=(
+ pw.BitField()
+ )# maximum of 64 fields. If we need more, use BigBitField
+ off_source=quality_flag.flag(QUALITY_OFFSOURCE)
+ bad_gating=quality_flag.flag(QUALITY_BADGATING)
+ noise_off=quality_flag.flag(QUALITY_NOISEOFF)
+
+ notes=pw.TextField(null=True)
+
+
+[docs]
+ @classmethod
+ deffrom_lst(
+ cls,
+ source,
+ start_day,
+ start_lst,
+ duration_lst,
+ quality_flag=QUALITY_GOOD,
+ notes=None,
+ ):
+"""Method to initialize a HolographyObservation from a start day,
+ start LST, and a stop day, stop LST.
+
+ Parameters
+ ----------
+ source : HolographySource
+ An instance of HolographySource.
+ start_day: string
+ Of format YYYMMDD-ABT, ABT can be one of (UTC, PST, PDT)
+ start_lst, duration: float
+ Hours and fraction of hours on a scale from 0-24.
+ quality_flag : int, default : 0
+ Flag for poor quality data. Good data is zero.
+ Sets a bitmask in the HolographyObservation instance.
+ notes : string, optional
+ Any notes on this observation.
+ """
+
+ start_time=ephemeris.lsa_to_unix(
+ start_lst*360/24,
+ ephemeris.datetime_to_unix(ephemeris.parse_date(start_day)),
+ )
+ duration_unix=duration_lst*(3600.0)*ephemeris.SIDEREAL_S
+
+ finish_time=start_time+duration_unix
+
+ returncls.create(
+ source=source,
+ start_time=start_time,
+ finish_time=finish_time,
+ quality_flag=quality_flag,
+ notes=notes,
+ )
+
+
+ # Aliases of source names in the spreadsheet to ones we use in the database
+ # (hard-coded at initialization, but user should be able to edit)
+ source_alias={
+ "B0329******":"B0329+54",
+ "B0950*****":"B0950+08",
+ "B1133+16*****":"B1133+16",
+ "B1929+10*****":"B1929+10",
+ "B0355+56":"B0355+54",
+ "3C218":"HydraA",
+ "C48":"3C48",
+ "3C_58":"3C58",
+ "3C348":"HerA",
+ "3C144":"TauA",
+ "PerB":"3C123",
+ "B0531+21*****":"B0531+21",
+ "B2016+28*****":"B2016+28",
+ "B1133*****":"B1133+16",
+ "B1937+21*****":"B1937+21",
+ "B2016*****":"B2016+28",
+ "B0950+08*****":"B0950+08",
+ "FAN":"FanRegion1",
+ "Fan Region 1":"FanRegion1",
+ "FAN1":"FanRegion1",
+ "Fan Region 2":"FanRegion2",
+ "FAN2":"FanRegion2",
+ "B0905*****":"B0905*****",
+ "VIRA":"VirA",
+ "3C274":"VirA",
+ "3C405":"CygA",
+ "3C461":"CasA",
+ "NCP_20H":"NCP 20H",
+ "NCP_4H":"NCP 4H",
+ }
+
+ # read the .POST_REPORT file and pull out source name, time, and observation
+ # duration
+
+[docs]
+ @classmethod
+ defparse_post_report(cls,post_report_file):
+"""
+ read a .POST_REPORT file from the nsched program which controls the
+ John Galt Telescope and extract the source name, estimated start time,
+ DRAO sidereal day, commanded duration, and estimated finish time
+
+ Parameters
+ ----------
+ post_report_file : str
+ path to the .POST_REPORT file to read
+
+ Returns
+ -------
+ output_params : dictionary
+ output_params['src'] : HolographySource object or string
+ If the source is a known source in the holography database,
+ return the HolographySource object. If not, return the name
+ of the source as a string
+ output_params['SID'] : int
+ DRAO sidereal day at the beginning of the observation
+ output_params['start_time'] : skyfield time object
+ UTC time at the beginning of the observation
+ output_params['DURATION'] : float
+ Commanded duration of the observation in sidereal hours
+ output_params['finish_time'] : skyfield time object
+ Calculated UTC time at the end of the observation
+ Calculated as start_time + duration * ephemeris.SIDEREAL_S
+
+ """
+ importre
+
+ ts=ephemeris.skyfield_wrapper.timescale
+
+ output_params={}
+
+ withopen(post_report_file,"r")asf:
+ lines=[lineforlineinf]
+ forlinlines:
+ if(l.find("Source"))!=-1:
+ srcnm=re.search("Source:\s+(.*?)\s+",l).group(1)
+ ifsrcnmincls.source_alias:
+ srcnm=cls.source_alias[srcnm]
+ if(l.find("DURATION"))!=-1:
+ output_params["DURATION"]=float(
+ re.search("DURATION:\s+(.*?)\s+",l).group(1)
+ )
+
+ # convert Julian Date to Skyfield time object
+ if(l.find("JULIAN DATE"))!=-1:
+ output_params["start_time"]=ts.ut1(
+ jd=float(re.search("JULIAN DATE:\s+(.*?)\s+",l).group(1))
+ )
+
+ ifl.find("SID:")!=-1:
+ output_params["SID"]=int(re.search("SID:\s(.*?)\s+",l).group(1))
+ try:
+ output_params["src"]=HolographySource.get(name=srcnm)
+ exceptpw.DoesNotExist:
+ print("Missing",srcnm)
+ output_params["src"]=srcnm
+
+ output_params["finish_time"]=ephemeris.unix_to_skyfield_time(
+ ephemeris.ensure_unix(output_params["start_time"])
+ +output_params["DURATION"]*3600.0*ephemeris.SIDEREAL_S
+ )
+
+ output_params["quality_flag"]=QUALITY_GOOD
+
+ returnoutput_params
+
+
+
+[docs]
+ @classmethod
+ defcreate_from_ant_logs(
+ cls,
+ logs,
+ verbose=False,
+ onsource_dist=0.1,
+ notes=None,
+ quality_flag=0,
+ **kwargs,
+ ):
+"""
+ Read John Galt Telescope log files and create an entry in the
+ holography database corresponding to the exact times on source
+
+ Parameters
+ ----------
+ logs : list of strings
+ log file archives (.zip files) to pass to parse_ant_logs()
+ onsource_dist : float (default: 0.1)
+ maximum angular distance at which to consider the Galt Telescope
+ on source (in degrees)
+
+ Returns
+ -------
+ none
+ """
+
+ fromch_util.ephemerisimportsphdist
+ fromskyfield.positionlibimportAngle
+
+ ts=ephemeris.skyfield_wrapper.timescale
+ DATE_FMT_STR="%Y-%m-%d %H:%M:%S %z"
+
+ pr_list,al_list=cls.parse_ant_logs(logs,return_post_report_params=True)
+
+ forpost_report_params,ant_log,curloginzip(pr_list,al_list,logs):
+ print(" ")
+ ifisinstance(post_report_params["src"],HolographySource):
+ ifverbose:
+ print(
+ "Processing {} from {}".format(
+ post_report_params["src"].name,curlog
+ )
+ )
+ dist=sphdist(
+ Angle(degrees=post_report_params["src"].ra),
+ Angle(degrees=post_report_params["src"].dec),
+ ant_log["ra"],
+ ant_log["dec"],
+ )
+ ifverbose:
+ print("onsource_dist = {:.2f} deg".format(onsource_dist))
+ onsource=np.where(dist.degrees<onsource_dist)[0]
+
+ iflen(onsource)>0:
+ stdoffset=np.std(dist.degrees[onsource[0]:onsource[-1]])
+ meanoffset=np.mean(dist.degrees[onsource[0]:onsource[-1]])
+ obs={
+ "src":post_report_params["src"],
+ "start_time":ant_log["t"][onsource[0]],
+ "finish_time":ant_log["t"][onsource[-1]],
+ "quality_flag":QUALITY_GOOD,
+ }
+ noteout="from .ANT log "+ts.now().utc_strftime(DATE_FMT_STR)
+ ifnotesisnotNone:
+ noteout=notes+" "+noteout
+ ifstdoffset>0.05ormeanoffset>ONSOURCE_DIST_TO_FLAG:
+ obs["quality_flag"]+=QUALITY_OFFSOURCE
+ print(
+ (
+ "Mean offset: {:.4f}. Std offset: {:.4f}. "
+ "Setting quality flag to {}."
+ ).format(meanoffset,stdoffset,QUALITY_OFFSOURCE)
+ )
+ noteout=(
+ "Questionable on source. Mean, STD(offset) : "
+ "{:.3f}, {:.3f}. {}".format(meanoffset,stdoffset,noteout)
+ )
+ obs["quality_flag"]|=quality_flag
+ ifverbose:
+ print(
+ "Times in .ANT log : {}{}".format(
+ ant_log["t"][onsource[0]].utc_strftime(DATE_FMT_STR),
+ ant_log["t"][onsource[-1]].utc_strftime(DATE_FMT_STR),
+ )
+ )
+ print(
+ "Times in .POST_REPORT: {}{}".format(
+ post_report_params["start_time"].utc_strftime(
+ DATE_FMT_STR
+ ),
+ post_report_params["finish_time"].utc_strftime(
+ DATE_FMT_STR
+ ),
+ )
+ )
+ print(
+ "Mean offset: {:.4f}. Std offset: {:.4f}.".format(
+ meanoffset,stdoffset
+ )
+ )
+
+ cls.create_from_dict(obs,verbose=verbose,notes=noteout,**kwargs)
+ else:
+ print(
+ (
+ "No on source time found for {}\n{}{}\n"
+ "Min distance from source {:.1f} degrees"
+ ).format(
+ curlog,
+ post_report_params["src"].name,
+ post_report_params["start_time"].utc_strftime(
+ "%Y-%m-%d %H:%M"
+ ),
+ np.min(dist.degrees),
+ )
+ )
+ else:
+ print(
+ "{} is not a HolographySource; need to add to database?".format(
+ post_report_params["src"]
+ )
+ )
+ print("Doing nothing")
+
+
+
+[docs]
+ @classmethod
+ defcreate_from_dict(
+ cls,
+ dict,
+ notes=None,
+ start_tol=60.0,
+ dryrun=True,
+ replace_dup=False,
+ verbose=False,
+ ):
+"""
+ Create a holography database entry from a dictionary
+
+ This routine checks for duplicates and overwrites duplicates if and
+ only if `replace_dup = True`
+
+ Parameters
+ ----------
+ dict : dict
+ src : :py:class:`HolographySource`
+ A HolographySource object for the source
+ start_time
+ Start time as a Skyfield Time object
+ finish_time
+ Finish time as a Skyfield Time object
+ """
+ DATE_FMT_STR="%Y-%m-%d %H:%M:%S %Z"
+
+ defcheck_for_duplicates(t,src,start_tol,ignore_src_mismatch=False):
+"""
+ Check for duplicate holography observations, comparing the given
+ observation to the existing database
+
+ Inputs
+ ------
+ t: Skyfield Time object
+ beginning time of observation
+ src: HolographySource
+ target source
+ start_tol: float
+ Tolerance in seconds within which to search for duplicates
+ ignore_src_mismatch: bool (default: False)
+ If True, consider observations a match if the time matches
+ but the source does not
+
+ Outputs
+ -------
+ If a duplicate is found: :py:class:`HolographyObservation` object for the
+ existing entry in the database
+
+ If no duplicate is found: None
+ """
+ ts=ephemeris.skyfield_wrapper.timescale
+
+ unixt=ephemeris.ensure_unix(t)
+
+ dup_found=False
+
+ existing_db_entry=cls.select().where(
+ cls.start_time.between(unixt-start_tol,unixt+start_tol)
+ )
+ iflen(existing_db_entry)>0:
+ iflen(existing_db_entry)>1:
+ print("Multiple entries found.")
+ forentryinexisting_db_entry:
+ tt=ts.utc(ephemeris.unix_to_datetime(entry.start_time))
+ # LST = GST + east longitude
+ ttlst=np.mod(tt.gmst+DRAO_lon,24.0)
+
+ # Check if source name matches. If not, print a warning
+ # but proceed anyway.
+ ifsrc.name.upper()==entry.source.name.upper():
+ dup_found=True
+ ifverbose:
+ print("Observation is already in database.")
+ else:
+ ifignore_src_mismatch:
+ dup_found=True
+ print(
+ "** Observation at same time but with different "
+ +"sources in database: ",
+ src.name,
+ entry.source.name,
+ tt.utc_datetime().isoformat(),
+ )
+ # if the observations match in start time and source,
+ # call them the same observation. Not the most strict
+ # check possible.
+
+ ifdup_found:
+ tf=ts.utc(ephemeris.unix_to_datetime(entry.finish_time))
+ print(
+ "Tried to add : {}{}; LST={:.3f}".format(
+ src.name,t.utc_datetime().strftime(DATE_FMT_STR),ttlst
+ )
+ )
+ print(
+ "Existing entry: {}{}; LST={:.3f}".format(
+ entry.source.name,
+ tt.utc_datetime().strftime(DATE_FMT_STR),
+ ttlst,
+ )
+ )
+ ifdup_found:
+ returnexisting_db_entry
+ else:
+ returnNone
+
+ # DRAO longitude in hours
+ DRAO_lon=ephemeris.chime.longitude*24.0/360.0
+
+ ifverbose:
+ print(" ")
+ addtodb=True
+
+ dup_entries=check_for_duplicates(dict["start_time"],dict["src"],start_tol)
+
+ ifdup_entriesisnotNone:
+ ifreplace_dup:
+ ifnotdryrun:
+ forentryindup_entries:
+ cls.delete_instance(entry)
+ ifverbose:
+ print("Deleted observation from database and replacing.")
+ elifverbose:
+ print("Would have deleted observation and replaced (dry run).")
+ addtodb=True
+ else:
+ addtodb=False
+ forentryindup_entries:
+ print(
+ "Not replacing duplicate {} observation {}".format(
+ entry.source.name,
+ ephemeris.unix_to_datetime(entry.start_time).strftime(
+ DATE_FMT_STR
+ ),
+ )
+ )
+
+ # we've appended this observation to obslist.
+ # Now add to the database, if we're supposed to.
+ ifaddtodb:
+ string="Adding to database: {}{} to {}"
+ print(
+ string.format(
+ dict["src"].name,
+ dict["start_time"].utc_datetime().strftime(DATE_FMT_STR),
+ dict["finish_time"].utc_datetime().strftime(DATE_FMT_STR),
+ )
+ )
+ ifdryrun:
+ print("Dry run; doing nothing")
+ else:
+ cls.create(
+ source=dict["src"],
+ start_time=ephemeris.ensure_unix(dict["start_time"]),
+ finish_time=ephemeris.ensure_unix(dict["finish_time"]),
+ quality_flag=dict["quality_flag"],
+ notes=notes,
+ )
+
+
+
+[docs]
+ @classmethod
+ defparse_ant_logs(cls,logs,return_post_report_params=False):
+"""
+ Unzip and parse .ANT log file output by nsched for John Galt Telescope
+ observations
+
+ Parameters
+ ----------
+ logs : list of strings
+ .ZIP filenames. Each .ZIP archive should include a .ANT file and
+ a .POST_REPORT file. This method unzips the archive, uses
+ `parse_post_report` to read the .POST_REPORT file and extract
+ the CHIME sidereal day corresponding to the DRAO sidereal day,
+ and then reads the lines in the .ANT file to obtain the pointing
+ history of the Galt Telescope during this observation.
+
+ (The DRAO sidereal day is days since the clock in Ev Sheehan's
+ office at DRAO was reset. This clock is typically only reset every
+ few years, but it does not correspond to any defined date, so the
+ date must be figured out from the .POST_REPORT file, which reports
+ both the DRAO sidereal day and the UTC date and time.
+
+ Known reset dates: 2017-11-21, 2019-3-10)
+
+ Returns
+ -------
+
+ if output_params == False:
+ ant_data: A dictionary consisting of lists containing the LST,
+ hour angle, RA, and dec (all as Skyfield Angle objects),
+ CHIME sidereal day, and DRAO sidereal day.
+
+ if output_params == True
+ output_params: dictionary returned by `parse_post_report`
+ and
+ ant_data: described above
+
+ Files
+ -----
+ the .ANT and .POST_REPORT files in the input .zip archive are
+ extracted into /tmp/26mlog/<loginname>/
+ """
+
+ fromskyfield.positionlibimportAngle
+ fromcaputimporttimeasctime
+
+ DRAO_lon=ephemeris.CHIMELONGITUDE*24.0/360.0
+
+ defsidlst_to_csd(sid,lst,sid_ref,t_ref):
+"""
+ Convert an integer DRAO sidereal day and LST to a float
+ CHIME sidereal day
+
+ Parameters
+ ----------
+ sid : int
+ DRAO sidereal day
+ lst : float, in hours
+ local sidereal time
+ sid_ref : int
+ DRAO sidereal day at the reference time t_ref
+ t_ref : skyfield time object, Julian days
+ Reference time
+
+ Returns
+ -------
+ output : float
+ CHIME sidereal day
+ """
+ csd_ref=int(
+ ephemeris.csd(ephemeris.datetime_to_unix(t_ref.utc_datetime()))
+ )
+ csd=sid-sid_ref+csd_ref
+ returncsd+lst/ephemeris.SIDEREAL_S/24.0
+
+ ant_data_list=[]
+ post_report_list=[]
+
+ forloginlogs:
+ doobs=True
+
+ filename=log.split("/")[-1]
+ basedir="/tmp/26mlog/{}/".format(os.getlogin())
+ basename,extension=filename.split(".")
+ post_report_file=basename+".POST_REPORT"
+ ant_file=basename+".ANT"
+
+ ifextension=="zip":
+ try:
+ zipfile.ZipFile(log).extract(post_report_file,path=basedir)
+ except:
+ print(
+ "Failed to extract {} into {}. Moving right along...".format(
+ post_report_file,basedir
+ )
+ )
+ doobs=False
+ try:
+ zipfile.ZipFile(log).extract(ant_file,path=basedir)
+ except:
+ print(
+ "Failed to extract {} into {}. Moving right along...".format(
+ ant_file,basedir
+ )
+ )
+ doobs=False
+
+ ifdoobs:
+ try:
+ post_report_params=cls.parse_post_report(
+ basedir+post_report_file
+ )
+
+ withopen(os.path.join(basedir,ant_file),"r")asf:
+ lines=[lineforlineinf]
+ ant_data={"sid":np.array([])}
+ lsth=[]
+ lstm=[]
+ lsts=[]
+
+ hah=[]
+ ham=[]
+ has=[]
+
+ decd=[]
+ decm=[]
+ decs=[]
+
+ forlinlines:
+ arr=l.split()
+
+ try:
+ lst_hms=[float(x)forxinarr[2].split(":")]
+
+ # do last element first: if this is going to
+ # crash because a line in the log is incomplete,
+ # we don't want it to append to any of the lists
+
+ decs.append(float(arr[8].replace('"',"")))
+ decm.append(float(arr[7].replace("'","")))
+ decd.append(float(arr[6].replace("D","")))
+
+ has.append(float(arr[5].replace("S","")))
+ ham.append(float(arr[4].replace("M","")))
+ hah.append(float(arr[3].replace("H","")))
+
+ lsts.append(float(lst_hms[2]))
+ lstm.append(float(lst_hms[1]))
+ lsth.append(float(lst_hms[0]))
+
+ ant_data["sid"]=np.append(
+ ant_data["sid"],int(arr[1])
+ )
+ except:
+ print(
+ "Failed in file {} for line \n{}".format(
+ ant_file,l
+ )
+ )
+ iflen(ant_data["sid"])!=len(decs):
+ print("WARNING: mismatch in list lengths.")
+
+ ant_data["lst"]=Angle(hours=(lsth,lstm,lsts))
+
+ ha=Angle(hours=(hah,ham,has))
+ dec=Angle(degrees=(decd,decm,decs))
+
+ ant_data["ha"]=Angle(
+ radians=ha.radians
+ -ephemeris.galt_pointing_model_ha(ha,dec).radians,
+ preference="hours",
+ )
+
+ ant_data["dec_cirs"]=Angle(
+ radians=dec.radians
+ -ephemeris.galt_pointing_model_dec(ha,dec).radians,
+ preference="degrees",
+ )
+
+ ant_data["csd"]=sidlst_to_csd(
+ np.array(ant_data["sid"]),
+ ant_data["lst"].hours,
+ post_report_params["SID"],
+ post_report_params["start_time"],
+ )
+
+ ant_data["t"]=ephemeris.unix_to_skyfield_time(
+ ephemeris.csd_to_unix(ant_data["csd"])
+ )
+
+ # Correct RA from equinox to CIRS coords (both in radians)
+ era=np.radians(
+ ctime.unix_to_era(ephemeris.ensure_unix(ant_data["t"]))
+ )
+ gast=ant_data["t"].gast*2*np.pi/24.0
+
+ ant_data["ra_cirs"]=Angle(
+ radians=ant_data["lst"].radians
+ -ant_data["ha"].radians
+ +(era-gast),
+ preference="hours",
+ )
+
+ obs=ephemeris.Star_cirs(
+ ra=ant_data["ra_cirs"],
+ dec=ant_data["dec_cirs"],
+ epoch=ant_data["t"],
+ )
+
+ ant_data["ra"]=obs.ra
+ ant_data["dec"]=obs.dec
+
+ ant_data_list.append(ant_data)
+ post_report_list.append(post_report_params)
+ except:
+ print("Parsing {} failed".format(post_report_file))
+
+ ifreturn_post_report_params:
+ returnpost_report_list,ant_data_list
+ returnant_data
+
+
+
+[docs]
+ @classmethod
+ defcreate_from_post_reports(
+ cls,
+ logs,
+ start_tol=60.0,
+ dryrun=True,
+ replace_dup=False,
+ verbose=True,
+ notes=None,
+ ):
+"""Create holography database entry from .POST_REPORT log files
+ generated by the nsched controller for the Galt Telescope.
+
+ Parameters
+ ----------
+ logs : string
+ list of paths to archives. Filenames should be, eg,
+ 01DEC17_1814.zip. Must be only one period in the filename,
+ separating the extension.
+
+ start_tol : float (optional; default: 60.)
+ Tolerance (in seconds) around which to search for duplicate
+ operations.
+
+ dryrun : boolean (optional; default: True)
+ Dry run only; do not add entries to database
+
+ replace_dup : boolean (optional; default: False)
+ Delete existing duplicate entries and replace. Only has effect if
+ dry_run == False
+
+ notes : string or list of strings (optional; default: None)
+ notes to be added. If a string, the same note will be added to all
+ observations. If a list of strings (must be same length as logs),
+ each element of the list will be added to the corresponding
+ database entry.
+ Nota bene: the text "Added by create_from_post_reports" with the
+ current date and time will also be included in the notes database
+ entry.
+
+ Example
+ -------
+ from ch_util import holography as hl
+ import glob
+
+ obs = hl.HolographyObservation
+ logs = glob.glob('/path/to/logs/*JUN18*.zip')
+ obs_list, dup_obs_list, missing = obs.create_from_post_reports(logs, dryrun=False)
+ """
+ # check notes. Can be a string (in which case duplicate it), None (in
+ # which case do nothing) or a list (in which case use it if same length
+ # as logs, otherwise crash)
+ ifnotesisNone:
+ print("Notes is None")
+ notesarr=[None]*len(logs)
+ elifisinstance(notes,str):
+ notesarr=[notes]*len(logs)
+ else:
+ assertlen(notes)==len(
+ logs
+ ),"notes must be a string or a list the same length as logs"
+ notesarr=notes
+
+ forlog,noteinzip(logs,notesarr):
+ ifverbose:
+ print("Working on {}".format(log))
+ filename=log.split("/")[-1]
+ # basedir = '/'.join(log.split('/')[:-1]) + '/'
+ basedir="/tmp/"
+
+ basename,extension=filename.split(".")
+
+ post_report_file=basename+".POST_REPORT"
+
+ doobs=True
+ ifextension=="zip":
+ try:
+ zipfile.ZipFile(log).extract(post_report_file,path=basedir)
+ exceptException:
+ print(
+ "failed to find {}. Moving right along...".format(
+ post_report_file
+ )
+ )
+ doobs=False
+ elifextension!="POST_REPORT":
+ print(
+ "WARNING: extension should be .zip or .POST_REPORT; is ",extension
+ )
+
+ ifdoobs:
+ # Read the post report file and pull out the HolographySource
+ # object, start time (LST), and duration (in LST hours) of the
+ # observation
+ output_params=cls.parse_post_report(basedir+post_report_file)
+ t=output_params["start_time"]
+ src=output_params["src"]
+
+ # if the source was found, src would be a HolographySource
+ # object otherwise (ie the source is missing), it's a string
+ ifisinstance(src,str):
+ warnings.warn(
+ f"Source {src} was not found for observation at time {t}."
+ )
+ else:
+ cls.create_from_dict(
+ output_params,
+ notes=notes,
+ start_tol=start_tol,
+ dryrun=dryrun,
+ replace_dup=replace_dup,
+ verbose=verbose,
+ )
+"""
+Interface to the CHIME components and graphs
+
+This module interfaces to the layout tables in the CHIME database.
+
+The :mod:`peewee` module is used for the ORM to the MySQL database. Because the
+layouts are event-driven, you should never attempt to enter events by raw
+inserts to the :class:`event` or :class:`timestamp` tables, as you could create
+inconsistencies. Rather, use the methods which are described in this document to
+do such alterations robustly.
+
+For most uses, you probably want to import the following:
+
+>>> from datetime import datetime
+>>> import logging
+>>> logging.basicConfig(level = logging.INFO)
+>>> import peewee
+>>> import layout
+>>> layout.connect_database()
+
+.. note::
+ The database must now be explicitly connected. This should not be done within
+ an import statement.
+
+.. note::
+ The :mod:`logging` module can be set to the level of your preference, or not
+ imported altogether if you don't want log messages from the :mod:`layout`
+ module. Note that the :mod:`peewee` module sends a lot of messages to the
+ DEBUG stream.
+
+If you will be altering the layouts, you will need to register as a user:
+
+>>> layout.set_user("Ahincks")
+
+Use your CHIME wiki username here. Make sure it starts with a capital letter.
+Note that different users have different permissions, stored in the
+:class:`user_permission` table. If you are simply reading from the layout,
+there is no need to register as a user.
+
+Choose Your Own Adventure
+=========================
+
+============================================= ==================================
+If you want to ... ... then see
+============================================= ==================================
+retrieve and examine layout graphs :class:`graph`
+add components :meth:`component.add<ch_util._db_tables.component.add>`,
+ :func:`add_component<ch_util._db_tables.add_component>`
+remove components :meth:`component.remove<ch_util._db_tables.component.remove>`,
+ :func:`remove_component<ch_util._db_tables.remove_component>`
+make connexions :func:`make_connexion<ch_util._db_tables.make_connexion>`
+sever connexions :func:`sever_connexion<ch_util._db_tables.sever_connexion>`
+set component properties :meth:`component.set_property<ch_util._db_tables.component.set_property>`
+ :func:`set_property<ch_util._db_tables.set_property>`
+get component properties :meth:`component.get_property<ch_util._db_tables.component.get_property>`
+make/sever many connexions and set many :func:`enter_ltf`
+component properties at the same time
+add component history notes :meth:`component.add_history<ch_util._db_tables.component.add_history>`
+add link to component documentation :meth:`component.add_doc<ch_util._db_tables.component.add_doc>`
+create a global flag :meth:`global_flag.start<ch_util._db_tables.global_flag.start>`
+============================================= ==================================
+
+Functions
+=========
+
+- :py:meth:`add_component<ch_util._db_tables.add_component>`
+- :py:meth:`compare_connexion<ch_util._db_tables.compare_connexion>`
+- :py:meth:`connect_database<ch_util._db_tables.connect_peewee_tables>`
+- :py:meth:`enter_ltf`
+- :py:meth:`make_connexion<ch_util._db_tables.make_connexion>`
+- :py:meth:`remove_component<ch_util._db_tables.remove_component>`
+- :py:meth:`set_user<ch_util._db_tables.set_user>`
+- :py:meth:`sever_connexion<ch_util._db_tables.sever_connexion>`
+- :py:meth:`global_flags_between`
+- :py:meth:`get_global_flag_times`
+
+Classes
+=======
+
+- :py:class:`subgraph_spec`
+- :py:class:`graph`
+
+Database Models
+===============
+
+- :py:class:`component<ch_util._db_tables.component>`
+- :py:class:`component_history<ch_util._db_tables.component_history>`
+- :py:class:`component_type<ch_util._db_tables.component_type>`
+- :py:class:`component_type_rev<ch_util._db_tables.component_type_rev>`
+- :py:class:`component_doc<ch_util._db_tables.component_doc>`
+- :py:class:`connexion<ch_util._db_tables.connexion>`
+- :py:class:`external_repo<ch_util._db_tables.external_repo>`
+- :py:class:`event<ch_util._db_tables.event>`
+- :py:class:`event_type<ch_util._db_tables.event_type>`
+- :py:class:`graph_obj<ch_util._db_tables.graph_obj>`
+- :py:class:`global_flag<ch_util._db_tables.global_flag>`
+- :py:class:`predef_subgraph_spec<ch_util._db_tables.predef_subgraph_spec>`
+- :py:class:`predef_subgraph_spec_param<ch_util._db_tables.predef_subgraph_spec_param>`
+- :py:class:`property<ch_util._db_tables.property>`
+- :py:class:`property_component<ch_util._db_tables.property_component>`
+- :py:class:`property_type<ch_util._db_tables.property_type>`
+- :py:class:`timestamp<ch_util._db_tables.timestamp>`
+- :py:class:`user_permission<ch_util._db_tables.user_permission>`
+- :py:class:`user_permission_type<ch_util._db_tables.user_permission_type>`
+
+Exceptions
+==========
+
+- :py:class:`NoSubgraph<ch_util._db_tables.NoSubgraph>`
+- :py:class:`BadSubgraph<ch_util._db_tables.BadSubgraph>`
+- :py:class:`DoesNotExist<ch_util._db_tables.DoesNotExist>`
+- :py:class:`UnknownUser<ch_util._db_tables.UnknownUser>`
+- :py:class:`NoPermission<ch_util._db_tables.NoPermission>`
+- :py:class:`LayoutIntegrity<ch_util._db_tables.LayoutIntegrity>`
+- :py:class:`PropertyType<ch_util._db_tables.PropertyType>`
+- :py:class:`PropertyUnchanged<ch_util._db_tables.PropertyUnchanged>`
+- :py:class:`ClosestDraw<ch_util._db_tables.ClosestDraw>`
+- :py:class:`NotFound<chimedb.core.NotFoundError>`
+
+Constants
+=========
+
+- :py:const:`EVENT_AT`
+- :py:const:`EVENT_BEFORE`
+- :py:const:`EVENT_AFTER`
+- :py:const:`EVENT_ALL`
+- :py:const:`ORDER_ASC`
+- :py:const:`ORDER_DESC`
+"""
+
+importdatetime
+importinspect
+importlogging
+importnetworkxasnx
+importos
+importpeeweeaspw
+importre
+
+importchimedb.core
+
+_property=property# Do this since there is a class "property" in _db_tables.
+from._db_tablesimport(
+ EVENT_AT,
+ EVENT_BEFORE,
+ EVENT_AFTER,
+ EVENT_ALL,
+ ORDER_ASC,
+ ORDER_DESC,
+ _check_fail,
+ _plural,
+ _are,
+ AlreadyExists,
+ NoSubgraph,
+ BadSubgraph,
+ DoesNotExist,
+ UnknownUser,
+ NoPermission,
+ LayoutIntegrity,
+ PropertyType,
+ PropertyUnchanged,
+ ClosestDraw,
+ set_user,
+ graph_obj,
+ global_flag_category,
+ global_flag,
+ component_type,
+ component_type_rev,
+ external_repo,
+ component,
+ component_history,
+ component_doc,
+ connexion,
+ property_type,
+ property_component,
+ property,
+ event_type,
+ timestamp,
+ event,
+ predef_subgraph_spec,
+ predef_subgraph_spec_param,
+ user_permission_type,
+ user_permission,
+ compare_connexion,
+ add_component,
+ remove_component,
+ set_property,
+ make_connexion,
+ sever_connexion,
+)
+
+# Legacy name
+fromchimedb.coreimportNotFoundErrorasNotFound
+
+os.environ["TZ"]="UTC"
+
+# Logging
+# =======
+
+# Set default logging handler to avoid "No handlers could be found for logger
+# 'layout'" warnings.
+fromloggingimportNullHandler
+
+
+# All peewee-generated logs are logged to this namespace.
+logger=logging.getLogger("layout")
+logger.addHandler(NullHandler())
+
+
+# Layout!
+# =======
+
+
+
+[docs]
+classsubgraph_spec(object):
+"""Specifications for extracting a subgraph from a full graph.
+
+ The subgraph specification can be created from scratch by passing the
+ appropriate parameters. They can also be pulled from the database using the
+ class method :meth:`FROM_PREDef`.
+
+ The parameters can be passed as ID's, names of compoenet types or
+ :obj:`component_type` instances.
+
+ Parameters
+ ----------
+ start : integer, :obj:`component_type` or string
+ The component type for the start of the subgraph.
+ terminate : list of integers, of :obj:`component_type` or of strings
+ Component type id's for terminating the subgraph.
+ oneway : list of list of integer pairs, of :obj:`component_type` or of strings
+ Pairs of component types for defining connexions that should only be
+ traced one way when moving from the starting to terminating components.
+ hide : list of integers, of :obj:`component_type` or of strings
+ Component types for components that should be hidden and skipped over in
+ the subgraph.
+
+ Examples
+ --------
+ To look at subgraphs of components between the outer bulkhead and the
+ correlator inputs, one could create the following specification:
+
+ >>> import layout
+ >>> from datetime import datetime
+ >>> sg_spec = layout.subgraph_spec(start = "c-can thru",
+ terminate = ["correlator input", "60m coax"],
+ oneway = [],
+ hide = ["60m coax", "SMA coax"])
+
+ What did we do? We specified that the subgraph starts at the C-Can bulkhead.
+ It terminates at the correlator input; in the other direction, it must also
+ terminate at a 60 m coaxial cable plugged into the bulkhead. We hide the 60 m
+ coaxial cable so that it doesn't show up in the subgraph. We also hide the SMA
+ cables so that they will be skipped over.
+
+ We can load all such subgraphs from the database now and see how many nodes
+ they contain:
+
+ >>> sg = layout.graph.from_db(datetime(2014, 10, 5, 12, 0), sg_spec)
+ print [s.order() for s in sg]
+ [903, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 903,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 903, 3, 3, 3, 903, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 903, 3, 1, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 903, 3, 903, 3, 3, 3, 3, 3, 3, 3, 903, 903, 5, 5]
+
+ Most of them are as short as we would expect, but there are some
+ complications. Let's look at that first one by printing out its LTF:
+
+ >>> print sg[0].ltf
+ # C-can thru to RFT thru.
+ CANAD0B
+ RFTA15B attenuation=10 therm_avail=ch7
+ <BLANKLINE>
+ # RFT thru to HK preamp.
+ RFTA15B attenuation=10 therm_avail=ch7
+ CHB036C7
+ HPA0002A
+ <BLANKLINE>
+ # HK preamp to HK readout.
+ HPA0002A
+ ATMEGA49704949575721220150
+ HKR00
+ <BLANKLINE>
+ # HK readout to HK ATMega.
+ HKR00
+ ATMEGA50874956504915100100
+ etc...
+ etc...
+ # RFT thru to FLA.
+ RFTA15B attenuation=10 therm_avail=ch7
+ FLA0159B
+
+ Some FLA's are connected to HK hydra cables and we need to terminate on these
+ as well. It turns out that some outer bulkheads are connected to 200 m
+ coaxial cables, and some FLA's are connected to 50 m delay cables, adding to
+ the list of terminations. Let's exclude these as well:
+
+ >>> sg_spec.terminate += ["200m coax", "HK hydra", "50m coax"]
+ >>> sg_spec.hide += ["200m coax", "HK hydra", "50m coax"]
+ >>> sg = layout.graph.from_db(datetime(2014, 10, 5, 12, 0), sg_spec)
+ >>> print [s.order() for s in sg]
+ [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 10, 10, 5, 5]
+
+ The remaining subgraphs with more than three components actually turn out to
+ be errors in the layout! Let's investigate the last one by removing any hidden
+ components and printing its LTF.
+
+ >>> sn = sg[-1].component(type = "C-can thru")[0].sn
+ CANBL1B
+ >>> sg_spec.hide = []
+ >>> bad_sg = layout.graph.from_db(datetime(2014, 10, 5, 12, 0), sg_spec, sn)
+ >>> print bad_sg.ltf()
+ # C-can thru to c-can thru.
+ CANBL1B
+ CXS0017
+ RFTQ00B
+ CXS0016
+ FLA0073B
+ RFTQ01B attenuation=9
+ CXS0015
+ CANBL0B
+
+ It appears that :code:`CXS0016` mistakenly connects :code:`RFTQ00B` to
+ :code:`FLA0073B`. This is an error that should be investigated and fixed. But
+ by way of illustration, let's cut this subgraph short by specifying a one-way
+ connection, and not allowing the subgrapher to trace backwards from the inner
+ bulkhead to an SMA cable:
+
+ >>> sg_spec.oneway = [["SMA coax", "RFT thru"]]
+ >>> bad_sg = layout.graph.from_db(datetime(2014, 10, 5, 12, 0), sg_spec, sn)
+ >>> print bad_sg.ltf()
+ # C-can thru to RFT thru.
+ CANBL1B
+ CXS0017
+ RFTQ00B
+ """
+
+ def__init__(self,start,terminate,oneway,hide):
+ self.start=start
+ self.terminate=terminate
+ self.oneway=oneway
+ self.hide=hide
+
+
+[docs]
+ @classmethod
+ deffrom_predef(cls,predef):
+"""Create a subgraph specification from a predefined version in the DB.
+
+ Parameters
+ ----------
+ predef : :class:`predef_subgraph_spec`
+ A predefined subgraph specification in the DB.
+ """
+ s=predef.start_type.id
+ t=[]
+ o=[]
+ h=[]
+ forparaminpredef_subgraph_spec_param.select(
+ predef_subgraph_spec_param.action,
+ predef_subgraph_spec_param.type1.alias("type1_id"),
+ predef_subgraph_spec_param.type2.alias("type2_id"),
+ ).where(predef_subgraph_spec_param.predef_subgraph_spec==predef):
+ ifparam.action=="T":
+ t.append(param.type1_id)
+ elifparam.action=="O":
+ o.append([param.type1_id,param.type2_id])
+ elifparam.action=="H":
+ h.append(param.type1_id)
+ else:
+ raiseRuntimeError('Unknown subgraph action type "%s".'%param.action)
+ returncls(s,t,o,h)
+
+
+ @_property
+ defstart(self):
+"""The component type ID starting the subgraph."""
+ returnself._start
+
+ @start.setter
+ defstart(self,val):
+ self._start=_id_from_multi(component_type,val)
+
+ @_property
+ defterminate(self):
+"""The component type ID(s) terminating the subgraph."""
+ returnself._terminate
+
+ @terminate.setter
+ defterminate(self,val):
+ self._terminate=[_id_from_multi(component_type,tt)forttinval]
+
+ @_property
+ defoneway(self):
+"""Pairs of component type ID(s) for one-way tracing of the subgraph."""
+ returnself._oneway
+
+ @oneway.setter
+ defoneway(self,val):
+ self._oneway=[
+ [
+ _id_from_multi(component_type,oo[0]),
+ _id_from_multi(component_type,oo[1]),
+ ]
+ forooinval
+ ]
+
+ @_property
+ defhide(self):
+"""The component type ID(s) that are skipped over in the subgraph."""
+ returnself._hide
+
+ @hide.setter
+ defhide(self,val):
+ self._hide=[_id_from_multi(component_type,h)forhinval]
+
+
+
+
+[docs]
+classgraph(nx.Graph):
+"""A graph of connexions.
+
+ This class inherits the
+ `networkx.Graph <http://networkx.github.io/documentation/networkx-1.9.1/>`_
+ class and adds CHIME-specific functionality.
+
+ Use the :meth:`from_db` class method to construct a graph from the database.
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which the graph is valid. Default is now().
+
+ Examples
+ --------
+
+ To load a graph from the database, use the :meth:`from_db` class method:
+
+ >>> from ch_util import graph
+ >>> from datetime import datetime
+ >>> g = layout.graph.from_db(datetime(2014, 10, 5, 12, 0))
+
+ You can now use any of the
+ `networkx.Graph <http://networkx.github.io/documentation/networkx-1.9.1/>`_
+ methods:
+
+ >>> print g.order(), g.size()
+ 2483 2660
+
+ There are some convenience methods for our implementation. For example, you
+ can easily find components by component type:
+
+ >>> print g.component(type = "reflector")
+ [<layout.component object at 0x7fd1b2cda710>, <layout.component object at 0x7fd1b2cda810>, <layout.component object at 0x7fd1b2cfb7d0>]
+
+ Note that the graph nodes are :obj:`component` objects. You can also use the
+ :meth:`component` method to search for components by serial number:
+
+ >>> ant = g.component(comp = "ANT0044B")
+
+ Node properties are stored as per usual for :class:`networkx.Graph` objects:
+
+ >>> print g.nodes[ant]
+ {'_rev_id': 11L, '_type_id': 2L, u'pol1_orient': <layout.property object at 0x7f31ed323fd0>, '_type_name': u'antenna', '_id': 32L, u'pol2_orient': <layout.property object at 0x7f31ed2c8790>, '_rev_name': u'B'}
+
+ Note, however, that there are some internally-used properties (starting with
+ an underscore). The :meth:`node_property` returns a dictionary of properties
+ without these private memebers:
+
+ >>> for p in g.node_property(ant).values():
+ ... print "%s = %s %s" % (p.type.name, p.value, p.type.units if p.type.units else "")
+ pol1_orient = S
+ pol2_orient = E
+
+ To search the graph for the closest component of a given type to a single
+ component, using :meth:`closest_of_type`:
+
+ >>> slt_type = layout.component_type.get(name = "cassette slot")
+ >>> print g.closest_of_type(ant, slt_type).sn
+ CSS004C0
+
+ Use of :meth:`closest_of_type` can be subtle for components separated by long
+ paths. See its documentation for more examples.
+
+ Subgraphs can be created using a subgraph specification, encoded in a
+ :class:`subgraph_spec` object. See the documentation for that class for
+ details, but briefly, this allows one to create a smaller, more manageable
+ graph containing only components and connexions you are interested in. Given a
+ subgraph, the :meth:`ltf` method can be useful.
+ """
+
+ def__init__(self,time=datetime.datetime.now()):
+ # Initialise the graph.
+ nx.Graph.__init__(self)
+ self._time=time
+ self._sg_spec=None
+ self._sg_spec_start=None
+ self._sn_dict=dict()
+ self._ctype_dict=dict()
+
+ # We will cache all the component types, revisions and properties now,
+ # since these will be used constantly by the graph.
+ component_type.fill_cache()
+ component_type_rev.fill_cache()
+ property_type.fill_cache()
+
+ # Aliases.
+ self.neighbours=self.neighbors
+ self.neighbor_of_type=self.neighbour_of_type
+
+
+[docs]
+ @classmethod
+ deffrom_db(cls,time=datetime.datetime.now(),sg_spec=None,sg_start_sn=None):
+"""Create a new graph by reading the database.
+
+ This method is designed to be efficient. It has customised SQL calls so that
+ only a couple of queries are required. Doing this with the standard peewee
+ functionality requires many more calls.
+
+ This method will establish a connection to the database if it doesn't
+ already exist.
+
+ Parameters
+ ----------
+ time : datetime.datetime
+ The time at which the graph is valid. Default is now().
+ sg_spec : :obj:`subgraph_spec`
+ The subgraph specificationto use; can be set to :obj:`None`.
+ sg_start_sn : string
+ If a serial number is specified, then only the subgraph starting with that
+ component will be returned. This parameter is ignored if sg_spec is
+ :obj:`None`.
+
+ Returns
+ -------
+ :obj:`graph`
+ If *sg_spec* is not :obj:`None`, and *sg_start_sn* is not specified, then
+ a list of :obj:`graph` objects is returned instead.
+
+ Raises
+ ------
+ If no graph is found, :exc:`NotFound` is raised.
+ """
+
+ # Initalise the database connections
+ connect_database()
+
+ g=cls(time)
+
+ # Add the connexions.
+ sql=(
+ "SELECT c1.*, c2.*, pt.id "
+ "FROM connexion c "
+ "JOIN component c1 ON c1.sn = c.comp_sn1 "
+ "JOIN event e1 ON e1.graph_obj_id = c1.id "
+ "JOIN timestamp e1t1 ON e1.start_id = e1t1.id "
+ "LEFT JOIN timestamp e1t2 ON e1.end_id = e1t2.id "
+ "JOIN component c2 ON c2.sn = c.comp_sn2 "
+ "JOIN event e2 ON e2.graph_obj_id = c2.id "
+ "JOIN timestamp e2t1 ON e2.start_id = e2t1.id "
+ "LEFT JOIN timestamp e2t2 ON e2.end_id = e2t2.id "
+ "JOIN event e ON e.graph_obj_id = c.id "
+ "JOIN event_type pt ON e.type_id = pt.id "
+ "JOIN timestamp t1 ON e.start_id = t1.id "
+ "LEFT JOIN timestamp t2 ON e.end_id = t2.id "
+ "WHERE e.active = 1 AND e1.type_id = 1 AND e2.type_id = 1 AND "
+ "e1t1.time <= '%s' AND "
+ "(e1.end_id IS NULL OR e1t2.time > '%s') AND "
+ "e2t1.time <= '%s' AND "
+ "(e2.end_id IS NULL OR e2t2.time > '%s') AND "
+ "t1.time <= '%s' AND "
+ "(e.end_id IS NULL OR t2.time > '%s');"
+ %(time,time,time,time,time,time)
+ )
+ # print sql
+ conn_list=chimedb.core.proxy.execute_sql(sql)
+ forrinconn_list:
+ c1=g._ensure_add(r[0],r[1],r[2],r[3])
+ c2=g._ensure_add(r[4],r[5],r[6],r[7])
+ ifr[8]==event_type.perm_connexion().id:
+ perm=True
+ else:
+ perm=False
+ g.add_edge(c1,c2,permanent=perm,hidden=False)
+
+ # Add the properties.
+ sql=(
+ "SELECT p.*, c.*, pt.name "
+ "FROM property p "
+ "JOIN property_type pt ON p.type_id = pt.id "
+ "JOIN component c ON p.comp_sn = c.sn "
+ "JOIN event ce ON ce.graph_obj_id = c.id "
+ "JOIN timestamp ct1 ON ce.start_id = ct1.id "
+ "LEFT JOIN timestamp ct2 ON ce.end_id = ct2.id "
+ "JOIN event e ON e.graph_obj_id = p.id "
+ "JOIN timestamp t1 ON e.start_id = t1.id "
+ "LEFT JOIN timestamp t2 ON e.end_id = t2.id "
+ "WHERE e.active = 1 AND ce.type_id = 1 AND "
+ "ct1.time <= '%s' AND "
+ "(ce.end_id IS NULL OR ct2.time > '%s') AND "
+ "t1.time <= '%s' AND "
+ "(e.end_id IS NULL OR t2.time > '%s');"%(time,time,time,time)
+ )
+ prop_list=chimedb.core.proxy.execute_sql(sql)
+ forrinprop_list:
+ p=property(id=r[0],comp=r[1],type=r[2],value=r[3])
+ p.type=property_type.from_id(r[2])
+ c=g._ensure_add(r[4],r[5],r[6],r[7])
+ g.nodes[c][r[8]]=p
+
+ ifsg_spec:
+ returngraph.from_graph(g,sg_spec,sg_start_sn)
+ else:
+ returng
+
+
+ def_ensure_add(self,id,sn,type,rev):
+"""Robustly add a component, avoiding duplication."""
+ try:
+ c=self.component(comp=sn)
+ exceptNotFound:
+ # Component ID is a foreign key to graph_obj, so we need to make an
+ # instance of this for that.
+ g=graph_obj(id=id)
+ c=component(id=g,sn=sn,type=type,type_rev=rev)
+
+ # We hydrate the component type and revision so that no further queries
+ # need to be made. When the graph was initialised, all of the types and
+ # revisions were cached, so the following requires no further queries.
+ c.type=component_type.from_id(type)
+ c.rev=component_type_rev.from_id(rev)
+ self.add_node(c)
+ self._sn_dict[sn]=c
+ try:
+ self._ctype_dict[type].append(c)
+ exceptKeyError:
+ self._ctype_dict[type]=[c]
+ returnc
+
+
+[docs]
+ defnode_property(self,n):
+"""Return the properties of a node excluding internally used properties.
+
+ If you iterate over a nodes properties, you will also get the
+ internally-used properties (starting with an underscore). This method gets
+ the dictionary of properties without these "private" properties.
+
+ Parameters
+ ----------
+ node : node object
+ The node for which to get the properties.
+
+ Returns
+ -------
+ A dictionary of properties.
+
+ Examples
+ --------
+ >>> from ch_util import graph
+ >>> from datetime import datetime
+ >>> g = layout.graph.from_db(datetime(2014, 10, 5, 12, 0))
+ >>> rft = g.component(comp = "RFTK07B")
+ >>> for p in g.node_property(rft).values():
+ ... print "%s = %s %s" % (p.type.name, p.value, p.type.units if p.type.units else "")
+ attenuation = 10 dB
+ therm_avail = ch1
+ """
+ ret=dict()
+ forkey,valinself.nodes[n].items():
+ ifkey[0]!="_":
+ ret[key]=val
+ returnret
+
+
+
+[docs]
+ defcomponent(self,comp=None,type=None,sort_sn=False):
+"""Return a component or list of components from the graph.
+
+ The components exist as graph nodes. This method provides searchable access
+ to them.
+
+ Parameters
+ ----------
+ comp : string or :obj:`component`
+ If not :obj:`None`, then return the component with this serial number, or
+ :obj:`None` if it does not exist in the graph. If this parameter is set,
+ then **type** is ignored. You can also pass a component object; the
+ instance of that component with the same serial number will be returned if
+ it exists in this graph.
+ type : string or :class:`component_type`
+ If not :obj:`None`, then only return components of this type. You may pass
+ either the name of the component type or an object.
+
+ Returns
+ -------
+ :class:`component` or list of such
+ If the **sn** parameter is passed, a single :class:`component` object is
+ returned. If the **type** parameter is passed, a list of
+ :class:`component` objects is returned.
+
+ Raises
+ ------
+ :exc:`NotFound`
+ Raised if no component is found.
+
+ Examples
+ --------
+ >>> from ch_util import graph
+ >>> from datetime import datetime
+ >>> g = layout.graph.from_db(datetime(2014, 10, 5, 12, 0))
+ >>> print g.component("CXA0005A").type_rev.name
+ B
+ >>> for r in g.component(type = "reflector"):
+ ... print r.sn
+ E_cylinder
+ W_cylinder
+ 26m_dish
+
+ """
+ ifcomp:
+ ret=None
+ try:
+ sn=comp.sn
+ exceptAttributeError:
+ sn=comp
+ try:
+ ret=self._sn_dict[sn]
+ exceptKeyError:
+ raiseNotFound('Serial number "%s" is not in the graph.'%(sn))
+ elifnottype:
+ ret=self.nodes()
+ else:
+ try:
+ type_id=type.id
+ type_name=type.name
+ exceptAttributeError:
+ type_id=component_type.from_name(type).id
+ type_name=type
+ try:
+ ret=list(self._ctype_dict[type_id])
+ ifsort_sn:
+ ret.sort(key=lambdax:x.sn)
+ exceptKeyError:
+ raiseNotFound(
+ 'No components of type "%s" are in the graph.'%type_name
+ )
+ returnret
+[docs]
+ @classmethod
+ deffrom_graph(cls,g,sg_spec=None,sg_start_sn=None):
+"""Find subgraphs within this graph.
+
+ Parameters
+ ----------
+ g : :obj:`graph`
+ The graph from which to get the new graph.
+ sg_spect : :obj:`subgraph_spec`
+ The subgraph specification to use; can be set to :obj:`None`.
+
+ Returns
+ -------
+ A list of :obj:`graph` objects, one for each subgraph found. If, however,
+ *g* is set to :obj:`None`, a reference to the input graph is returned.
+ """
+ ifsg_spec==None:
+ returng
+ ifsg_spec.startinsg_spec.terminate:
+ raiseBadSubgraph(
+ "You cannot terminate on the component type of the "
+ "starting component of your subgraph."
+ )
+ ifsg_spec.startinsg_spec.hide:
+ raiseBadSubgraph(
+ "You cannot hide the component type of the "
+ "starting component of a subgraph."
+ )
+
+ ret=[]
+ forstart_comping.component(type=component_type.from_id(sg_spec.start)):
+ ifsg_start_sn:
+ ifstart_comp.sn!=sg_start_sn:
+ continue
+ ret.append(cls(time=g.time))
+ g._subgraph_recurse(ret[-1],start_comp,sg_spec,[],None)
+ ret[-1]._sg_spec=sg_spec
+ ret[-1]._sg_spec_start=ret[-1].component(comp=start_comp.sn)
+
+ iflen(ret)<1:
+ raiseNotFound("No subgraph was found.")
+ ifsg_start_sn:
+ returnret[-1]
+ else:
+ returnret
+[docs]
+ defltf(self):
+"""Get an LTF representation of the graph. The graph must be a subgraph,
+ i.e., generated with a :obj:`predef_subgraph_spec`.
+
+ Returns
+ -------
+ ltf : string
+ The LTF representation of the graph.
+
+ Raises
+ ------
+ :exc:`NoSubgraph`
+ Raised if no subgraph specification is associate with this layout.
+
+ Examples
+ --------
+ Get the LTF for a subgraph of antenna to HK.
+
+ >>> import layout
+ >>> from datetime import datetime
+ >>> start = layout.component_type.get(name = "antenna").id
+ >>> terminate = [layout.component_type.get(name = "reflector").id,
+ layout.component_type.get(name = "cassette slot").id,
+ layout.component_type.get(name = "correlator input").id,
+ layout.component_type.get(name = "HK preamp").id,
+ layout.component_type.get(name = "HK hydra").id]
+ >>> hide = [layout.component_type.get(name = "reflector").id,
+ layout.component_type.get(name = "cassette slot").id,
+ layout.component_type.get(name = "HK preamp").id,
+ layout.component_type.get(name = "HK hydra").id]
+ >>> sg_spec = layout.subgraph_spec(start, terminate, [], hide)
+ >>> sg = layout.graph.from_db(datetime(2014, 11, 20, 12, 0), sg_spec, "ANT0108B")
+ >>> print sg.ltf()
+ # Antenna to correlator input.
+ ANT0108B pol1_orient=S pol2_orient=E
+ PL0108B1
+ LNA0249B
+ CXA0239C
+ CANBJ6B
+ CXS0042
+ RFTG00B attenuation=10
+ FLA0196B
+ CXS0058
+ K7BP16-00041606
+ <BLANKLINE>
+ # Antenna to correlator input.
+ ANT0108B pol1_orient=S pol2_orient=E
+ PL0108B2
+ LNA0296B
+ CXA0067B
+ CANBG6B
+ CXS0090
+ RFTG01B attenuation=10
+ FLA0269B
+ CXS0266
+ K7BP16-00041506
+ """
+ ifnotself._sg_spec:
+ raiseNoSubgraph(
+ "This layout is not a subgraph. You can only create "
+ "LTF representations of subgraphs generated from "
+ "predef_subgraph_spec objects."
+ )
+ returnself._ltf_recurse(self._sg_spec_start,[],None)
+
+
+
+[docs]
+ defshortest_path_to_type(self,comp,type,type_exclude=None,ignore_draws=True):
+"""Searches for the shortest path to a component of a given type.
+
+ Sometimes the closest component is through a long, convoluted path that you
+ do not wish to explore. You can cut out these cases by including a list of
+ component types that will block the search along a path.
+
+ The component may be passed by object or by serial number; similarly for
+ component types.
+
+ Parameters
+ ----------
+ comp : :obj:`component` or string or list of one of these
+ The component(s) to search from.
+ type : :obj:`component_type` or string
+ The component type to find.
+ type_exclude : list of :obj:`component_type` or strings
+ Any components of this type will prematurely cut off a line of
+ investigation.
+ ignore_draws : boolean
+ It is possible that there be more than one component of a given type the
+ same distance from the starting component. If this parameter is set to
+ :obj:`True`, then just return the first one that is found. If set to
+ :obj:`False`, then raise an exception.
+
+ Returns
+ -------
+ comp: :obj:`component` or list of such
+ The closest component of the given type to **start**. If no path to a
+ component of the specified type exists, return :obj:`None`.
+
+ Raises
+ ------
+ :exc:`ClosestDraw`
+ Raised if there is no unique closest component and **ignore_draws** is set
+ to :obj:`False`.
+
+ Examples
+ --------
+ See the examples for :meth:`closest_of_type`.
+ """
+ # Get the start node and the list of candidate end nodes.
+ one=False
+ ifisinstance(comp,str)orisinstance(comp,component):
+ comp=[comp]
+ one=True
+
+ start_list=[self.component(comp=c)forcincomp]
+
+ # Find end_candidates. If there are none in this graph, return None.
+ try:
+ end_candidate=self.component(type=type)
+ exceptNotFound:
+ returnNoneifoneelse[None]*len(comp)
+
+ ifend_candidateisNone:
+ returnNoneifoneelse[None]*len(comp)
+
+ # Get the list of components to exclude, based on the types in the
+ # **type_exclude** parameter.
+ exclude=[]
+ iftype_excludeisnotNone:
+ ifnotisinstance(type_exclude,list):
+ type_exclude=[type_exclude]
+ fortintype_exclude:
+ try:
+ exclude+=self.component(type=t)
+ exceptNotFound:
+ pass
+
+ # Construct a subgraph without the excluded nodes
+ graph=self.subgraph(set(self.nodes())-set(exclude)).copy()
+
+ # Add a type marking node into the graph connected to all components of
+ # the type we are looking for
+ tn="Type node marker"
+ graph.add_node(tn)
+ edges=[(tn,end)forendinend_candidate]
+ graph.add_edges_from(edges)
+
+ # Get the shortest path to type by searching for the shortest path from
+ # the start to the type marker, the actual path is the same after
+ # removing the type marker
+ shortest=[]
+ forstartinstart_list:
+ try:
+ path=nx.shortest_path(graph,source=start,target=tn)[:-1]
+ except(nx.NetworkXError,nx.NetworkXNoPath):
+ path=None
+
+ shortest.append(path)
+
+ # Return the shortest path (or None if not found)
+ ifone:
+ returnshortest[0]
+ else:
+ returnshortest
+
+
+
+[docs]
+ defclosest_of_type(self,comp,type,type_exclude=None,ignore_draws=True):
+"""Searches for the closest connected component of a given type.
+
+ Sometimes the closest component is through a long, convoluted path that you
+ do not wish to explore. You can cut out these cases by including a list of
+ component types that will block the search along a path.
+
+ The component may be passed by object or by serial number; similarly for
+ component types.
+
+ Parameters
+ ----------
+ comp : :obj:`component` or string or list of such
+ The component to search from.
+ type : :obj:`component_type` or string
+ The component type to find.
+ type_exclude : list of :obj:`component_type` or strings
+ Any components of this type will prematurely cut off a line of
+ investigation.
+ ignore_draws : boolean
+ It is possible that there be more than one component of a given type the
+ same distance from the starting component. If this parameter is set to
+ :obj:`True`, then just return the first one that is found. If set to
+ :obj:`False`, then raise an exception.
+
+ Returns
+ -------
+ comp: :obj:`component` or list of such
+ The closest component of the given type to **start**. If no component of
+ type is found :obj:`None` is returned.
+
+ Raises
+ ------
+ :exc:`ClosestDraw`
+ Raised if there is no unique closest component and **ignore_draws** is set
+ to :obj:`False`.
+
+ Examples
+ --------
+ Find the cassette slot an antenna is plugged into:
+
+ >>> import layout
+ >>> from datetime import datetime
+ >>> g = layout.graph.from_db(datetime(2014, 11, 5, 12, 0))
+ >>> print g.closest_of_type("ANT0044B", "cassette slot").sn
+ CSS004C0
+
+ The example above is simple as the two components are adjacent:
+
+ >>> print [c.sn for c in g.shortest_path_to_type("ANT0044B", "cassette slot")]
+ [u'ANT0044B', u'CSS004C0']
+
+ In general, though, you need to take care when
+ using this method and make judicious use of the **type_exclude** parameter.
+ For example, consider the following example:
+
+ >>> print g.closest_of_type("K7BP16-00040112", "RFT thru").sn
+ RFTB15B
+
+ It seems OK on the surface, but the path it has used is probably not what
+ you want:
+
+ >>> print [c.sn for c in g.shortest_path_to_type("K7BP16-00040112", "RFT thru")]
+ [u'K7BP16-00040112', u'K7BP16-000401', u'K7BP16-00040101', u'FLA0280B', u'RFTB15B']
+
+ We need to block the searcher from going into the correlator card slot and
+ then back out another input, which we can do like so:
+
+ >>> print g.closest_of_type("K7BP16-00040112", "RFT thru", type_exclude = "correlator card slot").sn
+ RFTQ15B
+
+ The reason the first search went through the correlator card slot is because
+ there are delay cables and splitters involved.
+
+ >>> print [c.sn for c in g.shortest_path_to_type("K7BP16-00040112", "RFT thru", type_exclude = "correlator card slot")]
+ [u'K7BP16-00040112', u'CXS0279', u'CXA0018A', u'CXA0139B', u'SPL001AP2', u'SPL001A', u'SPL001AP3', u'CXS0281', u'RFTQ15B']
+
+ The shortest path really was through the correlator card slot, until we
+ explicitly rejected such paths.
+ """
+
+ path=self.shortest_path_to_type(comp,type,type_exclude,ignore_draws)
+
+ try:
+ closest=[p[-1]ifpisnotNoneelseNoneforpinpath]
+ exceptTypeError:
+ closest=path[-1]ifpathisnotNoneelseNone
+ returnclosest
+
+
+
+[docs]
+ defneighbour_of_type(self,n,type):
+"""Get a list of neighbours of a given type.
+
+ This is like the :meth:`networkx.Graph.neighbors` method, but selects only
+ the neighbours of the specified type.
+
+ Parameters
+ ----------
+ comp : :obj:`component`
+ A node in the graph.
+ type : :obj:`component_type` or string
+ The component type to find.
+
+ Returns
+ -------
+ nlist : A list of nodes of type **type** adjacent to **n**.
+
+ Raises
+ ------
+ :exc:`networkx.NetworkXError`
+ Raised if **n** is not in the graph.
+ """
+ ret=[]
+ try:
+ type.name
+ exceptAttributeError:
+ type=component_type.from_name(type)
+ fornninself.neighbours(n):
+ ifnn.type==type:
+ ret.append(nn)
+ returnret
+
+
+ @_property
+ deftime(self):
+"""The time of the graph.
+
+ Returns
+ -------
+ time : datetime.datetime
+ The time at which this graph existed.
+ """
+ returnself._time
+
+ @_property
+ defsg_spec(self):
+"""The :obj:`subgraph_spec` (subgraph specification) used to get this graph.
+
+ Returns
+ -------
+ The :obj:`subgraph_spec` used to get this graph, if any.
+ """
+ returnself._sg_spec
+
+ @_property
+ defsg_spec_start(self):
+"""The subgraph starting component.
+
+ Returns
+ -------
+ The :obj:`component` that was used to begin the subgraph, if any.
+ """
+ returnself._sg_spec_start
+
+
+
+# Private Functions
+# ==================
+
+
+def_add_to_sever(sn1,sn2,sever,fail_comp):
+ ok=True
+ forsnin(sn1,sn2):
+ try:
+ component.get(sn=sn)
+ exceptpw.DoesNotExist:
+ fail_comp.append(sn)
+ ok=False
+ ifok:
+ conn=connexion.from_pair(sn1,sn2)
+ sever.append(conn)
+
+
+def_add_to_chain(chain,sn,prop,sever,fail_comp):
+ ifsn=="//":
+ ifnotlen(chain):
+ raiseSyntaxError("Stray sever mark (//) in LTF.")
+ ifchain[-1]=="//":
+ raiseSyntaxError("Consecutive sever marks (//) in LTF.")
+ chain.append("//")
+ return
+
+ iflen(chain):
+ ifchain[-1]=="//":
+ iflen(chain)<2:
+ raiseSyntaxError(
+ 'Confused about chain ending in "%s". Is the '
+ "first serial number valid?"%(chain[-1])
+ )
+ try:
+ _add_to_sever(chain[-2]["comp"].sn,sn,sever,fail_comp)
+ exceptKeyError:
+ pass
+ delchain[-2]
+ delchain[-1]
+
+ chain.append(dict())
+ try:
+ chain[-1]["comp"]=component.get(sn=sn)
+ forkinrange(len(prop)):
+ iflen(prop[k].split("="))!=2:
+ raiseSyntaxError('Confused by the property command "%s".'%prop[k])
+ chain[-1][prop[k].split("=")[0]]=prop[k].split("=")[1]
+ exceptpw.DoesNotExist:
+ ifnotsninfail_comp:
+ fail_comp.append(sn)
+
+
+def_id_from_multi(cls,o):
+ ifisinstance(o,int):
+ returno
+ elifisinstance(o,cls):
+ returno.id
+ else:
+ returncls.get(name=o).id
+
+
+# Public Functions
+# ================
+
+from._db_tablesimportconnect_peewee_tablesasconnect_database
+
+
+
+[docs]
+defenter_ltf(ltf,time=datetime.datetime.now(),notes=None,force=False):
+"""Enter an LTF into the database.
+
+ This is a special mark-up language for quickly entering events. See the "help"
+ box on the LTF page of the web interface for instructions.
+
+ Parameters
+ ----------
+ ltf : string
+ Pass either the path to a file containing the LTF, or a string containing
+ the LTF.
+ time : datetime.datetime
+ The time at which to apply the LTF.
+ notes : string
+ Notes for the timestamp.
+ force : bool
+ If :obj:`True`, then do nothing when events that would damage database
+ integrity are encountered; skip over them. If :obj:`False`, then a bad
+ propsed event will raise the appropriate exception.
+ """
+
+ try:
+ withopen(ltf,"r")asmyfile:
+ ltf=myfile.readlines()
+ exceptIOError:
+ try:
+ ltf=ltf.splitlines()
+ exceptAttributeError:
+ pass
+ chain=[]
+ fail_comp=[]
+ multi_sn=None
+ multi_prop=None
+ chain.append([])
+ sever=[]
+ i=0
+ forlinltf:
+ iflen(l)andl[0]=="#":
+ continue
+ severed=False
+ try:
+ ifl.split()[1]=="//":
+ severed=True
+ exceptIndexError:
+ pass
+
+ ifnotlen(l)orl.isspace()orseveredorl[0:2]=="$$":
+ ifsevered:
+ _add_to_sever(l.split()[0],l.split()[2],sever,fail_comp)
+ ifmulti_sn:
+ _add_to_chain(chain[i],multi_sn,prop,sever,fail_comp)
+ multi_sn=False
+ chain.append([])
+ i+=1
+ continue
+
+ l=l.replace("\n","")
+ l=l.strip()
+
+ sn=l.split()[0]
+ prop=l.split()[1:]
+
+ # Check to see if this is a multiple-line SN.
+ ifmulti_sn:
+ ifsn[0]=="+":
+ off=len(multi_sn)-len(sn)
+ else:
+ off=0
+ match=False
+ iflen(multi_sn)==len(sn)+off:
+ forjinrange(len(sn)):
+ ifsn[j]!="."andsn[j]!="-"andsn[j]!="+":
+ ifmulti_sn[j+off]=="."ormulti_sn[j+off]=="-":
+ match=True
+ multi_sn=(
+ multi_sn[:j+off]+sn[j]+multi_sn[j+off+1:]
+ )
+ multi_prop=prop
+ ifnotmatch:
+ _add_to_chain(chain[i],multi_sn,multi_prop,sever,fail_comp)
+ _add_to_chain(chain[i],sn,prop,sever,fail_comp)
+ multi_sn=None
+ multi_prop=[]
+ else:
+ ifsn.find("+")>=0orsn.find("-")>=0orsn.find(".")>=0:
+ multi_sn=sn
+ multi_prop=[]
+ else:
+ _add_to_chain(chain[i],sn,prop,sever,fail_comp)
+
+ ifmulti_sn:
+ _add_to_chain(chain[i],multi_sn,multi_prop,sever,fail_comp)
+
+ _check_fail(
+ fail_comp,
+ False,
+ DoesNotExist,
+ "The following component%s "
+ "%s not in the DB and must be added first"
+ %(_plural(fail_comp),_are(fail_comp)),
+ )
+
+ conn_list=[]
+ prop_list=[]
+ forcinchain:
+ foriinrange(1,len(c)):
+ comp1=c[i-1]["comp"]
+ comp2=c[i]["comp"]
+ ifcomp1.sn==comp2.sn:
+ logger.info(
+ "Skipping auto connexion: %s <=> %s."%(comp1.sn,comp2.sn)
+ )
+ else:
+ conn=connexion.from_pair(comp1,comp2)
+ try:
+ ifconn.is_permanent(time):
+ logger.info(
+ "Skipping permanent connexion: %s <=> %s."
+ %(comp1.sn,comp2.sn)
+ )
+ elifconnnotinconn_list:
+ conn_list.append(conn)
+ exceptpw.DoesNotExist:
+ conn_list.append(conn)
+ foriinrange(len(c)):
+ comp=c[i]["comp"]
+ forpinc[i].keys():
+ ifp=="comp":
+ continue
+ try:
+ prop_list.append([comp,property_type.get(name=p),c[i][p]])
+ exceptpw.DoesNotExist:
+ raiseDoesNotExist('Property type "%s" does not exist.'%p)
+ make_connexion(conn_list,time,False,notes,force)
+ sever_connexion(sever,time,notes,force)
+ forpinprop_list:
+ p[0].set_property(p[1],p[2],time,notes)
+
+
+
+
+[docs]
+defget_global_flag_times(flag):
+"""Convenience function to get global flag times by id or name.
+
+ Parameters
+ ----------
+ flag : integer or string
+ If an integer, this is a global flag id, e.g. `64`. If a string this is the
+ global flag's name e.g. 'run_pass0_e'.
+
+ Returns
+ -------
+ start : :class:`datetime.datetime`
+ Global flag start time (UTC).
+ end : :class:`datetime.datetime` or `None`
+ Global flag end time (UTC) or `None` if the flag hasn't ended.
+
+ """
+
+ ifisinstance(flag,str):
+ query_=global_flag.select().where(global_flag.name==flag)
+ else:
+ query_=global_flag.select().where(global_flag.id==flag)
+
+ flag_=query_.join(graph_obj).join(event).where(event.active==True).get()
+
+ event_=event.get(graph_obj=flag_.id,active=True)
+
+ start=event_.start.time
+ try:
+ end=event_.end.time
+ exceptpw.DoesNotExist:
+ end=None
+ returnstart,end
+
+
+
+
+[docs]
+defglobal_flags_between(start_time,end_time,severity=None):
+"""Find global flags that overlap a time interval.
+
+ Parameters
+ ----------
+ start_time
+ end_time
+ severity : str
+ One of 'comment', 'warning', 'severe', or None.
+
+ Returns
+ -------
+ flags : list
+ List of global_flag objects matching criteria.
+
+ """
+
+ from.importephemeris
+
+ start_time=ephemeris.ensure_unix(start_time)
+ end_time=ephemeris.ensure_unix(end_time)
+
+ query=global_flag.select()
+ ifseverity:
+ query=query.where(global_flag.severity==severity)
+ query=query.join(graph_obj).join(event).where(event.active==True)
+
+ # Set aliases for the join
+ ststamp=timestamp.alias()
+ etstamp=timestamp.alias()
+
+ # Add constraint for the start time
+ query=query.join(ststamp,on=event.start).where(
+ ststamp.time<ephemeris.unix_to_datetime(end_time)
+ )
+ # Constrain the end time (being careful to deal with open events properly)
+ query=(
+ query.switch(event)
+ .join(etstamp,on=event.end,join_type=pw.JOIN.LEFT_OUTER)
+ .where(
+ (etstamp.time>ephemeris.unix_to_datetime(start_time))
+ |event.end.is_null()
+ )
+ )
+
+ returnlist(query)
+[docs]
+defprocess_synced_data(data,ni_params=None,only_off=False):
+"""Turn a synced noise source observation into gated form.
+
+ This will decimate the visibility to only the noise source off bins, and
+ will add 1 or more gated on-off dataset according to the specification in
+ doclib:5.
+
+ Parameters
+ ----------
+ data : andata.CorrData
+ Correlator data with noise source switched synchronously with the
+ integration.
+ ni_params : dict
+ Dictionary with the noise injection parameters. Optional
+ for data after ctime=1435349183. ni_params has the following keys
+ - ni_period: Noise injection period in GPU integrations.
+ It is assummed to be the same for all the enabled noise sources
+ - ni_on_bins: A list of lists, one per enabled noise source,
+ with the corresponding ON gates (within a period). For each
+ noise source, the list contains the indices of the time frames
+ for which the source is ON.
+ Example: For 3 GPU integration period (3 gates: 0, 1, 2), two enabled
+ noise sources, one ON during gate 0, the other ON during gate 1,
+ and both OFF during gate 2, then
+ ```
+ ni_params = {'ni_period':3, 'ni_on_bins':[[0], [1]]}
+ ```
+ only_off : boolean
+ Only return the off dataset. Do not return gated datasets.
+
+ Returns
+ -------
+ newdata : andata.CorrData
+ Correlator data folded on the noise source.
+
+ Comments
+ --------
+ - The function assumes that the fpga frame counter, which is used to
+ determine the noise injection gating parameters, is unwrapped.
+ - For noise injection data before ctime=1435349183 (i.e. for noise
+ injection data before 20150626T200540Z_pathfinder_corr) the noise
+ injection information is not in the headers so this function cannot be
+ used to determine the noise injection parameters. A different method is
+ required. Although it is recommended to check the data directly in this
+ case, the previous version of this function assumed that
+ ni_params = {'ni_period':2, 'ni_on_bins':[[0],]}
+ for noise injection data before ctime=1435349183. Although this is not
+ always true, it is true for big old datasets like pass1g.
+ Use the value of ni_params recommended above to reproduce the
+ results of the old function with the main old datasets.
+ - Data (visibility, gain and weight datasets) are averaged for all the
+ off gates within the noise source period, and also for all the on
+ gates of each noise source.
+ - For the time index map, only one timestamp per noise period is kept
+ (no averaging)
+ """
+
+ ifni_paramsisNone:
+ # ctime before which the noise injection information is not in the
+ # headers so this function cannot be used to determine the noise
+ # injection parameters.
+ ctime_no_noise_inj_data=1435349183
+ ifdata.index_map["time"]["ctime"][0]>ctime_no_noise_inj_data:
+ # All the data required to figure out the noise inj gating is in
+ # the data header
+ try:
+ ni_params=_find_ni_params(data)
+ exceptValueError:
+ warn_str=(
+ "There are no enabled noise sources for these data. "
+ "Returning input"
+ )
+ warnings.warn(warn_str)
+ returndata
+ else:
+ # This is data before ctime = 1435349183. Noise injection
+ # parameters are not in the data header. Raise error
+ t=datetime.datetime.utcfromtimestamp(ctime_no_noise_inj_data)
+ t_str=t.strftime("%Y %b %d %H:%M:%S UTC")
+ err_str=(
+ "ni_params parameter is required for data before "
+ "%s (ctime=%i)."%(t_str,ctime_no_noise_inj_data)
+ )
+ raiseException(err_str)
+
+ iflen([sforsindata.datasets.keys()if"gated_vis"ins]):
+ # If there are datasets with gated_vis in their names then assume
+ # this is fast gating data, where the vis dataset has on+off and
+ # the vis_gatedxx has onxx-off. Process separatedly since in
+ # this case the noise injection parameters are not in gpu
+ # integration frames but in fpga frames and the gates are already
+ # separated
+ newdata=process_gated_data(data,only_off=only_off)
+ else:
+ # time bins with noise ON for each source (within a noise period)
+ # This is a list of lists, each list corresponding to the ON time bins
+ # for each noise source.
+ ni_on_bins=ni_params["ni_on_bins"]
+
+ # Number of enabled noise sources
+ N_ni_sources=len(ni_on_bins)
+
+ # Noise injection period (assume all sources have same period)
+ ni_period=ni_params["ni_period"]
+
+ # time bins with all noise sources off (within a noise period)
+ ni_off_bins=np.delete(list(range(ni_period)),np.concatenate(ni_on_bins))
+
+ # Find largest number of exact noise injection periods
+ nt=ni_period*(data.ntime//ni_period)
+
+ # Make sure we're distributed over something other than time
+ data.redistribute("freq")
+
+ # Get distribution parameters
+ dist=isinstance(data.vis,memh5.MemDatasetDistributed)
+ comm=data.vis.comm
+
+ # Construct new CorrData object for gated dataset
+ newdata=andata.CorrData.__new__(andata.CorrData)
+ ifdist:
+ memh5.BasicCont.__init__(newdata,distributed=dist,comm=comm)
+ else:
+ memh5.BasicCont.__init__(newdata,distributed=dist)
+ memh5.copyattrs(data.attrs,newdata.attrs)
+
+ # Add index maps to newdata
+ newdata.create_index_map("freq",data.index_map["freq"])
+ newdata.create_index_map("prod",data.index_map["prod"])
+ newdata.create_index_map("input",data.input)
+ # Extract timestamps for OFF bins. Only one timestamp per noise period is
+ # kept. These will be the timestamps for both the noise on ON and OFF data
+ time=data.index_map["time"][ni_off_bins[0]:nt:ni_period]
+ folding_period=time["ctime"][1]-time["ctime"][0]
+ folding_start=time["ctime"][0]
+ # Add index map for noise OFF timestamps.
+ newdata.create_index_map("time",time)
+
+ # Add datasets (for noise OFF) to newdata
+ # Extract the noise source off data
+ iflen(ni_off_bins)>1:
+ # Average all time bins with noise OFF within a period
+ vis_sky=[data.vis[...,gate:nt:ni_period]forgateinni_off_bins]
+ vis_sky=np.mean(vis_sky,axis=0)
+ else:
+ vis_sky=data.vis[...,ni_off_bins[0]:nt:ni_period]
+
+ # Turn vis_sky into MPIArray if we are distributed
+ ifdist:
+ vis_sky=mpiarray.MPIArray.wrap(vis_sky,axis=0,comm=comm)
+
+ # Add new visibility dataset
+ vis_dset=newdata.create_dataset("vis",data=vis_sky,distributed=dist)
+ memh5.copyattrs(data.vis.attrs,vis_dset.attrs)
+
+ # Add gain dataset (if exists) for noise OFF data.
+ # Gain dataset also averaged (within a period)
+ # These will be the gains for both the noise on ON and OFF data
+ if"gain"indata:
+ iflen(ni_off_bins)>1:
+ gain=[data.gain[...,gate:nt:ni_period]forgateinni_off_bins]
+ gain=np.mean(gain,axis=0)
+ else:
+ gain=data.gain[...,ni_off_bins[0]:nt:ni_period]
+
+ # Turn gain into MPIArray if we are distributed
+ ifdist:
+ gain=mpiarray.MPIArray.wrap(gain,axis=0,comm=comm)
+
+ # Add new gain dataset
+ gain_dset=newdata.create_dataset("gain",data=gain,distributed=dist)
+ memh5.copyattrs(data.gain.attrs,gain_dset.attrs)
+
+ # Pull out weight dataset if it exists.
+ # vis_weight dataset also averaged (within a period)
+ # These will be the weights for both the noise on ON and OFF data
+ if"vis_weight"indata.flags:
+ iflen(ni_off_bins)>1:
+ vis_weight=[
+ data.weight[...,gate:nt:ni_period]forgateinni_off_bins
+ ]
+ vis_weight=np.mean(vis_weight,axis=0)
+ else:
+ vis_weight=data.weight[...,ni_off_bins[0]:nt:ni_period]
+
+ # Turn vis_weight into MPIArray if we are distributed
+ ifdist:
+ vis_weight=mpiarray.MPIArray.wrap(vis_weight,axis=0,comm=comm)
+
+ # Add new vis_weight dataset
+ vis_weight_dset=newdata.create_flag(
+ "vis_weight",data=vis_weight,distributed=dist
+ )
+ memh5.copyattrs(data.weight.attrs,vis_weight_dset.attrs)
+
+ # Add gated datasets for each noise source:
+ ifnotonly_off:
+ foriinrange(N_ni_sources):
+ # Construct the noise source only data
+ vis_noise=[data.vis[...,gate:nt:ni_period]forgateinni_on_bins[i]]
+ vis_noise=np.mean(vis_noise,axis=0)# Averaging
+ vis_noise-=vis_sky# Subtracting sky contribution
+
+ # Turn vis_noise into MPIArray if we are distributed
+ ifdist:
+ vis_noise=mpiarray.MPIArray.wrap(vis_noise,axis=0,comm=comm)
+
+ # Add noise source dataset
+ gate_dset=newdata.create_dataset(
+ "gated_vis{0}".format(i+1),data=vis_noise,distributed=dist
+ )
+ gate_dset.attrs["axis"]=np.array(
+ ["freq","prod","gated_time{0}".format(i+1)]
+ )
+ gate_dset.attrs["folding_period"]=folding_period
+ gate_dset.attrs["folding_start"]=folding_start
+
+ # Construct array of gate weights (sum = 0)
+ gw=np.zeros(ni_period,dtype=np.float64)
+ gw[ni_off_bins]=-1.0/len(ni_off_bins)
+ gw[ni_on_bins[i]]=1.0/len(ni_on_bins[i])
+ gate_dset.attrs["gate_weight"]=gw
+
+ returnnewdata
+
+
+
+def_find_ni_params(data,verbose=0):
+"""
+ Finds the noise injection gating parameters.
+
+ Parameters
+ ----------
+ data : andata.CorrData
+ Correlator data with noise source switched synchronously with the
+ integration.
+ verbose: bool
+ If True, print messages.
+
+ Returns
+ -------
+ ni_params : dict
+ Dictionary with the noise injection parameters. ni_params has the
+ following keys
+ ni_period: Noise injection period in GPU integrations. It is
+ assummed to be the same for all the enabled noise sources
+ ni_on_bins: A list of lists, one per enabled noise source,
+ with the corresponding ON gates (within a period). For each
+ noise source, the list contains the indices of the time frames
+ for which the source is ON.
+
+ Example: For 3 GPU integration period (3 gates: 0, 1, 2), two enabled
+ noise sources, one ON during gate 0, the other ON during gate 1,
+ and both OFF during gate 2, then
+ ni_params = {'ni_period':3, 'ni_on_bins':[[0], [1]]}
+
+ Comments
+ --------
+ - The function assumes that the fpga frame counter, which is used to
+ determine the noise injection gating parameters, is unwrapped.
+ - For noise injection data before ctime=1435349183 (i.e. for noise
+ injection data before 20150626T200540Z_pathfinder_corr) the noise
+ injection information is not in the headers so this function cannot be
+ used to determine the noise injection parameters. A different method is
+ required (e.g. check the data directly). The previous version of this
+ function assumed that
+ ni_params = {'ni_period':2, 'ni_on_bins':[[0],]}
+ for noise injection data before ctime=1435349183. Although this is not
+ always true, it is true for big old datasets like pass1g.
+ Use the value of ni_params recommended above to reproduce the
+ results of the old function with the main old datasets.
+ """
+
+ # ctime before which the noise injection information is not in the headers
+ # so this function cannot be used to determine the noise injection
+ # parameters.
+ ctime_no_noise_inj_data=1435349183
+
+ # ctime of first data frame
+ ctime0=data.index_map["time"]["ctime"][0]
+ ifctime0<ctime_no_noise_inj_data:
+ # This is data before ctime = 1435349183. Noise injection parameters
+ # are not in the data header. Raise error
+ err_str=(
+ "Noise injection parameters are not in the header for "
+ "these data. See help for details."
+ )
+ raiseException(err_str)
+
+ ni_period=[]# Noise source period in GPU integrations
+ ni_high_time=[]# Noise source high time in GPU integrations
+ ni_offset=[]# Noise source offset in GPU integrations
+ ni_board=[]# Noise source PWM board
+
+ # Noise inj information is in the headers. Assume the fpga frame
+ # counter is unwrapped
+ ifverbose:
+ print("Reading noise injection data from header")
+
+ # Read noise injection parameters from header. Currently the system
+ # Can handle up to two noise sources. Only the enabled sources are
+ # analyzed
+ if("fpga.ni_enable"indata.attrs)and(data.attrs["fpga.ni_enable"][0]):
+ # It seems some old data.attrs may have 'fpga.ni_enable' but not
+ # 'fpga.ni_high_time' (this has to be checked!!)
+ if"fpga.ni_period"indata.attrs:
+ ni_period.append(data.attrs["fpga.ni_period"][0])
+ else:
+ ni_period.append(2)
+ ifverbose:
+ debug_str=(
+ '"fpga.ni_period" not in data header. '
+ "Assuming noise source period = 2"
+ )
+ print(debug_str)
+
+ if"fpga.ni_high_time"indata.attrs:
+ ni_high_time.append(data.attrs["fpga.ni_high_time"][0])
+ else:
+ ni_high_time.append(1)
+ ifverbose:
+ debug_str=(
+ '"fpga.ni_high_time" not in data header. '
+ "Assuming noise source high time = 1"
+ )
+ print(debug_str)
+
+ if"fpga.ni_offset"indata.attrs:
+ ni_offset.append(data.attrs["fpga.ni_offset"][0])
+ else:
+ ni_offset.append(0)
+ ifverbose:
+ debug_str=(
+ '"fpga.ni_offset" not in data header. '
+ "Assuming noise source offset = 0"
+ )
+ print(debug_str)
+
+ if"fpga.ni_board"indata.attrs:
+ ni_board.append(data.attrs["fpga.ni_board"])
+ else:
+ ni_board.append("")
+ ifverbose:
+ debug_str='"fpga.ni_board" not in data header.'
+ print(debug_str)
+
+ if("fpga.ni_enable_26m"indata.attrs)and(data.attrs["fpga.ni_enable_26m"][0]):
+ # It seems some old data.attrs may have 'fpga.ni_enable_26m' but
+ # not 'fpga.ni_high_time_26m' (this has to be checked!!)
+ if"fpga.ni_period_26m"indata.attrs:
+ ni_period.append(data.attrs["fpga.ni_period_26m"][0])
+ else:
+ ni_period.append(2)
+ ifverbose:
+ debug_str=(
+ '"fpga.ni_period_26m" not in data header. '
+ "Assuming noise source period = 2"
+ )
+ print(debug_str)
+
+ if"fpga.ni_high_time_26m"indata.attrs:
+ ni_high_time.append(data.attrs["fpga.ni_high_time_26m"][0])
+ else:
+ ni_high_time.append(1)
+ ifverbose:
+ debug_str=(
+ '"fpga.ni_high_time_26m" not in data header.'
+ " Assuming noise source high time = 1"
+ )
+ print(debug_str)
+
+ if"fpga.ni_offset_26m"indata.attrs:
+ ni_offset.append(data.attrs["fpga.ni_offset_26m"][0])
+ else:
+ ni_offset.append(0)
+ ifverbose:
+ debug_str=(
+ '"fpga.ni_offset_26m" not in data header. '
+ "Assuming noise source offset = 0"
+ )
+ print(debug_str)
+
+ if"fpga.ni_board_26m"indata.attrs:
+ ni_board.append(data.attrs["fpga.ni_board_26m"])
+ else:
+ ni_board.append("")
+ ifverbose:
+ debug_str='"fpga.ni_board_26m" not in data header.'
+ print(debug_str)
+
+ # Number of enabled noise sources
+ N_ni_sources=len(ni_period)
+ ifN_ni_sources==0:
+ # There are not enabled noise sources. Raise error
+ raiseValueError("There are no enabled noise sources for these data")
+
+ ifnp.any(np.array(ni_period-ni_period[0])):
+ # Enabled sources do not have same period. Raise error
+ raiseException("Enabled sources do not have same period")
+
+ # Period of first noise source (assume all have same period)
+ ni_period=ni_period[0]
+
+ ifverbose:
+ foriinrange(N_ni_sources):
+ print("\nPWM signal from board %s is enabled"%ni_board[i])
+ print("Period: %i GPU integrations"%ni_period)
+ print("High time: %i GPU integrations"%ni_high_time[i])
+ print("FPGA offset: %i GPU integrations\n"%ni_offset[i])
+
+ # Number of fpga frames within a GPU integration
+ int_period=data.attrs["gpu.gpu_intergration_period"][0]
+
+ # fpga counts for first period
+ fpga_counts=data.index_map["time"]["fpga_count"][:ni_period]
+
+ # Start of high time for each noise source (within a noise period)
+ ni_on_start_bin=[
+ np.argmin(np.remainder((fpga_counts//int_period-ni_offset[i]),ni_period))
+ foriinrange(N_ni_sources)
+ ]
+
+ # time bins with noise ON for each source (within a noise period)
+ ni_on_bins=[
+ np.arange(ni_on_start_bin[i],ni_on_start_bin[i]+ni_high_time[i])
+ foriinrange(N_ni_sources)
+ ]
+
+ ni_params={"ni_period":ni_period,"ni_on_bins":ni_on_bins}
+
+ returnni_params
+
+
+
+[docs]
+defprocess_gated_data(data,only_off=False):
+"""
+ Processes fast gating data and turns it into gated form.
+
+ Parameters
+ ----------
+ data : andata.CorrData
+ Correlator data with noise source switched synchronously with the
+ integration.
+ only_off : boolean
+ Only return the off dataset. Do not return gated datasets.
+
+ Returns
+ -------
+ newdata : andata.CorrData
+ Correlator data folded on the noise source.
+
+ Comments
+ --------
+ For now the correlator only supports fast gating with one gate
+ (gated_vis1) and 50% duty cycle. The vis dataset contains on+off
+ and the gated_vis1 contains on-off. This function returns a new
+ andata object with vis containing the off data only and gated_vis1
+ as in the original andata object. The attribute
+ 'gpu.gpu_intergration_period' is divided by 2 since during an
+ integration half of the frames have on data.
+ """
+ # Make sure we're distributed over something other than time
+ data.redistribute("freq")
+
+ # Get distribution parameters
+ dist=isinstance(data.vis,memh5.MemDatasetDistributed)
+ comm=data.vis.comm
+
+ # Construct new CorrData object for gated dataset
+ newdata=andata.CorrData.__new__(andata.CorrData)
+ ifdist:
+ memh5.BasicCont.__init__(newdata,distributed=dist,comm=comm)
+ else:
+ memh5.BasicCont.__init__(newdata,distributed=dist)
+ memh5.copyattrs(data.attrs,newdata.attrs)
+
+ # Add index maps to newdata
+ newdata.create_index_map("freq",data.index_map["freq"])
+ newdata.create_index_map("prod",data.index_map["prod"])
+ newdata.create_index_map("input",data.input)
+ newdata.create_index_map("time",data.index_map["time"])
+
+ # Add datasets (for noise OFF) to newdata
+ # Extract the noise source off data
+ vis_off=0.5*(
+ data.vis[:].view(np.ndarray)-data["gated_vis1"][:].view(np.ndarray)
+ )
+
+ # Turn vis_off into MPIArray if we are distributed
+ ifdist:
+ vis_off=mpiarray.MPIArray.wrap(vis_off,axis=0,comm=comm)
+
+ # Add new visibility dataset
+ vis_dset=newdata.create_dataset("vis",data=vis_off,distributed=dist)
+ memh5.copyattrs(data.vis.attrs,vis_dset.attrs)
+
+ # Add gain dataset (if exists) for vis_off.
+ # These will be the gains for both the noise on ON and OFF data
+ if"gain"indata:
+ gain=data.gain[:].view(np.ndarray)
+ # Turn gain into MPIArray if we are distributed
+ ifdist:
+ gain=mpiarray.MPIArray.wrap(gain,axis=0,comm=comm)
+
+ gain_dset=newdata.create_dataset("gain",data=gain,distributed=dist)
+ memh5.copyattrs(data.gain.attrs,gain_dset.attrs)
+
+ # Pull out weight dataset if it exists.
+ # These will be the weights for both the noise on ON and OFF data
+ if"vis_weight"indata.flags:
+ vis_weight=data.weight[:].view(np.ndarray)
+ # Turn vis_weight into MPIArray if we are distributed
+ ifdist:
+ vis_weight=mpiarray.MPIArray.wrap(vis_weight,axis=0,comm=comm)
+
+ vis_weight_dset=newdata.create_flag(
+ "vis_weight",data=vis_weight,distributed=dist
+ )
+ memh5.copyattrs(data.weight.attrs,vis_weight_dset.attrs)
+
+ # Add gated dataset (only gated_vis1 currently supported by correlator
+ # with 50% duty cycle)
+ ifnotonly_off:
+ gated_vis1=data["gated_vis1"][:].view(np.ndarray)
+ # Turn gated_vis1 into MPIArray if we are distributed
+ ifdist:
+ gated_vis1=mpiarray.MPIArray.wrap(gated_vis1,axis=0,comm=comm)
+
+ gate_dset=newdata.create_dataset(
+ "gated_vis1",data=gated_vis1,distributed=dist
+ )
+ memh5.copyattrs(data["gated_vis1"].attrs,gate_dset.attrs)
+
+ # The CHIME pipeline uses gpu.gpu_intergration_period to estimate the integration period
+ # for both the on and off gates. That number has to be changed (divided by 2) since
+ # with fast gating one integration period has 1/2 of data for the on gate and 1/2
+ # for the off gate
+ newdata.attrs["gpu.gpu_intergration_period"]=(
+ data.attrs["gpu.gpu_intergration_period"]//2
+ )
+
+ returnnewdata
+
+
+
+
+[docs]
+classni_data(object):
+"""Provides analysis utilities for CHIME noise injection data.
+
+ This is just a wrapper for all the utilities created in this module.
+
+ Parameters
+ -----------
+ Reader_read_obj : andata.Reader.read() like object
+ Contains noise injection data. Must have 'vis' and 'timestamp' property.
+ Assumed to contain all the Nadc_channels*(Nadc_channels+1)/2 correlation
+ products, in chime's canonical vector, for an
+ Nadc_channels x Nadc_channels correlation matrix
+ Nadc_channels : int
+ Number of channels read in Reader_read_obj
+ adc_ch_ref : int in the range 0 <= adc_ch_ref <= Nadc_channels-1
+ Reference channel (used to find on/off points).
+ fbin_ref : int in the range
+ 0 <= fbin_ref <= np.size(Reader_read_obj.vis, 0)-1
+ Reference frequency bin (used to find on/off points).
+
+ Methods
+ -------
+ subtract_sky_noise : Removes sky and system noise contributions from noise
+ injection visibility data.
+ get_ni_gains : Solve for gains from decimated sky-and-noise-subtracted
+ visibilities
+ get_als_gains : Compute gains, sky and system noise covariance matrices from
+ a combination of noise injection gains and point source gains
+ """
+
+ def__init__(self,Reader_read_obj,Nadc_channels,adc_ch_ref=None,fbin_ref=None):
+"""Processes raw noise injection data so it is ready to compute gains."""
+
+ self.adc_channels=np.arange(Nadc_channels)
+ self.Nadc_channels=Nadc_channels
+ self.raw_vis=Reader_read_obj.vis
+ self.Nfreqs=np.size(self.raw_vis,0)# Number of frequencies
+ ifadc_ch_ref!=None:
+ self.adc_ch_ref=adc_ch_ref
+ else:
+ self.adc_ch_ref=self.adc_channels[0]# Default reference channel
+
+ iffbin_ref!=None:
+ self.fbin_ref=fbin_ref
+ else:# Default reference frequency bin (rather arbitrary)
+ self.fbin_ref=self.Nfreqs//3
+
+ self.timestamp=Reader_read_obj.timestamp
+ try:
+ self.f_MHz=Reader_read_obj.freq
+ exceptAttributeError:
+ pass# May happen if TimeStream type does not have this property
+
+ self.subtract_sky_noise()
+
+
+[docs]
+ defsubtract_sky_noise(self):
+"""Removes sky and system noise contributions from noise injection
+ visibility data.
+
+ See also
+ --------
+ subtract_sky_noise function
+ """
+
+ ni_dict=subtract_sky_noise(
+ self.raw_vis,
+ self.Nadc_channels,
+ self.timestamp,
+ self.adc_ch_ref,
+ self.fbin_ref,
+ )
+ self.time_index_on=ni_dict["time_index_on"]
+ self.time_index_off=ni_dict["time_index_off"]
+ self.vis_on_dec=ni_dict["vis_on_dec"]
+ self.vis_off_dec=ni_dict["vis_off_dec"]
+ self.vis_dec_sub=ni_dict["vis_dec_sub"]
+ self.timestamp_on_dec=ni_dict["timestamp_on_dec"]
+ self.timestamp_off_dec=ni_dict["timestamp_off_dec"]
+ self.timestamp_dec=ni_dict["timestamp_dec"]
+ self.cor_prod_ref=ni_dict["cor_prod_ref"]
+
+
+
+[docs]
+ defget_ni_gains(self,normalize_vis=False,masked_channels=None):
+"""Computes gains and evalues from noise injection visibility data.
+
+ See also
+ --------
+ ni_gains_evalues_tf
+
+ Additional parameters
+ ---------------------
+ masked_channels : list of integers
+ channels which are not considered in the calculation of the gains.
+ """
+
+ self.channels=np.arange(self.Nadc_channels)
+ ifmasked_channels!=None:
+ self.channels=np.delete(self.channels,masked_channels)
+
+ self.Nchannels=len(self.channels)
+ # Correlation product indices for selected channels
+ cor_prod=gen_prod_sel(self.channels,total_N_channels=self.Nadc_channels)
+ self.ni_gains,self.ni_evals=ni_gains_evalues_tf(
+ self.vis_dec_sub[:,cor_prod,:],self.Nchannels,normalize_vis
+ )
+
+
+
+[docs]
+ defget_als_gains(self):
+"""Compute gains, sky and system noise covariance matrices from a
+ combination of noise injection gains and point source gains
+ """
+
+ pass
+
+
+
+[docs]
+ defsave(self):
+"""Save gain solutions"""
+
+ pass
+
+
+
+
+
+[docs]
+defgen_prod_sel(channels_to_select,total_N_channels):
+"""Generates correlation product indices for selected channels.
+
+ For a correlation matrix with total_N_channels total number of channels,
+ generates indices for correlation products corresponding to channels in
+ the list channels_to_select.
+
+ Parameters
+ ----------
+ channels_to_select : list of integers
+ Indices of channels to select
+ total_N_channels : int
+ Total number of channels
+
+ Returns
+ -------
+ prod_sel : array
+ indices of correlation products for channels in channels_to_select
+
+ """
+
+ prod_sel=[]
+ k=0
+ foriinrange(total_N_channels):
+ forjinrange(i,total_N_channels):
+ if(iinchannels_to_select)and(jinchannels_to_select):
+ prod_sel.append(k)
+
+ k=k+1
+
+ returnnp.array(prod_sel)
+
+
+
+
+[docs]
+defmat2utvec(A):
+"""Vectorizes its upper triangle of the (hermitian) matrix A.
+
+ Parameters
+ ----------
+ A : 2d array
+ Hermitian matrix
+
+ Returns
+ -------
+ 1d array with vectorized form of upper triangle of A
+
+ Example
+ -------
+ if A is a 3x3 matrix then the output vector is
+ outvector = [A00, A01, A02, A11, A12, A22]
+
+ See also
+ --------
+ utvec2mat
+ """
+
+ iu=np.triu_indices(np.size(A,0))# Indices for upper triangle of A
+
+ returnA[iu]
+
+
+
+
+[docs]
+defutvec2mat(n,utvec):
+"""
+ Recovers a hermitian matrix a from its upper triangle vectorized version.
+
+ Parameters
+ ----------
+ n : int
+ order of the output hermitian matrix
+ utvec : 1d array
+ vectorized form of upper triangle of output matrix
+
+ Returns
+ -------
+ A : 2d array
+ hermitian matrix
+ """
+
+ iu=np.triu_indices(n)
+ A=np.zeros((n,n),dtype=np.complex128)
+ A[iu]=utvec# Filling uppper triangle of A
+ A=A+np.triu(A,1).conj().T# Filling lower triangle of A
+ returnA
+
+
+
+
+[docs]
+defktrprod(A,B):
+"""Khatri-Rao or column-wise Kronecker product of two matrices.
+
+ A and B have the same number of columns
+
+ Parameters
+ ----------
+ A : 2d array
+ B : 2d array
+
+ Returns
+ -------
+ C : 2d array
+ Khatri-Rao product of A and B
+ """
+ nrowsA=np.size(A,0)
+ nrowsB=np.size(B,0)
+ ncols=np.size(A,1)
+ C=np.zeros((nrowsA*nrowsB,ncols),dtype=np.complex128)
+ foriinrange(ncols):
+ C[:,i]=np.kron(A[:,i],B[:,i])
+
+ returnC
+
+
+
+
+[docs]
+defni_als(R,g0,Gamma,Upsilon,maxsteps,abs_tol,rel_tol,weighted_als=True):
+"""Implementation of the Alternating Least Squares algorithm for noise
+ injection.
+
+ Implements the Alternating Least Squares algorithm to recover the system
+ gains, sky covariance matrix and system output noise covariance matrix
+ from the data covariance matrix R. All the variables and definitions are as
+ in http://bao.phas.ubc.ca/doc/library/doc_0103/rev_01/chime_calibration.pdf
+
+ Parameters
+ ----------
+ R : 2d array
+ Data covariance matrix
+ g0 : 1d array
+ First estimate of system gains
+ Gamma : 2d array
+ Matrix that characterizes parametrization of sky covariance matrix
+ Upsilon : 2d array
+ Matrix characterizing parametrization of system noise covariance matrix
+ maxsteps : int
+ Maximum number of iterations
+ abs_tol : float
+ Absolute tolerance on error function
+ rel_tol : float
+ Relative tolerance on error function
+ weighted_als : bool
+ If True, perform weighted ALS
+
+ Returns
+ -------
+ g : 1d array
+ System gains
+ C : 2d array
+ Sky covariance matrix
+ N : 2d array
+ System output noise covariance matrix
+ err : 1d array
+ Error function for every step
+
+ See also
+ --------
+ http://bao.phas.ubc.ca/doc/library/doc_0103/rev_01/chime_calibration.pdf
+ """
+
+ g=g0.copy()
+ G=np.diag(g)
+ Nchannels=np.size(R,0)# Number of receiver channels
+ rank_Gamma=np.size(Gamma,1)# Number of sky covariance matrix parameters
+ # Calculate initial weight matrix
+ ifweighted_als:
+ inv_W=sciLA.sqrtm(R)
+ W=LA.inv(inv_W)
+ else:
+ W=np.eye(Nchannels)
+ inv_W=W.copy()
+
+ W_kron_W=np.kron(W.conj(),W)
+ G_kron_G=np.kron(G.conj(),G)
+ Psi=np.hstack((np.dot(G_kron_G,Gamma),Upsilon))
+ psi=np.dot(np.dot(np.linalg.pinv(np.dot(W_kron_W,Psi)),W_kron_W),R)
+ gamma=psi[:rank_Gamma]
+ upsilon=psi[rank_Gamma:]
+ # Estimate of sky covariance matrix
+ C=np.dot(Gamma,gamma).reshape((Nchannels,Nchannels),order="F")
+ # Estimate of output noise covariance matrix
+ N=np.dot(Upsilon,upsilon).reshape((Nchannels,Nchannels),order="F")
+ # Make sure C and N are positive (semi-)definite
+ evals,V=LA.eigh(C,"U")# Get eigens of C
+ D=np.diag(np.maximum(evals,0.0))# Replace negative eigenvalues by zeros
+ C=np.dot(V,np.dot(D,V.conj().T))# Positive (semi-)definite version of C
+ evals,V=LA.eigh(N,"U")
+ D=np.diag(np.maximum(evals,0))
+ N=np.dot(V,np.dot(D,V.conj().T))
+ # Calculate error
+ err=[
+ LA.norm(np.dot(W,np.dot(R-np.dot(G,np.dot(C,G.conj()))-N,W)),ord="fro")
+ ]
+
+ foriinrange(1,maxsteps):
+ if(err[-1]>=abs_tol)or(
+ (i>1)and(abs(err[-2]-err[-1])<=rel_tol*err[-2])
+ ):
+ break
+
+ ifweighted_als:
+ inv_W=sciLA.sqrtm(R+np.dot(G,np.dot(C,G.conj()))+N)
+ W=LA.inv(inv_W)
+ else:
+ W=np.eye(Nchannels)
+ inv_W=W.copy()
+
+ W_pow2=np.dot(W,W)
+ W_pow2GC=np.dot(W_pow2,np.dot(G,C))
+ g=np.dot(
+ LA.pinv(np.dot(C,np.dot(G.conj().T,W_pow2GC)).conj()*W_pow2),
+ np.dot(
+ ktrprod(W_pow2GC,W_pow2).conj().T,
+ (R-N).reshape(Nchannels**2,order="F"),
+ ),
+ )
+
+ G=np.diag(g)
+ G_kron_G=np.kron(G.conj(),G)
+ Psi=np.hstack((np.dot(G_kron_G,Gamma),Upsilon))
+ psi=np.dot(np.dot(np.linalg.pinv(np.dot(W_kron_W,Psi)),W_kron_W),R)
+ gamma=psi[:rank_Gamma]
+ upsilon=psi[rank_Gamma:]
+ C=np.dot(Gamma,gamma).reshape((Nchannels,Nchannels),order="F")
+ N=np.dot(Upsilon,upsilon).reshape((Nchannels,Nchannels),order="F")
+ evals,V=LA.eigh(C,"U")
+ D=np.diag(np.maximum(evals,0.0))
+ C=np.dot(V,np.dot(D,V.conj().T))
+ evals,V=LA.eigh(N,"U")
+ D=np.diag(np.maximum(evals,0))
+ N=np.dot(V,np.dot(D,V.conj().T))
+ err.append(
+ LA.norm(
+ np.dot(W,np.dot(R-np.dot(G,np.dot(C,G.conj()))-N,W)),ord="fro"
+ )
+ )
+
+ returng,C,N,np.array(err)
+
+
+
+
+[docs]
+defsort_evalues_mag(evalues):
+"""Sorts eigenvalue array by magnitude for all frequencies and time frames
+
+ Parameters
+ ----------
+ evalues : 3d array
+ Array of evalues. Its shape is [Nfreqs, Nevalues, Ntimeframes]
+
+ Returns
+ -------
+ ev : 3d array
+ Array of same shape as evalues
+ """
+
+ ev=np.zeros(evalues.shape,dtype=float)
+ forfinrange(np.size(ev,0)):
+ fortinrange(np.size(ev,2)):
+ ev[f,:,t]=evalues[f,np.argsort(abs(evalues[f,:,t])),t]
+
+ returnev
+
+
+
+
+[docs]
+defni_gains_evalues(C,normalize_vis=False):
+"""Basic algorithm to compute gains and evalues from noise injection data.
+
+ C is a correlation matrix from which the gains are calculated.
+ If normalize_vis = True, the visibility matrix is weighted by the diagonal
+ matrix that turns it into a crosscorrelation coefficient matrix before the
+ gain calculation. The eigenvalues are not sorted. The returned gain solution
+ vector is normalized (LA.norm(g) = 1.)
+
+ Parameters
+ ----------
+ C : 2d array
+ Data covariance matrix from which the gains are calculated. It is
+ assumed that both the sky and system noise contributions have already
+ been subtracted using noise injection
+ normalize_vis : bool
+ If True, the visibility matrix is weighted by the diagonal matrix that
+ turns it into a crosscorrelation coefficient matrix before the
+ gain calculation.
+
+ Returns
+ -------
+ g : 1d array
+ Noise injection gains
+ ev : 1d array
+ Noise injection eigenvalues
+
+ See also
+ --------
+ ni_gains_evalues_tf, subtract_sky_noise
+ """
+
+ Nchannels=np.size(C,0)# Number of receiver channels
+ ifnormalize_vis:# Convert to correlation coefficient matrix
+ W=np.diag(1/np.sqrt(np.diag(C).real))
+ Winv=np.diag(np.sqrt(np.diag(C).real))
+ else:
+ W=np.identity(Nchannels)
+ Winv=np.identity(Nchannels)
+
+ ev,V=LA.eigh(np.dot(np.dot(W,C),W),"U")
+ g=np.sqrt(ev.max())*np.dot(Winv,V[:,ev.argmax()])
+
+ returng,ev
+
+
+
+
+[docs]
+defni_gains_evalues_tf(
+ vis_gated,Nchannels,normalize_vis=False,vis_on=None,vis_off=None,niter=0
+):
+"""Computes gains and evalues from noise injection visibility data.
+
+ Gains and eigenvalues are calculated for all frames and
+ frequencies in vis_gated. The returned gain solution
+ vector is normalized (LA.norm(gains[f, :, t]) = 1.)
+
+ Parameters
+ ----------
+ vis_gated : 3d array
+ Visibility array in chime's canonical format. vis_gated has dimensions
+ [frequency, corr. number, time]. It is assumed that both the sky and
+ system noise contributions have already been subtracted using noise
+ injection.
+ Nchannels : int
+ Order of the visibility matrix (number of channels)
+ normalize_vis : bool
+ If True, then the visibility matrix is weighted by the diagonal matrix that
+ turns it into a crosscorrelation coefficient matrix before the
+ gain calculation.
+ vis_on : 3d array
+ If input and normalize_vis is True, then vis_gated is weighted
+ by the diagonal elements of the matrix vis_on.
+ vis_on must be the same shape as vis_gated.
+ vis_off : 3d array
+ If input and normalize_vis is True, then vis_gated is weighted
+ by the diagonal elements of the matrix: vis_on = vis_gated + vis_off.
+ vis_off must be the same shape as vis_gated. Keyword vis_on
+ supersedes keyword vis_off.
+ niter : 0
+ Number of iterations to perform. At each iteration, the diagonal
+ elements of vis_gated are replaced with their rank 1 approximation.
+ If niter == 0 (default), then no iterations are peformed and the
+ autocorrelations are used instead.
+
+ Returns
+ -------
+ gains : 3d array
+ Noise injection gains
+ evals : 3d array
+ Noise injection eigenvalues
+
+ Dependencies
+ ------------
+ tools.normalise_correlations, tools.eigh_no_diagonal
+
+ See also
+ --------
+ ni_gains_evalues, subtract_sky_noise
+ """
+
+ from.toolsimportnormalise_correlations
+ from.toolsimporteigh_no_diagonal
+
+ # Determine the number of frequencies and time frames
+ Nfreqs=np.size(vis_gated,0)
+ Ntimeframes=np.size(vis_gated,2)
+
+ # Create NaN matrices to hold the gains and eigenvalues
+ gains=np.zeros((Nfreqs,Nchannels,Ntimeframes),dtype=np.complex)*(
+ np.nan+1j*np.nan
+ )
+ evals=np.zeros((Nfreqs,Nchannels,Ntimeframes),dtype=np.float64)*np.nan
+
+ # Determine if we will weight by the square root of the autos
+ # of the matrix vis_on = vis_gated + vis_off
+ vis_on_is_input=(vis_onisnotNone)and(vis_on.shape==vis_gated.shape)
+ vis_off_is_input=(vis_offisnotNone)and(vis_off.shape==vis_gated.shape)
+ weight_by_autos_on=normalize_visand(vis_on_is_inputorvis_off_is_input)
+
+ sqrt_autos=np.ones(Nchannels)
+
+ # Loop through the input frequencies and time frames
+ forfinrange(Nfreqs):
+ fortinrange(Ntimeframes):
+ # Create Nchannel x Nchannel matrix of noise-injection-on visibilities
+ ifweight_by_autos_on:
+ ifvis_on_is_input:
+ mat_slice_vis_on=utvec2mat(Nchannels,vis_on[f,:,t])
+ else:
+ mat_slice_vis_on=utvec2mat(
+ Nchannels,np.add(vis_gated[f,:,t],vis_off[f,:,t])
+ )
+ else:
+ mat_slice_vis_on=None
+
+ # Create Nchannel x Nchannel matrix of gated visibilities
+ mat_slice_vis_gated=utvec2mat(Nchannels,vis_gated[f,:,t])
+
+ # If requested, then normalize the gated visibilities
+ # by the square root of the autocorrelations
+ ifnormalize_vis:
+ mat_slice_vis_gated,sqrt_autos=normalise_correlations(
+ mat_slice_vis_gated,norm=mat_slice_vis_on
+ )
+
+ # Solve for eigenvalues and eigenvectors.
+ # The gain solutions for the zero'th feed
+ # are forced to be real and positive.
+ # This means that the phases of the gain
+ # solutions are relative phases with respect
+ # to the zero'th feed.
+ try:
+ eigenvals,eigenvecs=eigh_no_diagonal(
+ mat_slice_vis_gated,niter=niter
+ )
+
+ ifeigenvecs[0,eigenvals.argmax()]<0:
+ sign0=-1
+ else:
+ sign0=1
+
+ gains[f,:,t]=(
+ sign0
+ *sqrt_autos
+ *eigenvecs[:,eigenvals.argmax()]
+ *np.sqrt(np.abs(eigenvals.max()))
+ )
+ evals[f,:,t]=eigenvals
+
+ exceptLA.LinAlgError:
+ pass
+
+ returngains,evals
+
+
+
+
+[docs]
+defsubtract_sky_noise(vis,Nchannels,timestamp,adc_ch_ref,fbin_ref):
+"""Removes sky and system noise contributions from noise injection visibility
+ data.
+
+ By looking at the autocorrelation of the reference channel adc_ch_ref
+ for frequency bin fbin_ref, finds timestamps indices for which the signal is
+ on and off. For every noise signal period, the subcycles with the noise
+ signal on and off are averaged separatedly and then subtracted.
+
+ It is assumed that there are at least 5 noise signal cycles in the data.
+ The first and last noise on subcycles are discarded since those cycles may
+ be truncated.
+
+ Parameters
+ ----------
+ vis: 3d array
+ Noise injection visibility array in chime's canonical format. vis has
+ dimensions [frequency, corr. number, time].
+ Nchannels : int
+ Order of the visibility matrix (number of channels)
+ timestamp : 1d array
+ Timestamps for the visibility array vis
+ adc_ch_ref : int in the range 0 <= adc_ch_ref <= N_channels-1
+ Reference channel (typically, but not necessaritly the channel
+ corresponding to the directly injected noise signal) used to find
+ timestamps indices for which the signal is on and off.
+ on and off.
+ fbin_ref : int in the range 0 <= fbin_ref <= np.size(vis, 0)-1
+ frequency bin used to find timestamps indices for which the signal is
+ on and off
+
+ Returns
+ -------
+ A dictionary with keys
+ time_index_on : 1d array
+ timestamp indices for noise signal on.
+ time_index_off : 1d array
+ timestamp indices for noise signal off.
+ timestamp_on_dec : 1d array
+ timestamps for noise signal on after averaging.
+ timestamp_off_dec : 1d array
+ timestamps for noise signal off after averaging.
+ timestamp_dec : 1d array
+ timestamps for visibility data after averaging and subtracting on and
+ off subcycles. These timestaps represent the time for every noise cycle
+ and thus, these are the timestaps for the gain solutions.
+ vis_on_dec : 3d array
+ visibilities for noise signal on after averaging.
+ vis_off_dec : 3d array
+ visibilities for noise signal off after averaging.
+ vis_dec_sub : 3d array
+ visibilities data after averaging and subtracting on and
+ off subcycles.
+ cor_prod_ref : int
+ correlation index corresponding to the autocorrelation of the reference
+ channel
+ """
+
+ # Find correlation product of autocorrelation of ref channel in read data
+ # Indices of autocorrelations for selected channels
+ cor_prod_auto=[k*Nchannels-(k*(k-1))//2forkinrange(Nchannels)]
+ cor_prod_ref=cor_prod_auto[adc_ch_ref]
+ auto_ref=np.real(vis[fbin_ref,cor_prod_ref,:])
+
+ # Find timestamp indices for noise signal on and off
+ # auto_ref points above/below auto_ref_mean are considered to be on/off
+ auto_ref_mean=np.mean(auto_ref)
+ time_index_on=np.where(auto_ref>=auto_ref_mean)[0]
+ time_index_off=np.where(auto_ref<auto_ref_mean)[0]
+ diff_index_on=np.diff(time_index_on)
+ # Indices indicating ends of noise-on subsets
+ index_end_on_cycle=time_index_on[np.where(diff_index_on>1)[0]]
+ # Indices indicating starts of noise-on subsets
+ index_start_on_cycle=time_index_on[np.where(diff_index_on>1)[0]+1]
+ vis_on_dec=[]# Decimated visibility on points
+ vis_off_dec=[]
+ timestamp_on_dec=[]# Timestamps of visibility on points
+ timestamp_off_dec=[]
+ timestamp_dec=[]# Timestamp of decimated visibility (on minus off)
+
+ foriinrange(len(index_end_on_cycle)-1):
+ # Visibilities with noise on for cycle i
+ vis_on_cycle_i=vis[
+ :,:,index_start_on_cycle[i]:index_end_on_cycle[i+1]+1
+ ]
+ # Visibilities with noise off for cycle i
+ vis_off_cycle_i=vis[:,:,index_end_on_cycle[i]+1:index_start_on_cycle[i]]
+
+ # New lines to find indices of maximum and minimum point of each cycle based on the reference channel
+ index_max_i=auto_ref[
+ index_start_on_cycle[i]:index_end_on_cycle[i+1]+1
+ ].argmax()
+ index_min_i=auto_ref[
+ index_end_on_cycle[i]+1:index_start_on_cycle[i]
+ ].argmin()
+ vis_on_dec.append(vis_on_cycle_i[:,:,index_max_i])
+ vis_off_dec.append(vis_off_cycle_i[:,:,index_min_i])
+
+ # Instead of averaging all the data with noise on of a cycle, we take the median
+ # vis_on_dec.append(np.median(vis_on_cycle_i.real, axis=2)+1j*np.median(vis_on_cycle_i.imag, axis=2))
+ # vis_off_dec.append(np.median(vis_off_cycle_i.real, axis=2)+1j*np.median(vis_off_cycle_i.imag, axis=2))
+ timestamp_on_dec.append(
+ np.mean(timestamp[index_start_on_cycle[i]:index_end_on_cycle[i+1]+1])
+ )
+ timestamp_off_dec.append(
+ np.mean(timestamp[index_end_on_cycle[i]+1:index_start_on_cycle[i]])
+ )
+ timestamp_dec.append(
+ np.mean(
+ timestamp[index_end_on_cycle[i]+1:index_end_on_cycle[i+1]+1]
+ )
+ )
+
+ vis_on_dec=np.dstack(vis_on_dec)
+ vis_off_dec=np.dstack(vis_off_dec)
+ vis_dec_sub=vis_on_dec-vis_off_dec
+ timestamp_on_dec=np.array(timestamp_on_dec)
+ timestamp_off_dec=np.array(timestamp_off_dec)
+ timestamp_dec=np.array(timestamp_dec)
+
+ return{
+ "time_index_on":time_index_on,
+ "time_index_off":time_index_off,
+ "vis_on_dec":vis_on_dec,
+ "vis_off_dec":vis_off_dec,
+ "vis_dec_sub":vis_dec_sub,
+ "timestamp_on_dec":timestamp_on_dec,
+ "timestamp_off_dec":timestamp_off_dec,
+ "timestamp_dec":timestamp_dec,
+ "cor_prod_ref":cor_prod_ref,
+ }
+
+
+
+
+[docs]
+defgains2utvec_tf(gains):
+"""Converts gain array to CHIME visibility format for all frequencies and
+ time frames.
+
+ For every frequency and time frame, converts a gain vector into an outer
+ product matrix and then vectorizes its upper triangle to obtain a vector in
+ the same format as the CHIME visibility matrix.
+
+ Converting the gain arrays to CHIME visibility format makes easier to
+ apply the gain corrections to the visibility data. See example below.
+
+ Parameters
+ ----------
+ gains : 3d array
+ Input array with the gains for all frequencies, channels and time frames
+ in the fromat of ni_gains_evalues_tf. g has dimensions
+ [frequency, channels, time].
+
+ Returns
+ -------
+ G_ut : 3d array
+ Output array with dimmensions [frequency, corr. number, time]. For
+ every frequency and time frame, contains the vectorized form of upper
+ triangle for the outer product of the respective gain vector.
+
+ Example
+ -------
+ To compute the gains from a set of noise injection pass0 data and apply the
+ gains to the visibilities run:
+
+ >>> from ch_util import andata
+ >>> from ch_util import import ni_utils as ni
+ >>> data = andata.Reader('/scratch/k/krs/jrs65/chime_archive/20140916T173334Z_blanchard_corr/000[0-3]*.h5')
+ >>> readdata = data.read()
+ >>> nidata = ni.ni_data(readdata, 16)
+ >>> nidata.get_ni_gains()
+ >>> G_ut = ni.gains2utvec(nidata.ni_gains)
+ >>> corrected_vis = nidata.vis_off_dec/G_ut
+
+ See also
+ --------
+ gains2utvec, ni_gains_evalues_tf
+ """
+
+ Nfreqs=np.size(gains,0)# Number of frequencies
+ Ntimeframes=np.size(gains,2)# Number of time frames
+ Nchannels=np.size(gains,1)
+ Ncorrprods=Nchannels*(Nchannels+1)//2# Number of correlation products
+ G_ut=np.zeros((Nfreqs,Ncorrprods,Ntimeframes),dtype=np.complex)
+
+ forfinrange(Nfreqs):
+ fortinrange(Ntimeframes):
+ G_ut[f,:,t]=gains2utvec(gains[f,:,t])
+
+ returnG_ut
+
+
+
+
+[docs]
+defgains2utvec(g):
+"""Converts a vector into an outer product matrix and vectorizes its upper
+ triangle to obtain a vector in same format as the CHIME visibility matrix.
+
+ Parameters
+ ----------
+ g : 1d array
+ gain vector
+
+ Returns
+ -------
+ 1d array with vectorized form of upper triangle for the outer product of g
+ """
+
+ n=len(g)
+ G=np.dot(g.reshape(n,1),g.conj().reshape(1,n))
+ returnmat2utvec(G)
+[docs]
+defwaterfall(
+ data,freq_sel=None,prod_sel=None,time_sel=None,part_sel=None,**kwargs
+):
+"""Two dimensional plot of a visibility vs time and frequency.
+
+ Parameters
+ ----------
+ data : numpy array or :class:`~ch_util.andata.AnData` object
+ Data to plot. If a numpy array, must be 2D or 3D.
+ freq_sel : valid numpy index
+ Selects data to include along the frequency axis.
+ prod_sel : valid numpy index
+ Selects data to include along the correlation product axis. If *data*
+ is a 2D array, this argument is ignored.
+ time_sel : valid numpy index
+ Selects data to include along the time axis.
+ part_sel : string, one of: 'real', 'imag', 'mag', 'phase' or 'complex'
+ Selects what part of data to plot. If 'None', plot real part.
+
+ Examples
+ --------
+
+ >>> data = np.ones((100, 100))
+ >>> waterfall(data)
+
+ >>> data = andata.AnData.from_acq("...")
+ >>> waterfall(data, prod_sel=5, out_file='filename.png')
+
+ To make a plot normalized by a baseline of the median-filtered
+ power spectrum averaged over 200 time bins starting at bin 0 with
+ a median filter window of 40 bins:
+ >>> data = andata.AnData.from_acq("...")
+ >>> med_filt_arg = ['new',200,0,40]
+ >>> waterfall(data, prod_sel=21, med_filt=med_filt_arg)
+
+ You can also make it save the calculated baseline to a file,
+ by providing the filename:
+ >>> data = andata.AnData.from_acq("...")
+ >>> med_filt_arg = ['new',200,0,40,'base_filename.dat']
+ >>> waterfall(data, prod_sel=21, med_filt=med_filt_arg)
+
+ ...or to use a previously obtained baseline to normalize data:
+ (where bsln is either a numpy array or a list with length equal
+ to the frequency axis of the data)
+ >>> data = andata.AnData.from_acq("...")
+ >>> med_filt_arg = ['old',bsln]
+ >>> waterfall(data, prod_sel=21, med_filt=med_filt_arg)
+
+ To make a full day plot of 01/14/2014,
+ rebinned to 4000 time bins:
+ >>> data = andata.AnData.from_acq("...")
+ >>> full_day_arg = [[2014,01,14],4000,'time']
+ >>> waterfall(data, prod_sel=21, full_day=full_day_arg)
+
+ """
+ ########## Section for retrieving keyword arguments.##############
+ # Please remove from the kwargs dictionary any arguments for
+ # which you provided functionality in waterfall(). The resulting
+ # dictionary is going to be passed on to imshow()
+
+ aspect=kwargs.pop("aspect",None)# float. Aspect ratio of image
+ show_plot=kwargs.pop("show_plot",None)# True or False. Interactive plot
+ out_file=kwargs.pop("out_file",None)# str. File name to save to
+ res=kwargs.pop("res",None)# int. Resolution of saved image in dpi
+ title=kwargs.pop("title",None)# str. Graph title.
+ x_label=kwargs.pop("x_label",None)# str.
+ y_label=kwargs.pop("y_label",None)# str.
+ med_filt=kwargs.pop("med_filt",None)# List of length 2 or 4. See examples.
+ full_day=kwargs.pop("full_day",None)# List of length 3. See examples.
+ cbar_label=kwargs.pop("cbar_label",None)# str. Colorbar label.
+
+ ##################################################################
+
+ # waterfall() does not accept 'complex'
+ ifpart_sel=="complex":
+ msg='waterfall() does not take "complex" for "part_sel"'" argument."
+ raiseValueError(msg)
+
+ fig=plt.figure()
+ ax=fig.add_subplot(111)
+
+ # Set title, if given:
+ iftitle!=None:
+ ax.set_title(title)
+
+ # Setting labels, if given:
+ ifx_label!=None:
+ ax.set_xlabel(x_label)
+ ify_label!=None:
+ ax.set_ylabel(y_label)
+
+ # Preparing data shape for plotting:
+ plt_data=_coerce_data_shape(
+ data,freq_sel,prod_sel,time_sel,part_sel,axes=(1,)
+ )
+ ifisinstance(data,andata.AnData):
+ tmstp=_select_time(data,time_sel)
+
+ # Apply median filter, if 'med_filt' is given:
+ ifmed_filt!=None:
+ msg="Warning: Wrong value for 'med_filt'. Ignoring argument."
+ ifmed_filt[0]=="new":
+ # Apply median filter:
+ plt_data,baseline=_med_filter(
+ plt_data,med_filt[1],med_filt[2],med_filt[3]
+ )
+ iflen(med_filt)==5:
+ # Save baseline to file, if given:
+ fileBaseOut=open(med_filt[4],"w")
+ foriiinrange(len(baseline)):
+ fileBaseOut.write("{0}\n".format(baseline[ii,0]))
+ fileBaseOut.close()
+ elifmed_filt[0]=="old":
+ # Reshape baseline to ensure type and shape:
+ baseline=np.array(med_filt[1]).reshape(len(med_filt[1]),1)
+ # Normalize data:
+ plt_data=plt_data/baseline
+ else:
+ print(msg)
+
+ # Shape data to full day, if 'full_day' is given:
+ iffull_day!=None:
+ plt_data=_full_day_shape(
+ plt_data,
+ tmstp,
+ date=full_day[0],
+ n_bins=full_day[1],
+ axis=full_day[2],
+ ax=ax,
+ )
+
+ # Call imshow reversing frequency order
+ wtfl=ax.imshow(plt_data[::-1,:],**kwargs)
+
+ # Ajust aspect ratio of image if aspect is provided:
+ ifaspect!=None:
+ _force_aspect(ax,aspect)
+
+ # Ajust colorbar size:
+ ifaspect>=1.0:
+ shrink=1/float(aspect)
+ else:
+ shrink=1.0
+ cbar=fig.colorbar(wtfl,shrink=shrink)
+ else:
+ cbar=fig.colorbar(wtfl)
+
+ # Set label to colorbar, if given:
+ ifcbar_label!=None:
+ cbar.set_label(cbar_label)
+
+ # Output depends on keyword arguments:
+ ifshow_plot==True:
+ plt.show()
+ elif(show_plot!=None)and(show_plot!=False):
+ msg=(
+ 'Optional keyword argument "show_plot" should receive either'
+ ' "True" or "False". Received "{0}". Ignoring argument.'.format(show_plot)
+ )
+ warnings.warn(msg,SyntaxWarning)
+
+ # Save to file if filename is provided:
+ ifout_file!=None:
+ ifres!=None:
+ fig.savefig(out_file,dpi=res)
+ else:
+ fig.savefig(out_file)
+
+ plt.close(fig)
+
+
+
+
+[docs]
+defspectra(data,freq_sel=None,prod_sel=None,time_sel=None,part_sel=None,**kwargs):
+"""Plots spectra at different times and for different correlation products."""
+
+ plt_data=_coerce_data_shape(data,freq_sel,prod_sel,time_sel,axes=())
+ ntime=plt_data.shape[2]
+ nprod=plt_data.shape[1]
+ foriiinrange(ntime):
+ forjjinrange(nprod):
+ plt.plot(plt_data[:,ii,jj])
+
+
+
+
+[docs]
+deftime_ordered(
+ data,freq_sel=None,prod_sel=None,time_sel=None,part_sel=None,**kwargs
+):
+"""Plots data vs time for different frequencies and corr-pords."""
+
+ pass
+
+
+
+def_coerce_data_shape(
+ data,freq_sel=None,prod_sel=None,time_sel=None,part_sel=None,axes=()
+):
+"""Gets well shaped data array for plotting.
+
+ Parameters
+ ----------
+ data : numpy array or :class:`~ch_util.andata.AnData` object
+ Data to coerse.
+ freq_sel : valid numpy index
+ Selects data to include along the frequency axis. Default slices the
+ full axis.
+ prod_sel : valid numpy index
+ Selects data to include along the correlation product axis. If *data*
+ is a 2D array, this argument is ignored. Default slices the
+ full axis.
+ time_sel : valid numpy index
+ Selects data to include along the time axis. Default slices the
+ full axis.
+ part_sel : string, one of: 'real', 'imag', 'mag', 'phase' or 'complex'
+ Selects what part of data to plot. If 'None', plot real part.
+ axes : tuple or axis numbers
+ Axes to eliminate
+
+ Returns
+ -------
+ plt_data : numpy array
+ The dimentionality of the array is guaranteed to be
+ ``plt_data == 3 - len(axes)``.
+
+ Raises
+ ------
+ ValueError
+ If data provided could not be coersed.
+
+ Examples
+ --------
+
+ Lets start with simple sliceing of numpy array data.
+
+ >>> data = np.ones((5, 7, 3))
+ >>> _coerse_data_shape(data, [2, 3, 4], 3, None).shape
+ (3, 1, 3)
+
+ Notice that the out put is 3D even though normal numpy indexing would have
+ eliminated the correation-product axis. This is because the *axes*
+ parameter is set to it's default meaning 3D output is required. If we
+ instead tell it to eliminate the product axis:
+
+ >>> _coerse_data_shape(data, [2, 3, 4], 3, None, axes=(1,)).shape
+ (3, 3)
+
+ If an axis to be eliminated is not length 1, a :exc:`ValueError` is raised.
+
+ >>> _coerse_data_shape(data, [2, 3, 4], [3, 2], None, axes=(1,))
+ Traceback (most recent call last)
+ ...
+ ValueError: Need to eliminate axis 1 but it is not length 1.
+
+ The input data may be less than 3D. In this case *axes* indicates which axes
+ are missing.
+
+ >>> data = np.ones((2, 3))
+ >>> _coerse_data_shape(data, 1, None, None, axes=(1,)).shape
+ (1, 3)
+
+ Example of selecting part to plot:
+
+ >>> data = np.ones((2,3,4))*(5+6j)
+ >>> _coerce_data_shape(data,None,1,None,part_sel='imag',axes=(1,))
+ array([[ 6., 6., 6., 6.],
+ [ 6., 6., 6., 6.]])
+
+ All this works with :class:`~chutil.andata.AnData` input data, where the
+ visibilities are treated as a 3D array.
+
+ """
+
+ axes=sorted(axes)
+ ifisinstance(data,andata.AnData):
+ data=data.vis
+ ifdata.ndim!=3:
+ ifdata.ndim!=3-len(axes):
+ msg=(
+ "Could no interpret input data axes. Got %dD data and need"
+ " coerse to %dD data"
+ )%(data.ndim,3-len(axes))
+ raiseValueError(msg)
+ # Temporarily make the data 3D (for slicing), will reshape in the end.
+ shape=data.shape
+ foraxisinaxes:
+ shape=shape[:axis]+(1,)+shape[axis:]
+ data=np.reshape(data,shape)
+ # Select data.
+ ifisinstance(freq_sel,int):
+ freq_sel=[freq_sel]
+ eliffreq_selisNone:
+ freq_sel=slice(None)
+ data=data[freq_sel]
+ ifisinstance(prod_sel,int):
+ prod_sel=[prod_sel]
+ elifprod_selisNone:
+ prod_sel=slice(None)
+ data=data[:,prod_sel]
+ ifisinstance(time_sel,int):
+ time_sel=[time_sel]
+ eliftime_selisNone:
+ time_sel=slice(None)
+ data=data[:,:,time_sel]
+ ifdata.ndim!=3:
+ raiseRuntimeError("Shouldn't have happend")
+ # Now reshape to the correct dimensionality.
+ shape=data.shape
+ axes.reverse()
+ foraxisinaxes:
+ ifnotshape[axis]==1:
+ msg="Need to eliminate axis %d but it is not length 1."%axis
+ raiseValueError(msg)
+ shape=shape[:axis]+shape[axis+1:]
+ data.shape=shape
+
+ # Selects what part to plot:
+ # Defaults to plotting real part of data.
+ ifpart_sel=="real"orpart_sel==None:
+ data=data.real
+ elifpart_sel=="imag":
+ data=data.imag
+ elifpart_sel=="mag":
+ data=(data.real**2+data.imag**2)**(0.5)
+ elifpart_sel=="phase":
+ data=np.arctan(data.imag/data.real)
+ elifpart_sel=="complex":
+ pass
+ else:
+ msg=(
+ 'Optional keyword argument "part_sel" has to receive'
+ ' one of "real", "imag", "mag", "phase" or "complex".'
+ ' Received "{0}"'.format(part_sel)
+ )
+ raiseValueError(msg)
+
+ returndata
+
+
+def_select_time(data,time_sel):
+"""Reshape time stamp vector acording to 'time_sel'
+
+ Parameters
+ ----------
+ data : class:`~ch_util.andata.AnData` object
+ Data to take time stamp from.
+ time_sel : valid numpy index
+ Selects data to include along the time axis. Default slices the
+ full axis.
+
+ Returns
+ -------
+ tmstp : numpy array
+ time stamp with selected times
+
+ """
+ ifisinstance(data,andata.AnData):
+ tmstp=data.timestamp
+ ifisinstance(time_sel,int):
+ time_sel=[time_sel]
+ eliftime_selisNone:
+ time_sel=slice(None)
+ tmstp=tmstp[time_sel]
+
+ returntmstp
+
+
+def_full_day_shape(data,tmstp,date,n_bins=8640,axis="solar",ax=None):
+"""Rebin data in linear time or solar azimuth.
+
+ Parameters
+ ----------
+ data : numpy array
+ Data to plot. Must be 2D.
+ tmstp : numpy array
+ Time stamp of data to plot.
+ ax : matplotlib.axes.Axes instance
+ Axes to receive plot. Time/azimuth ticks
+ and labels will be set acordingly
+ date : python list of length 3
+ Date of day to plot in the format:
+ [yyyy,mm,dd], al entries 'int'.
+ n_bins : int
+ Number of time/azimuth bins in new matrix.
+ axis : str
+ If 'solar': rebin by solar azimuth
+ If 'time': rebin by time
+
+ Returns
+ -------
+ Z : numpy ndarray
+ New rebinned matrix
+
+ Example
+ -------
+ For example of usage, see plot.waterfall() documentation
+
+ """
+ n_bins=int(n_bins)
+ start_time=datetime.datetime(date[0],date[1],date[2],8,0,0)# UTC-8
+ end_time=start_time+datetime.timedelta(days=1)
+ unix_start=ephemeris.datetime_to_unix(start_time)
+ unix_end=ephemeris.datetime_to_unix(end_time)
+ print("Re-binning full day data to plot")
+
+ ifaxis=="solar":
+ bin_width=float(2*np.pi)/float(n_bins)
+ bin_ranges=[]
+ foriiinrange(n_bins):
+ az1=ii*bin_width
+ az2=az1+bin_width
+ bin_ranges.append([az1,az2])
+
+ values_to_sum=[]
+ foriiinrange(n_bins):
+ values_to_sum.append([])
+
+ start_range=[unix_start-1.5*3600,unix_start+0.5*3600]
+ end_range=[unix_end-1.5*3600,unix_end+0.5*3600]
+
+ n_added=0
+
+ foriiinrange(len(tmstp)):
+ in_range=(tmstp[ii]>start_range[0])and(tmstp[ii]<end_range[1])
+ ifin_range:
+ sf_time=ephemeris.unix_to_skyfield_time(tmstp[ii])
+ sun=ephemeris.skyfield_wrapper.ephemeris["sun"]
+ obs=ephemeris.chime.skyfield_obs().at(sf_time)
+ azim=obs.observe(sun).apparent().altaz()[1].radians
+
+ in_start_range=(tmstp[ii]>start_range[0])and(
+ tmstp[ii]<start_range[1]
+ )
+ in_end_range=(tmstp[ii]>end_range[0])and(tmstp[ii]<end_range[1])
+
+ ifin_start_range:
+ forjjinrange(int(n_bins//2)):
+ if(azim>bin_ranges[jj][0])and(azim<=bin_ranges[jj][1]):
+ values_to_sum[jj].append(ii)
+ n_added=n_added+1
+ break
+ elifin_end_range:
+ forjjinrange(int(n_bins//2)):
+ kk=n_bins-jj-1
+ if(azim>bin_ranges[kk][0])and(azim<=bin_ranges[kk][1]):
+ values_to_sum[kk].append(ii)
+ n_added=n_added+1
+ break
+ else:
+ forjjinrange(n_bins):
+ if(azim>bin_ranges[jj][0])and(azim<=bin_ranges[jj][1]):
+ values_to_sum[jj].append(ii)
+ n_added=n_added+1
+ break
+
+ # Set azimuth ticks, if given:
+ ifax!=None:
+ tck_stp=n_bins/6.0
+ ticks=np.array(
+ [
+ int(tck_stp),
+ int(2*tck_stp),
+ int(3*tck_stp),
+ int(4*tck_stp),
+ int(5*tck_stp),
+ ]
+ )
+ ax.set_xticks(ticks)
+ labels=["60","120","180","240","300"]
+ ax.set_xticklabels(labels)
+ # Set label:
+ ax.set_xlabel("Solar azimuth (degrees)")
+
+ elifaxis=="time":
+ bin_width=float(86400)/float(n_bins)
+ bin_ranges=[]
+ foriiinrange(n_bins):
+ t1=unix_start+ii*bin_width
+ t2=t1+bin_width
+ bin_ranges.append([t1,t2])
+
+ values_to_sum=[]
+ foriiinrange(n_bins):
+ values_to_sum.append([])
+
+ n_added=0
+
+ foriiinrange(len(tmstp)):
+ in_range=(tmstp[ii]>=unix_start)and(tmstp[ii]<=unix_end)
+ ifin_range:
+ time=tmstp[ii]
+ forjjinrange(n_bins):
+ if(time>bin_ranges[jj][0])and(time<=bin_ranges[jj][1]):
+ values_to_sum[jj].append(ii)
+ n_added=n_added+1
+ break
+
+ # Set time ticks, if given:
+ ifax!=None:
+ tck_stp=n_bins/6.0
+ ticks=np.array(
+ [
+ int(tck_stp),
+ int(2*tck_stp),
+ int(3*tck_stp),
+ int(4*tck_stp),
+ int(5*tck_stp),
+ ]
+ )
+ ax.set_xticks(ticks)
+ labels=["04:00","08:00","12:00","16:00","20:00"]
+ ax.set_xticklabels(labels)
+ # Set label:
+ ax.set_xlabel("Time (UTC-8 hours)")
+
+ print("Number of 10-second bins added to full day data: {0}".format(n_added))
+
+ # Set new array to NaN for subsequent masking:
+ Z=np.ones((1024,n_bins))
+ foriiinrange(1024):
+ forjjinrange(n_bins):
+ Z[ii,jj]=float("NaN")
+
+ foriiinrange(n_bins):
+ n_col=len(values_to_sum[ii])
+ ifn_col>0:
+ col=np.zeros((1024))
+ forjjinrange(n_col):
+ col=col+data[:,values_to_sum[ii][jj]]
+ Z[:,ii]=col/float(n_col)
+
+ returnZ
+
+
+def_force_aspect(ax,aspect=1.0):
+"""Force desired aspect ratio into image axes.
+
+ Parameters
+ ----------
+ ax : matplotlib.axes.Axes instance
+ Axes that will be set to the desired aspect ratio
+ aspect : float or int
+ Desired aspect ratio in horizontal/vertical order
+
+ Motivation
+ ----------
+ Apparently, the 'aspect' keyword argument in Imshow() is
+ not working properlly in this version of matplotlib (1.1.1rc)
+
+ Example
+ -------
+
+ data = np.ones((100,200))
+ fig = plt.figure()
+ ax = fig.add_subplot(111)
+ ax.imshow(data)
+ _force_aspect(ax,aspect=1.)
+ plt.show()
+
+ Will produce a square solid image.
+
+ """
+
+ im=ax.get_images()
+ extent=im[0].get_extent()
+ ax.set_aspect(abs((extent[1]-extent[0])/float(extent[3]-extent[2]))/aspect)
+
+
+def_med_filter(data,n_bins=200,i_bin=0,filt_window=37):
+"""Normalize a 2D array by its power spectrum averaged over 'n_bins' starting at 'i_bin'.
+
+ Parameters
+ ----------
+ data : numpy.ndarray
+ Data to be normalized
+
+ n_bins : integer
+ Number of bins over which to average the power spectrum
+
+ i_bin : integer
+ First bin of the range over which to average the power spectrum
+
+ filt_window : integer
+ Width of the window for the median filter. The filter is applied
+ once with this window and a second time with 1/3 of this window width.
+
+ Returns
+ -------
+ rel_power : 2d array normalized by average power spectrum (baseline)
+ medfilt_baseline : Average power spectrum
+
+ Issues
+ ------
+ Assumes frequency in first index and time in second index
+ Assumes data has the standard 1024 frequency bins
+
+ Comments
+ --------
+ If entry is 0 in data and in baseline, entry is set to 1. in
+ normalized matrix
+
+ """
+ # If n_bins biger than array, average over entire array:
+ ifdata.shape[1]>n_bins:
+ sliced2darray=data[:,0:(n_bins-1)]
+ else:
+ sliced2darray=data
+
+ # Mean of range selected:
+ mean_arr=np.mean(sliced2darray,axis=-1)
+ # Standard deviation:
+ std_arr=np.std(sliced2darray,axis=-1)
+ # Standard deviation of the mean:
+ sigma=np.median(std_arr)/(sliced2darray.shape[1])**(0.5)
+ print("Taking median filter")
+ medfilt_arr=sig.medfilt(mean_arr,filt_window)
+ # Extract RFI:
+ non_rfi_mask=(mean_arr-medfilt_arr)<5*sigma
+ print("Number of good data points for baseline: ",np.sum(non_rfi_mask))
+ print("out of 1024 points - ",np.sum(non_rfi_mask/float(1024))*100,"%")
+ # Interpolate result:
+ freq=np.linspace(400,800,1024)
+ interpolat_arr_baseline=np.interp(
+ freq,freq[non_rfi_mask],mean_arr[non_rfi_mask]
+ )
+ # Median filter a second time:
+ small_window=int(filt_window//3)
+ # Has to be odd:
+ ifsmall_window%2==0:
+ small_window=small_window+1
+ medfilt_baseline=np.reshape(
+ sig.medfilt(interpolat_arr_baseline,small_window),
+ (interpolat_arr_baseline.shape[0],1),
+ )
+
+ # Boolean mask for entries where original data and baseline are zero:
+ mask=np.where(medfilt_baseline==0,data,1)==0
+ # Normalize data:
+ rel_power=data/medfilt_baseline
+ # Set masked entries to 1:
+ rel_power[mask]=1.0
+
+ returnrel_power,medfilt_baseline
+
+"""Tools for RFI flagging
+
+This module contains tools for finding and removing Radio Frequency Interference
+(RFI).
+
+Note that this generates masks where the elements containing RFI are marked as
+:obj:`True`, and the remaining elements are marked :obj:`False`. This is in
+contrast to the routines in :mod:`ch_pipeline.rfi` which generates a inverse
+noise weighting, where RFI containing elements are effectively :obj:`False`, and
+the remainder are :obj:`True`.
+
+There are general purpose routines for flagging RFI in `andata` like datasets:
+
+- :py:meth:`flag_dataset`
+- :py:meth:`number_deviations`
+
+For more control there are specific routines that can be called:
+
+- :py:meth:`mad_cut_2d`
+- :py:meth:`mad_cut_1d`
+- :py:meth:`mad_cut_rolling`
+- :py:meth:`spectral_cut`
+- :py:meth:`frequency_mask`
+- :py:meth:`sir1d`
+- :py:meth:`sir`
+"""
+
+importwarnings
+importlogging
+fromtypingimportTuple,Optional,Union
+
+importnumpyasnp
+importscipy.signalassig
+
+from.importtools,ephemeris
+
+# Set up logging
+logger=logging.getLogger(__name__)
+logger.addHandler(logging.NullHandler())
+
+
+# Ranges of bad frequencies given by their start time (in unix time) and corresponding start and end frequencies (in MHz)
+# If the start time is not specified, t = [], the flag is applied to all CSDs
+BAD_FREQUENCIES={
+ "chime":[
+ ### Bad bands at first light
+ [[None,None],[449.41,450.98]],
+ [[None,None],[454.88,456.05]],
+ [[None,None],[457.62,459.18]],
+ [[None,None],[483.01,485.35]],
+ [[None,None],[487.70,494.34]],
+ [[None,None],[497.85,506.05]],
+ [[None,None],[529.10,536.52]],
+ [[None,None],[541.60,548.00]],
+ ### Additional bad bands
+ # Narrow, high power bands visible in sensitivities and
+ # some longer baselines. There is some sporadic rfi in between
+ # the two bands
+ [[None,None],[460.15,460.55]],
+ [[None,None],[464.00,470.32]],
+ # 6 MHz band (reported by Simon)
+ [[None,None],[505.85,511.71]],
+ # Bright band which has been present since early on
+ [[None,None],[517.97,525.00]],
+ # UHF TV Channel 27 ending CSD 3212 inclusive (2022/08/24)
+ # This is extended until CSD 3446 (2023/04/13) to account for gain errors
+ [[None,1681410777],[548.00,554.49]],
+ [[None,None],[564.65,578.00]],
+ # UHF TV Channel 32 ending CSD 3213 inclusive (2022/08/25)
+ # This is extended until CSD 3446 (2023/04/13) to account for gain errors
+ [[None,1681410777],[578.00,585.35]],
+ # from CSD 2893 (2021/10/09 - ) UHF TV Channel 33 (reported by Seth)
+ [[1633758888,None],[584.00,590.00]],
+ # UHF TV Channel 35
+ [[1633758888,None],[596.00,602.00]],
+ # Low power band visible in long baselines
+ [[None,None],[602.00,607.82]],
+ # from CSD 2243 (2019/12/31 - ) Rogers’ new 600 MHz band
+ [[1577755022,None],[617.00,627.00]],
+ [[None,None],[693.16,693.55]],
+ [[None,None],[694.34,696.68]],
+ # from CSD 2080 (2019/07/21 - ) Blobs, Channels 55 and 56
+ [[1564051033,None],[716.00,728.00]],
+ [[None,None],[729.88,745.12]],
+ [[None,None],[746.29,756.45]],
+ ],
+ "kko":[
+ # Bad bands from statistical analysis of Jan 20, 2023 N2 data
+ [[None,None],[433.59,433.98]],
+ [[None,None],[439.84,440.62]],
+ [[None,None],[483.20,484.38]],
+ [[None,None],[616.80,626.95]],
+ [[None,None],[799.61,800.00]],
+ # Notch filter stoppband + leakage
+ [[None,None],[710.55,757.81]],
+ ],
+ "gbo":[],
+ "hco":[],
+}
+
+
+
+[docs]
+defflag_dataset(
+ data,freq_width=10.0,time_width=420.0,threshold=5.0,flag1d=False,rolling=False
+):
+"""RFI flag the dataset. This function wraps `number_deviations`,
+ and remains largely for backwards compatability. The pipeline code
+ now calls `number_deviations` directly.
+
+ Parameters
+ ----------
+ data : `andata.CorrData`
+ Must contain vis and weight attribute that are both
+ `np.ndarray[nfreq, nprod, ntime]`. Note that this
+ function does not work with CorrData that has
+ been stacked over redundant baselines.
+ freq_width : float
+ Frequency interval in *MHz* to compare across.
+ time_width : float
+ Time interval in *seconds* to compare.
+ threshold : float
+ Threshold in MAD over which to cut out RFI.
+ rolling : bool
+ Use a rolling window instead of distinct blocks.
+ flag1d : bool, optional
+ Only apply the MAD cut in the time direction. This is useful if the
+ frequency coverage is sparse.
+
+ Returns
+ -------
+ mask : np.ndarray
+ RFI mask, output shape is the same as input visibilities.
+ """
+
+ auto_ii,auto_vis,auto_ndev=number_deviations(
+ data,
+ freq_width=freq_width,
+ time_width=time_width,
+ flag1d=flag1d,
+ rolling=rolling,
+ stack=False,
+ )
+
+ auto_mask=np.abs(auto_ndev)>threshold
+
+ # Apply the frequency cut to the data (add here because we are distributed
+ # over products and its easy)
+ if"time"indata.index_map:
+ timestamp=data.time[0]
+ elif"ra"indata.index_map:
+ timestamp=ephemeris.csd_to_unix(data.attrs["lsd"])
+
+ freq_mask=frequency_mask(data.freq[:],timestamp=timestamp)
+ auto_ii,auto_mask=np.logical_or(auto_mask,freq_mask[:,np.newaxis,np.newaxis])
+
+ # Create an empty mask for the full dataset
+ mask=np.zeros(data.vis[:].shape,dtype=bool)
+
+ # Loop over all products and flag if either inputs auto correlation was flagged
+ forpiinrange(data.nprod):
+ ii,ij=data.index_map["prod"][pi]
+
+ ifiiinauto_ii:
+ ai=auto_ii.index(ii)
+ mask[:,pi]=np.logical_or(mask[:,pi],auto_mask[:,ai])
+
+ ifijinauto_ii:
+ aj=auto_ii.index(ij)
+ mask[:,pi]=np.logical_or(mask[:,pi],auto_mask[:,aj])
+
+ returnmask
+
+
+
+
+[docs]
+defnumber_deviations(
+ data,
+ freq_width=10.0,
+ time_width=420.0,
+ flag1d=False,
+ apply_static_mask=False,
+ rolling=False,
+ stack=False,
+ normalize=False,
+ fill_value=None,
+):
+"""Calculate the number of median absolute deviations (MAD)
+ of the autocorrelations from the local median.
+
+ Parameters
+ ----------
+ data : `andata.CorrData`
+ Must contain vis and weight attributes that are both
+ `np.ndarray[nfreq, nprod, ntime]`.
+ freq_width : float
+ Frequency interval in *MHz* to compare across.
+ time_width : float
+ Time interval in *seconds* to compare across.
+ flag1d : bool
+ Only apply the MAD cut in the time direction. This is useful if the
+ frequency coverage is sparse.
+ apply_static_mask : bool
+ Apply static mask obtained from `frequency_mask` before computing
+ the median absolute deviation.
+ rolling : bool
+ Use a rolling window instead of distinct blocks.
+ stack: bool
+ Average over all autocorrelations.
+ normalize : bool
+ Normalize by the median value over time prior to averaging over
+ autocorrelations. Only relevant if `stack` is True.
+ fill_value: float
+ Data that was already flagged as bad will be set to this value in
+ the output array. Should be a large positive value that is greater
+ than the threshold that will be placed. Default is float('Inf').
+
+ Returns
+ -------
+ auto_ii: np.ndarray[ninput,]
+ Index of the inputs that have been processed.
+ If stack is True, then [0] will be returned.
+
+ auto_vis: np.ndarray[nfreq, ninput, ntime]
+ The autocorrelations that were used to calculate
+ the number of deviations.
+
+ ndev : np.ndarray[nfreq, ninput, ntime]
+ Number of median absolute deviations of the autocorrelations
+ from the local median.
+ """
+ fromcaputimportmemh5,mpiarray
+
+ iffill_valueisNone:
+ fill_value=float("Inf")
+
+ # Check if dataset is parallel
+ parallel=isinstance(data.vis,memh5.MemDatasetDistributed)
+
+ data.redistribute("freq")
+
+ # Extract the auto correlations
+ auto_ii,auto_vis,auto_flag=get_autocorrelations(data,stack,normalize)
+
+ # Calculate time interval in samples. If the data has an ra axis instead,
+ # use an estimation of the time per sample
+ if"time"indata.index_map:
+ twidth=int(time_width/np.median(np.abs(np.diff(data.time))))+1
+ timestamp=data.time[0]
+ elif"ra"indata.index_map:
+ twidth=int(time_width*len(data.ra[:])/86164.0)+1
+ timestamp=ephemeris.csd_to_unix(data.attrs["lsd"])
+ else:
+ raiseTypeError(
+ f"Expected data type with a `time` or `ra` axis. Got {type(data)}."
+ )
+
+ # Create static flag of frequencies that are known to be bad
+ static_flag=(
+ ~frequency_mask(data.freq[:],timestamp=timestamp)
+ ifapply_static_mask
+ elsenp.ones(len(data.freq[:]),dtype=bool)
+ )[:,np.newaxis]
+
+ ifparallel:
+ # Ensure these are distributed across frequency
+ auto_vis=auto_vis.redistribute(0)
+ auto_flag=auto_flag.redistribute(0)
+ static_flag=mpiarray.MPIArray.wrap(static_flag[auto_vis.local_bounds],axis=0)
+
+ # Calculate frequency interval in bins
+ fwidth=(
+ int(freq_width/np.median(np.abs(np.diff(data.freq))))+1ifnotflag1delse1
+ )
+
+ # Create an empty array for number of median absolute deviations
+ ndev=np.zeros_like(auto_vis,dtype=np.float32)
+
+ auto_flag_view=auto_flag.allgather()ifparallelelseauto_flag
+ static_flag_view=static_flag.allgather()ifparallelelsestatic_flag
+ ndev_view=ndev.local_arrayifparallelelsendev
+
+ # Loop over extracted autos and create a mask for each
+ forindinrange(auto_vis.shape[1]):
+ flg=static_flag_view&auto_flag_view[:,ind]
+ # Gather enire array onto each rank
+ arr=auto_vis[:,ind].allgather()ifparallelelseauto_vis[:,ind]
+ # Use NaNs to ignore previously flagged data when computing the MAD
+ arr=np.where(flg,arr.real,np.nan)
+ local_bounds=auto_vis.local_boundsifparallelelseslice(None)
+ # Apply RFI flagger
+ ifrolling:
+ # Limit bounds to the local portion of the array
+ ndev_i=mad_cut_rolling(
+ arr,twidth=twidth,fwidth=fwidth,mask=False,limit_range=local_bounds
+ )
+ elifflag1d:
+ ndev_i=mad_cut_1d(arr[local_bounds,:],twidth=twidth,mask=False)
+ else:
+ ndev_i=mad_cut_2d(
+ arr[local_bounds,:],twidth=twidth,fwidth=fwidth,mask=False
+ )
+
+ ndev_view[:,ind,:]=ndev_i
+
+ # Fill any values equal to NaN with the user specified fill value
+ ndev_view[~np.isfinite(ndev_view)]=fill_value
+
+ returnauto_ii,auto_vis,ndev
+
+
+
+
+[docs]
+defget_autocorrelations(
+ data,stack:bool=False,normalize:bool=False
+)->Tuple[np.ndarray,np.ndarray,np.ndarray]:
+"""Extract autocorrelations from a data stack.
+
+ Parameters
+ ----------
+ data : `andata.CorrData`
+ Must contain vis and weight attributes that are both
+ `np.ndarray[nfreq, nprod, ntime]`.
+ stack: bool, optional
+ Average over all autocorrelations.
+ normalize : bool, optional
+ Normalize by the median value over time prior to averaging over
+ autocorrelations. Only relevant if `stack` is True.
+
+ Returns
+ -------
+ auto_ii: np.ndarray[ninput,]
+ Index of the inputs that have been processed.
+ If stack is True, then [0] will be returned.
+
+ auto_vis: np.ndarray[nfreq, ninput, ntime]
+ The autocorrelations that were used to calculate
+ the number of deviations.
+
+ auto_flag: np.ndarray[nfreq, ninput, ntime]
+ Indices where data weights are positive
+ """
+ # Extract the auto correlations
+ prod=data.index_map["prod"][data.index_map["stack"]["prod"]]
+ auto_ii,auto_pi=np.array(
+ list(zip(*[(pp[0],ind)forind,ppinenumerate(prod)ifpp[0]==pp[1]]))
+ )
+
+ auto_vis=data.vis[:,auto_pi,:].real.copy()
+
+ # If requested, average over all inputs to construct the stacked autocorrelations
+ # for the instrument (also known as the incoherent beam)
+ ifstack:
+ weight=(data.weight[:,auto_pi,:]>0.0).astype(np.float32)
+
+ # Do not include bad inputs in the average
+ partial_stack=data.index_map["stack"].size<data.index_map["prod"].size
+
+ ifnotpartial_stackandhasattr(data,"input_flags"):
+ input_flags=data.input_flags[:]
+ logger.info(
+ "There are on average %d good inputs."
+ %np.mean(np.sum(input_flags,axis=0),axis=-1)
+ )
+
+ ifnp.any(input_flags)andnotnp.all(input_flags):
+ logger.info("Applying input_flags to weight.")
+ weight*=input_flags[np.newaxis,auto_ii,:].astype(weight.dtype)
+
+ ifnormalize:
+ logger.info("Normalizing autocorrelations prior to stacking.")
+ med_auto=nanmedian(
+ np.where(weight,auto_vis,np.nan),axis=-1,keepdims=True
+ )
+ med_auto=np.where(np.isfinite(med_auto),med_auto,0.0)
+ auto_vis*=tools.invert_no_zero(med_auto)
+
+ norm=np.sum(weight,axis=1,keepdims=True)
+
+ auto_vis=np.sum(
+ weight*auto_vis,axis=1,keepdims=True
+ )*tools.invert_no_zero(norm)
+
+ auto_flag=norm>0.0
+ auto_ii=np.zeros(1,dtype=int)
+
+ else:
+ auto_flag=data.weight[:,auto_pi,:]>0.0
+
+ returnauto_ii,auto_vis,auto_flag
+
+
+
+
+[docs]
+defspectral_cut(data,fil_window=15,only_autos=False):
+"""Flag out the TV bands, or other constant spectral RFI.
+
+ Parameters
+ ----------
+ data : `andata.obj`
+ If `only_autos` shape is (freq, n_feeds, time), else (freq, n_prod,
+ time).
+ fil_window : integer
+ Window of median filter for baseline of chime spectrum. Default is 15.
+ only_autos : boolean
+ Whether data contains only autos or not.
+
+ Returns
+ -------
+ mask: np.ndarray[freq,time]
+ RFI mask (no product axis).
+ """
+
+ ifonly_autos:
+ data_vis=data.vis[:].real
+ else:
+ nfeed=int((2*data.vis.shape[1])**0.5)
+ auto_ind=[tools.cmap(i,i,nfeed)foriinrange(nfeed)]
+ data_vis=data.vis[:,auto_ind].real
+
+ stack_autos=np.mean(data_vis,axis=1)
+ stack_autos_time_ave=np.mean(stack_autos,axis=-1)
+
+ # Locations of the generally decent frequency bands
+ if"time"indata.index_map:
+ timestamp=data.time[0]
+ elif"ra"indata.index_map:
+ timestamp=ephemeris.csd_to_unix(data.attrs["lsd"])
+
+ drawn_bool_mask=frequency_mask(data.freq[:],timestamp=timestamp)
+ good_data=np.logical_not(drawn_bool_mask)
+
+ # Calculate standard deivation of the average channel
+ std_arr=np.std(stack_autos,axis=-1)
+ sigma=np.median(std_arr)/np.sqrt(
+ stack_autos.shape[1]
+ )# standard deviation of the mean
+
+ # Smooth with a median filter, and then interpolate to estimate the
+ # baseline of the spectrum
+ fa=np.arange(data_vis.shape[0])
+ medfilt=sig.medfilt(stack_autos_time_ave[good_data],fil_window)
+ interpolat_arr_baseline=np.interp(fa,fa[good_data],medfilt)
+ rel_pow=stack_autos_time_ave-interpolat_arr_baseline
+
+ # Mask out frequencies with too much power
+ mask_1d=rel_pow>10*sigma
+
+ # Generate mask
+ mask=np.zeros((data_vis.shape[0],data_vis.shape[2]),dtype=bool)
+ mask[:]=mask_1d[:,None]
+
+ returnmask
+
+
+
+
+[docs]
+deffrequency_mask(
+ freq_centre:np.ndarray,
+ freq_width:Optional[Union[np.ndarray,float]]=None,
+ timestamp:Optional[Union[np.ndarray,float]]=None,
+ instrument:Optional[str]="chime",
+)->np.ndarray:
+"""Flag known bad frequencies.
+
+ Time dependent static RFI flags that affect the recent observations are added.
+
+ Parameters
+ ----------
+ freq_centre
+ Centre of each frequency channel
+ freq_width
+ Width of each frequency channel. If `None` (default), calculate the width from
+ the frequency centre separation. If supplied as an array it must be
+ broadcastable
+ against `freq_centre`.
+ timestamp
+ UNIX observing time. If `None` (default) mask all specified bands regardless of
+ their start/end times, otherwise mask only timestamps within the band start and
+ end times. If supplied as an array it must be broadcastable against
+ `freq_centre`.
+ instrument
+ Telescope name. [kko, gbo, hco, chime (default)]
+
+ Returns
+ -------
+ mask
+ An array marking the bad frequency channels. The final shape is the result of
+ broadcasting `freq_centre` and `timestamp` together.
+ """
+ iffreq_widthisNone:
+ freq_width=np.abs(np.median(np.diff(freq_centre)))
+
+ freq_start=freq_centre-freq_width/2
+ freq_end=freq_centre+freq_width/2
+
+ # Broadcast to get the output mask
+ mask=np.zeros(np.broadcast(freq_centre,timestamp).shape,dtype=bool)
+
+ try:
+ bad_freq=BAD_FREQUENCIES[instrument]
+ exceptKeyErrorase:
+ raiseValueError(f"No RFI flags defined for {instrument}")frome
+
+ for(start_time,end_time),(fs,fe)inbad_freq:
+ fmask=(freq_end>fs)&(freq_start<fe)
+
+ # If we don't have a timestamp then just mask all bands
+ iftimestampisNone:
+ tmask=True
+ else:
+ # Otherwise calculate the mask based on the start and end times
+ tmask=np.ones_like(timestamp,dtype=bool)
+
+ ifstart_timeisnotNone:
+ tmask&=timestamp>=start_time
+
+ ifend_timeisnotNone:
+ tmask&=timestamp<=end_time
+
+ # Mask frequencies and times specified in this band
+ mask|=tmask&fmask
+
+ returnmask
+
+
+
+
+[docs]
+defmad_cut_2d(data,fwidth=64,twidth=42,threshold=5.0,freq_flat=True,mask=True):
+"""Mask out RFI using a median absolute deviation cut in time-frequency blocks.
+
+ Parameters
+ ----------
+ data : np.ndarray[freq, time]
+ Array of data to mask.
+ fwidth : integer, optional
+ Number of frequency samples to average median over.
+ twidth : integer, optional
+ Number of time samples to average median over.
+ threshold : scalar, optional
+ Number of median deviations above which we cut the data.
+ freq_flat : boolean, optional
+ Flatten in the frequency direction by dividing through by the median.
+ mask : boolean, optional
+ If True return the mask, if False return the number of
+ median absolute deviations.
+
+ Returns
+ -------
+ mask : np.ndarray[freq, time]
+ Mask or number of median absolute deviations for each sample.
+ """
+
+ median=nanmedianifnp.any(~np.isfinite(data))elsenp.median
+
+ flen=int(np.ceil(data.shape[0]*1.0/fwidth))
+ tlen=int(np.ceil(data.shape[1]*1.0/twidth))
+
+ ifmask:
+ madmask=np.ones(data.shape,dtype="bool")
+ else:
+ madmask=np.ones(data.shape,dtype=np.float64)
+
+ iffreq_flat:
+ # Flatten
+ mfd=tools.invert_no_zero(median(data,axis=1))
+ data*=mfd[:,np.newaxis]
+
+ ## Iterate over all frequency and time blocks
+ #
+ # This can be done more quickly by reshaping the arrays into blocks, but
+ # only works when there are an integer number of blocks. Probably best to
+ # rewrite in cython.
+ forfiinrange(flen):
+ fs=fi*fwidth
+ fe=min((fi+1)*fwidth,data.shape[0])
+
+ fortiinrange(tlen):
+ ts=ti*twidth
+ te=min((ti+1)*twidth,data.shape[1])
+
+ dsec=data[fs:fe,ts:te]
+ msec=madmask[fs:fe,ts:te]
+
+ mval=median(dsec.flatten())
+ dev=dsec-mval
+ med_abs_dev=median(np.abs(dev).flatten())
+
+ med_inv=tools.invert_no_zero(med_abs_dev)
+
+ ifmask:
+ msec[:]=(np.abs(dev)*med_inv)>threshold
+ else:
+ msec[:]=dev*med_inv
+
+ returnmadmask
+
+
+
+
+[docs]
+defmad_cut_1d(data,twidth=42,threshold=5.0,mask=True):
+"""Mask out RFI using a median absolute deviation cut in the time direction.
+
+ This is useful for datasets with sparse frequency coverage. Functionally
+ this routine is equivalent to :func:`mad_cut_2d` with `fwidth = 1`, but will
+ be much faster.
+
+ Parameters
+ ----------
+ data : np.ndarray[freq, time]
+ Array of data to mask.
+ twidth : integer, optional
+ Number of time samples to average median over.
+ threshold : scalar, optional
+ Number of median deviations above which we cut the data.
+ mask : boolean, optional
+ If True return the mask, if False return the number of
+ median absolute deviations.
+
+ Returns
+ -------
+ mask : np.ndarray[freq, time]
+ Mask or number of median absolute deviations for each sample.
+ """
+
+ median=nanmedianifnp.any(~np.isfinite(data))elsenp.median
+
+ tlen=int(np.ceil(data.shape[1]*1.0/twidth))
+
+ ifmask:
+ madmask=np.ones(data.shape,dtype="bool")
+ else:
+ madmask=np.ones(data.shape,dtype=np.float64)
+
+ ## Iterate over all time chunks
+ fortiinrange(tlen):
+ ts=ti*twidth
+ te=min((ti+1)*twidth,data.shape[1])
+
+ dsec=data[:,ts:te]
+ msec=madmask[:,ts:te]
+
+ mval=median(dsec,axis=1)
+ dev=dsec-mval[:,np.newaxis]
+ med_abs_dev=median(np.abs(dev),axis=1)
+
+ med_inv=tools.invert_no_zero(med_abs_dev[:,np.newaxis])
+
+ ifmask:
+ msec[:]=(np.abs(dev)*med_inv)>threshold
+ else:
+ msec[:]=dev*med_inv
+
+ returnmadmask
+[docs]
+defmad_cut_rolling(
+ data,
+ fwidth=64,
+ twidth=42,
+ threshold=5.0,
+ freq_flat=True,
+ mask=True,
+ limit_range:slice=slice(None),
+):
+"""Mask out RFI by placing a cut on the absolute deviation.
+ Compared to `mad_cut_2d`, this function calculates
+ the median and median absolute deviation using a rolling
+ 2D median filter, i.e., for every (freq, time) sample a
+ separate estimates of these statistics is obtained for a
+ window that is centered on that sample.
+
+ For sparsely sampled frequency axis, set fwidth = 1.
+
+ Parameters
+ ----------
+ data : np.ndarray[freq, time]
+ Array of data to mask.
+ fwidth : integer, optional
+ Number of frequency samples to calculate median over.
+ twidth : integer, optional
+ Number of time samples to calculate median over.
+ threshold : scalar, optional
+ Number of median absolute deviations above which we cut the data.
+ freq_flat : boolean, optional
+ Flatten in the frequency direction by dividing each frequency
+ by the median over time.
+ mask : boolean, optional
+ If True return the mask, if False return the number of
+ median absolute deviations.
+ limit_range : slice, optional
+ Data is limited to this range in the freqeuncy axis. Defaults to slice(None).
+
+ Returns
+ -------
+ mask : np.ndarray[freq, time]
+ Mask or number of median absolute deviations for each sample.
+ """
+ # Make sure we have an odd number of samples
+ fwidth+=int(not(fwidth%2))
+ twidth+=int(not(twidth%2))
+
+ foff=fwidth//2
+ toff=twidth//2
+
+ nfreq,ntime=data.shape
+
+ # If requested, flatten over the frequency direction.
+ iffreq_flat:
+ mfd=tools.invert_no_zero(nanmedian(data,axis=1))
+ data*=mfd[:,np.newaxis]
+
+ # Add NaNs around the edges of the array so that we don't have to treat them separately
+ eshp=[nfreq+fwidth-1,ntime+twidth-1]
+ exp_data=np.full(eshp,np.nan,dtype=data.dtype)
+ exp_data[foff:foff+nfreq,toff:toff+ntime]=data
+
+ iflimit_range!=slice(None):
+ # Get only desired slice
+ expsl=slice(
+ max(limit_range.start,0),
+ min(limit_range.stop+2*foff,exp_data.shape[0]),
+ )
+ dsl=slice(max(limit_range.start,0),min(limit_range.stop,data.shape[0]))
+ exp_data=exp_data[expsl,:]
+ data=data[dsl,:]
+
+ # Use numpy slices to construct the rolling windowed data
+ win_data=_rolling_window(exp_data,(fwidth,twidth))
+
+ # Compute the local median and median absolute deviation
+ med=nanmedian(win_data,axis=(-2,-1))
+ med_abs_dev=nanmedian(
+ np.abs(win_data-med[...,np.newaxis,np.newaxis]),axis=(-2,-1)
+ )
+
+ inv_med_abs_dev=tools.invert_no_zero(med_abs_dev)
+
+ # Calculate and return the mask or the number of median absolute deviations
+ ifmask:
+ madmask=(np.abs(data-med)*inv_med_abs_dev)>threshold
+ else:
+ madmask=(data-med)*inv_med_abs_dev
+
+ returnmadmask
+
+
+
+defnanmedian(*args,**kwargs):
+ withwarnings.catch_warnings():
+ warnings.filterwarnings("ignore",r"All-NaN (slice|axis) encountered")
+ returnnp.nanmedian(*args,**kwargs)
+
+
+# Iterative HPF masking for identifying narrow-band features in gains or spectra
+
+[docs]
+defhighpass_delay_filter(freq,tau_cut,flag,epsilon=1e-10):
+"""Construct a high-pass delay filter.
+
+ The stop band will range from [-tau_cut, tau_cut].
+ DAYENU is used to construct the filter in the presence
+ of masked frequencies. See Ewall-Wice et al. 2021
+ (arXiv:2004.11397) for a description.
+
+ Parameters
+ ----------
+ freq : np.ndarray[nfreq,]
+ Frequency in MHz.
+ tau_cut : float
+ The half width of the stop band in micro-seconds.
+ flag : np.ndarray[nfreq,]
+ Boolean flag that indicates what frequencies are valid.
+ epsilon : float
+ The stop-band rejection of the filter.
+
+ Returns
+ -------
+ pinv : np.ndarray[nfreq, nfreq]
+ High pass delay filter.
+ """
+
+ nfreq=freq.size
+ assert(flag.ndim==1)and(flag.size==nfreq)
+
+ mflag=(flag[:,np.newaxis]&flag[np.newaxis,:]).astype(np.float64)
+
+ cov=np.eye(nfreq,dtype=np.float64)
+ cov+=(
+ np.sinc(2.0*tau_cut*(freq[:,np.newaxis]-freq[np.newaxis,:]))/epsilon
+ )
+
+ pinv=np.linalg.pinv(cov*mflag,hermitian=True)*mflag
+
+ returnpinv
+
+
+
+
+[docs]
+defiterative_hpf_masking(
+ freq,
+ y,
+ flag=None,
+ tau_cut=0.60,
+ epsilon=1e-10,
+ window=65,
+ threshold=6.0,
+ nperiter=1,
+ niter=40,
+ timestamp=None,
+):
+"""Mask features in a spectrum that have significant power at high delays.
+
+ Uses the following iterative procedure to generate the mask:
+
+ - Apply a high-pass filter to the spectrum.
+ - For each frequency channel, calculate the median absolute
+ deviation of nearby frequency channels to get an estimate
+ of the noise. Divide the high-pass filtered spectrum by
+ the noise estimate.
+ - Mask excursions with the largest signal to noise.
+ - Regenerate the high-pass filter using the new mask.
+ - Repeat.
+
+ The procedure stops when the maximum number of iterations is reached
+ or there are no excursions beyond some threshold.
+
+ Parameters
+ ----------
+ freq: np.ndarray[nfreq,]
+ Frequency in MHz.
+ y: np.ndarray[nfreq,]
+ Spectrum to search for narrowband features.
+ flag: np.ndarray[nfreq,]
+ Boolean flag where True indicates valid data.
+ tau_cut: float
+ Cutoff of the high-pass filter in microseconds.
+ epsilon: float
+ Stop-band rejection of the filter.
+ threshold: float
+ Number of median absolute deviations beyond which
+ a frequency channel is considered an outlier.
+ window: int
+ Width of the window used to estimate the noise
+ (by calculating a local median absolute deviation).
+ nperiter: int
+ Maximum number of frequency channels to flag
+ on any iteration.
+ niter: int
+ Maximum number of iterations.
+ timestamp : float
+ Start observing time (in unix time)
+
+ Returns
+ -------
+ yhpf: np.ndarray[nfreq,]
+ The high-pass filtered spectrum generated using
+ the mask from the last iteration.
+ flag: np.ndarray[nfreq,]
+ Boolean flag where True indicates valid data.
+ This is the logical complement to the mask
+ from the last iteration.
+ rsigma: np.ndarray[nfreq,]
+ The local median absolute deviation from the last
+ iteration.
+ """
+
+ fromcaputimportweighted_median
+
+ asserty.ndim==1
+
+ # Make sure the frequencies are float64, otherwise
+ # can have problems with construction of filter
+ freq=freq.astype(np.float64)
+
+ # Make sure the size of the window is odd
+ window=window+int(not(window%2))
+
+ # If an initial flag was not provided, then use the static rfi mask.
+ ifflagisNone:
+ flag=~frequency_mask(freq,timestamp=timestamp)
+
+ # We will be updating the flags on each iteration. Make a copy of
+ # the input so that we do not overwrite.
+ new_flag=flag.copy()
+
+ # Iterate
+ itt=0
+ whileitt<niter:
+ # Construct the filter using the current mask
+ NF=highpass_delay_filter(freq,tau_cut,new_flag,epsilon=epsilon)
+
+ # Apply the filter
+ yhpf=np.matmul(NF,y)
+
+ # Calculate the local median absolute deviation
+ ry=np.ascontiguousarray(yhpf.astype(np.float64))
+ w=np.ascontiguousarray(new_flag.astype(np.float64))
+
+ rsigma=1.48625*weighted_median.moving_weighted_median(
+ np.abs(ry),w,window,method="split"
+ )
+
+ # Calculate the signal to noise
+ rs2n=np.abs(yhpf*tools.invert_no_zero(rsigma))
+
+ # Identify frequency channels that are above the signal to noise threshold
+ above_threshold=np.flatnonzero(rs2n>threshold)
+
+ ifabove_threshold.size==0:
+ break
+
+ # Find the largest nperiter frequency channels that are above the threshold
+ ibad=above_threshold[np.argsort(-np.abs(yhpf[above_threshold]))[0:nperiter]]
+
+ # Flag those frequency channels, increment the counter
+ new_flag[ibad]=False
+
+ itt+=1
+
+ # Construct and apply the filter using the final flag
+ NF=highpass_delay_filter(freq,tau_cut,new_flag,epsilon=epsilon)
+
+ yhpf=np.matmul(NF,y)
+
+ returnyhpf,new_flag,rsigma
+
+
+
+# Scale-invariant rank (SIR) functions
+
+[docs]
+defsir1d(basemask,eta=0.2):
+"""Numpy implementation of the scale-invariant rank (SIR) operator.
+
+ For more information, see arXiv:1201.3364v2.
+
+ Parameters
+ ----------
+ basemask : numpy 1D array of boolean type
+ Array with the threshold mask previously generated.
+ 1 (True) for flagged points, 0 (False) otherwise.
+ eta : float
+ Aggressiveness of the method: with eta=0, no additional samples are
+ flagged and the function returns basemask. With eta=1, all samples
+ will be flagged. The authors in arXiv:1201.3364v2 seem to be convinced
+ that 0.2 is a mostly universally optimal value, but no optimization
+ has been done on CHIME data.
+
+ Returns
+ -------
+ mask : numpy 1D array of boolean type
+ The mask after the application of the (SIR) operator. Same shape and
+ type as basemask.
+ """
+ n=basemask.size
+ psi=basemask.astype(np.float64)-1.0+eta
+
+ M=np.zeros(n+1,dtype=np.float64)
+ M[1:]=np.cumsum(psi)
+
+ MP=np.minimum.accumulate(M)[:-1]
+ MQ=np.concatenate((np.maximum.accumulate(M[-2::-1])[-2::-1],M[-1,np.newaxis]))
+
+ return(MQ-MP)>=0.0
+
+
+
+
+[docs]
+defsir(basemask,eta=0.2,only_freq=False,only_time=False):
+"""Apply the SIR operator over the frequency and time axes for each product.
+
+ This is a wrapper for `sir1d`. It loops over times, applying `sir1d`
+ across the frequency axis. It then loops over frequencies, applying `sir1d`
+ across the time axis. It returns the logical OR of these two masks.
+
+ Parameters
+ ----------
+ basemask : np.ndarray[nfreq, nprod, ntime] of boolean type
+ The previously generated threshold mask.
+ 1 (True) for masked points, 0 (False) otherwise.
+ eta : float
+ Aggressiveness of the method: with eta=0, no additional samples are
+ flagged and the function returns basemask. With eta=1, all samples
+ will be flagged.
+ only_freq : bool
+ Only apply the SIR operator across the frequency axis.
+ only_time : bool
+ Only apply the SIR operator across the time axis.
+
+ Returns
+ -------
+ mask : np.ndarray[nfreq, nprod, ntime] of boolean type
+ The mask after the application of the SIR operator.
+ """
+ ifonly_freqandonly_time:
+ raiseValueError("Only one of only_freq and only_time can be True.")
+
+ nfreq,nprod,ntime=basemask.shape
+
+ newmask=basemask.astype(bool).copy()
+
+ forppinrange(nprod):
+ ifnotonly_time:
+ forttinrange(ntime):
+ newmask[:,pp,tt]|=sir1d(basemask[:,pp,tt],eta=eta)
+
+ ifnotonly_freq:
+ forffinrange(nfreq):
+ newmask[ff,pp,:]|=sir1d(basemask[ff,pp,:],eta=eta)
+
+ returnnewmask
+"""Tools for timing jitter and delay corrections.
+
+This module contains tools for using noise sources to correct
+timing jitter and timing delay.
+
+
+Example
+=======
+
+The function :meth:`construct_delay_template` generates a delay template from
+measurements of the visibility between noise source inputs, which can
+be used to remove the timing jitter in other data.
+
+The user seldom needs to work with :meth:`construct_delay_template`
+directly and can instead use several high-level functions and containers
+that load the timing data, derive the timing correction using
+:meth:`construct_delay_template`, and then enable easy application of
+the timing correction to other data.
+
+For example, to load the timing data and derive the timing correction from
+a list of timing acquisition files (i.e., `YYYYMMSSTHHMMSSZ_chimetiming_corr`),
+use the following:
+
+ ```tdata = TimingData.from_acq_h5(timing_acq_filenames)```
+
+This results in a :class:`andata.CorrData` object that has additional
+methods avaiable for applying the timing correction to other data.
+For example, to obtain the complex gain for some freq, input, and time
+that upon multiplication will remove the timing jitter, use the following:
+
+ ```tgain, tweight = tdata.get_gain(freq, input, time)```
+
+To apply the timing correction to the visibilities in an :class:`andata.CorrData`
+object called `data`, use the following:
+
+ ```tdata.apply_timing_correction(data)```
+
+The timing acquisitions must cover the span of time that you wish to correct.
+If you have a list of data acquisition files and would like to obtain
+the appropriate timing correction by searching the archive for the
+corresponding timing acquisitons files, then use:
+
+ ```tdata = load_timing_correction(data_acq_filenames_full_path)```
+
+To print a summary of the timing correction, use:
+
+ ```print(tdata)```
+
+"""
+
+importos
+importglob
+importnumpyasnp
+importinspect
+importlogging
+importgc
+
+importscipy.interpolate
+importscipy.optimize
+
+from.importtools,andata,ephemeris,rfi
+fromcaputimportmemh5,mpiarray,tod
+
+FREQ_TO_OMEGA=2.0*np.pi*1e-6
+FREQ_PIVOT=600.0
+
+AXES=["freq","noise_source","input","time","param"]
+
+DSET_SPEC={
+ "tau":{"axis":["noise_source","time"],"flag":False},
+ "alpha":{"axis":["noise_source","time"],"flag":False},
+ "weight_tau":{"axis":["noise_source","time"],"flag":True},
+ "weight_alpha":{"axis":["noise_source","time"],"flag":True},
+ "static_phi":{"axis":["freq","noise_source"],"flag":False},
+ "static_amp":{"axis":["freq","noise_source"],"flag":False},
+ "weight_static_phi":{"axis":["freq","noise_source"],"flag":True},
+ "weight_static_amp":{"axis":["freq","noise_source"],"flag":True},
+ "static_phi_fit":{"axis":["param","noise_source"],"flag":False},
+ "num_freq":{"axis":["noise_source","time"],"flag":True},
+ "phi":{"axis":["freq","noise_source","time"],"flag":False},
+ "amp":{"axis":["freq","noise_source","time"],"flag":False},
+ "weight_phi":{"axis":["freq","noise_source","time"],"flag":True},
+ "weight_amp":{"axis":["freq","noise_source","time"],"flag":True},
+ "coeff_tau":{"axis":["input","noise_source"],"flag":False},
+ "coeff_alpha":{"axis":["input","noise_source"],"flag":False},
+ "reference_noise_source":{"axis":["input"],"flag":False},
+}
+
+# Set up logging
+logger=logging.getLogger(__name__)
+logger.addHandler(logging.NullHandler())
+
+
+
+[docs]
+classTimingCorrection(andata.BaseData):
+"""
+ Container that holds a timing correction.
+
+ Provides methods for applying that correction to other datasets.
+ """
+
+
+[docs]
+ @classmethod
+ deffrom_dict(self,**kwargs):
+"""
+ Instantiate a TimingCorrection object.
+
+ Parameters
+ ----------
+ freq: np.ndarray[nfreq, ] of dtype=('centre', 'width')
+ Frequencies in MHz that were used to construct the timing correction.
+ noise_source: np.ndarray[nsource,] of dtype=('chan_id', 'correlator_input')
+ Correlator inputs that were used to construct the timing correction.
+ input: np.ndarray[ninput, ] of dtype=('chan_id', 'correlator_input')
+ Correlator inputs to which the timing correction will be applied.
+ time: np.ndarray[ntime, ]
+ Unix time.
+ param: np.ndarray[nparam, ]
+ Parameters of the model fit to the static phase versus frequency.
+ tau: np.ndarray[nsource, ntime]
+ The actual timing correction, which is the relative delay of each of the
+ noise source inputs with respect to a reference input versus time.
+ weight_tau: np.ndarray[nsource, ntime]
+ Estimate of the uncertainty (inverse variance) on the timing correction.
+ static_phi: np.ndarray[nfreq, nsource]
+ The phase that was subtracted from each frequency and input prior to
+ fitting for the timing correction. This is necessary to remove the
+ approximately static ripple pattern caused by reflections.
+ weight_static_phi: np.ndarray[nfreq, nsource]
+ Inverse variance on static_phi.
+ static_phi_fit: np.ndarray[nparam, nsource]
+ Best-fit parameters of a fit to the static phase versus frequency
+ for each of the noise source inputs.
+ alpha: np.ndarray[nsource, ntime]
+ The coefficient of the spectral model of the amplitude variations of
+ each of the noise source inputs versus time.
+ weight_alpha: np.ndarray[nsource, ntime]
+ Estimate of the uncertainty (inverse variance) on the amplitude coefficients.
+ static_amp: np.ndarray[nfreq, nsource]
+ The amplitude that was subtracted from each frequency and input prior to
+ fitting for the amplitude variations. This is necessary to remove the
+ approximately static ripple pattern caused by reflections.
+ weight_static_amp: np.ndarray[nfreq, nsource]
+ Inverse variance on static_amp.
+ num_freq: np.ndarray[nsource, ntime]
+ The number of frequencies used to determine the delay and alpha quantities.
+ If num_freq is 0, then that time is ignored when deriving the timing correction.
+ coeff_tau: np.ndarray[ninput, nsource]
+ If coeff is provided, then the timing correction applied to a particular
+ input will be the linear combination of the tau correction from the
+ noise source inputs, with the coefficients set by this array.
+ coeff_alpha: np.ndarray[ninput, nsource]
+ If coeff is provided, then the timing correction applied to a particular
+ input will be adjusted by the linear combination of the alpha correction
+ from the noise source inputs, with the coefficients set by this array.
+ reference_noise_source: np.ndarray[ninput]
+ The noise source input that was used as reference when fitting coeff_tau.
+ """
+ index_map={key:kwargs.pop(key)forkeyinAXESifkeyinkwargs}
+ datasets={key:kwargs.pop(key)forkeyinDSET_SPEC.keys()ifkeyinkwargs}
+
+ # Run base initialiser
+ tcorr=TimingCorrection(**kwargs)
+
+ # Create index maps
+ forname,datainindex_map.items():
+ tcorr.create_index_map(name,data)
+
+ # Create datasets
+ forname,dataindatasets.items():
+ ifdataisNone:
+ continue
+ spec=DSET_SPEC[name]
+ ifspec["flag"]:
+ dset=tcorr.create_flag(name,data=data)
+ else:
+ dset=tcorr.create_dataset(name,data=data)
+
+ dset.attrs["axis"]=np.array(spec["axis"],dtype=np.string_)
+
+ returntcorr
+
+
+ @classmethod
+ def_interpret_and_read(cls,acq_files,start,stop,datasets,out_group):
+ # Instantiate an object for each file
+ objs=[cls.from_file(d,ondisk=False)fordinacq_files]
+
+ # Reference all dynamic datasets to the static quantities
+ # defined in the first file
+ iref=0
+
+ freq=objs[iref].freq
+
+ # Determine the overall delay offset relative to the reference file
+ phi=np.stack([obj.static_phi[:]forobjinobjs],axis=-1)
+ weight=np.stack([obj.weight_static_phi[:]forobjinobjs],axis=-1)
+
+ phi_ref=phi[...,iref,np.newaxis]
+ weight_ref=weight[...,iref,np.newaxis]
+
+ flag=(weight>0.0)&(weight_ref>0.0)
+ err=np.sqrt(tools.invert_no_zero(weight)+tools.invert_no_zero(weight_ref))
+ err*=flag.astype(err.dtype)
+
+ dphi=phi-phi_ref
+
+ fortt,objinenumerate(objs):
+ fornninrange(dphi.shape[1]):
+ ifnp.sum(flag[:,nn,tt],dtype=int)>2:
+ # Fit the difference in the static phase between this file and the
+ # reference file to a linear relationship with frequency. Uses
+ # nonlinear-least-squares that is insensitive to phase wrapping
+ param=fit_poly_to_phase(
+ freq,np.exp(1.0j*dphi[:,nn,tt]),err[:,nn,tt],nparam=2
+ )[0]
+
+ # Add the best-fit slope to the delay template for this file
+ obj.tau[nn,:]+=param[1]
+
+ # Determine the overall amplitude offset relative to the reference file
+ amp=np.stack([obj.static_amp[:]forobjinobjs],axis=-1)
+ weight=np.stack([obj.weight_static_amp[:]forobjinobjs],axis=-1)
+
+ amp_ref=amp[...,iref,np.newaxis]
+ weight_ref=weight[...,iref,np.newaxis]
+
+ flag=(weight>0.0)&(weight_ref>0.0)
+ weight=tools.invert_no_zero(
+ tools.invert_no_zero(weight)+tools.invert_no_zero(weight_ref)
+ )
+ weight*=flag.astype(weight.dtype)
+
+ damp=amp-amp_ref
+
+ asc=amp_ref*_amplitude_scaling(freq[:,np.newaxis,np.newaxis])
+
+ alpha=np.sum(weight*asc*damp,axis=0)*tools.invert_no_zero(
+ np.sum(weight*asc**2,axis=0)
+ )
+
+ fortt,objinenumerate(objs):
+ # Add the offset to the amplitude template for this file
+ obj.alpha[:]+=alpha[:,tt,np.newaxis]
+
+ # Now concatenate the files. Dynamic datasets will be concatenated.
+ # Static datasets will be extracted from the first file.
+ data=tod.concatenate(
+ objs,out_group=out_group,start=start,stop=stop,datasets=datasets
+ )
+
+ returndata
+
+ @property
+ deffreq(self):
+"""Provide convenience access to the frequency bin centres."""
+ returnself.index_map["freq"]["centre"]
+
+ @property
+ defnoise_source(self):
+"""Provide convenience access to the noise source inputs.
+
+ Note that in older versions of the timing correction, the
+ noise_source axis does not exist. Instead, the equivalent
+ quantity is labeled as input. Since the addition of the
+ coeff dataset it has become necessary to distinguish between the
+ noise source inputs from which the timing correction is derived
+ and the correlator inputs to which the timing correction is applied.
+ """
+ key="noise_source"if"noise_source"inself.index_mapelse"input"
+ returnself.index_map[key]
+
+ @property
+ defnsource(self):
+"""Provide convenience access to the number of noise source inputs."""
+ returnself.noise_source.size
+
+ @property
+ definput(self):
+"""Provide convenience access to the correlator inputs."""
+ returnself.index_map["input"]
+
+ @property
+ deftau(self):
+"""Provide convenience access to the tau array."""
+ returnself.datasets["tau"]
+
+ @property
+ defweight_tau(self):
+"""Provide convenience access to the weight_tau array."""
+ if"weight_tau"notinself.flags:
+ # weight_tau does not exist. This is the case for timing
+ # corrections generated with older versions of the code.
+ # Create a default weight_tau dataset and return that.
+ ifself.has_num_freq:
+ weight_tau=(self.num_freq[:]>0).astype(np.float32)
+ else:
+ weight_tau=np.ones_like(self.tau[:])
+
+ dset=self.create_flag("weight_tau",data=weight_tau)
+ dset.attrs["axis"]=np.array(
+ DSET_SPEC["weight_tau"]["axis"],dtype=np.string_
+ )
+
+ returnself.flags["weight_tau"]
+
+ @property
+ defstatic_phi(self):
+"""Provide convenience access to the static_phi array."""
+ returnself.datasets["static_phi"]
+
+ @property
+ defweight_static_phi(self):
+"""Provide convenience access to the weight_static_phi array."""
+ returnself.flags["weight_static_phi"]
+
+ @property
+ defstatic_phi_fit(self):
+"""Provide convenience access to the static_phi_fit array."""
+ returnself.datasets["static_phi_fit"]
+
+ @property
+ defalpha(self):
+"""Provide convenience access to the alpha array."""
+ returnself.datasets["alpha"]
+
+ @property
+ defweight_alpha(self):
+"""Provide convenience access to the weight_alpha array."""
+ if"weight_alpha"notinself.flags:
+ # weight_alpha does not exist. This is the case for timing
+ # corrections generated with older versions of the code.
+ # Create a default weight_alpha dataset and return that.
+ ifself.has_num_freq:
+ weight_alpha=(self.num_freq[:]>0).astype(np.float32)
+ else:
+ weight_alpha=np.ones_like(self.alpha[:])
+
+ scale=(self.amp_to_delayor1.0)**2
+ weight_alpha*=scale
+
+ dset=self.create_flag("weight_alpha",data=weight_alpha)
+ dset.attrs["axis"]=np.array(
+ DSET_SPEC["weight_alpha"]["axis"],dtype=np.string_
+ )
+
+ returnself.flags["weight_alpha"]
+
+ @property
+ defstatic_amp(self):
+"""Provide convenience access to the static_amp array."""
+ returnself.datasets["static_amp"]
+
+ @property
+ defweight_static_amp(self):
+"""Provide convenience access to the weight_static_amp array."""
+ returnself.flags["weight_static_amp"]
+
+ @property
+ defnum_freq(self):
+"""Provide convenience access to the num_freq array."""
+ returnself.flags["num_freq"]
+
+ @property
+ defhas_num_freq(self):
+"""Inidicates if there is a num_freq flag that identifies missing data."""
+ return"num_freq"inself.flags
+
+ @property
+ defcoeff_tau(self):
+"""Provide convenience access to the coeff_tau array."""
+ returnself.datasets["coeff_tau"]
+
+ @property
+ defhas_coeff_tau(self):
+"""Indicates if there are valid coeff that map noise source tau to inputs."""
+ return(
+ "coeff_tau"inself.datasets
+ and"noise_source"inself.index_map
+ and"input"inself.index_map
+ )
+
+ @property
+ defcoeff_alpha(self):
+"""Provide convenience access to the coeff_alpha array."""
+ returnself.datasets["coeff_alpha"]
+
+ @property
+ defhas_coeff_alpha(self):
+"""Indicates if there are valid coeff that map noise source alpha to inputs."""
+ return(
+ "coeff_alpha"inself.datasets
+ and"noise_source"inself.index_map
+ and"input"inself.index_map
+ )
+
+ @property
+ defamp_to_delay(self):
+"""Return conversion from noise source amplitude variations to delay variations."""
+ returnself.attrs.get("amp_to_delay",None)
+
+ @amp_to_delay.setter
+ defamp_to_delay(self,val):
+"""Sets the conversion from noise source amplitude variations to delay variations.
+
+ Note that setting this quantity will result in the following modification to the
+ timing correction: tau --> tau - amp_to_delay * alpha. This can be used to remove
+ variations introduced by the noise source distribution system from the timing correction
+ using the amplitude variations as a proxy for temperature.
+ """
+ ifself.has_coeff_alpha:
+ raiseAttributeError(
+ "The amplitude variations are already being used to "
+ "correct the delay variations through the coeff_alpha dataset."
+ )
+ elifvalisnotNone:
+ self.attrs["amp_to_delay"]=val
+
+ @amp_to_delay.deleter
+ defamp_to_delay(self):
+"""Remove any conversion from noise source amplitude variations to delay variations."""
+ if"amp_to_delay"inself.attrs:
+ delself.attrs["amp_to_delay"]
+
+ @property
+ defhas_amplitude(self):
+"""Determine if this timing correction contains amplitude data."""
+ return"alpha"inself.datasets
+
+ @property
+ defreference_noise_source(self):
+"""Return the index of the reference noise source."""
+ if"reference_noise_source"inself.datasets:
+ iref=self.datasets["reference_noise_source"][:]
+ returnirefifnp.unique(iref).size>1elseiref[0]
+ else:
+ returnself.zero_delay_noise_source
+
+ @property
+ defzero_delay_noise_source(self):
+"""Return the index of the noise source with zero delay."""
+ zero_tau=np.flatnonzero(np.all(np.abs(self.tau[:])<1e-5,axis=-1))
+ ifzero_tau.size==0:
+ raiseAttributeError(
+ "Could not determine which input the delay template "
+ "is referenced with respect to."
+ )
+ else:
+ returnzero_tau[0]
+
+
+[docs]
+ defset_coeff(
+ self,
+ coeff_tau,
+ inputs,
+ noise_source,
+ coeff_alpha=None,
+ reference_noise_source=None,
+ ):
+"""Use coefficients to construct timing correction.
+
+ Setting the coefficients changes how the timing corretion for a particular
+ correlator input is derived. Without coefficients, each input is matched
+ to the timing correction from a single noise source input through the
+ map_input_to_noise_source method. With coefficients, each input is a
+ linear combination of the timing correction from all noise source inputs.
+
+ Parameters
+ ----------
+ coeff_tau: np.ndarray[ninput, nsource]
+ The timing correction applied to a particular input will be the
+ linear combination of the tau correction from the noise source inputs,
+ with the coefficients set by this array.
+ inputs: np.ndarray[ninput, ] of dtype=('chan_id', 'correlator_input')
+ Correlator inputs to which the timing correction will be applied.
+ noise_source: np.ndarray[nsource,] of dtype=('chan_id', 'correlator_input')
+ Correlator inputs that were used to construct the timing correction.
+ coeff_alpha: np.ndarray[ninput, nsource]
+ The timing correction applied to a particular input will be adjusted by
+ the linear combination of the alpha correction from the noise source inputs,
+ with the coefficients set by this array.
+ reference_noise_source: np.ndarray[ninput,]
+ For each input, the index into noise_source that was used as
+ reference in the fit for coeff_tau.
+ """
+ sn_lookup={
+ sn:iiforii,sninenumerate(noise_source["correlator_input"][:])
+ }
+
+ reod=np.array(
+ [sn_lookup[sn]forsninself.noise_source["correlator_input"][:]]
+ )
+
+ datasets={"coeff_tau":coeff_tau}
+ ifcoeff_alphaisnotNone:
+ ifself.amp_to_delayisNone:
+ datasets["coeff_alpha"]=coeff_alpha
+ else:
+ raiseAttributeError(
+ "The amplitude variations are already "
+ "being used to correct the delay variations "
+ "through the amp_to_delay parameter."
+ )
+
+ forname,coeffindatasets.items():
+ spec=DSET_SPEC[name]
+ ifspec["flag"]:
+ dset=self.create_flag(name,data=coeff[:,reod])
+ else:
+ dset=self.create_dataset(name,data=coeff[:,reod])
+ dset.attrs["axis"]=np.array(spec["axis"],dtype=np.string_)
+
+ ifreference_noise_sourceisnotNone:
+ ref_sn_lookup={
+ sn:iiforii,sninenumerate(self.noise_source["correlator_input"][:])
+ }
+
+ reference_reodered=np.array(
+ [
+ ref_sn_lookup[sn]
+ forsninnoise_source["correlator_input"][reference_noise_source]
+ ]
+ )
+
+ name="reference_noise_source"
+ spec=DSET_SPEC[name]
+ ifspec["flag"]:
+ dset=self.create_flag(name,data=reference_reodered)
+ else:
+ dset=self.create_dataset(name,data=reference_reodered)
+ dset.attrs["axis"]=np.array(spec["axis"],dtype=np.string_)
+
+ self.create_index_map("input",inputs)
+
+
+
+[docs]
+ defdelete_coeff(self):
+"""Stop using coefficients to construct timing correction.
+
+ Calling this method will delete the `coeff_tau`, `coeff_alpha`,
+ and `reference_noise_source` datasets if they exist.
+ """
+ fornamein["coeff_tau","coeff_alpha","reference_noise_source"]:
+ spec=DSET_SPEC[name]
+ group=self["flag"]ifspec["flag"]elseself
+ ifnameingroup:
+ delgroup[name]
+
+
+
+[docs]
+ defsearch_input(self,inputs):
+"""Find inputs in the input axis.
+
+ Parameters
+ ----------
+ inputs: np.ndarray[ninput,] of dtype=('chan_id', 'correlator_input')
+
+ Returns
+ -------
+ index: np.ndarray[ninput,] of .int
+ Indices of the input axis that yield the requested inputs.
+ """
+ ifnothasattr(self,"_input_lookup"):
+ self._input_lookup={
+ sn:indforind,sninenumerate(self.input["correlator_input"][:])
+ }
+
+ returnnp.array(
+ [self._input_lookup[sn]forsnininputs["correlator_input"][:]]
+ )
+
+
+
+[docs]
+ defset_global_reference_time(self,tref,window=0.0,interpolate=False,**kwargs):
+"""Normalize the delay and alpha template to the value at a single time.
+
+ Useful for referencing the template to the value at the time that
+ you plan to calibrate.
+
+ Parameters
+ ----------
+ tref : unix time
+ Reference the templates to the values at this time.
+ window: float
+ Reference the templates to the median value over a window (in seconds)
+ around tref. If nonzero, this will override the interpolate keyword.
+ interpolate : bool
+ Interpolate the delay template to time tref. Otherwise take the measured time
+ nearest to tref. The get_tau method is use to perform the interpolation, and
+ kwargs for that method will be passed along.
+ """
+ tref=ephemeris.ensure_unix(tref)
+ tref_string=ephemeris.unix_to_datetime(tref).strftime("%Y-%m-%d %H:%M:%S %Z")
+ logger.info("Referencing timing correction with respect to %s."%tref_string)
+ ifwindow>0.0:
+ iref=np.flatnonzero(
+ (self.time>=(tref-window))&(self.time<=(tref+window))
+ )
+ ifiref.size>0:
+ logger.info(
+ "Using median of %d samples around reference time."%iref.size
+ )
+ ifself.has_num_freq:
+ tau_ref=np.zeros((self.nsource,1),dtype=self.tau.dtype)
+ alpha_ref=np.zeros((self.nsource,1),dtype=self.alpha.dtype)
+
+ forssinrange(self.nsource):
+ good=np.flatnonzero(self.num_freq[ss,iref]>0)
+ ifgood.size>0:
+ tau_ref[ss]=np.median(self.tau[ss,iref[good]])
+ alpha_ref[ss]=np.median(self.alpha[ss,iref[good]])
+
+ else:
+ tau_ref=np.median(self.tau[:,iref],axis=-1,keepdims=True)
+ alpha_ref=np.median(self.alpha[:,iref],axis=-1,keepdims=True)
+
+ else:
+ raiseValueError(
+ "Timing correction not available for time %s."%tref_string
+ )
+
+ elif(tref<self.time[0])or(tref>self.time[-1]):
+ raiseValueError(
+ "Timing correction not available for time %s."%tref_string
+ )
+
+ else:
+ ifnotinterpolate:
+ kwargs["interp"]="nearest"
+
+ tau_ref,_=self.get_tau(np.atleast_1d(tref),ignore_amp=True,**kwargs)
+ alpha_ref,_=self.get_alpha(np.atleast_1d(tref),**kwargs)
+
+ self.tau[:]=self.tau[:]-tau_ref
+ self.alpha[:]=self.alpha[:]-alpha_ref
+
+
+
+[docs]
+ defset_reference_time(
+ self,
+ tref,
+ tstart,
+ tend=None,
+ tinit=None,
+ tau_init=None,
+ alpha_init=None,
+ interpolate=False,
+ **kwargs
+ ):
+"""Normalize the delay and alpha template to specific times.
+
+ Required if applying the timing correction to data that has
+ already been calibrated.
+
+ Parameters
+ ----------
+ tref : np.ndarray[nref]
+ Reference the delays to the values at this unix time.
+ tstart : np.ndarray[nref]
+ Begin transition to the reference delay at this unix time.
+ tend : np.ndarray[nref]
+ Complete transition to the reference delay at this unix time.
+ tinit : float
+ Use the delay at this time for the period before the first tstart.
+ Takes prescendent over tau_init.
+ tau_init : np.ndarray[nsource]
+ Use this delay for times before the first tstart. Must provide a value
+ for each noise source input. If None, then will reference with respect
+ to the average delay over the full time series.
+ alpha_init : np.ndarray[nsource]
+ Use this alpha for times before the first tstart. Must provide a value
+ for each noise source input. If None, then will reference with respect
+ to the average alpha over the full time series.
+ interpolate : bool
+ Interpolate the delay template to times tref. Otherwise take the measured
+ times nearest to tref. The get_tau method is use to perform the
+ interpolation, and kwargs for that method will be passed along.
+ """
+ tref=np.atleast_1d(ephemeris.ensure_unix(tref))
+
+ ifnotinterpolate:
+ kwargs["interp"]="nearest"
+
+ tau_ref,_=self.get_tau(tref,ignore_amp=True,**kwargs)
+ alpha_ref,_=self.get_alpha(tref,**kwargs)
+
+ iftinitisnotNone:
+ tinit=ephemeris.ensure_unix(tinit)
+ tau_init,_=self.get_tau(tinit,ignore_amp=True,**kwargs)
+ alpha_init,_=self.get_alpha(tinit,**kwargs)
+
+ iftau_initisNone:
+ tau_init=np.zeros((tau_ref.shape[0],1),dtype=tau_ref.dtype)
+ else:
+ iftau_init.size==tau_ref.shape[0]:
+ tau_init=tau_init[:,np.newaxis]
+ else:
+ raiseValueError(
+ "Initial tau has size %d, but there are %d noise sources."
+ %(tau_init.size,tau_ref.shape[0])
+ )
+
+ ifalpha_initisNone:
+ alpha_init=np.zeros((alpha_ref.shape[0],1),dtype=alpha_ref.dtype)
+ else:
+ ifalpha_init.size==alpha_ref.shape[0]:
+ alpha_init=alpha_init[:,np.newaxis]
+ else:
+ raiseValueError(
+ "Initial alpha has size %d, but there are %d noise sources."
+ %(alpha_init.size,alpha_ref.shape[0])
+ )
+
+ tau_ref=np.concatenate((tau_init,tau_ref),axis=-1)
+ alpha_ref=np.concatenate((alpha_init,alpha_ref),axis=-1)
+
+ tstart=np.atleast_1d(ephemeris.ensure_unix(tstart))
+ istart=np.digitize(self.time,tstart)
+
+ iftendisnotNone:
+ tend=np.atleast_1d(ephemeris.ensure_unix(tend))
+ iend=np.digitize(self.time,tend)
+ else:
+ tend=tstart
+ iend=istart
+
+ coeff=np.full(self.time.size,0.5,dtype=np.float32)
+ forts,teinzip(tstart,tend):
+ ifte>ts:
+ fill=np.flatnonzero((self.time>=ts)&(self.time<=te))
+ coeff[fill]=np.hanning(2*fill.size-1)[0:fill.size]
+
+ coeff=coeff[np.newaxis,:]
+ tau_ref_full=coeff*tau_ref[:,istart]+(1.0-coeff)*tau_ref[:,iend]
+ alpha_ref_full=(
+ coeff*alpha_ref[:,istart]+(1.0-coeff)*alpha_ref[:,iend]
+ )
+
+ self.tau[:]=self.tau[:]-tau_ref_full
+ self.alpha[:]=self.alpha[:]-alpha_ref_full
+
+
+
+[docs]
+ defget_tau(self,timestamp,ignore_amp=False,interp="linear",extrap_limit=None):
+"""Return the delay for each noise source at the requested times.
+
+ Uses the TimingInterpolator to interpolate to the requested times.
+
+ Parameters
+ ----------
+ timestamp: np.ndarray[ntime,]
+ Unix timestamp.
+ ignore_amp: bool
+ Do not apply a noise source based amplitude correction, even if one exists.
+ interp: string
+ Method to interpolate over time. Options include 'linear', 'nearest',
+ 'zero', 'slinear', 'quadratic', 'cubic', 'previous', and 'next'.
+ extrap_limit: float
+ Do not extrapolate the underlying data beyond its boundaries by this
+ amount in seconds. Default is 2 integrations.
+
+ Returns
+ -------
+ tau: np.ndarray[nsource, ntime]
+ Delay as a function of time for each of the noise sources.
+ weight : np.ndarray[nsource, ntime]
+ The uncertainty on the delay, expressed as an inverse variance.
+ """
+ flag=self.num_freq[:]>0ifself.has_num_freqelseNone
+
+ ifignore_ampor(self.amp_to_delayisNone)ornotself.has_amplitude:
+ tau_interpolator=TimingInterpolator(
+ self.time[:],
+ self.tau[:],
+ weight=self.weight_tau[:],
+ flag=flag,
+ kind=interp,
+ extrap_limit=extrap_limit,
+ )
+
+ tau,weight=tau_interpolator(timestamp)
+
+ else:
+ logger.info(
+ "Correcting delay template using amplitude template "
+ "with coefficient %0.1f."%self.amp_to_delay
+ )
+
+ # Determine which input the delay template is referenced to
+ iref=self.zero_delay_noise_source
+
+ # Subtract the referenced, scaled alpha template from the delay template
+ tau_corrected=self.tau[:]-self.amp_to_delay*(
+ self.alpha[:]-self.alpha[iref,np.newaxis,:]
+ )
+
+ # Extract the weights
+ weight_corrected=_weight_propagation_addition(
+ self.weight_tau[:],
+ self.weight_alpha[:]/self.amp_to_delay**2,
+ self.weight_alpha[iref,np.newaxis,:]/self.amp_to_delay**2,
+ )
+
+ # Interpolate to the requested times
+ tau_interpolator=TimingInterpolator(
+ self.time[:],
+ tau_corrected,
+ weight=weight_corrected,
+ flag=flag,
+ kind=interp,
+ extrap_limit=extrap_limit,
+ )
+
+ tau,weight=tau_interpolator(timestamp)
+
+ returntau,weight
+
+
+
+[docs]
+ defget_alpha(self,timestamp,interp="linear",extrap_limit=None):
+"""Return the amplitude variation for each noise source at the requested times.
+
+ Uses the TimingInterpolator to interpolate to the requested times.
+
+ Parameters
+ ----------
+ timestamp: np.ndarray[ntime,]
+ Unix timestamp.
+ interp: string
+ Method to interpolate over time. Options include 'linear', 'nearest',
+ 'zero', 'slinear', 'quadratic', 'cubic', 'previous', and 'next'.
+ extrap_limit: float
+ Do not extrapolate the underlying data beyond its boundaries by this
+ amount in seconds. Default is 2 integrations.
+
+ Returns
+ -------
+ alpha: np.ndarray[nsource, ntime]
+ Amplitude coefficient as a function of time for each of the noise sources.
+ weight : np.ndarray[nsource, ntime]
+ The uncertainty on the amplitude coefficient, expressed as an inverse variance.
+ """
+ flag=self.num_freq[:]>0ifself.has_num_freqelseNone
+
+ alpha_interpolator=TimingInterpolator(
+ self.time[:],
+ self.alpha[:],
+ weight=self.weight_alpha[:],
+ flag=flag,
+ kind=interp,
+ extrap_limit=extrap_limit,
+ )
+
+ alpha,weight=alpha_interpolator(timestamp)
+
+ returnalpha,weight
+
+
+
+[docs]
+ defget_stacked_tau(
+ self,timestamp,inputs,prod,reverse_stack,input_flags=None,**kwargs
+ ):
+"""Return the appropriate delay for each stacked visibility at the requested time.
+
+ Averages the delays from the noise source inputs that map to the set of redundant
+ baseline included in each stacked visibility. This yields the appropriate
+ common-mode delay correction. If input_flags is provided, then the bad inputs
+ that were excluded from the stack are also excluded from the delay template averaging.
+
+ Parameters
+ ----------
+ timestamp: np.ndarray[ntime,]
+ Unix timestamp.
+ inputs: np.ndarray[ninput,]
+ Must contain 'correlator_input' field.
+ prod: np.ndarray[nprod,]
+ The products that were included in the stack.
+ Typically found in the `index_map['prod']` attribute of the
+ `andata.CorrData` object.
+ reverse_stack: np.ndarray[nprod,] of dtype=('stack', 'conjugate')
+ The index of the stack axis that each product went into.
+ Typically found in `reverse_map['stack']` attribute
+ of the `andata.CorrData`.
+ input_flags : np.ndarray [ninput, ntime]
+ Array indicating which inputs were good at each time.
+ Non-zero value indicates that an input was good.
+
+ Returns
+ -------
+ tau: np.ndarray[nstack, ntime]
+ Delay as a function of time for each stacked visibility.
+ """
+ # Use the get_tau method to get the data for the noise source inputs
+ # at the requested times.
+ data,_=self.get_tau(timestamp,**kwargs)
+
+ ifself.has_coeff_tau:
+ # This tau correction has a coefficient array.
+ # Find the coefficients for the requested inputs.
+ reod=andata._convert_to_slice(self.search_input(inputs))
+ coeff=self.coeff_tau[reod,:]
+
+ # Determine how the noise source delays were referenced
+ # when fitting for the coefficients
+ iref=self.reference_noise_source
+ ifnp.isscalar(iref):
+ ifiref!=self.zero_delay_noise_source:
+ data=data-data[iref,np.newaxis,:]
+ iref=None
+ else:
+ iref=iref[reod]
+ else:
+ coeff=None
+ iref=None
+
+ # Stack the data from the noise source inputs
+ returnself._stack(
+ data,
+ inputs,
+ prod,
+ reverse_stack,
+ coeff=coeff,
+ input_flags=input_flags,
+ reference_noise_source=iref,
+ )
+
+
+
+[docs]
+ defget_stacked_alpha(
+ self,timestamp,inputs,prod,reverse_stack,input_flags=None,**kwargs
+ ):
+"""Return the equivalent of `get_stacked_tau` for the noise source amplitude variations.
+
+ Averages the alphas from the noise source inputs that map to the set of redundant
+ baseline included in each stacked visibility. If input_flags is provided, then the
+ bad inputs that were excluded from the stack are also excluded from the alpha
+ template averaging. This method can be used to generate a stacked alpha template
+ that can be used to correct a stacked tau template for variations in the noise source
+ distribution system. However, it is recommended that the tau template be corrected
+ before stacking. This is accomplished by setting the `amp_to_delay` property
+ prior to calling `get_stacked_tau`.
+
+ Parameters
+ ----------
+ timestamp: np.ndarray[ntime,]
+ Unix timestamp.
+ inputs: np.ndarray[ninput,]
+ Must contain 'correlator_input' field.
+ prod: np.ndarray[nprod,]
+ The products that were included in the stack.
+ Typically found in the `index_map['prod']` attribute of the
+ `andata.CorrData` object.
+ reverse_stack: np.ndarray[nprod,] of dtype=('stack', 'conjugate')
+ The index of the stack axis that each product went into.
+ Typically found in `reverse_map['stack']` attribute
+ of the `andata.CorrData`.
+ input_flags : np.ndarray [ninput, ntime]
+ Array indicating which inputs were good at each time.
+ Non-zero value indicates that an input was good.
+
+ Returns
+ -------
+ alpha: np.ndarray[nstack, ntime]
+ Noise source amplitude variation as a function of time for each stacked visibility.
+ """
+ ifnotself.has_amplitude:
+ raiseAttributeError(
+ "This timing correction does not include "
+ "an adjustment based on the noise soure amplitude."
+ )
+
+ ifself.has_coeff_alpha:
+ # This alpha correction has a coefficient array.
+ # Find the coefficients for the requested inputs.
+ reod=andata._convert_to_slice(self.search_input(inputs))
+ coeff=self.coeff_alpha[reod,:]
+ else:
+ coeff=None
+
+ # Use the get_alpha method to get the data for the noise source inputs.
+ data,_=self.get_alpha(timestamp,**kwargs)
+
+ # Stack the data from the noise source inputs
+ returnself._stack(
+ data,inputs,prod,reverse_stack,coeff=coeff,input_flags=input_flags
+ )
+
+
+ def_stack(
+ self,
+ data,
+ inputs,
+ prod,
+ reverse_stack,
+ coeff=None,
+ input_flags=None,
+ reference_noise_source=None,
+ ):
+ stack_index=reverse_stack["stack"][:]
+ stack_conj=reverse_stack["conjugate"][:].astype(bool)
+
+ nstack=np.max(stack_index)+1
+ nprod=prod.size
+ ninput=inputs.size
+
+ # Sort the products based on the index of the stack axis they went into.
+ isort=np.argsort(stack_index)
+ sorted_stack_index=stack_index[isort]
+ sorted_stack_conj=stack_conj[isort]
+ sorted_prod=prod[isort]
+
+ temp=sorted_prod.copy()
+ sorted_prod["input_a"]=np.where(
+ sorted_stack_conj,temp["input_b"],temp["input_a"]
+ )
+ sorted_prod["input_b"]=np.where(
+ sorted_stack_conj,temp["input_a"],temp["input_b"]
+ )
+
+ # Find boundaries into the sorted products that separate stacks.
+ boundary=np.concatenate(
+ (
+ np.atleast_1d(0),
+ np.flatnonzero(np.diff(sorted_stack_index)>0)+1,
+ np.atleast_1d(nprod),
+ )
+ )
+
+ # Check for coefficient array that encodes the contribution of
+ # each noise source to each input.
+ ifcoeffisNone:
+ # This timing correction does not have a coefficient array.
+ # Construct from the output of the map_input_to_noise_source method.
+ index=np.array(map_input_to_noise_source(inputs,self.noise_source))
+ coeff=np.zeros((ninput,self.nsource),dtype=np.float64)
+ coeff[np.arange(ninput),index]=1.0
+
+ # Expand the coefficient array to have single element time axis
+ nsource=coeff.shape[-1]
+ coeff=coeff[:,:,np.newaxis]
+
+ # Construct separate coefficient array that handles the reference noise source
+ with_ref=reference_noise_sourceisnotNone
+ ifwith_ref:
+ cref=np.zeros((ninput,nsource,nsource,1),dtype=np.float64)
+ cref[np.arange(ninput),reference_noise_source]=coeff
+
+ # If input_flags was not provided, or if it is all True or all False, then we
+ # assume all inputs are good and carry out a faster calculation.
+ no_input_flags=(
+ (input_flagsisNone)ornotnp.any(input_flags)ornp.all(input_flags)
+ )
+
+ ifno_input_flags:
+ # No input flags provided. All inputs considered good.
+ uniq_input_flags=np.ones((ninput,1),dtype=np.float64)
+ index_time=slice(None)
+ else:
+ # Find the unique sets of input flags.
+ uniq_input_flags,index_time=np.unique(
+ input_flags,return_inverse=True,axis=1
+ )
+
+ ntime_uniq=uniq_input_flags.shape[-1]
+
+ # Initialize arrays to hold the stacked coefficients
+ stack_coeff=np.zeros((nstack,nsource,ntime_uniq),dtype=np.float64)
+ weight_norm=np.zeros((nstack,ntime_uniq),dtype=np.float64)
+
+ # Loop over stacked products
+ forss,ssiinenumerate(np.unique(sorted_stack_index)):
+ # Get the input pairs that went into this stack
+ prodo=sorted_prod[boundary[ss]:boundary[ss+1]]
+ aa=prodo["input_a"]
+ bb=prodo["input_b"]
+
+ # Sum the difference in coefficients over pairs of inputs,
+ # weighted by the product of the input flags for those inputs.
+ ww=uniq_input_flags[aa]*uniq_input_flags[bb]
+ weight_norm[ssi]=np.sum(ww,axis=0)
+ stack_coeff[ssi]=np.sum(
+ ww[:,np.newaxis,:]*(coeff[aa]-coeff[bb]),axis=0
+ )
+
+ ifwith_ref:
+ stack_coeff[ssi]-=np.sum(
+ ww[:,np.newaxis,:]*np.sum(cref[aa]-cref[bb],axis=2),axis=0
+ )
+
+ # The delay for each stacked product is a linear combination of the
+ # delay from the noise source inputs.
+ stacked_data=np.sum(
+ stack_coeff[:,:,index_time]*data[np.newaxis,:,:],axis=1
+ )
+ stacked_data*=tools.invert_no_zero(weight_norm[:,index_time])
+
+ returnstacked_data
+
+
+[docs]
+ defget_timing_correction(self,freq,timestamp,**kwargs):
+"""Return the phase correction from each noise source at the requested frequency and time.
+
+ Assumes the phase correction scales with frequency nu as phi = 2 pi nu tau and uses the
+ get_tau method to interpolate over time. It acccepts and passes along keyword arguments
+ for that method.
+
+ Parameters
+ ----------
+ freq: np.ndarray[nfreq, ]
+ Frequency in MHz.
+ timestamp: np.ndarray[ntime, ]
+ Unix timestamp.
+
+ Returns
+ -------
+ gain: np.ndarray[nfreq, nsource, ntime]
+ Complex gain containing a pure phase correction for each of the noise sources.
+ weight: np.ndarray[nfreq, nsource, ntime]
+ Uncerainty on the gain for each of the noise sources, expressed as an inverse variance.
+ """
+ tau,wtau=self.get_tau(timestamp,**kwargs)
+
+ gain=np.exp(
+ -1.0j
+ *FREQ_TO_OMEGA
+ *freq[:,np.newaxis,np.newaxis]
+ *tau[np.newaxis,:,:]
+ )
+
+ weight=(
+ wtau[np.newaxis,:,:]
+ *tools.invert_no_zero(FREQ_TO_OMEGA*freq[:,np.newaxis,np.newaxis])**2
+ )
+
+ returngain,weight
+
+
+
+[docs]
+ defget_gain(self,freq,inputs,timestamp,**kwargs):
+"""Return the complex gain for the requested frequencies, inputs, and times.
+
+ Multiplying the visibilities by the outer product of these gains will remove
+ the fluctuations in phase due to timing jitter. This method uses the
+ get_tau method. It acccepts and passes along keyword arguments for that method.
+
+ Parameters
+ ----------
+ freq: np.ndarray[nfreq, ]
+ Frequency in MHz.
+ inputs: np.ndarray[ninput, ]
+ Must contain 'correlator_input' field.
+ timestamp: np.ndarray[ntime, ]
+ Unix timestamps.
+
+ Returns
+ -------
+ gain : np.ndarray[nfreq, ninput, ntime]
+ Complex gain. Multiplying the visibilities by the
+ outer product of this vector at a given time and
+ frequency will correct for the timing jitter.
+ weight: np.ndarray[nfreq, ninput, ntime]
+ Uncerainty on the gain expressed as an inverse variance.
+ """
+ ifself.has_coeff_tau:
+ # Get the delay template for the noise source inputs
+ # at the requested times
+ tau,wtau=self.get_tau(timestamp,**kwargs)
+
+ vartau=tools.invert_no_zero(wtau)
+
+ # Find the coefficients for the requested inputs
+ reod=andata._convert_to_slice(self.search_input(inputs))
+
+ C=self.coeff_tau[reod,:]
+
+ # Different calculation dependening on whether or not the
+ # reference noise source changes with input
+ iref=self.reference_noise_source
+ ifnp.isscalar(iref):
+ # There is a single reference for all inputs.
+ # Check if it is different than the current reference.
+ ifiref!=self.zero_delay_noise_source:
+ tau=tau-tau[iref,np.newaxis,:]
+ vartau=vartau+vartau[iref,np.newaxis,:]
+
+ # The delay for each input is a linear combination of the
+ # delay from the noise source inputs
+ tau=np.matmul(C,tau)
+ vartau=np.matmul(C**2,vartau)
+
+ else:
+ # Find the reference for the requested inputs
+ iref=iref[reod]
+
+ # The delay for each input is a linear combination of the
+ # delay from the noise source inputs
+ sumC=np.sum(C,axis=-1,keepdims=True)
+
+ tau=np.matmul(C,tau)-sumC*tau[iref,:]
+
+ vartau=np.matmul(C**2,vartau)+sumC**2*vartau[iref,:]
+
+ # Check if we need to correct the delay using the noise source amplitude
+ ifself.has_amplitudeandself.has_coeff_alpha:
+ # Get the alpha template for the noise source inputs
+ # at the requested times
+ alpha,walpha=self.get_alpha(timestamp,**kwargs)
+
+ varalpha=tools.invert_no_zero(walpha)
+
+ Calpha=self.coeff_alpha[reod,:]
+
+ # Adjust the delay for each input by the linear combination of the
+ # amplitude from the noise source inputs
+ tau+=np.matmul(Calpha,alpha)
+
+ vartau+=np.matmul(Calpha**2,varalpha)
+
+ # Scale by 2 pi nu to convert to gain
+ gain=np.exp(
+ -1.0j
+ *FREQ_TO_OMEGA
+ *freq[:,np.newaxis,np.newaxis]
+ *tau[np.newaxis,:,:]
+ )
+
+ weight=tools.invert_no_zero(
+ vartau[np.newaxis,:,:]
+ *(FREQ_TO_OMEGA*freq[:,np.newaxis,np.newaxis])**2
+ )
+
+ else:
+ # Get the timing correction for the noise source inputs at the
+ # requested times and frequencies
+ gain,weight=self.get_timing_correction(freq,timestamp,**kwargs)
+
+ # Determine which noise source to use for each input
+ index=map_input_to_noise_source(inputs,self.noise_source)
+
+ gain=gain[:,index,:]
+ weight=weight[:,index,:]
+
+ # Return gains
+ returngain,weight
+
+
+
+[docs]
+ defapply_timing_correction(self,timestream,copy=False,**kwargs):
+"""Apply the timing correction to another visibility dataset.
+
+ This method uses the get_gain or get_stacked_tau method, depending
+ on whether or not the visibilities have been stacked. It acccepts
+ and passes along keyword arguments for those method.
+
+ Parameters
+ ----------
+ timestream : andata.CorrData / equivalent or np.ndarray[nfreq, nprod, ntime]
+ If timestream is an np.ndarray containing the visiblities, then you
+ must also pass the corresponding freq, prod, input, and time axis as kwargs.
+ Otherwise these quantities are obtained from the attributes of CorrData.
+ If the visibilities have been stacked, then you must additionally pass the
+ stack and reverse_stack axis as kwargs, and (optionally) the input flags.
+ copy : bool
+ Create a copy of the input visibilities. Apply the timing correction to
+ the copy and return it, leaving the original untouched. Default is False.
+ freq : np.ndarray[nfreq, ]
+ Frequency in MHz.
+ Must be passed as keyword argument if timestream is an np.ndarray.
+ prod: np.ndarray[nprod, ]
+ Product map.
+ Must be passed as keyword argument if timestream is an np.ndarray.
+ time: np.ndarray[ntime, ]
+ Unix time.
+ Must be passed as keyword argument if timestream is an np.ndarray.
+ input: np.ndarray[ninput, ] of dtype=('chan_id', 'correlator_input')
+ Input axis.
+ Must be passed as keyword argument if timestream is an np.ndarray.
+ stack : np.ndarray[nstack, ]
+ Stack axis.
+ Must be passed as keyword argument if timestream is an np.ndarray
+ and the visibilities have been stacked.
+ reverse_stack : np.ndarray[nprod, ] of dtype=('stack', 'conjugate')
+ The index of the stack axis that each product went into.
+ Typically found in `reverse_map['stack']` attribute.
+ Must be passed as keyword argument if timestream is an np.ndarray
+ and the visibilities have been stacked.
+ input_flags : np.ndarray [ninput, ntime]
+ Array indicating which inputs were good at each time. Non-zero value
+ indicates that an input was good. Optional. Only used for stacked visibilities.
+
+ Returns
+ -------
+ If copy == True:
+ vis : np.ndarray[nfreq, nprod(nstack), ntime]
+ New set of visibilities with timing correction applied.
+ else:
+ None
+ Correction is applied to the input visibility data. Also,
+ if timestream is an andata.CorrData instance and the gain dataset exists,
+ then it will be updated with the complex gains that have been applied.
+ """
+ ifisinstance(timestream,np.ndarray):
+ is_obj=False
+
+ vis=timestreamifnotcopyelsetimestream.copy()
+
+ freq=kwargs.pop("freq")
+ prod=kwargs.pop("prod")
+ inputs=kwargs.pop("input")
+ timestamp=kwargs.pop("time")
+ stack=kwargs.pop("stack",None)
+ reverse_stack=kwargs.pop("reverse_stack",None)
+
+ else:
+ is_obj=True
+
+ # This works for both distributed and non-distributed datasets
+ vis=timestream.vis[:].view(np.ndarray)
+
+ ifcopy:
+ vis=vis.copy()
+
+ freq=kwargs.pop("freq")if"freq"inkwargselsetimestream.freq[:]
+ prod=(
+ kwargs.pop("prod")
+ if"prod"inkwargs
+ elsetimestream.index_map["prod"][:]
+ )
+ inputs=kwargs.pop("input")if"input"inkwargselsetimestream.input[:]
+ timestamp=kwargs.pop("time")if"time"inkwargselsetimestream.time[:]
+ stack=(
+ kwargs.pop("stack")
+ if"stack"inkwargs
+ elsetimestream.index_map["stack"][:]
+ )
+ reverse_stack=(
+ kwargs.pop("reverse_stack")
+ if"reverse_stack"inkwargs
+ elsetimestream.reverse_map["stack"][:]
+ )
+
+ input_flags=kwargs.pop("input_flags",None)
+
+ # Determine if the visibilities have been stacked
+ is_stack=(
+ (stackisnotNone)
+ and(reverse_stackisnotNone)
+ and(stack.size<prod.size)
+ )
+
+ ifis_stack:
+ logger.info("Applying timing correction to stacked data.")
+ # Visibilities have been stacked.
+ # Stack the timing correction before applying it.
+ tau=self.get_stacked_tau(
+ timestamp,
+ inputs,
+ prod,
+ reverse_stack,
+ input_flags=input_flags,
+ **kwargs
+ )
+
+ ifself.has_amplitudeandself.has_coeff_alpha:
+ tau+=self.get_stacked_alpha(
+ timestamp,
+ inputs,
+ prod,
+ reverse_stack,
+ input_flags=input_flags,
+ **kwargs
+ )
+
+ # Loop over local frequencies and apply the timing correction
+ forffinrange(freq.size):
+ vis[ff]*=np.exp(-1.0j*FREQ_TO_OMEGA*freq[ff]*tau)
+
+ else:
+ logger.info("Applying timing correction to unstacked data.")
+ # Visibilities have not been stacked yet. Use the timing correction as is.
+ # Get the gain corrections for the times and frequencies in timestream.
+ gain,_=self.get_gain(freq,inputs,timestamp,**kwargs)
+
+ # Loop over products and apply the timing correction
+ forii,(aa,bb)inenumerate(prod):
+ vis[:,ii,:]*=gain[:,aa,:]*gain[:,bb,:].conj()
+
+ # If andata object was input then update the gain
+ # dataset so that we have record of what was done
+ ifis_objandnotcopyand"gain"intimestream:
+ timestream.gain[:]*=gain
+
+ # If a copy was requested, then return the
+ # new vis with phase correction applied
+ ifcopy:
+ returnvis
+
+
+
+[docs]
+ defsummary(self):
+"""Provide a summary of the timing correction.
+
+ Returns
+ -------
+ summary : list of strings
+ Contains useful information about the timing correction.
+ Specifically contains for each noise source input the
+ time averaged phase offset and delay. Also contains
+ estimates of the variance in the timing for both the
+ shortest and longest timescale probed by the underlying
+ dataset. Meant to be joined with new lines and printed.
+ """
+ span=(self.time[-1]-self.time[0])/3600.0
+ sig_tau=np.std(self.tau[:],axis=-1)
+
+ step=np.median(np.diff(self.time))
+ sig2_tau=np.sqrt(
+ np.sum(np.diff(self.tau[:],axis=-1)**2,axis=-1)
+ /(2.0*(self.tau.shape[-1]-1.0))
+ )
+
+ fmt="%-10s%10s%10s%15s%15s"
+ hdr=fmt%("","PHI0","TAU0","SIGMA(TAU)","SIGMA(TAU)")
+ per=fmt%("","","","@ %0.2f sec"%step,"@ %0.2f hr"%span)
+ unt=fmt%("INPUT","[rad]","[nsec]","[psec]","[psec]")
+ line="".join(["-"]*65)
+ summary=[line,hdr,per,unt,line]
+
+ fmt="%-10s%10.2f%10.2f%15.2f%15.2f"
+ forii,inpinenumerate(self.noise_source):
+ summary.append(
+ fmt
+ %(
+ inp["correlator_input"],
+ self.static_phi_fit[0,ii],
+ self.static_phi_fit[1,ii]*1e-3,
+ sig2_tau[ii],
+ sig_tau[ii],
+ )
+ )
+
+ returnsummary
+
+
+ def__repr__(self):
+"""Return a summary of the timing correction nicely formatted for printing.
+
+ Calls the method summary and joins the list of strings with new lines.
+ """
+ summary=self.summary()
+ summary.insert(0,self.__class__.__name__)
+
+ return"\n".join(summary)
+
+
+
+
+[docs]
+classTimingData(andata.CorrData,TimingCorrection):
+"""
+ Subclass of :class:`andata.CorrData` for timing data.
+
+ Automatically computes the timing correction when data is loaded and
+ inherits the methods of :class:`TimingCorrection` that enable the application
+ of that correction to other datasets.
+ """
+
+
+[docs]
+ @classmethod
+ deffrom_acq_h5(cls,acq_files,only_correction=False,**kwargs):
+"""Load a list of acquisition files and computes the timing correction.
+
+ Accepts and passes on all keyword arguments for andata.CorrData.from_acq_h5
+ and the construct_delay_template function.
+
+ Parameters
+ ----------
+ acq_files: str or list of str
+ Path to file(s) containing the timing data.
+ only_correction: bool
+ Only return the timing correction. Do not return the underlying
+ data from which that correction was derived.
+
+ Returns
+ -------
+ data: TimingData or TimingCorrection
+ """
+ # Separate the kwargs for construct_delay_template. This is necessary
+ # because andata will not accept extraneous kwargs.
+ insp=inspect.getargspec(construct_delay_template)
+ cdt_kwargs_list=set(insp[0][-len(insp[-1]):])&set(kwargs)
+
+ cdt_kwargs={}
+ fornameincdt_kwargs_list:
+ cdt_kwargs[name]=kwargs.pop(name)
+
+ # Change some of the default parameters for CorrData.from_acq_h5 to reflect
+ # the fact that this data will be used to compute a timing correction.
+ apply_gain=kwargs.pop("apply_gain",False)
+ datasets=kwargs.pop(
+ "datasets",["vis","flags/vis_weight","flags/frac_lost"]
+ )
+
+ # Load the data into an andata.CorrData object
+ corr_data=super(TimingData,cls).from_acq_h5(
+ acq_files,apply_gain=apply_gain,datasets=datasets,**kwargs
+ )
+
+ # Instantiate a TimingCorrection or TimingData object
+ dist_kwargs={"distributed":corr_data.distributed,"comm":corr_data.comm}
+ data=(
+ TimingCorrection(**dist_kwargs)
+ ifonly_correction
+ elseTimingData(**dist_kwargs)
+ )
+
+ # Redefine input axis to contain only noise sources
+ isource=np.unique(corr_data.prod.tolist())
+ noise_source=corr_data.input[isource]
+ data.create_index_map("noise_source",noise_source)
+
+ # Copy over relevant data to the newly instantiated object
+ ifonly_correction:
+ # We are only returning a correction, so we only need to
+ # copy over a subset of index_map.
+ fornamein["time","freq"]:
+ data.create_index_map(name,corr_data.index_map[name][:])
+
+ else:
+ # We are returning the data in addition to the correction.
+ # Redefine prod axis to contain only noise sources.
+ prod=np.zeros(corr_data.prod.size,dtype=corr_data.prod.dtype)
+ prod["input_a"]=andata._search_array(isource,corr_data.prod["input_a"])
+ prod["input_b"]=andata._search_array(isource,corr_data.prod["input_b"])
+ data.create_index_map("prod",prod)
+
+ # Copy over remaining index maps
+ forname,index_mapincorr_data.index_map.items():
+ ifnamenotindata.index_map:
+ data.create_index_map(name,index_map[:])
+
+ # Copy over the attributes
+ memh5.copyattrs(corr_data.attrs,data.attrs)
+
+ # Iterate over the datasets and copy them over
+ forname,old_dsetincorr_data.datasets.items():
+ new_dset=data.create_dataset(
+ name,data=old_dset[:],distributed=old_dset.distributed
+ )
+ memh5.copyattrs(old_dset.attrs,new_dset.attrs)
+
+ # Iterate over the flags and copy them over
+ forname,old_dsetincorr_data.flags.items():
+ new_dset=data.create_flag(
+ name,data=old_dset[:],distributed=old_dset.distributed
+ )
+ memh5.copyattrs(old_dset.attrs,new_dset.attrs)
+
+ # Construct delay template
+ res=construct_delay_template(corr_data,**cdt_kwargs)
+
+ # If we are only returning the timing correction, then remove
+ # the amplitude and phase of the noise source
+ ifonly_correction:
+ fornamein["amp","weight_amp","phi","weight_phi"]:
+ res.pop(name)
+
+ # Create index map containing names of parameters
+ param=["intercept","slope","quad","cube","quart","quint"]
+ param=param[0:res["static_phi_fit"].shape[0]]
+ data.create_index_map("param",np.array(param,dtype=np.string_))
+
+ # Create datasets containing the timing correction
+ forname,arrinres.items():
+ spec=DSET_SPEC[name]
+ ifspec["flag"]:
+ dset=data.create_flag(name,data=arr)
+ else:
+ dset=data.create_dataset(name,data=arr)
+
+ dset.attrs["axis"]=np.array(spec["axis"],dtype=np.string_)
+
+ # Delete the temporary corr_data object
+ delcorr_data
+ gc.collect()
+
+ # Return timing data object
+ returndata
+
+
+
+[docs]
+ defsummary(self):
+"""Provide a summary of the timing data and correction.
+
+ Returns
+ -------
+ summary : list of strings
+ Contains useful information about the timing correction
+ and data. Includes the reduction in the standard deviation
+ of the phase after applying the timing correction. This is
+ presented as quantiles over frequency for each of the
+ noise source products.
+ """
+ summary=super(TimingData,self).summary()
+
+ vis=self.apply_timing_correction(
+ self.vis[:],
+ copy=True,
+ freq=self.freq,
+ time=self.time,
+ prod=self.prod,
+ input=self.noise_source,
+ )
+
+ phi_before=np.angle(self.vis[:])
+ phi_after=np.angle(vis)
+
+ phi_before=_correct_phase_wrap(
+ phi_before-np.median(phi_before,axis=-1)[...,np.newaxis]
+ )
+ phi_after=_correct_phase_wrap(
+ phi_after-np.median(phi_after,axis=-1)[...,np.newaxis]
+ )
+
+ sig_before=np.median(
+ np.abs(phi_before-np.median(phi_before,axis=-1)[...,np.newaxis]),
+ axis=-1,
+ )
+ sig_after=np.median(
+ np.abs(phi_after-np.median(phi_after,axis=-1)[...,np.newaxis]),axis=-1
+ )
+
+ ratio=sig_before*tools.invert_no_zero(sig_after)
+
+ stats=np.percentile(ratio,[0,25,50,75,100],axis=0)
+
+ fmt="%-23s%5s%5s%8s%5s%5s"
+ hdr1="Factor Reduction in RMS Phase Noise (Quantiles Over Frequency)"
+ hdr2=fmt%("PRODUCT","MIN","25%","MEDIAN","75%","MAX")
+ line="".join(["-"]*65)
+ summary+=["",line,hdr1,hdr2,line]
+
+ fmt="%-10s x %-10s%5d%5d%8d%5d%5d"
+ forii,ppinenumerate(self.prod):
+ ifpp[0]!=pp[1]:
+ summary.append(
+ fmt
+ %(
+ (
+ self.noise_source[pp[0]]["correlator_input"],
+ self.noise_source[pp[1]]["correlator_input"],
+ )
+ +tuple(stats[:,ii])
+ )
+ )
+
+ returnsummary
+
+
+
+
+
+[docs]
+classTimingInterpolator(object):
+"""Interpolation that is aware of flagged data and weights.
+
+ Flagged data is ignored during the interpolation. The weights from
+ the data are propagated to obtain weights for the interpolated points.
+ """
+
+ def__init__(self,x,y,weight=None,flag=None,kind="linear",extrap_limit=None):
+"""Instantiate a callable TimingInterpolator object.
+
+ Parameters
+ ----------
+ x : np.ndarray[nsample,]
+ The points where the data was sampled.
+ Must be monotonically increasing.
+ y : np.ndarray[..., nsample]
+ The data to interpolate.
+ weight : np.ndarray[..., nsample]
+ The uncertainty on the data, expressed as an
+ inverse variance.
+ flag : np.ndarray[..., nsample]
+ Boolean indicating if the data is to be
+ included in the interpolation.
+ kind : str
+ String that specifies the kind of interpolation.
+ The value `nearest`, `previous`, `next`, and `linear` will use
+ custom methods that propagate uncertainty to obtain the interpolated
+ weights. The value `zero`, `slinear`, `quadratic`, and `cubic`
+ will use spline interpolation from scipy.interpolation.interp1d
+ and use the weight from the nearest point.
+
+ Returns
+ -------
+ interpolator : TimingInterpolator
+ Callable that will interpolate the data that was provided
+ to a new set of x values.
+ """
+ self.x=x
+ self.y=y
+
+ self._shape=y.shape[:-1]
+
+ ifweightisNone:
+ self.var=np.ones(y.shape,dtype=np.float32)
+ else:
+ self.var=tools.invert_no_zero(weight)
+
+ ifflagisNone:
+ self.flag=np.ones(y.shape,dtype=bool)
+ else:
+ self.flag=flag
+
+ ifextrap_limitisNone:
+ self._extrap_limit=2.0*np.median(np.diff(self.x))
+ else:
+ self._extrap_limit=extrap_limit
+
+ self._interp=INTERPOLATION_LOOKUP.get(kind,_interpolation_scipy(kind))
+
+ def__call__(self,xeval):
+"""Interpolate the data.
+
+ Parameters
+ ----------
+ xeval : np.ndarray[neval,]
+ Evaluate the interpolant at these points.
+
+ Returns
+ -------
+ yeval : np.ndarray[neval,]
+ Interpolated values.
+ weval : np.ndarray[neval,]
+ Uncertainty on the interpolated values,
+ expressed as an inverse variance.
+ """
+ # Make sure we are not extrapolating too much
+ dx_beg=self.x[0]-np.min(xeval)
+ dx_end=np.max(xeval)-self.x[-1]
+
+ if(dx_beg>self._extrap_limit)or(dx_end>self._extrap_limit):
+ raiseValueError("Extrapolating beyond span of data.")
+
+ # Create arrays to hold interpolation
+ shape=self._shapeifnp.isscalar(xeval)elseself._shape+(xeval.size,)
+
+ yeval=np.zeros(shape,dtype=self.y.dtype)
+ weval=np.zeros(shape,dtype=np.float32)
+
+ # Loop over other axes and interpolate along last axis
+ forindinnp.ndindex(*self._shape):
+ to_interp=np.flatnonzero(self.flag[ind])
+ ifto_interp.size>0:
+ yeval[ind],weval[ind]=self._interp(
+ self.x[to_interp],
+ self.y[ind][to_interp],
+ self.var[ind][to_interp],
+ xeval,
+ )
+
+ returnyeval,weval
+
+
+
+
+[docs]
+defload_timing_correction(
+ files,start=None,stop=None,window=43200.0,instrument="chime",**kwargs
+):
+"""Find and load the appropriate timing correction for a list of corr acquisition files.
+
+ For example, if the instrument keyword is set to 'chime',
+ then this function will accept all types of chime corr acquisition files,
+ such as 'chimetiming', 'chimepb', 'chimeN2', 'chimecal', and then find
+ the relevant set of 'chimetiming' files to load.
+
+ Accepts and passes on all keyword arguments for the functions
+ andata.CorrData.from_acq_h5 and construct_delay_template.
+
+ Should consider modifying this method to use Finder at some point in future.
+
+ Parameters
+ ----------
+ files : string or list of strings
+ Absolute path to corr acquisition file(s).
+ start : integer, optional
+ What frame to start at in the full set of files.
+ stop : integer, optional
+ What frame to stop at in the full set of files.
+ window : float
+ Use the timing data -window from start and +window from stop.
+ Default is 12 hours.
+ instrument : string
+ Name of the instrument. Default is 'chime'.
+
+ Returns
+ -------
+ data: TimingData
+ """
+ files=np.atleast_1d(files)
+
+ # Check that a single acquisition was requested
+ input_dirs=[os.path.dirname(ff)forffinfiles]
+ iflen(set(input_dirs))>1:
+ raiseRuntimeError("Input files span multiple acquisitions!")
+
+ # Extract relevant information from the filename
+ node=os.path.dirname(input_dirs[0])
+ acq=os.path.basename(input_dirs[0])
+
+ acq_date,acq_inst,acq_type=acq.split("_")
+ ifnotacq_inst.startswith(instrument)or(acq_type!="corr"):
+ raiseRuntimeError(
+ "This function is only able to parse corr type files "
+ "from the specified instrument (currently %s)."%instrument
+ )
+
+ # Search for all timing acquisitions on this node
+ tdirs=sorted(
+ glob.glob(os.path.join(node,"_".join(["*",instrument+"timing",acq_type])))
+ )
+ ifnottdirs:
+ raiseRuntimeError("No timing acquisitions found on node %s."%node)
+
+ # Determine the start time of the requested acquistion and the available timing acquisitions
+ acq_start=ephemeris.datetime_to_unix(ephemeris.timestr_to_datetime(acq_date))
+
+ tacq_start=np.array(
+ [ephemeris.timestr_to_datetime(os.path.basename(tt))forttintdirs]
+ )
+ tacq_start=ephemeris.datetime_to_unix(tacq_start)
+
+ # Find the closest timing acquisition to the requested acquisition
+ iclose=np.argmin(np.abs(acq_start-tacq_start))
+ ifnp.abs(acq_start-tacq_start[iclose])>60.0:
+ raiseRuntimeError("Cannot find appropriate timing acquisition for %s."%acq)
+
+ # Grab all timing files from this acquisition
+ tfiles=sorted(glob.glob(os.path.join(tdirs[iclose],"*.h5")))
+
+ tdata=andata.CorrData.from_acq_h5(tfiles,datasets=())
+
+ # Find relevant span of time
+ data=andata.CorrData.from_acq_h5(files,start=start,stop=stop,datasets=())
+
+ time_start=data.time[0]-window
+ time_stop=data.time[-1]+window
+
+ tstart=int(np.argmin(np.abs(time_start-tdata.time)))
+ tstop=int(np.argmin(np.abs(time_stop-tdata.time)))
+
+ # Load into TimingData object
+ data=TimingData.from_acq_h5(tfiles,start=tstart,stop=tstop,**kwargs)
+
+ returndata
+[docs]
+defconstruct_delay_template(
+ data,
+ min_frac_kept=0.0,
+ threshold=0.50,
+ min_freq=420.0,
+ max_freq=780.0,
+ mask_rfi=False,
+ max_iter_weight=None,
+ check_amp=False,
+ nsigma_amp=None,
+ check_phi=True,
+ nsigma_phi=None,
+ nparam=2,
+ static_phi=None,
+ weight_static_phi=None,
+ static_phi_fit=None,
+ static_amp=None,
+ weight_static_amp=None,
+):
+"""Construct a relative time delay template.
+
+ Fits the phase of the cross-correlation between noise source inputs
+ to a model that increases linearly with frequency.
+
+ Parameters
+ ----------
+ data: andata.CorrData
+ Correlation data. Must contain the following attributes:
+ freq: np.ndarray[nfreq, ]
+ Frequency in MHz.
+ vis: np.ndarray[nfreq, nprod, ntime]
+ Upper-triangle, product packed visibility matrix
+ containing ONLY the noise source inputs.
+ weight: np.ndarray[nfreq, nprod, ntime]
+ Flag indicating the data points to fit.
+ flags/frac_lost: np.ndarray[nfreq, ntime]
+ Flag indicating the fraction of data lost.
+ If provided, then data will be weighted by the
+ fraction of data that remains when solving
+ for the delay template.
+ min_frac_kept: float
+ Do not include frequencies and times where the fraction
+ of data that remains is less than this threshold.
+ Default is 0.0.
+ threshold: float
+ A (frequency, input) must pass the checks specified above
+ more than this fraction of the time, otherwise it will be
+ flaged as bad for all times. Default is 0.50.
+ min_freq: float
+ Minimum frequency in MHz to include in the fit.
+ Default is 420.
+ max_freq: float
+ Maximum frequency in MHz to include in the fit.
+ Default is 780.
+ mask_rfi: bool
+ Mask frequencies that occur within known RFI bands. Note that the
+ noise source data does not contain RFI, however the real-time pipeline
+ does not distinguish between noise source inputs and sky inputs, and as
+ a result will discard large amounts of data in these bands.
+ max_iter_weight: int
+ The weight for each frequency is estimated from the variance of the
+ residuals of the template fit from the previous iteration. Outliers
+ are also flagged at each iteration with an increasingly aggresive threshold.
+ This is the total number of times to iterate. Setting to 1 corresponds
+ to linear least squares. Default is 1, unless check_amp or check_phi is True,
+ in which case this defaults to the maximum number of thresholds provided.
+ check_amp: bool
+ Do not fit frequencies and times where the residual amplitude is an outlier.
+ Default is False.
+ nsigma_amp: list of float
+ If check_amp is True, then residuals greater than this number of sigma
+ will be considered an outlier. Provide a list containing the value to be used
+ at each iteration. If the length of the list is less than max_iter_weight,
+ then the last value in the list will be repeated for the remaining iterations.
+ Default is [1000, 500, 200, 100, 50, 20, 10, 5].
+ check_phi: bool
+ Do not fit frequencies and times where the residual phase is an outlier.
+ Default is True.
+ nsigma_phi: list of float
+ If check_phi is True, then residuals greater than this number of sigma
+ will be considered an outlier. Provide a list containing the value to be used
+ at each iteration. If the length of the list is less than max_iter_weight,
+ then the last value in the list will be repeated for the remaining iterations.
+ Default is [1000, 500, 200, 100, 50, 20, 10, 5].
+ nparam: int
+ Number of parameters for polynomial fit to the
+ time averaged phase versus frequency. Default is 2.
+ static_phi: np.ndarray[nfreq, nsource]
+ Subtract this quantity from the noise source phase prior to fitting
+ for the timing correction. If None, then this will be estimated from the median
+ of the noise source phase over time.
+ weight_static_phi: np.ndarray[nfreq, nsource]
+ Inverse variance of the time averaged phased. Set to zero for frequencies and inputs
+ that are missing or should be ignored. If None, then this will be estimated from the
+ residuals of the fit.
+ static_phi_fit: np.ndarray[nparam, nsource]
+ Polynomial fit to static_phi versus frequency.
+ static_amp: np.ndarray[nfreq, nsource]
+ Subtract this quantity from the noise source amplitude prior to fitting
+ for the amplitude variations. If None, then this will be estimated from the median
+ of the noise source amplitude over time.
+ weight_static_amp: np.ndarray[nfreq, nsource]
+ Inverse variance of the time averaged amplitude. Set to zero for frequencies and inputs
+ that are missing or should be ignored. If None, then this will be estimated from the
+ residuals of the fit.
+
+ Returns
+ -------
+ phi: np.ndarray[nfreq, nsource, ntime]
+ Phase of the signal from the noise source.
+ weight_phi: np.ndarray[nfreq, nsource, ntime]
+ Inverse variance of the phase of the signal from the noise source.
+ tau: np.ndarray[nsource, ntime]
+ Delay template for each noise source input.
+ weight_tau: np.ndarray[nfreq, nsource]
+ Estimate of the uncertainty on the delay template (inverse variance).
+ static_phi: np.ndarray[nfreq, nsource]
+ Time averaged phase versus frequency.
+ weight_static_phi: np.ndarray[nfreq, nsource]
+ Inverse variance of the time averaged phase.
+ static_phi_fit: np.ndarray[nparam, nsource]
+ Best-fit parameters of the polynomial fit to the
+ time averaged phase versus frequency.
+ amp: np.ndarray[nfreq, nsource, ntime]
+ Amplitude of the signal from the noise source.
+ weight_amp: np.ndarray[nfreq, nsource, ntime]
+ Inverse variance of the amplitude of the signal from the noise source.
+ alpha: np.ndarray[nsource, ntime]
+ Amplitude coefficient for each noise source input.
+ weight_alpha: np.ndarray[nfreq, nsource]
+ Estimate of the uncertainty on the amplitude coefficient (inverse variance).
+ static_amp: np.ndarray[nfreq, nsource]
+ Time averaged amplitude versus frequency.
+ weight_static_amp: np.ndarray[nfreq, nsource]
+ Inverse variance of the time averaged amplitude.
+ num_freq: np.ndarray[nsource, ntime]
+ Number of frequencies used to construct the delay and amplitude templates.
+ """
+ # Check if we are distributed. If so make sure we are distributed over time.
+ parallel=isinstance(data.vis,memh5.MemDatasetDistributed)
+ ifparallel:
+ data.redistribute("time")
+ comm=data.vis.comm
+
+ # Extract relevant datasets
+ freq=data.freq[:]
+ vis=data.vis[:].view(np.ndarray)
+ weight=data.weight[:].view(np.ndarray)
+
+ # Check dimensions
+ nfreq,nprod,ntime=vis.shape
+ nsource=int((np.sqrt(8*nprod+1)-1)//2)
+ ilocal=range(0,nsource)
+
+ assertnfreq==freq.size
+ assertnsource>=2
+ assertnparam>=2
+
+ ifstatic_phiisnotNone:
+ static_phi,sphi_shp,sphi_ind=_resolve_distributed(static_phi,axis=1)
+ assertsphi_shp==(nfreq,nsource)
+
+ ifweight_static_phiisnotNone:
+ weight_static_phi,wsphi_shp,wsphi_ind=_resolve_distributed(
+ weight_static_phi,axis=1
+ )
+ assertwsphi_shp==(nfreq,nsource)
+
+ ifstatic_phi_fitisnotNone:
+ static_phi_fit,sphifit_shp,sphifit_ind=_resolve_distributed(
+ static_phi_fit,axis=1
+ )
+ assertsphifit_shp==(nparam,nsource)
+
+ ifstatic_ampisnotNone:
+ static_amp,samp_shp,samp_ind=_resolve_distributed(static_amp,axis=1)
+ assertsamp_shp==(nfreq,nsource)
+
+ ifweight_static_ampisnotNone:
+ weight_static_amp,wsamp_shp,wsamp_ind=_resolve_distributed(
+ weight_static_amp,axis=1
+ )
+ assertwsamp_shp==(nfreq,nsource)
+
+ # Set default nsigma for flagging outliers
+ ifnsigma_ampisNone:
+ nsigma_amp=[1000.0,500.0,200.0,100.0,50.0,20.0,10.0,5.0]
+ elifnp.isscalar(nsigma_amp):
+ nsigma_amp=[nsigma_amp]
+
+ ifnsigma_phiisNone:
+ nsigma_phi=[1000.0,500.0,200.0,100.0,50.0,20.0,10.0,5.0]
+ elifnp.isscalar(nsigma_phi):
+ nsigma_phi=[nsigma_phi]
+
+ ifmax_iter_weightisNone:
+ max_iter_weight=max(
+ len(nsigma_amp)+1ifcheck_ampelse1,
+ len(nsigma_phi)+1ifcheck_phielse1,
+ )
+ else:
+ max_iter_weight=max(max_iter_weight,1)
+
+ nsigma_amp=[
+ nsigma_amp[min(ii,len(nsigma_amp)-1)]foriiinrange(max_iter_weight)
+ ]
+ nsigma_phi=[
+ nsigma_phi[min(ii,len(nsigma_phi)-1)]foriiinrange(max_iter_weight)
+ ]
+
+ # Compute amplitude of noise source signal from autocorrelation
+ iauto=np.array([int(k*(2*nsource-k+1)//2)forkinrange(nsource)])
+
+ amp=np.sqrt(vis[:,iauto,:].real)
+
+ # Determine which data points to fit
+ flg=amp>0.0
+ ifweightisnotNone:
+ flg&=weight[:,iauto,:]>0.0
+
+ # If requested discard frequencies and times that have high frac_lost
+ ifhasattr(data,"flags")and("frac_lost"indata.flags):
+ logger.info("Fraction of data kept must be greater than %0.2f."%min_frac_kept)
+
+ frac_kept=1.0-data.flags["frac_lost"][:].view(np.ndarray)
+ flg&=frac_kept[:,np.newaxis,:]>=min_frac_kept
+
+ else:
+ frac_kept=np.ones((nfreq,ntime),dtype=np.float32)
+
+ # Restrict the range of frequencies that are fit to avoid bandpass edges
+ limit_freq=(freq>min_freq)&(freq<max_freq)
+ ifmask_rfi:
+ logger.info("Masking RFI bands.")
+ limit_freq&=~rfi.frequency_mask(
+ freq,freq_width=data.index_map["freq"]["width"][:]
+ )
+
+ flg=(flg&limit_freq[:,np.newaxis,np.newaxis]).astype(np.float32)
+
+ # If we only have two noise source inputs, then we use the cross-correlation
+ # between them to characterize their relative response to the noise source signal.
+ # If we have more than two noise source inputs, then we perform an eigenvalue
+ # decomposition of the cross-correlation matrix to obtain an improved estimate
+ # of the response of each input to the noise source signal.
+ ifnsource>2:
+ response=eigen_decomposition(vis,flg)
+
+ phi=np.angle(response)
+ amp=np.abs(response)
+
+ ww=flg
+
+ else:
+ phi=np.zeros((nfreq,nsource,ntime),dtype=np.float32)
+ phi[:,1,:]=np.angle(vis[:,1,:].conj())
+
+ amp=np.sqrt(vis[:,iauto,:].real)
+
+ ww=np.repeat(flg[:,0,np.newaxis,:]*flg[:,1,np.newaxis,:],2,axis=1)
+
+ # Scale the flag by the fraction of data that was kept
+ ww*=frac_kept[:,np.newaxis,:]
+
+ # If parallelized we need to redistribute over inputs for the
+ # operations below, which require full frequency and time coverage.
+ ifparallel:
+ amp=mpiarray.MPIArray.wrap(amp,axis=2,comm=comm)
+ phi=mpiarray.MPIArray.wrap(phi,axis=2,comm=comm)
+ ww=mpiarray.MPIArray.wrap(ww,axis=2,comm=comm)
+
+ amp=amp.redistribute(1)
+ phi=phi.redistribute(1)
+ ww=ww.redistribute(1)
+
+ nsource=amp.local_shape[1]
+ ilocal=range(amp.local_offset[1],amp.local_offset[1]+nsource)
+
+ logger.info("I am processing %d noise source inputs."%nsource)
+
+ amp=amp[:].view(np.ndarray)
+ phi=phi[:].view(np.ndarray)
+ ww=ww[:].view(np.ndarray)
+
+ # If a frequency is flagged more than `threshold` fraction of the time, then flag it entirely
+ ww*=(
+ (
+ np.sum(ww>0.0,axis=-1,dtype=np.float32,keepdims=True)
+ /float(ww.shape[-1])
+ )
+ >threshold
+ ).astype(np.float32)
+
+ logger.info(
+ "%0.1f percent of frequencies will be used to construct timing correction."
+ %(
+ 100.0
+ *np.sum(np.any(ww>0.0,axis=(1,2)),dtype=np.float32)
+ /float(ww.shape[0]),
+ )
+ )
+
+ # If the starting values for the mean and variance were not provided,
+ # then estimate them from the data.
+ ifstatic_phiisNone:
+ static_phi=_flagged_median(phi,ww,axis=-1)
+ else:
+ sphi_ind=np.array([sphi_ind.index(ilcl)forilclinilocal])
+ static_phi=static_phi[:,sphi_ind]
+
+ ifweight_static_phiisNone:
+ weight_static_phi=np.ones(ww.shape[0:2],dtype=np.float32)
+ else:
+ wsphi_ind=np.array([wsphi_ind.index(ilcl)forilclinilocal])
+ weight_static_phi=weight_static_phi[:,wsphi_ind]
+
+ ifstatic_ampisNone:
+ static_amp=_flagged_median(amp,ww,axis=-1)
+ else:
+ samp_ind=np.array([samp_ind.index(ilcl)forilclinilocal])
+ static_amp=static_amp[:,samp_ind]
+
+ ifweight_static_ampisNone:
+ weight_static_amp=np.ones(ww.shape[0:2],dtype=np.float32)
+ else:
+ wsamp_ind=np.array([wsamp_ind.index(ilcl)forilclinilocal])
+ weight_static_amp=weight_static_amp[:,wsamp_ind]
+
+ # Fit frequency dependence of amplitude and phase
+ # damp = asc * dalpha and dphi = omega * dtau
+ asc=(
+ _amplitude_scaling(freq[:,np.newaxis,np.newaxis])
+ *static_amp[:,:,np.newaxis]
+ )
+
+ omega=FREQ_TO_OMEGA*freq[:,np.newaxis,np.newaxis]
+
+ # Estimate variance of each frequency from residuals
+ foriter_weightinrange(max_iter_weight):
+ msg=["Iteration %d of %d"%(iter_weight+1,max_iter_weight)]
+
+ dphi=_correct_phase_wrap(phi-static_phi[:,:,np.newaxis])
+ damp=amp-static_amp[:,:,np.newaxis]
+
+ weight_amp=ww*weight_static_amp[:,:,np.newaxis]
+ weight_phi=ww*weight_static_phi[:,:,np.newaxis]
+
+ # Construct alpha template
+ alpha=np.sum(weight_amp*asc*damp,axis=0)*tools.invert_no_zero(
+ np.sum(weight_amp*asc**2,axis=0)
+ )
+
+ # Construct delay template
+ tau=np.sum(weight_phi*omega*dphi,axis=0)*tools.invert_no_zero(
+ np.sum(weight_phi*omega**2,axis=0)
+ )
+
+ # Calculate amplitude residuals
+ ramp=damp-asc*alpha[np.newaxis,:,:]
+
+ # Calculate phase residuals
+ rphi=dphi-omega*tau[np.newaxis,:,:]
+
+ # Calculate the mean and variance of the amplitude residuals
+ inv_num=tools.invert_no_zero(np.sum(ww,axis=-1))
+ mu_ramp=np.sum(ww*ramp,axis=-1)*inv_num
+ var_ramp=(
+ np.sum(ww*(ramp-mu_ramp[:,:,np.newaxis])**2,axis=-1)*inv_num
+ )
+
+ # Calculate the mean and variance of the phase residuals
+ mu_rphi=np.sum(ww*rphi,axis=-1)*inv_num
+ var_rphi=(
+ np.sum(ww*(rphi-mu_rphi[:,:,np.newaxis])**2,axis=-1)*inv_num
+ )
+
+ # Update the static quantities
+ static_amp=static_amp+mu_ramp
+ static_phi=static_phi+mu_rphi
+
+ weight_static_amp=tools.invert_no_zero(var_ramp)
+ weight_static_phi=tools.invert_no_zero(var_rphi)
+
+ # Flag outliers
+ not_outlier=np.ones_like(ww)
+ ifcheck_amp:
+ nsigma=np.abs(ramp)*np.sqrt(weight_static_amp[:,:,np.newaxis])
+ not_outlier*=(nsigma<nsigma_amp[iter_weight]).astype(np.float32)
+ msg.append("nsigma_amp = %0.1f"%nsigma_amp[iter_weight])
+
+ ifcheck_phi:
+ nsigma=np.abs(rphi)*np.sqrt(weight_static_phi[:,:,np.newaxis])
+ not_outlier*=(nsigma<nsigma_phi[iter_weight]).astype(np.float32)
+ msg.append("nsigma_phi = %0.1f"%nsigma_phi[iter_weight])
+
+ ifcheck_amporcheck_phi:
+ ww*=not_outlier
+
+ logger.info(" | ".join(msg))
+
+ # Calculate the number of frequencies used in the fit
+ num_freq=np.sum(weight_amp>0.0,axis=0,dtype=int)
+
+ # Calculate the uncertainties on the fit parameters
+ weight_tau=np.sum(weight_phi*omega**2,axis=0)
+ weight_alpha=np.sum(weight_amp*asc**2,axis=0)
+
+ # Calculate the average delay over this period using non-linear
+ # least squares that is insensitive to phase wrapping
+ ifstatic_phi_fitisNone:
+ err_static_phi=np.sqrt(tools.invert_no_zero(weight_static_phi))
+
+ static_phi_fit=np.zeros((nparam,nsource),dtype=np.float64)
+ fornninrange(nsource):
+ ifnp.sum(err_static_phi[:,nn]>0.0,dtype=int)>nparam:
+ static_phi_fit[:,nn]=fit_poly_to_phase(
+ freq,
+ np.exp(1.0j*static_phi[:,nn]),
+ err_static_phi[:,nn],
+ nparam=nparam,
+ )[0]
+ else:
+ sphifit_ind=np.array([sphifit_ind.index(ilcl)forilclinilocal])
+ static_phi_fit=static_phi_fit[:,sphifit_ind]
+
+ # Convert the outputs to MPIArrays distributed over input
+ ifparallel:
+ tau=mpiarray.MPIArray.wrap(tau,axis=0,comm=comm)
+ alpha=mpiarray.MPIArray.wrap(alpha,axis=0,comm=comm)
+
+ weight_tau=mpiarray.MPIArray.wrap(weight_tau,axis=0,comm=comm)
+ weight_alpha=mpiarray.MPIArray.wrap(weight_alpha,axis=0,comm=comm)
+
+ static_phi=mpiarray.MPIArray.wrap(static_phi,axis=1,comm=comm)
+ static_amp=mpiarray.MPIArray.wrap(static_amp,axis=1,comm=comm)
+
+ weight_static_phi=mpiarray.MPIArray.wrap(weight_static_phi,axis=1,comm=comm)
+ weight_static_amp=mpiarray.MPIArray.wrap(weight_static_amp,axis=1,comm=comm)
+
+ static_phi_fit=mpiarray.MPIArray.wrap(static_phi_fit,axis=1,comm=comm)
+
+ num_freq=mpiarray.MPIArray.wrap(num_freq,axis=0,comm=comm)
+
+ phi=mpiarray.MPIArray.wrap(phi,axis=1,comm=comm)
+ amp=mpiarray.MPIArray.wrap(amp,axis=1,comm=comm)
+
+ weight_phi=mpiarray.MPIArray.wrap(weight_phi,axis=1,comm=comm)
+ weight_amp=mpiarray.MPIArray.wrap(weight_amp,axis=1,comm=comm)
+
+ data.redistribute("freq")
+
+ # Return results
+ returndict(
+ tau=tau,
+ alpha=alpha,
+ weight_tau=weight_tau,
+ weight_alpha=weight_alpha,
+ static_phi=static_phi,
+ static_amp=static_amp,
+ weight_static_phi=weight_static_phi,
+ weight_static_amp=weight_static_amp,
+ static_phi_fit=static_phi_fit,
+ num_freq=num_freq,
+ phi=phi,
+ amp=amp,
+ weight_phi=weight_phi,
+ weight_amp=weight_amp,
+ )
+
+
+
+
+[docs]
+defmap_input_to_noise_source(inputs,noise_sources):
+"""Find the appropriate noise source to use to correct the phase of each input.
+
+ Searches for a noise source connected to the same slot,
+ then crate, then hut, then correlator.
+
+ Parameters
+ ----------
+ inputs: np.ndarray[ninput, ] of dtype=('chan_id', 'correlator_input')
+ The input axis from a data acquisition file.
+ noise_sources: np.ndarray[nsource, ] of dtype=('chan_id', 'correlator_input')
+ The noise sources.
+ """
+
+ # Define functions
+ defparse_serial(input_serial):
+ # Have to distinguish between CHIME WRH and ERH
+ # Otherwise serial numbers already have the
+ # desired hierarchical structure.
+
+ # Serial from file is often bytes, ensure it is unicode
+ ifnotisinstance(input_serial,str):
+ input_serial=input_serial.decode("utf-8")
+
+ ifinput_serial.startswith("FCC"):
+ ifint(input_serial[3:5])<4:
+ name="FCCW"+input_serial[3:]
+ else:
+ name="FCCE"+input_serial[3:]
+ else:
+ name=input_serial
+
+ returnname
+
+ defcount_startswith(x,y):
+ cnt=0
+ foriiinrange(min(len(x),len(y))):
+ ifx[ii]==y[ii]:
+ cnt+=1
+ else:
+ break
+
+ returncnt
+
+ # Create hierarchical identifier from serial number for the
+ # noise sources and requested inputs
+ input_names=list(map(parse_serial,inputs["correlator_input"]))
+ source_names=list(map(parse_serial,noise_sources["correlator_input"]))
+
+ # Map each input to a noise source
+ imap=[
+ np.argmax([count_startswith(inp,src)forsrcinsource_names])
+ forinpininput_names
+ ]
+
+ returnimap
+
+
+
+
+[docs]
+defeigen_decomposition(vis,flag):
+"""Eigenvalue decomposition of the visibility matrix.
+
+ Parameters
+ ----------
+ vis: np.ndarray[nfreq, nprod, ntime]
+ Upper-triangle, product packed visibility matrix.
+ flag: np.ndarray[nfreq, nsource, ntime] (optional)
+ Array of 1 or 0 indicating the inputs that should be included
+ in the eigenvalue decomposition for each frequency and time.
+
+ Returns
+ -------
+ resp: np.ndarray[nfreq, nsource, ntime]
+ Eigenvector corresponding to the largest eigenvalue for
+ each frequency and time.
+ """
+ nfreq,nprod,ntime=vis.shape
+ nsource=int((np.sqrt(8*nprod+1)-1)//2)
+
+ # Do not bother performing the eigen-decomposition for
+ # times and frequencies that are entirely flagged
+ ind=np.where(np.any(flag,axis=1))
+ ind=(ind[0],slice(None),ind[1])
+
+ # Indexing the flag and vis datasets with ind flattens
+ # the frequency and time dimension. This results in
+ # flg having shape (nfreq x ntime, nsource) and
+ # Q having shape (nfreq x ntime, nsource, nsource).
+ flg=flag[ind].astype(np.float32)
+
+ Q=(
+ flg[:,:,np.newaxis]
+ *flg[:,np.newaxis,:]
+ *tools.unpack_product_array(vis[ind],axis=1)
+ )
+
+ # Solve for eigenvectors and eigenvalues
+ evals,evecs=np.linalg.eigh(Q)
+
+ # Set phase convention
+ sign0=1.0-2.0*(evecs[:,np.newaxis,0,-1].real<0.0)
+
+ # Determine response of each source
+ resp=np.zeros((nfreq,nsource,ntime),dtype=vis.dtype)
+ resp[ind]=flg*sign0*evecs[:,:,-1]*evals[:,np.newaxis,-1]**0.5
+
+ returnresp
+
+
+
+
+[docs]
+deffit_poly_to_phase(freq,resp,resp_error,nparam=2):
+"""Fit complex data versus frequency to a model consisting of a polynomial in phase.
+
+ Nonlinear least squares algorithm is applied to the complex data to avoid problems
+ caused by phase wrapping.
+
+ Parameters
+ ----------
+ freq: np.ndarray[nfreq, ]
+ Frequency in MHz.
+ resp: np.ndarray[nfreq, ]
+ Complex data with magnitude equal to 1.0.
+ resp_error: np.ndarray[nfreq, ]
+ Uncertainty on the complex data.
+ nparam: int
+ Number of parameters in the polynomial.
+ Default is 2 (i.e, linear).
+
+ Returns
+ -------
+ popt: np.ndarray[nparam, ]
+ Best-fit parameters.
+ pcov: np.ndarray[nparam, nparam]
+ Covariance of the best-fit parameters.
+ Assumes that it obtained a good fit
+ and returns the errors
+ necessary to achieve that.
+ """
+ flg=np.flatnonzero(resp_error>0.0)
+
+ ifflg.size<(nparam+1):
+ msg=(
+ "Number of data points must be greater than number of parameters (%d)."
+ %nparam
+ )
+ raiseRuntimeError(msg)
+
+ # We will fit the complex data. Break n-element complex array g(ra)
+ # into 2n-element real array [Re{g(ra)}, Im{g(ra)}] for fit.
+ y_complex=resp[flg]
+ y=np.concatenate((y_complex.real,y_complex.imag)).astype(np.float64)
+
+ x=np.tile(freq[flg],2).astype(np.float64)
+
+ err=np.tile(resp_error[flg],2).astype(np.float64)
+
+ # Initial guess for parameters
+ p0=np.zeros(nparam,dtype=np.float64)
+ p0[1]=np.median(
+ np.diff(np.angle(y_complex))/(FREQ_TO_OMEGA*np.diff(freq[flg]))
+ )
+ p0[0]=np.median(
+ _correct_phase_wrap(np.angle(y_complex)-p0[1]*FREQ_TO_OMEGA*freq[flg])
+ )
+
+ # Try nonlinear least squares fit
+ try:
+ popt,pcov=scipy.optimize.curve_fit(
+ _func_poly_phase,x,y,p0=p0.copy(),sigma=err,absolute_sigma=False
+ )
+
+ exceptExceptionasexcep:
+ logger.warning("Nonlinear phase fit failed with error: %s"%excep)
+ # Fit failed, return the initial parameter estimates
+ popt=p0
+ pcov=np.zeros((nparam,nparam),dtype=np.float64)
+
+ finally:
+ returnpopt,pcov
+
+
+
+
+[docs]
+defmodel_poly_phase(freq,*param):
+"""Evaluate a polynomial model for the phase.
+
+ To be used with the parameters output from fit_poly_to_phase.
+
+ Parameters
+ ----------
+ freq: np.ndarray[nfreq, ]
+ Frequency in MHz.
+ *param: float
+ Coefficients of the polynomial.
+
+ Returns
+ -------
+ phi: np.ndarray[nfreq, ]
+ Phase in radians between -pi and +pi.
+ """
+ x=FREQ_TO_OMEGA*freq
+
+ model_phase=np.zeros_like(freq)
+ forpp,parinenumerate(param):
+ model_phase+=par*x**pp
+
+ model_phase=model_phase%(2.0*np.pi)
+ model_phase-=2.0*np.pi*(model_phase>np.pi)
+
+ returnmodel_phase
+"""
+Tools for CHIME analysis
+
+A collection of miscellaneous utility routines.
+
+
+Correlator Inputs
+=================
+
+Query the layout database to find out what is ultimately connected at the end
+of correlator inputs. This is done by calling the routine
+:func:`get_correlator_inputs`, which returns a list of the inputs. Routines
+such as :func:`get_feed_positions` operate on this list.
+
+- :py:meth:`get_correlator_inputs`
+- :py:meth:`get_feed_positions`
+- :py:meth:`get_feed_polarisations`
+- :py:meth:`is_array`
+- :py:meth:`is_array_x`
+- :py:meth:`is_array_y`
+- :py:meth:`is_array_on`
+- :py:meth:`is_chime`
+- :py:meth:`is_pathfinder`
+- :py:meth:`is_holographic`
+- :py:meth:`is_noise_source`
+- :py:meth:`reorder_correlator_inputs`
+- :py:meth:`redefine_stack_index_map`
+- :py:meth:`serial_to_id`
+- :py:meth:`serial_to_location`
+- :py:meth:`parse_chime_serial`
+- :py:meth:`parse_pathfinder_serial`
+- :py:meth:`parse_old_serial`
+- :py:meth:`get_noise_source_index`
+- :py:meth:`get_holographic_index`
+- :py:meth:`change_pathfinder_location`
+- :py:meth:`change_chime_location`
+
+This can determine if we are connected to any of the following:
+
+- :py:class:`HolographyAntenna`
+- :py:class:`ArrayAntenna`
+- :py:class:`PathfinderAntenna`
+- :py:class:`CHIMEAntenna`
+- :py:class:`RFIAntenna`
+- :py:class:`NoiseSource`
+- :py:class:`Blank`
+
+Example
+-------
+
+Fetch the inputs for blanchard during layout 38::
+
+ >>> from datetime import datetime
+ >>> inputs = get_correlator_inputs(datetime(2016,05,23,00), correlator='pathfinder')
+ >>> inputs[1]
+ CHIMEAntenna(id=1, reflector=u'W_cylinder', antenna=u'ANT0123B', powered=True, pos=9.071800000000001, input_sn=u'K7BP16-00040401', pol=u'S', corr=u'K7BP16-0004', cyl=0)
+ >>> print "NS position:", inputs[1].pos
+ NS position: 9.0718
+ >>> print "Polarisation:", inputs[1].pol
+ Polarisation: S
+ >>> inputs[3]
+ CHIMEAntenna(id=3, reflector=u'W_cylinder', antenna=u'ANT0128B', powered=True, pos=9.681400000000002, input_sn=u'K7BP16-00040403', pol=u'S', corr=u'K7BP16-0004', cyl=0)
+
+Housekeeping Inputs
+===================
+
+Functions
+---------
+
+- :py:meth:`antenna_to_lna`
+- :py:meth:`calibrate_temperature`
+- :py:meth:`hk_to_sensor`
+- :py:meth:`lna_to_antenna`
+- :py:meth:`sensor_to_hk`
+
+Classes
+-------
+
+- :py:class:`HKInput`
+
+
+Product Array Mapping
+=====================
+
+Tools for mapping between products stored in upper triangular format, and the
+underlying pairs of inputs.
+
+- :py:meth:`cmap`
+- :py:meth:`icmap`
+- :py:meth:`fast_pack_product_array`
+- :py:meth:`pack_product_array`
+- :py:meth:`unpack_product_array`
+
+
+Matrix Factorisation
+====================
+
+A few useful routines for factorising matrices, usually for calibration.
+
+- :py:meth:`eigh_no_diagonal`
+- :py:meth:`rankN_approx`
+- :py:meth:`normalise_correlations`
+- :py:meth:`apply_gain`
+- :py:meth:`subtract_rank1_signal`
+
+
+Fringestopping
+==============
+
+Routines for undoing the phase rotation of a fixed celestial source. The
+routine :func:`fringestop` is an easy to use routine for fringestopping data
+given a list of the feeds in the data. For more advanced usage
+:func:`fringestop_phase` can be used.
+
+- :py:meth:`fringestop_phase`
+- :py:meth:`fringestop`
+
+Miscellaneous
+=============
+
+- :py:meth:`invert_no_zero`
+"""
+
+importdatetime
+importnumpyasnp
+importscipy.linalgasla
+importre
+fromtypingimportTuple
+
+fromcaputimportpfb
+fromcaput.interferometryimportprojected_distance,fringestop_phase
+
+fromch_utilimportephemeris
+
+# Currently the position between the Pathfinder and 26m have been
+# calibrated with holography, but positions between CHIME and
+# Pathfinder/26m have not (they were determined from high-res
+# satellite images and are only approximate). We need to
+# use CHIME holography data to constrain distance [x, y, z] between
+# CHIME and 26m. I then recommend defining our coordinate system
+# such that center of CHIME array is the origin (so leaving variable
+# _CHIME_POS alone, and updating _PF_POS and _26M_POS appropriately.)
+
+# CHIME geometry
+_CHIME_POS=[0.0,0.0,0.0]
+# CHIME rotation from north. Anti-clockwise looking at the ground (degrees).
+# See DocLib #695 for more information.
+_CHIME_ROT=-0.071
+
+# 26m geometry
+_26M_POS=[254.162124,21.853934,18.93]
+_26M_B=2.14# m
+
+# Pathfinder geometry
+_PF_POS=[373.754961,-54.649866,0.0]
+_PF_ROT=1.986# Pathfinder rotation from north (towards west) in degrees
+_PF_SPACE=22.0# Pathfinder cylinder spacing
+
+# KKO geometry
+_KKO_POS=[0.0,0.0,0.0]
+_KKO_ROT=0.6874
+_KKO_ROLL=0.5888
+_PCO_POS=_KKO_POS
+_PCO_ROT=_KKO_ROT# Aliases for backwards-compatibility
+# KKO_ROT = rotation of cylinder axis from North. Anti-clockwise looking at the ground (degrees).
+# KKO_ROLL = roll of cylinder toward east from Vertical. Anti-clockwise looking North along the focal line.
+# See Doclib #1530 and #1121 for more information.
+
+# GBO geometry
+_GBO_POS=[0.0,0.0,0.0]
+_GBO_ROT=-27.3745
+_GBO_ROLL=-30.0871
+
+# HCO geometry
+_HCO_POS=[0.0,0.0,0.0]
+_HCO_ROT=-0.8023
+_HCO_ROLL=1.0556
+
+
+# Lat/Lon
+_LAT_LON={
+ "chime":[49.3207125,-119.623670],
+ "pathfinder":[49.3202245,-119.6183635],
+ "galt_26m":[49.320909,-119.620174],
+ "gbo_tone":[38.4292962636,-79.8451625395],
+}
+
+# Classes
+# =======
+
+
+
+[docs]
+classHKInput(object):
+"""A housekeeping input.
+
+ Parameters
+ ----------
+ atmel : :obj:`layout.component`
+ The ATMEL board.
+ chan : int
+ The channel number.
+ mux : int
+ The mux number; if this HK stream has no multiplexer, this will simply
+ remain as :obj:`Null`
+
+ Attributes
+ ----------
+ atmel : :obj:`layout.component`
+ The ATMEL board.
+ chan : int
+ The channel number.
+ mux : int
+ The mux number; if this HK stream has no multiplexer, this will simply
+ remain as :obj:`Null`
+ """
+
+ atmel=None
+ chan=None
+ mux=None
+
+ def__init__(self,atmel=None,chan=None,mux=None):
+ self.atmel=atmel
+ self.chan=chan
+ self.mux=mux
+
+ def__repr__(self):
+ ret="<HKInput atmel=%s chan=%d "%(self.atmel.sn,self.chan)
+ ifself.mux:
+ ret+="mux=%d>"%self.mux
+ else:
+ ret+="(no mux)>"
+ returnret
+
+
+
+
+[docs]
+classCorrInput(object):
+"""Base class for describing a correlator input.
+
+ Meant to be subclassed by actual types of inputs.
+
+ Attributes
+ ----------
+ input_sn : str
+ Unique serial number of input.
+ corr : str
+ Unique serial number of correlator.
+ Set to `None` if no correlator is connected.
+ corr_order : int
+ Order of input for correlator internal datastream.
+ crate : int
+ Crate number within the correlator.
+ Set to `None` if correlator consists of single crate.
+ slot : int
+ Slot number of the fpga motherboard within the crate.
+ Ranges from 0 to 15, left to right.
+ Set to `None` if correlator consists of single slot.
+ sma : int
+ SMA number on the fpga motherboard within the slot.
+ Ranges from 0 to 15, bottom to top.
+ """
+
+ def__init__(self,**input_dict):
+ importinspect
+
+ forbaseclsininspect.getmro(type(self))[::-1]:
+ fork,attrinbasecls.__dict__.items():
+ ifk[0]!="_":
+ ifnotisinstance(attr,property):
+ self.__dict__[k]=input_dict.get(k,None)
+
+ elifattr.fsetisnotNone:
+ attr.fset(self,input_dict.get(k,None))
+
+ def_attribute_strings(self):
+ prop=[
+ (k,getattr(self,k))
+ forkin["id","crate","slot","sma","corr_order","delay"]
+ ]
+
+ kv=["%s=%s"%(k,repr(v))fork,vinpropifvisnotNone]+[
+ "%s=%s"%(k,repr(v))fork,vinself.__dict__.items()ifk[0]!="_"
+ ]
+
+ returnkv
+
+ def__repr__(self):
+ kv=self._attribute_strings()
+
+ return"%s(%s)"%(self.__class__.__name__,", ".join(kv))
+
+ @property
+ defid(self):
+"""Channel ID. Automatically calculated from the serial number
+ if id is not explicitly set.
+
+ Returns
+ -------
+ id : int
+ Channel id. Calculated from the serial.
+ """
+ ifhasattr(self,"_id"):
+ returnself._id
+ else:
+ returnserial_to_id(self.input_sn)
+
+ @id.setter
+ defid(self,val):
+ ifvalisnotNone:
+ self._id=val
+
+ @property
+ defcorr_order(self):
+ returnserial_to_location(self.input_sn)[0]
+
+ @property
+ defcrate(self):
+ returnserial_to_location(self.input_sn)[1]
+
+ @property
+ defslot(self):
+ returnserial_to_location(self.input_sn)[2]
+
+ @property
+ defsma(self):
+ returnserial_to_location(self.input_sn)[3]
+
+ @property
+ defdelay(self):
+"""The delay along the signal chain in seconds.
+
+ Postive delay values mean signals arriving later than the nominal value.
+
+ Note that these are always relative. Here CHIME inputs are chosen as
+ the delay=0 reference.
+ """
+ returngetattr(self,"_delay",0)
+
+ input_sn=None
+ corr=None
+[docs]
+classAntenna(CorrInput):
+"""An antenna input.
+
+ Attributes
+ ----------
+ reflector : str
+ The name of the reflector the antenna is on.
+ antenna : str
+ Serial number of the antenna.
+ rf_thru : str
+ Serial number of the RF room thru that
+ the connection passes.
+ """
+
+ reflector=None
+ antenna=None
+ rf_thru=None
+[docs]
+classNoiseSource(CorrInput):
+"""Broad band noise calibration source."""
+
+ pass
+
+
+
+
+[docs]
+classArrayAntenna(Antenna):
+"""Antenna that is part of a cylindrical interferometric array.
+
+ Attributes
+ ----------
+ cyl : int
+ Index of the cylinder.
+ pos : [x, y, z]
+ Position of the antenna in meters in right-handed coordinates
+ where x is eastward, y is northward, and z is upward.
+ pol : str
+ Orientation of the polarisation.
+ flag : bool
+ Flag indicating whether or not the antenna is good.
+ """
+
+ _rotation=0.0
+ _roll=0.0
+ _offset=[0.0]*3
+
+ cyl=None
+ pol=None
+ flag=None
+
+ def_attribute_strings(self):
+ kv=super(ArrayAntenna,self)._attribute_strings()
+ ifself.posisnotNone:
+ pos=", ".join(["%0.2f"%ppforppinself.pos])
+ kv.append("pos=[%s]"%pos)
+ returnkv
+
+ @property
+ defpos(self):
+ ifhasattr(self,"_pos"):
+ pos=self._pos
+
+ ifself._rotation:
+ t=np.radians(self._rotation)
+ c,s=np.cos(t),np.sin(t)
+
+ pos=[c*pos[0]-s*pos[1],s*pos[0]+c*pos[1],pos[2]]
+
+ ifany(self._offset):
+ pos=[pos[dim]+offfordim,offinenumerate(self._offset)]
+
+ returnpos
+
+ else:
+ returnNone
+
+ @pos.setter
+ defpos(self,val):
+ if(valisnotNone)andhasattr(val,"__iter__")and(len(val)>1):
+ self._pos=[0.0]*3
+ forind,vvinenumerate(val):
+ self._pos[ind]=vv
+
+
+
+
+[docs]
+classPathfinderAntenna(ArrayAntenna):
+"""Antenna that is part of the Pathfinder.
+
+ Attributes
+ ----------
+ powered : bool
+ Flag indicating that the antenna is powered.
+ """
+
+ _rotation=_PF_ROT
+ _offset=_PF_POS
+
+ # The delay relative to other inputs isn't really known. Set to NaN so we
+ # don't make any mistakes
+ _delay=np.nan
+
+ powered=None
+
+
+
+
+[docs]
+classCHIMEAntenna(ArrayAntenna):
+"""Antenna that is part of CHIME."""
+
+ _rotation=_CHIME_ROT
+ _offset=_CHIME_POS
+ _delay=0# Treat CHIME antennas as defining the delay zero point
+
+
+
+
+[docs]
+classKKOAntenna(ArrayAntenna):
+"""KKO outrigger antenna for the CHIME/FRB project."""
+
+ _rotation=_KKO_ROT
+ _roll=_KKO_ROLL
+ _offset=_KKO_POS
+ _delay=np.nan
+
+
+
+PCOAntenna=KKOAntenna# Alias for backwards-compatibility
+
+
+
+[docs]
+classGBOAntenna(ArrayAntenna):
+"""GBO outrigger antenna for the CHIME/FRB project."""
+
+ _rotation=_GBO_ROT
+ _roll=_GBO_ROLL
+ _offset=_GBO_POS
+ _delay=np.nan
+
+
+
+
+[docs]
+classHCOAntenna(ArrayAntenna):
+"""HCRO outrigger antenna for the CHIME/FRB project."""
+
+ _rotation=_HCO_ROT
+ _roll=_HCO_ROLL
+ _offset=_HCO_POS
+ _delay=np.nan
+
+
+
+
+[docs]
+classTONEAntenna(ArrayAntenna):
+"""Antenna that is part of GBO/TONE Outrigger.
+ Let's allow for a global rotation and offset.
+ """
+
+ _rotation=0.00
+ _offset=[0.00,0.00,0.00]
+ _delay=np.nan
+
+
+
+
+[docs]
+classHolographyAntenna(Antenna):
+"""Antenna used for holography.
+
+ Attributes
+ ----------
+ pos : [x, y, z]
+ Position of the antenna in meters in right-handed coordinates
+ where x is eastward, y is northward, and z is upward.
+ pol : str
+ Orientation of the polarisation.
+ """
+
+ pos=None
+ pol=None
+ _delay=1.475e-6# From doclib:1093
+
+
+
+# Private Functions
+# =================
+
+
+def_ensure_graph(graph):
+ from.importlayout
+
+ try:
+ graph.sg_spec
+ except:
+ graph=layout.graph(graph)
+ returngraph
+
+
+def_get_feed_position(lay,rfl,foc,cas,slt,slot_factor):
+"""Calculate feed position from node properties.
+
+ Parameters
+ ----------
+ lay : layout.graph
+ Layout instance to search from.
+ rfl : layout.component
+ Reflector.
+ foc : layout.component
+ Focal line slot.
+ cas : layout.component
+ Cassette.
+ slt : layout.component
+ Cassette slot.
+ slot_factor : float
+ 1.5 for CHIME, 0.5 for Outriggers
+
+ Returns
+ -------
+ pos : list
+ x,y,z coordinates of the feed relative to the centre of the focal line.
+ """
+ try:
+ pos=[0.0]*3
+
+ fornodein[rfl,foc,cas,slt]:
+ prop=lay.node_property(node)
+
+ forind,diminenumerate(["x_offset","y_offset","z_offset"]):
+ ifdiminprop:
+ pos[ind]+=float(prop[dim].value)# in metres
+
+ if"y_offset"notinlay.node_property(slt):
+ pos[1]+=(float(slt.sn[-1])-slot_factor)*0.3048
+
+ except:
+ pos=None
+
+ returnpos
+
+
+def_get_input_props(lay,corr_input,corr,rfl_path,rfi_antenna,noise_source):
+"""Fetch all the required properties of an ADC channel or correlator input.
+
+ Parameters
+ ----------
+ lay : layout.graph
+ Layout instance to search from.
+ corr_input : layout.component
+ ADC channel or correlator input.
+ corr : layout.component
+ Correlator.
+ rfl_path : [layout.component]
+ Path from input to reflector, or None.
+ rfi_antenna : layout.component
+ Closest RFI antenna
+ noise_source : layout.component
+ Closest noise source.
+
+ Returns
+ -------
+ channel : CorrInput
+ An instance of `CorrInput` containing the channel properties.
+ """
+
+ ifcorrisnotNone:
+ corr_sn=corr.sn
+ else:
+ corr_sn=None
+
+ # Check if the correlator input component contains a chan_id property
+ corr_prop=lay.node_property(corr_input)
+ chan_id=int(corr_prop["chan_id"].value)if"chan_id"incorr_propelseNone
+
+ rfl=None
+ cas=None
+ slt=None
+ ant=None
+ pol=None
+ rft=None
+ ifrfl_pathisnotNone:
+ rfl=rfl_path[-1]
+
+ deffind(name):
+ f=[aforainrfl_path[1:-1]ifa.type.name==name]
+ returnf[0]iflen(f)==1elseNone
+
+ foc=find("focal line slot")
+ cas=find("cassette")
+ slt=find("cassette slot")
+ ant=find("antenna")
+ pol=find("polarisation")
+
+ forrft_namein["rf room thru","RFT thru"]:
+ rft=find(rft_name)
+ ifrftisnotNone:
+ break
+
+ # If the antenna does not exist, it might be the RFI antenna, the noise source, or empty
+ ifantisNone:
+ ifrfi_antennaisnotNone:
+ rfl=lay.closest_of_type(
+ rfi_antenna,
+ "reflector",
+ type_exclude=["correlator card slot","ADC board"],
+ )
+ rfl_sn=rfl.snifrflisnotNoneelseNone
+ returnRFIAntenna(
+ id=chan_id,
+ input_sn=corr_input.sn,
+ corr=corr_sn,
+ reflector=rfl_sn,
+ antenna=rfi_antenna.sn,
+ )
+
+ # Check to see if it is a noise source
+ ifnoise_sourceisnotNone:
+ returnNoiseSource(id=chan_id,input_sn=corr_input.sn,corr=corr_sn)
+
+ # If we get to here, it's probably a blank input
+ returnBlank(id=chan_id,input_sn=corr_input.sn,corr=corr_sn)
+
+ # Determine polarization from antenna properties
+ try:
+ keydict={
+ "H":"hpol_orient",
+ "V":"vpol_orient",
+ "1":"pol1_orient",
+ "2":"pol2_orient",
+ }
+
+ pkey=keydict[pol.sn[-1]]
+ pdir=lay.node_property(ant)[pkey].value
+
+ except:
+ pdir=None
+
+ # Determine serial number of RF thru
+ rft_sn=getattr(rft,"sn",None)
+
+ # If the cassette does not exist, must be holography antenna
+ ifsltisNone:
+ returnHolographyAntenna(
+ id=chan_id,
+ input_sn=corr_input.sn,
+ corr=corr_sn,
+ reflector=rfl.sn,
+ pol=pdir,
+ antenna=ant.sn,
+ rf_thru=rft_sn,
+ pos=_26M_POS,
+ )
+
+ # If we are still here, we are a CHIME/Pathfinder feed
+
+ # Determine if the correlator input has been manually flagged as good or bad
+ flag=(
+ bool(int(corr_prop["manual_flag"].value))
+ if"manual_flag"incorr_prop
+ elseTrue
+ )
+
+ # Map the cylinder name in the database into a number. This might
+ # be worth changing, such that we could also map into letters
+ # (i.e. A, B, C, D) to save confusion.
+ pos_dict={
+ "W_cylinder":0,
+ "E_cylinder":1,
+ "cylinder_A":2,
+ "cylinder_B":3,
+ "cylinder_C":4,
+ "cylinder_D":5,
+ "pco_cylinder":6,
+ "gbo_cylinder":7,
+ "hcro_cylinder":8,
+ }
+
+ cyl=pos_dict[rfl.sn]
+
+ # Different conventions for CHIME, PCO, GBO, HCRO, and Pathfinder
+ ifcyl>=2andcyl<=5:
+ # Dealing with a CHIME feed
+
+ # Determine position
+ pos=_get_feed_position(
+ lay=lay,rfl=rfl,foc=foc,cas=cas,slt=slt,slot_factor=1.5
+ )
+
+ # Return CHIMEAntenna object
+ returnCHIMEAntenna(
+ id=chan_id,
+ input_sn=corr_input.sn,
+ corr=corr_sn,
+ reflector=rfl.sn,
+ cyl=cyl,
+ pos=pos,
+ pol=pdir,
+ antenna=ant.sn,
+ rf_thru=rft_sn,
+ flag=flag,
+ )
+
+ elifcyl==0orcyl==1:
+ # Dealing with a pathfinder feed
+
+ # Determine y_offset
+ try:
+ pos=[0.0]*3
+
+ pos[0]=cyl*_PF_SPACE
+
+ cas_prop=lay.node_property(cas)
+ slt_prop=lay.node_property(slt)
+
+ d1=float(cas_prop["dist_to_n_end"].value)/100.0# in metres
+ d2=float(slt_prop["dist_to_edge"].value)/100.0# in metres
+ orient=cas_prop["slot_zero_pos"].value
+
+ pos[1]=d1+d2iforient=="N"elsed1-d2
+
+ # Turn into distance increasing from South to North.
+ pos[1]=20.0-pos[1]
+
+ except:
+ pos=None
+
+ # Try and determine if the FLA is powered or not. Paths without an
+ # FLA (e.g. RFoF paths) are assumed to be powered on.
+ pwd=True
+
+ ifrftisnotNone:
+ rft_prop=lay.node_property(rft)
+
+ if"powered"inrft_prop:
+ pwd=rft_prop["powered"].value
+ pwd=bool(int(pwd))
+
+ # Return PathfinderAntenna object
+ returnPathfinderAntenna(
+ id=chan_id,
+ input_sn=corr_input.sn,
+ corr=corr_sn,
+ reflector=rfl.sn,
+ cyl=cyl,
+ pos=pos,
+ pol=pdir,
+ antenna=ant.sn,
+ rf_thru=rft_sn,
+ powered=pwd,
+ flag=flag,
+ )
+
+ elifcyl==6:
+ # Dealing with an KKO feed
+
+ # Determine position
+ pos=_get_feed_position(
+ lay=lay,rfl=rfl,foc=foc,cas=cas,slt=slt,slot_factor=0.5
+ )
+
+ # Return KKOAntenna object
+ returnKKOAntenna(
+ id=chan_id,
+ input_sn=corr_input.sn,
+ corr=corr_sn,
+ reflector=rfl.sn,
+ cyl=cyl,
+ pos=pos,
+ pol=pdir,
+ antenna=ant.sn,
+ rf_thru=rft_sn,
+ flag=flag,
+ )
+
+ elifcyl==7:
+ # Dealing with a GBO feed
+
+ # Determine position
+ pos=_get_feed_position(
+ lay=lay,rfl=rfl,foc=foc,cas=cas,slt=slt,slot_factor=0.5
+ )
+
+ # Return GBOAntenna object
+ returnGBOAntenna(
+ id=chan_id,
+ input_sn=corr_input.sn,
+ corr=corr_sn,
+ reflector=rfl.sn,
+ cyl=cyl,
+ pos=pos,
+ pol=pdir,
+ antenna=ant.sn,
+ rf_thru=rft_sn,
+ flag=flag,
+ )
+
+ elifcyl==8:
+ # Dealing with a HCO feed
+
+ # Determine position
+ pos=_get_feed_position(
+ lay=lay,rfl=rfl,foc=foc,cas=cas,slt=slt,slot_factor=0.5
+ )
+
+ # Return HCOAntenna object
+ returnHCOAntenna(
+ id=chan_id,
+ input_sn=corr_input.sn,
+ corr=corr_sn,
+ reflector=rfl.sn,
+ cyl=cyl,
+ pos=pos,
+ pol=pdir,
+ antenna=ant.sn,
+ rf_thru=rft_sn,
+ flag=flag,
+ )
+
+
+# Public Functions
+# ================
+
+
+
+[docs]
+defcalibrate_temperature(raw):
+"""Calibrate housekeeping temperatures.
+
+ The offset used here is rough; the results are therefore not absolutely
+ precise.
+
+ Parameters
+ ----------
+ raw : numpy array
+ The raw values.
+
+ Returns
+ -------
+ t : numpy array
+ The temperature in degrees Kelvin.
+ """
+ importnumpy
+
+ off=150.0
+ r_t=2000.0*(8320.0/(raw-off)-1.0)
+ return1.0/(1.0/298.0+numpy.log(r_t/1.0e4)/3950.0)
+
+
+
+
+[docs]
+defantenna_to_lna(graph,ant,pol):
+"""Find an LNA connected to an antenna.
+
+ Parameters
+ ----------
+ graph : obj:`layout.graph` or :obj:`datetime.datetime`
+ The graph in which to do the search. If you pass a time, then the graph
+ will be constructed internally. (Note that the latter option will be
+ quite slow if you do repeated calls!)
+ ant : :obj:`layout.component`
+ The antenna.
+ pol : integer
+ There can be up to two LNA's connected to the two polarisation outputs
+ of an antenna. Select which by passing :obj:`1` or :obj:`2`. (Note that
+ conversion to old-style naming 'A' and 'B' is done automatically.)
+
+ Returns
+ -------
+ lna : :obj:`layout.component` or string
+ The LNA.
+
+ Raises
+ ------
+ :exc:`layout.NotFound`
+ Raised if the polarisation connector could not be found in the graph.
+ """
+ from.importlayout
+
+ graph=_ensure_graph(graph)
+ pol_obj=None
+ forpingraph.neighbour_of_type(graph.component(comp=ant),"polarisation"):
+ ifp.sn[-1]==str(pol)orp.sn[-1]==chr(ord("A")+pol):
+ pol_obj=p
+ break
+ ifnotpol_obj:
+ raiselayout.NotFound
+ try:
+ returngraph.neighbour_of_type(pol_obj,"LNA")[0]
+ exceptIndexError:
+ returnNone
+
+
+
+
+[docs]
+deflna_to_antenna(graph,lna):
+"""Find an antenna connected to an LNA.
+
+ Parameters
+ ----------
+ graph : obj:`layout.graph` or :obj:`datetime.datetime`
+ The graph in which to do the search. If you pass a time, then the graph
+ will be constructed internally. (Note that the latter option will be
+ quite slow if you do repeated calls!)
+ lna : :obj:`layout.component` or string
+ The LNA.
+
+ Returns
+ -------
+ antenna : :obj:`layout.component`
+ The antenna.
+ """
+ graph=_ensure_graph(graph)
+ returngraph.closest_of_type(
+ graph.component(comp=lna),"antenna",type_exclude="60m coax"
+ )
+
+
+
+
+[docs]
+defsensor_to_hk(graph,comp):
+"""Find what housekeeping channel a component is connected to.
+
+ Parameters
+ ----------
+ graph : obj:`layout.graph` or :obj:`datetime.datetime`
+ The graph in which to do the search. If you pass a time, then the graph
+ will be constructed internally. (Note that the latter option will be
+ quite slow if you do repeated calls!)
+ comp : :obj:`layout.component` or string
+ The component to search for (you can pass by serial number if you wish).
+ Currently, only components of type LNA, FLA and RFT thru are accepted.
+
+ Returns
+ -------
+ inp : :obj:`HKInput`
+ The housekeeping input channel the sensor is connected to.
+ """
+ graph=_ensure_graph(graph)
+ comp=graph.component(comp=comp)
+
+ ifcomp.type.name=="LNA":
+ # Find the closest mux.
+ mux=graph.closest_of_type(
+ comp,"HK mux",type_exclude=["polarisation","cassette","60m coax"]
+ )
+ ifnotmux:
+ returnNone
+ try:
+ hydra=graph.neighbour_of_type(comp,"HK hydra")[0]
+ exceptIndexError:
+ returnNone
+ chan=int(hydra.sn[-1])
+ ifmux.sn[-1]=="B":
+ chan+=8
+
+ # Find the ATMEL board.
+ atmel=graph.closest_of_type(
+ hydra,"HK ATMega",type_exclude=["cassette","antenna"]
+ )
+
+ returnHKInput(atmel,chan,int(mux.sn[-2]))
+
+ elifcomp.type.name=="FLA"orcomp.type.name=="RFT thru":
+ ifcomp.type.name=="FLA":
+ try:
+ comp=graph.neighbour_of_type(comp,"RFT thru")[0]
+ exceptIndexError:
+ returnNone
+ try:
+ hydra=graph.neighbour_of_type(comp,"HK hydra")[0]
+ exceptIndexError:
+ returnNone
+
+ # Find the ATMEL board.
+ atmel=graph.closest_of_type(
+ hydra,"HK ATMega",type_exclude=["RFT thru","FLA","SMA coax"]
+ )
+
+ returnHKInput(atmel,int(hydra.sn[-1]),None)
+ else:
+ raiseValueError("You can only pass components of type LNA, FLA or RFT thru.")
+
+
+
+
+[docs]
+defhk_to_sensor(graph,inp):
+"""Find what component a housekeeping channel is connected to.
+
+ This method is for finding either LNA or FLA's that your housekeeping
+ channel is connected to. (It currently cannot find accelerometers, other
+ novel housekeeping instruments that may later exist; nor will it work if the
+ FLA/LNA is connected via a very non-standard chain of components.)
+
+ Parameters
+ ----------
+ graph : obj:`layout.graph` or :obj:`datetime.datetime`
+ The graph in which to do the search. If you pass a time, then the graph
+ will be constructed internally. (Note that the latter option will be
+ quite slow if you do repeated calls!)
+ inp : :obj:`HKInput`
+ The housekeeping input to search.
+
+ Returns
+ -------
+ comp : :obj:`layout.component`
+ The LNA/FLA connected to the specified channel; :obj:`None` is returned
+ if none is found.
+
+ Raises
+ ------
+ :exc:`ValueError`
+ Raised if one of the channels or muxes passed in **hk_chan** is out of
+ range.
+ """
+
+ from.importlayout
+
+ graph=_ensure_graph(graph)
+
+ # Figure out what it is connected to.
+ forthingingraph.neighbours(graph.component(comp=inp.atmel)):
+ ifthing.type.name=="HK preamp":
+ # OK, this is a preamp going to FLA's.
+ ifinp.chan<0orinp.chan>7:
+ raiseValueError(
+ "For FLA housekeeping, the channel number "
+ "must be in the range [0, 7]."
+ )
+ forhydraingraph.neighbour_of_type(thing,"HK hydra"):
+ ifhydra.sn[-1]==str(inp.chan):
+ returngraph.closest_of_type(hydra,"FLA",type_exclude="HK preamp")
+
+ ifthing.type.name=="HK mux box":
+ # OK, this is a mux box going to LNA's.
+ ifinp.mux<0orinp.mux>7:
+ raiseValueError(
+ "For LNA housekeeping, the mux number must be "
+ "in the range [0, 7]."
+ )
+ ifinp.chan<0orinp.chan>15:
+ raiseValueError(
+ "For LNA housekeeping, the channel number "
+ "must be in the range [0, 15]."
+ )
+
+ # Construct the S/N of the mux connector and get it.
+ sn="%s%d%s"%(thing.sn,inp.mux,"A"ifinp.chan<8else"B")
+ try:
+ mux_card=graph.component(comp=sn)
+ exceptlayout.NotFound:
+ returnNone
+
+ # Find the closest preamp and the hydra cable corresponding to the
+ # channel requested.
+ preamp=graph.closest_of_type(
+ mux_card,"HK preamp",type_exclude="HK mux box"
+ )
+ ifnotpreamp:
+ returnNone
+
+ forhydraingraph.neighbour_of_type(preamp,"HK hydra"):
+ ifhydra.sn[-1]==str(inp.chan%8):
+ try:
+ returngraph.neighbour_of_type(hydra,"LNA")[0]
+ exceptIndexError:
+ returnNone
+ returnNone
+
+
+
+# Parse a serial number into crate, slot, and sma number
+defparse_chime_serial(sn):
+ mo=re.match("FCC(\d{2})(\d{2})(\d{2})",sn)
+
+ ifmoisNone:
+ raiseRuntimeError(
+ "Serial number %s does not match expected CHIME format."%sn
+ )
+
+ crate=int(mo.group(1))
+ slot=int(mo.group(2))
+ sma=int(mo.group(3))
+
+ returncrate,slot,sma
+
+
+defparse_pathfinder_serial(sn):
+ mo=re.match("(\w{6}\-\d{4})(\d{2})(\d{2})",sn)
+
+ ifmoisNone:
+ raiseRuntimeError(
+ "Serial number %s does not match expected Pathfinder format."%sn
+ )
+
+ crate=mo.group(1)
+ slot=int(mo.group(2))
+ sma=int(mo.group(3))
+
+ returncrate,slot,sma
+
+
+defparse_old_serial(sn):
+ mo=re.match("(\d{5}\-\d{4}\-\d{4})\-C(\d{1,2})",sn)
+
+ ifmoisNone:
+ raiseRuntimeError(
+ "Serial number %s does not match expected 8/16 channel format."%sn
+ )
+
+ slot=mo.group(1)
+ sma=int(mo.group(2))
+
+ returnslot,sma
+
+
+
+[docs]
+defserial_to_id(serial):
+"""Get the channel ID corresponding to a correlator input serial number.
+
+ Parameters
+ ----------
+ serial : string
+ Correlator input serial number.
+
+ Returns
+ -------
+ id : int
+ """
+
+ # Map a slot and SMA to channel id for Pathfinder
+ defget_pathfinder_channel(slot,sma):
+ c=[
+ None,
+ 80,
+ 16,
+ 64,
+ 0,
+ 208,
+ 144,
+ 192,
+ 128,
+ 240,
+ 176,
+ 224,
+ 160,
+ 112,
+ 48,
+ 96,
+ 32,
+ ]
+ channel=c[slot]+smaifslot>0elsesma
+ returnchannel
+
+ # Determine ID
+ try:
+ res=parse_chime_serial(serial)
+ # CHIME chan_id is defined in layout database
+ return-1
+ exceptRuntimeError:
+ pass
+
+ try:
+ res=parse_pathfinder_serial(serial)
+ returnget_pathfinder_channel(*(res[1:]))
+ exceptRuntimeError:
+ pass
+
+ try:
+ res=parse_old_serial(serial)
+ returnres[1]
+ exceptRuntimeError:
+ pass
+
+ return-1
+
+
+
+
+[docs]
+defserial_to_location(serial):
+"""Get the internal correlator ordering and the
+ crate, slot, and sma number from a correlator input serial number.
+
+ Parameters
+ ----------
+ serial : string
+ Correlator input serial number.
+
+ Returns
+ -------
+ location : 4-tuple
+ (corr_order, crate, slot, sma)
+ """
+
+ default=(None,)*4
+ ifserialisNone:
+ returndefault
+
+ # Map slot and sma to position within
+ defget_crate_channel(slot,sma):
+ sma_to_adc=[12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3]
+ returnslot*16+sma_to_adc[sma]
+
+ # Determine ID
+ try:
+ res=parse_chime_serial(serial)
+ corr_id=res[0]*256+get_crate_channel(*res[1:])
+ return(corr_id,)+res
+ exceptRuntimeError:
+ pass
+
+ try:
+ res=list(parse_pathfinder_serial(serial))
+ # Use convention that slot number starts at 0 for consistency with CHIME
+ res[1]-=1
+ corr_id=get_crate_channel(*res[1:])
+ return(corr_id,None)+tuple(res[1:])
+ exceptRuntimeError:
+ pass
+
+ try:
+ res=parse_old_serial(serial)
+ return(res[1],None,None,res[1])
+ exceptRuntimeError:
+ pass
+
+ returndefault
+
+
+
+
+[docs]
+defget_default_frequency_map_stream()->Tuple[np.ndarray]:
+"""Get the default CHIME frequency map stream.
+
+ Level order is [shuffle, crate, slot, link].
+
+ Returns
+ -------
+ stream
+ [shuffle, crate, slot, link] for each frequency bin
+ stream_id
+ stream_id for each map combination
+ shuffle*2**12 + crate*2**8 + slot*2**4 + link
+ """
+ stream=np.empty((1024,4),dtype=np.int32)
+
+ # shuffle
+ stream[:,0]=3
+ # crate
+ stream[:,1]=np.tile(np.arange(2).repeat(16),32)
+ # slot
+ stream[:,2]=np.tile(np.arange(16),64)
+ # link
+ stream[:,3]=np.tile(np.arange(8).repeat(32),4)
+
+ stream_id=(
+ stream[:,0]*2**12+stream[:,1]*2**12+stream[:,2]*2**4+stream[:,3]
+ ).astype(np.int64)
+
+ returnstream,stream_id
+
+
+
+
+[docs]
+deforder_frequency_map_stream(fmap:np.ndarray,stream_id:np.ndarray)->np.ndarray:
+"""Order stream_id components based on a frequency map.
+
+ Level order is [shuffle, crate, slot, link]
+
+ Parameters
+ ----------
+ fmap
+ frequency map
+ stream_id
+ 1-D array of stream_ids associated with each row in fmap
+
+ Returns
+ -------
+ stream
+ shuffle, crate, slot, link for each frequency
+ """
+
+ defdecode_stream_id(sid:int)->Tuple[int]:
+ link=sid&15
+ slot=(sid>>4)&15
+ crate=(sid>>8)&15
+ shuffle=(sid>>12)&15
+
+ return(shuffle,crate,slot,link)
+
+ decoded_stream=[decode_stream_id(i)foriinstream_id[:]]
+ x=[[]for_inrange(len(stream_id))]
+
+ forii,freqsinenumerate(fmap):
+ forfinfreqs:
+ x[f].append(decoded_stream[ii])
+
+ # TODO: maybe implement some checks here
+ stream=np.array([i[0]foriinx],dtype=np.int32)
+
+ returnstream
+
+
+
+
+[docs]
+defget_correlator_inputs(lay_time,correlator=None,connect=True):
+"""Get the information for all channels in a layout.
+
+ Parameters
+ ----------
+ lay_time : layout.graph or datetime
+ layout.graph object, layout tag id, or datetime.
+ correlator : str, optional
+ Fetch only for specified correlator. Use the serial number in database,
+ or `pathfinder` or `chime`, which will substitute the correct serial.
+ If `None` return for all correlators.
+ Option `tone` added for GBO 12 dish outrigger prototype array.
+ connect : bool, optional
+ Connect to database and set the user to Jrs65 prior to query.
+ Default is True.
+
+ Returns
+ -------
+ channels : list
+ List of :class:`CorrInput` instances. Returns `None` for MPI ranks
+ other than zero.
+ """
+
+ fromch_utilimportlayout
+ importnetworkxasnx
+ fromchimedb.core.connectdbimportconnect_this_rank
+
+ coax_type=["SMA coax","3.25m SMA coax"]
+
+ block=[
+ "correlator card slot",
+ "ADC board",
+ "rf room bulkhead",
+ "c-can bulkhead",
+ "50m coax bundle",
+ "HK hydra",
+ "connector plate pol 1",
+ "connector plate pol 2",
+ "thermometer",
+ ]
+
+ # Replace 'pathfinder' or 'chime' with serial number
+ ifisinstance(correlator,str):
+ ifcorrelator.lower()=="pathfinder":
+ correlator="K7BP16-0004"
+ elifcorrelator.lower()=="chime":
+ correlator="FCC"
+ elifcorrelator.lower()=="pco":
+ correlator="FCA"
+ elifcorrelator.lower()=="kko":
+ correlator="FCA"
+ elifcorrelator.lower()=="gbo":
+ correlator="FCG"
+ elifcorrelator.lower()=="tone":
+ # A hack to return GBO correlator inputs
+ correlator="tone"
+ connect=False
+ laytime=0
+ returnfake_tone_database()
+
+ ifnotconnect_this_rank():
+ returnNone
+
+ ifconnect:
+ layout.connect_database(read_write=False)
+ layout.set_user("Jrs65")
+
+ # Fetch layout_tag start time if we received a layout num
+ ifisinstance(lay_time,int):
+ raiseValueError("Layout IDs are no longer supported.")
+ elifisinstance(lay_time,datetime.datetime):
+ layout_graph=layout.graph.from_db(lay_time)
+ elifisinstance(lay_time,layout.graph):
+ layout_graph=lay_time
+ else:
+ raiseValueError("Unsupported argument lay_time=%s"%repr(lay_time))
+
+ # Fetch all the input components
+ inputs=[]
+ try:
+ inputs+=layout_graph.component(type="ADC channel")
+ exceptlayout.NotFound:
+ pass
+
+ try:
+ inputs+=layout_graph.component(type="correlator input")
+ exceptlayout.NotFound:
+ pass
+
+ # Restrict the inputs processed to only those directly connected to the
+ # specified correlator
+ ifcorrelatorisnotNone:
+ try:
+ corr=layout_graph.component(correlator)
+ exceptlayout.NotFound:
+ raiseValueError("Unknown correlator %s"%correlator)
+
+ # Cut out SMA coaxes so we don't go outside of the correlator
+ sg=set(layout_graph.nodes())
+ forcotyincoax_type:
+ try:
+ comp_coty=layout_graph.component(type=coty)
+ exceptlayout.NotFound:
+ pass
+ else:
+ sg-=set(comp_coty)
+ sg=layout_graph.subgraph(sg)
+
+ # Use only inputs that are connected to the correlator
+ inputs=nx.node_connected_component(sg,corr)&set(inputs)
+
+ inputs=sorted(inputs,key=lambdaadc:adc.sn)
+
+ # Perform nearly all the graph queries in one huge batcn to speed things up,
+ # and pass the results into _get_input_props for further processing
+ corrs=layout_graph.closest_of_type(inputs,"correlator",type_exclude=coax_type)
+
+ rfls=layout_graph.shortest_path_to_type(inputs,"reflector",type_exclude=block)
+
+ block.append("reflector")
+ rfi_ants=layout_graph.closest_of_type(inputs,"RFI antenna",type_exclude=block)
+ noise_sources=layout_graph.closest_of_type(
+ inputs,"noise source",type_exclude=block
+ )
+
+ inputlist=[
+ _get_input_props(layout_graph,*args)
+ forargsinzip(inputs,corrs,rfls,rfi_ants,noise_sources)
+ ]
+
+ # Filter to include only inputs attached to the given correlator. In theory
+ # this shouldn't be necessary if the earlier filtering worked, but I think
+ # it'll help catch some odd cases
+ ifcorrelatorisnotNone:
+ inputlist=[input_forinput_ininputlistifinput_.corr==correlator]
+
+ # Sort by channel ID
+ inputlist.sort(key=lambdainput_:input_.id)
+
+ returninputlist
+
+
+
+
+[docs]
+defchange_pathfinder_location(rotation=None,location=None,default=False):
+"""Change the orientation or location of Pathfinder.
+
+ Parameters
+ ----------
+ rotation : float
+ Rotation of the telescope from true north in degrees.
+ location: list
+ [x, y, z] of the telescope in meters,
+ where x is eastward, y is northward, and z is upward.
+ default: bool
+ Set parameters back to default value. Overides other keywords.
+ """
+
+ ifdefault:
+ rotation=_PF_ROT
+ location=_PF_POS
+
+ ifrotationisnotNone:
+ PathfinderAntenna._rotation=rotation
+
+ iflocationisnotNone:
+ offset=[location[ii]ifii<len(location)else0.0foriiinrange(3)]
+ PathfinderAntenna._offset=offset
+
+
+
+
+[docs]
+defchange_chime_location(rotation=None,location=None,default=False):
+"""Change the orientation or location of CHIME.
+
+ Parameters
+ ----------
+ rotation : float
+ Rotation of the telescope from true north in degrees.
+ location: list
+ [x, y, z] of the telescope in meters,
+ where x is eastward, y is northward, and z is upward.
+ default: bool
+ Set parameters back to default value. Overides other keywords.
+ """
+
+ ifdefault:
+ rotation=_CHIME_ROT
+ location=_CHIME_POS
+
+ ifrotationisnotNone:
+ CHIMEAntenna._rotation=rotation
+
+ iflocationisnotNone:
+ offset=[location[ii]ifii<len(location)else0.0foriiinrange(3)]
+ CHIMEAntenna._offset=offset
+
+
+
+
+[docs]
+defget_feed_positions(feeds,get_zpos=False):
+"""Get the positions of the CHIME antennas.
+
+ Parameters
+ ----------
+ feeds : list of CorrInput
+ List of feeds to compute positions of.
+ get_zpos: bool
+ Return a third column with elevation information.
+
+ Returns
+ -------
+ positions : np.ndarray[nfeed, 2]
+ Array of feed positions. The first column is the E-W position
+ (increasing to the E), and the second is the N-S position (increasing
+ to the N). Non CHIME feeds get set to `NaN`.
+ """
+
+ # Extract positions for all array antennas or holographic antennas, fill other
+ # inputs with NaNs
+ pos=np.array(
+ [
+ feed.posif(is_array(feed)oris_holographic(feed))else[np.nan]*3
+ forfeedinfeeds
+ ]
+ )
+
+ # Drop z coordinate if not explicitely requested
+ ifnotget_zpos:
+ pos=pos[:,0:2]
+
+ returnpos
+[docs]
+defget_feed_polarisations(feeds):
+"""Get an array of the feed polarisations.
+
+ Parameters
+ ----------
+ feeds : list of CorrInput
+ List of feeds to compute positions of.
+
+ Returns
+ -------
+ pol : np.ndarray
+ Array of characters giving polarisation. If not an array feed returns '0'.
+ """
+ pol=np.array([(f.polifis_array(f)else"0")forfinfeeds])
+
+ returnpol
+
+
+
+
+[docs]
+defis_array(feed):
+"""Is this feed part of an array?
+
+ Parameters
+ ----------
+ feed : CorrInput
+
+ Returns
+ -------
+ isarr : bool
+ """
+ returnisinstance(feed,ArrayAntenna)
+
+
+
+
+[docs]
+defis_array_x(feed):
+"""Is this an X-polarisation antenna in an array?"""
+ returnis_array(feed)andfeed.pol=="E"
+
+
+
+
+[docs]
+defis_array_y(feed):
+"""Is this a Y-polarisation antenna in an array?"""
+ returnis_array(feed)andfeed.pol=="S"
+[docs]
+defget_holographic_index(inputs):
+"""Find the indices of the holography antennas.
+
+ Parameters
+ ----------
+ inputs : list of :class:`CorrInput`
+
+ Returns
+ -------
+ ixholo : list of int
+ Returns None if holographic antenna not found.
+ """
+ ixholo=[ixforix,inpinenumerate(inputs)ifis_holographic(inp)]
+ returnixholoorNone
+[docs]
+defget_noise_source_index(inputs):
+"""Find the indices of the noise sources.
+
+ Parameters
+ ----------
+ inputs : list of :class:`CorrInput`
+
+ Returns
+ -------
+ ixns : list of int
+ Returns None if noise source not found.
+ """
+ ixns=[ixforix,inpinenumerate(inputs)ifis_noise_source(inp)]
+ returnixnsorNone
+
+
+
+
+[docs]
+defget_noise_channel(inputs):
+"""Returns the index of the noise source with
+ the lowest chan id (for backwards compatability).
+ """
+ noise_sources=get_noise_source_index(inputs)
+ return(noise_sourcesor[None])[0]
+
+
+
+
+[docs]
+defis_array_on(inputs,*args):
+"""Check if inputs are attached to an array antenna AND powered on AND flagged as good.
+
+ Parameters
+ ----------
+ inputs : CorrInput or list of CorrInput objects
+
+ Returns
+ -------
+ pwds : boolean or list of bools.
+ If list, it is the same length as inputs. Value is True if input is
+ attached to a ArrayAntenna *and* powered-on and False otherwise
+ """
+
+ iflen(args)>0:
+ raiseRuntimeError("This routine no longer accepts a layout time argument.")
+
+ # Treat scalar case
+ ifisinstance(inputs,CorrInput):
+ return(
+ is_array(inputs)
+ andgetattr(inputs,"powered",True)
+ andgetattr(inputs,"flag",True)
+ )
+
+ # Assume that the argument is a sequence otherwise
+ else:
+ return[is_array_on(inp)forinpininputs]
+
+
+
+# Create an is_chime_on alias for backwards compatibility
+is_chime_on=is_array_on
+
+
+
+[docs]
+defreorder_correlator_inputs(input_map,corr_inputs):
+"""Sort a list of correlator inputs into the order given in input map.
+
+ Parameters
+ ----------
+ input_map : np.ndarray
+ Index map of correlator inputs.
+ corr_inputs : list
+ List of :class:`CorrInput` objects, e.g. the output from
+ :func:`get_correlator_inputs`.
+
+ Returns
+ -------
+ corr_input_list: list
+ List of :class:`CorrInput` instances in the new order. Returns `None`
+ where the serial number had no matching entry in parameter ``corr_inputs``.
+ """
+ serials=input_map["correlator_input"]
+
+ sorted_inputs=[]
+
+ forserialinserials:
+ forcorr_inputincorr_inputs:
+ ifserial==corr_input.input_sn:
+ sorted_inputs.append(corr_input)
+ break
+ else:
+ sorted_inputs.append(None)
+
+ returnsorted_inputs
+
+
+
+
+[docs]
+defredefine_stack_index_map(input_map,prod,stack,reverse_stack):
+"""Ensure that only baselines between array antennas are used to represent the stack.
+
+ The correlator will have inputs that are not connected to array antennas. These inputs
+ are flagged as bad and are not included in the stack, however, products that contain
+ their `chan_id` can still be used to represent a characteristic baseline in the `stack`
+ index map. This method creates a new `stack` index map that, if possible, only contains
+ products between two array antennas. This new `stack` index map should be used when
+ calculating baseline distances to fringestop stacked data.
+
+ Parameters
+ ----------
+ input_map : list of :class:`CorrInput`
+ List describing the inputs as they are in the file, output from
+ `tools.get_correlator_inputs`
+ prod : np.ndarray[nprod,] of dtype=('input_a', 'input_b')
+ The correlation products as pairs of inputs.
+ stack : np.ndarray[nstack,] of dtype=('prod', 'conjugate')
+ The index into the `prod` axis of a characteristic baseline included in the stack.
+ reverse_stack : np.ndarray[nprod,] of dtype=('stack', 'conjugate')
+ The index into the `stack` axis that each `prod` belongs.
+
+ Returns
+ -------
+ stack_new : np.ndarray[nstack,] of dtype=('prod', 'conjugate')
+ The updated `stack` index map, where each element is an index to a product
+ consisting of a pair of array antennas.
+ stack_flag : np.ndarray[nstack,] of dtype=bool
+ Boolean flag that is True if this element of the stack index map is now valid,
+ and False if none of the baselines that were stacked contained array antennas.
+ """
+ feed_flag=np.array([is_array(inp)forinpininput_map])
+ example_prod=prod[stack["prod"]]
+ stack_flag=feed_flag[example_prod["input_a"]]&feed_flag[example_prod["input_b"]]
+
+ stack_new=stack.copy()
+
+ bad_stack_index=np.flatnonzero(~stack_flag)
+ forindinbad_stack_index:
+ this_stack=np.flatnonzero(reverse_stack["stack"]==ind)
+ fortsinthis_stack:
+ tp=prod[ts]
+ iffeed_flag[tp[0]]andfeed_flag[tp[1]]:
+ stack_new[ind]["prod"]=ts
+ stack_new[ind]["conjugate"]=reverse_stack[ts]["conjugate"]
+ stack_flag[ind]=True
+ break
+
+ returnstack_new,stack_flag
+
+
+
+
+[docs]
+defcmap(i,j,n):
+"""Given a pair of feed indices, return the pair index.
+
+ Parameters
+ ----------
+ i, j : integer
+ Feed index.
+ n : integer
+ Total number of feeds.
+
+ Returns
+ -------
+ pi : integer
+ Pair index.
+ """
+ ifi<=j:
+ return(n*(n+1)//2)-((n-i)*(n-i+1)//2)+(j-i)
+ else:
+ returncmap(j,i,n)
+[docs]
+defunpack_product_array(prod_arr,axis=1,feeds=None):
+"""Expand packed products to correlation matrices.
+
+ This turns an axis of the packed upper triangle set of products into the
+ full correlation matrices. It replaces the specified product axis with two
+ axes, one for each feed. By setting `feeds` this routine can also
+ pull out a subset of feeds.
+
+ Parameters
+ ----------
+ prod_arr : np.ndarray[..., nprod, :]
+ Array containing products packed in upper triangle format.
+ axis : int, optional
+ Axis the products are contained on.
+ feeds : list of int, optional
+ Indices of feeds to include. If :obj:`None` (default) use all feeds.
+
+ Returns
+ -------
+ corr_arr : np.ndarray[..., nfeed, nfeed, ...]
+ Expanded array.
+ """
+
+ nprod=prod_arr.shape[axis]
+ nfeed=int((2*nprod)**0.5)
+
+ ifnprod!=(nfeed*(nfeed+1)//2):
+ raiseException(
+ "Product axis size does not look correct (not exactly n(n+1)/2)."
+ )
+
+ shape0=prod_arr.shape[:axis]
+ shape1=prod_arr.shape[(axis+1):]
+
+ # Construct slice objects representing the axes before and after the product axis
+ slice0=(np.s_[:],)*len(shape0)
+ slice1=(np.s_[:],)*len(shape1)
+
+ # If no feeds specified use all of them
+ feeds=list(range(nfeed))iffeedsisNoneelsefeeds
+
+ outfeeds=len(feeds)
+
+ exp_arr=np.zeros(shape0+(outfeeds,outfeeds)+shape1,dtype=prod_arr.dtype)
+
+ # Iterate over products and copy into correct location of expanded array
+ # Use a python loop, but should be fast if other axes are large
+ forii,fiinenumerate(feeds):
+ forij,fjinenumerate(feeds):
+ pi=cmap(fi,fj,nfeed)
+
+ iffi<=fj:
+ exp_arr[slice0+(ii,ij)+slice1]=prod_arr[slice0+(pi,)+slice1]
+ else:
+ exp_arr[slice0+(ii,ij)+slice1]=prod_arr[
+ slice0+(pi,)+slice1
+ ].conj()
+
+ returnexp_arr
+
+
+
+
+[docs]
+defpack_product_array(exp_arr,axis=1):
+"""Pack full correlation matrices into upper triangular form.
+
+ It replaces the two feed axes of the matrix, with a single upper triangle product axis.
+
+
+ Parameters
+ ----------
+ exp_arr : np.ndarray[..., nfeed, nfeed, ...]
+ Array of full correlation matrices.
+ axis : int, optional
+ Index of the first feed axis. The second feed axis must be the next one.
+
+ Returns
+ -------
+ prod_arr : np.ndarray[..., nprod, ...]
+ Array containing products packed in upper triangle format.
+ """
+
+ nfeed=exp_arr.shape[axis]
+ nprod=nfeed*(nfeed+1)//2
+
+ ifnfeed!=exp_arr.shape[axis+1]:
+ raiseException("Does not look like correlation matrices (axes must be equal).")
+
+ shape0=exp_arr.shape[:axis]
+ shape1=exp_arr.shape[(axis+2):]
+
+ slice0=(np.s_[:],)*len(shape0)
+ slice1=(np.s_[:],)*len(shape1)
+
+ prod_arr=np.zeros(shape0+(nprod,)+shape1,dtype=exp_arr.dtype)
+
+ # Iterate over products and copy from correct location of expanded array
+ forpiinrange(nprod):
+ fi,fj=icmap(pi,nfeed)
+
+ prod_arr[slice0+(pi,)+slice1]=exp_arr[slice0+(fi,fj)+slice1]
+
+ returnprod_arr
+
+
+
+
+[docs]
+deffast_pack_product_array(arr):
+"""
+ Equivalent to ch_util.tools.pack_product_array(arr, axis=0),
+ but 10^5 times faster for full CHIME!
+
+ Currently assumes that arr is a 2D array of shape (nfeeds, nfeeds),
+ and returns a 1D array of length (nfeed*(nfeed+1))/2. This case
+ is all we need for phase calibration, but pack_product_array() is
+ more general.
+ """
+
+ assertarr.ndim==2
+ assertarr.shape[0]==arr.shape[1]
+
+ nfeed=arr.shape[0]
+ nprod=(nfeed*(nfeed+1))//2
+
+ ret=np.zeros(nprod,dtype=np.float64)
+ iout=0
+
+ foriinrange(nfeed):
+ ret[iout:(iout+nfeed-i)]=arr[i,i:]
+ iout+=nfeed-i
+
+ returnret
+
+
+
+
+[docs]
+defrankN_approx(A,rank=1):
+"""Create the rank-N approximation to the matrix A.
+
+ Parameters
+ ----------
+ A : np.ndarray
+ Matrix to approximate
+ rank : int, optional
+
+ Returns
+ -------
+ B : np.ndarray
+ Low rank approximation.
+ """
+
+ N=A.shape[0]
+
+ evals,evecs=la.eigh(A,eigvals=(N-rank,N-1))
+
+ returnnp.dot(evecs,evals*evecs.T.conj())
+
+
+
+
+[docs]
+defeigh_no_diagonal(A,niter=5,eigvals=None):
+"""Eigenvalue decomposition ignoring the diagonal elements.
+
+ The diagonal elements are iteratively replaced with those from a rank=1 approximation.
+
+ Parameters
+ ----------
+ A : np.ndarray[:, :]
+ Matrix to decompose.
+ niter : int, optional
+ Number of iterations to perform.
+ eigvals : (lo, hi), optional
+ Indices of eigenvalues to select (inclusive).
+
+ Returns
+ -------
+ evals : np.ndarray[:]
+ evecs : np.ndarray[:, :]
+ """
+
+ Ac=A.copy()
+
+ ifniter>0:
+ Ac[np.diag_indices(Ac.shape[0])]=0.0
+
+ foriinrange(niter):
+ Ac[np.diag_indices(Ac.shape[0])]=rankN_approx(Ac).diagonal()
+
+ returnla.eigh(Ac,eigvals=eigvals)
+
+
+
+
+[docs]
+defnormalise_correlations(A,norm=None):
+"""Normalise to make a correlation matrix from a covariance matrix.
+
+ Parameters
+ ----------
+ A : np.ndarray[:, :]
+ Matrix to normalise.
+ norm : np.ndarray[:,:]
+ Normalize by diagonals of norm.
+ If None, then normalize by diagonals of A.
+
+ Returns
+ -------
+ X : np.ndarray[:, :]
+ Normalised correlation matrix.
+ ach : np.ndarray[:]
+ Array of the square root diagonal elements that normalise the matrix.
+ """
+
+ ifnormisNone:
+ ach=A.diagonal()**0.5
+ else:
+ ach=norm.diagonal()**0.5
+
+ aci=invert_no_zero(ach)
+
+ X=A*np.outer(aci,aci.conj())
+
+ returnX,ach
+
+
+
+
+[docs]
+defapply_gain(vis,gain,axis=1,out=None,prod_map=None):
+"""Apply per input gains to a set of visibilities packed in upper
+ triangular format.
+
+ This allows us to apply the gains while minimising the intermediate
+ products created.
+
+ Parameters
+ ----------
+ vis : np.ndarray[..., nprod, ...]
+ Array of visibility products.
+ gain : np.ndarray[..., ninput, ...]
+ Array of gains. One gain per input.
+ axis : integer, optional
+ The axis along which the inputs (or visibilities) are
+ contained. Currently only supports axis=1.
+ out : np.ndarray
+ Array to place output in. If :obj:`None` create a new
+ array. This routine can safely use `out = vis`.
+ prod_map : ndarray of integer pairs
+ Gives the mapping from product axis to input pairs. If not supplied,
+ :func:`icmap` is used.
+
+ Returns
+ -------
+ out : np.ndarray
+ Visibility array with gains applied. Same shape as :obj:`vis`.
+
+ """
+
+ nprod=vis.shape[axis]
+ ninput=gain.shape[axis]
+
+ ifprod_mapisNoneandnprod!=(ninput*(ninput+1)//2):
+ raiseException("Number of inputs does not match the number of products.")
+
+ ifprod_mapisnotNone:
+ iflen(prod_map)!=nprod:
+ msg="Length of *prod_map* does not match number of input products."
+ raiseValueError(msg)
+ # Could check prod_map contents as well, but the loop should give a
+ # sensible error if this is wrong, and checking is expensive.
+ else:
+ prod_map=[icmap(pp,ninput)forppinrange(nprod)]
+
+ ifoutisNone:
+ out=np.empty_like(vis)
+ elifout.shape!=vis.shape:
+ raiseException("Output array is wrong shape.")
+
+ # Iterate over input pairs and set gains
+ forppinrange(nprod):
+ # Determine the inputs.
+ ii,ij=prod_map[pp]
+
+ # Fetch the gains
+ gi=gain[:,ii]
+ gj=gain[:,ij].conj()
+
+ # Apply the gains and save into the output array.
+ out[:,pp]=vis[:,pp]*gi*gj
+
+ returnout
+
+
+
+
+[docs]
+defsubtract_rank1_signal(vis,signal,axis=1,out=None,prod_map=None):
+"""Subtract a rank 1 signal from a set of visibilities packed in upper
+ triangular format.
+
+ This allows us to subtract the noise injection solutions
+ while minimising the intermediate products created.
+
+ Parameters
+ ----------
+ vis : np.ndarray[..., nprod, ...]
+ Array of visibility products.
+ signal : np.ndarray[..., ninput, ...]
+ Array of underlying signals. One signal per input.
+ axis : integer, optional
+ The axis along which the inputs (or visibilities) are
+ contained. Currently only supports axis=1.
+ out : np.ndarray
+ Array to place output in. If :obj:`None` create a new
+ array. This routine can safely use `out = vis`.
+ prod_map : ndarray of integer pairs
+ Gives the mapping from product axis to input pairs. If not supplied,
+ :func:`icmap` is used.
+
+ Returns
+ -------
+ out : np.ndarray
+ Visibility array with signal subtracted. Same shape as :obj:`vis`.
+ """
+
+ nprod=vis.shape[axis]
+ ninput=signal.shape[axis]
+
+ ifprod_mapisNoneandnprod!=(ninput*(ninput+1)//2):
+ raiseException("Number of inputs does not match the number of products.")
+
+ ifprod_mapisnotNone:
+ iflen(prod_map)!=nprod:
+ msg="Length of *prod_map* does not match number of input products."
+ raiseValueError(msg)
+ # Could check prod_map contents as well, but the loop should give a
+ # sensible error if this is wrong, and checking is expensive.
+ else:
+ prod_map=[icmap(pp,ninput)forppinrange(nprod)]
+
+ ifoutisNone:
+ out=np.empty_like(vis)
+ elifout.shape!=vis.shape:
+ raiseException("Output array is wrong shape.")
+
+ # Iterate over input pairs and set signals
+ forppinrange(nprod):
+ # Determine the inputs.
+ ii,ij=prod_map[pp]
+
+ # Fetch the signals
+ si=signal[:,ii]
+ sj=signal[:,ij].conj()
+
+ # Apply the signals and save into the output array.
+ out[:,pp]=vis[:,pp]-si*sj
+
+ returnout
+
+
+
+
+[docs]
+deffringestop_time(
+ timestream,
+ times,
+ freq,
+ feeds,
+ src,
+ wterm=False,
+ bterm=True,
+ prod_map=None,
+ csd=False,
+ inplace=False,
+ static_delays=True,
+ obs=ephemeris.chime,
+):
+"""Fringestop timestream data to a fixed source.
+
+ Parameters
+ ----------
+ timestream : np.ndarray[nfreq, nprod, times]
+ Array containing the visibility timestream.
+ times : np.ndarray[times]
+ The UNIX time of each sample, or (if csd=True), the CSD of each sample.
+ freq : np.ndarray[nfreq]
+ The frequencies in the array (in MHz).
+ feeds : list of CorrInputs
+ The feeds in the timestream.
+ src : ephem.FixedBody
+ A PyEphem object representing the source to fringestop.
+ wterm: bool, optional
+ Include elevation information in the calculation.
+ bterm: bool, optional
+ Include a correction for baselines including the 26m Galt telescope.
+ prod_map: np.ndarray[nprod]
+ The products in the `timestream` array.
+ csd: bool, optional
+ Interpret the times parameter as CSDs.
+ inplace: bool, optional
+ Fringestop the visibilities in place. If not set, leave the originals intact.
+ static_delays: bool, optional
+ Correct for static cable delays in the system.
+
+ Returns
+ -------
+ fringestopped_timestream : np.ndarray[nfreq, nprod, times]
+ """
+
+ # Check the shapes match
+ nfeed=len(feeds)
+ nprod=len(prod_map)ifprod_mapisnotNoneelsenfeed*(nfeed+1)//2
+ expected_shape=(len(freq),nprod,len(times))
+
+ iftimestream.shape!=expected_shape:
+ raiseValueError(
+ "The shape of the timestream %s does not match the expected shape %s"
+ %(timestream.shape,expected_shape)
+ )
+
+ delays=delay(
+ times,
+ feeds,
+ src,
+ wterm=wterm,
+ bterm=bterm,
+ prod_map=prod_map,
+ csd=csd,
+ static_delays=static_delays,
+ obs=obs,
+ )
+
+ # Set any non CHIME feeds to have zero phase
+ delays=np.nan_to_num(delays,copy=False)
+
+ # If modifying inplace, loop to try and save some memory on large datasets
+ ifinplace:
+ forfi,frinenumerate(freq):
+ fs_phase=np.exp(2.0j*np.pi*delays*fr*1e6)
+ timestream[fi]*=fs_phase
+ fs_timestream=timestream
+ # Otherwise we might as well generate the entire phase array in onestop
+ else:
+ fs_timestream=2.0j*np.pi*delays*freq[:,np.newaxis,np.newaxis]*1e6
+ fs_timestream=np.exp(fs_timestream,out=fs_timestream)
+ fs_timestream*=timestream
+
+ returnfs_timestream
+[docs]
+defdecorrelation(
+ timestream,
+ times,
+ feeds,
+ src,
+ wterm=True,
+ bterm=True,
+ prod_map=None,
+ csd=False,
+ inplace=False,
+ static_delays=True,
+):
+"""Apply the decorrelation corrections to a timestream from observing a source.
+
+ Parameters
+ ----------
+ timestream : np.ndarray[nfreq, nprod, times]
+ Array containing the timestream.
+ times : np.ndarray[times]
+ The UNIX time of each sample, or (if csd=True), the CSD of each sample.
+ feeds : list of CorrInputs
+ The feeds in the timestream.
+ src : ephem.FixedBody
+ A PyEphem object representing the source to fringestop.
+ wterm: bool, optional
+ Include elevation information in the calculation.
+ bterm: bool, optional
+ Include a correction for baselines including the 26m Galt telescope.
+ prod_map: np.ndarray[nprod]
+ The products in the `timestream` array.
+ csd: bool, optional
+ Interpret the times parameter as CSDs.
+ inplace: bool, optional
+ Fringestop the visibilities in place. If not set, leave the originals intact.
+ static_delays: bool, optional
+ Correct for static cable delays in the system.
+
+ Returns
+ -------
+ corrected_timestream : np.ndarray[nfreq, nprod, times]
+ """
+
+ # Check the shapes match
+ nfeed=len(feeds)
+ nprod=len(prod_map)ifprod_mapisnotNoneelsenfeed*(nfeed+1)//2
+ expected_shape=(nprod,len(times))
+
+ iftimestream.shape[1:]!=expected_shape:
+ raiseValueError(
+ "The shape of the timestream %s does not match the expected shape %s"
+ %(timestream.shape,expected_shape)
+ )
+
+ delays=delay(
+ times,
+ feeds,
+ src,
+ wterm=wterm,
+ bterm=bterm,
+ prod_map=prod_map,
+ csd=csd,
+ static_delays=static_delays,
+ )
+
+ # Set any non CHIME feeds to have zero delay
+ delays=np.nan_to_num(delays,copy=False)
+
+ ratio_correction=invert_no_zero(
+ _chime_pfb.decorrelation_ratio(delays*800e6)[np.newaxis,...]
+ )
+
+ ifinplace:
+ timestream*=ratio_correction
+ else:
+ timestream=timestream*ratio_correction
+
+ returntimestream
+
+
+
+
+[docs]
+defdelay(
+ times,
+ feeds,
+ src,
+ wterm=True,
+ bterm=True,
+ prod_map=None,
+ csd=False,
+ static_delays=True,
+ obs=ephemeris.chime,
+):
+"""Calculate the delay in a visibilities observing a given source.
+
+ This includes both the geometric delay and static (cable) delays.
+
+ Parameters
+ ----------
+ times : np.ndarray[times]
+ The UNIX time of each sample, or (if csd=True), the CSD of each sample.
+ feeds : list of CorrInputs
+ The feeds in the timestream.
+ src : ephem.FixedBody
+ A PyEphem object representing the source to fringestop.
+ wterm: bool, optional
+ Include elevation information in the calculation.
+ bterm: bool, optional
+ Include a correction for baselines which include the 26m Galt telescope.
+ prod_map: np.ndarray[nprod]
+ The products in the `timestream` array.
+ csd: bool, optional
+ Interpret the times parameter as CSDs.
+ static_delays: bool, optional
+ If set the returned value includes both geometric and static delays.
+ If `False` only geometric delays are included.
+
+ Returns
+ -------
+ delay : np.ndarray[nprod, nra]
+ """
+
+ importscipy.constants
+
+ ra=(times%1.0)*360.0ifcsdelseobs.unix_to_lsa(times)
+ src_ra,src_dec=ephemeris.object_coords(src,times.mean(),obs=obs)
+ ha=(np.radians(ra)-src_ra)[np.newaxis,:]
+ latitude=np.radians(obs.latitude)
+ # Get feed positions / c
+ feedpos=get_feed_positions(feeds,get_zpos=wterm)/scipy.constants.c
+ feed_delays=np.array([f.delayforfinfeeds])
+ # Calculate the geometric delay between the feed and the reference position
+ delay_ref=-projected_distance(ha,latitude,src_dec,*feedpos.T[...,np.newaxis])
+
+ # Add in the static delays
+ ifstatic_delays:
+ delay_ref+=feed_delays[:,np.newaxis]
+
+ # Calculate baseline separations and pack into product array
+ ifprod_mapisNone:
+ delays=fast_pack_product_array(
+ delay_ref[:,np.newaxis]-delay_ref[np.newaxis,:]
+ )
+ else:
+ delays=delay_ref[prod_map["input_a"]]-delay_ref[prod_map["input_b"]]
+
+ # Add the b-term for baselines including the 26m Galt telescope
+ ifbterm:
+ b_delay=_26M_B/scipy.constants.c*np.cos(src_dec)
+
+ galt_feeds=get_holographic_index(feeds)
+
+ galt_conj=np.where(np.isin(prod_map["input_a"],galt_feeds),-1,0)
+ galt_noconj=np.where(np.isin(prod_map["input_b"],galt_feeds),1,0)
+
+ conj_flag=galt_conj+galt_noconj
+
+ delays+=conj_flag[:,np.newaxis]*b_delay
+
+ returndelays