def _pol(f):
     if tools.is_array(f):
         if tools.is_array_x(f):  # feed is X polarisation
             return "X"
         else:  # feed is Y polarisation
             return "Y"
     return "N"
    def beam(self, feed, freq, angpos=None):
        """Parameterized fit to driftscan cylinder beam model for CHIME telescope.

        Parameters
        ----------
        feed : int
            Index for the feed.
        freq : int
            Index for the frequency.
        angpos : np.ndarray[nposition, 2], optional
            Angular position on the sky (in radians).
            If not provided, default to the _angpos
            class attribute.

        Returns
        -------
        beam : np.ndarray[nposition, 2]
            Amplitude vector of beam at each position on the sky.
        """

        feed_obj = self.feeds[feed]

        # Check that feed exists and is a CHIME cylinder antenna
        if feed_obj is None:
            raise ValueError(
                "Craziness. The requested feed doesn't seem to exist.")

        if not tools.is_array(feed_obj):
            raise ValueError("Requested feed is not a CHIME antenna.")

        # If the angular position was not provided, then use the values in the
        # class attribute.
        if angpos is None:
            angpos = self._angpos

        dec = 0.5 * np.pi - angpos[:, 0]
        ha = angpos[:, 1]

        # We can only support feeds angled parallel or perp to the cylinder
        # axis. Check for these and throw exception for anything else.
        if tools.is_array_x(feed_obj):
            pol = 0
        elif tools.is_array_y(feed_obj):
            pol = 1
        else:
            raise RuntimeError(
                "Given polarisation (feed.pol=%s) not supported." %
                feed_obj.pol)

        beam = np.zeros((angpos.shape[0], 2), dtype=np.float64)
        beam[:, 0] = np.sqrt(
            self._beam_amplitude(pol, dec) *
            np.exp(-((ha / self._sigma(pol, freq, dec))**2)))

        # Normalize the beam
        if self._beam_normalization is not None:
            beam *= self._beam_normalization[freq, feed, np.newaxis, :]

        return beam
        def _feedclass(f, redundant_cyl=False):
            if tools.is_array(f):
                if tools.is_array_x(f):  # feed is X polarisation
                    pol = 0
                else:  # feed is Y polarisation
                    pol = 1

                if redundant_cyl:
                    return 2 * f.cyl + pol
                else:
                    return pol
            return -1
    def beamclass(self):
        """Beam class definition for the CHIME/Pathfinder.

        When `self.stack_type` is `redundant`, the X-polarisation feeds get
        `beamclass = 0`, and the Y-polarisation gets `beamclass = 1`.
        When `self.stack_type` is `redundant_cyl`, feeds of same polarisation
        and cylinder have same beam class. The beam class is given by
        `beamclass = 2*cyl + pol` where `cyl` is the cylinder number according to
        `ch_util.tools` convention and `pol` is the polarisation (0 for X and 1
        for Y polarisation)
        When `self.stack_type` is `unique`, then the feeds are just given an
        increasing unique class.
        In all cases, any other type of feed gets set to `-1` and should be
        ignored.
        """

        # Make beam class just channel number.

        def _feedclass(f, redundant_cyl=False):
            if tools.is_array(f):
                if tools.is_array_x(f):  # feed is X polarisation
                    pol = 0
                else:  # feed is Y polarisation
                    pol = 1

                if redundant_cyl:
                    return 2 * f.cyl + pol
                else:
                    return pol
            return -1

        if self.stack_type == "redundant":
            return np.array([_feedclass(f) for f in self.feeds])
        elif self.stack_type == "redundant_cyl":
            return np.array(
                [_feedclass(f, redundant_cyl=True) for f in self.feeds])
        else:
            beamclass = [
                fi if tools.is_array(feed) else -1
                for fi, feed in enumerate(self.feeds)
            ]
            return np.array(beamclass)
    def _set_beam_normalization(self):
        """Determine the beam normalization for each feed and frequency.

        The beam will be normalized by its value at transit at the declination
        provided in the dec_normalized config parameter.  If this config parameter
        is set to None, then there is no additional normalization applied.
        """

        self._beam_normalization = None

        if self.dec_normalized is not None:

            angpos = np.array([(0.5 * np.pi - np.radians(self.dec_normalized)),
                               0.0]).reshape(1, -1)

            beam = np.ones((self.nfreq, self.nfeed, 2), dtype=np.float64)

            beam_lookup = {}

            for fe, feed in enumerate(self.feeds):

                if not tools.is_array(feed):
                    continue

                beamclass = self.beamclass[fe]

                if beamclass not in beam_lookup:

                    beam_lookup[beamclass] = np.ones((self.nfreq, 2),
                                                     dtype=np.float64)
                    for fr in range(self.nfreq):
                        beam_lookup[beamclass][fr] = self.beam(fe, fr,
                                                               angpos)[0]

                beam[:, fe, :] = beam_lookup[beamclass]

            self._beam_normalization = tools.invert_no_zero(
                np.sqrt(np.sum(beam**2, axis=-1)))
Exemple #6
0
    def process(self, data, inputmap):
        """Package a holography transit into a beam container.

        Parameters
        ----------
        data : SiderealStream
            Transit observation as generated by `TransitRegridder`.
        inputmap : list of `CorrInput`
            A list describing the inputs as they are in the file, output from
            `ch_util.tools.get_correlator_inputs()`

        Returns
        -------
        track : TrackBeam
            The transit in a beam container.
        """

        # redistribute if needed
        data.redistribute("freq")

        prod = data.index_map["prod"]
        inputs = data.index_map["input"]

        # Figure out which inputs are the 26m
        input_26m = prod["input_a"][np.where(prod["input_a"] == prod["input_b"])[0]]
        if len(input_26m) != 2:
            msg = "Did not find exactly two 26m inputs in the data."
            self.log.error(msg)
            raise PipelineRuntimeError(msg)

        # Separate products by 26 m inputs
        prod_groups = []
        for i in input_26m:
            prod_groups.append(
                np.where(np.logical_or(prod["input_a"] == i, prod["input_b"] == i))[0]
            )

        # Check we have the expected number of products
        if (
            prod_groups[0].shape[0] != inputs.shape[0]
            or prod_groups[1].shape[0] != inputs.shape[0]
        ):
            msg = (
                "Products do not separate into two groups with the length of the input map. "
                "({:d}, {:d}) != {:d}"
            ).format(prod_groups[0].shape[0], prod_groups[1].shape[0], inputs.shape[0])
            self.log.error(msg)
            raise PipelineRuntimeError(msg)

        # Sort based on the id in the layout database
        corr_id = np.array([inp.id for inp in inputmap])
        isort = np.argsort(corr_id)

        # Create new input axis using id and serial number in database
        inputs_sorted = np.array(
            [(inputmap[ii].id, inputmap[ii].input_sn) for ii in isort],
            dtype=inputs.dtype,
        )

        # Sort the products based on the input id in database and
        # determine which products should be conjugated.
        conj = []
        prod_groups_sorted = []
        for i, pg in enumerate(prod_groups):
            group_prod = prod[pg]
            group_conj = group_prod["input_a"] == input_26m[i]
            group_inputs = np.where(
                group_conj, group_prod["input_b"], group_prod["input_a"]
            )
            group_sort = np.argsort(corr_id[group_inputs])

            prod_groups_sorted.append(pg[group_sort])
            conj.append(group_conj[group_sort])

        # Regroup by co/cross-pol
        copol, xpol = [], []
        prod_groups_cox = [pg.copy() for pg in prod_groups_sorted]
        conj_cox = [pg.copy() for pg in conj]
        input_pol = np.array(
            [
                ipt.pol
                if (tools.is_array(ipt) or tools.is_holographic(ipt))
                else inputmap[input_26m[0]].pol
                for ipt in inputmap
            ]
        )
        for i, pg in enumerate(prod_groups_sorted):
            group_prod = prod[pg]
            # Determine co/cross in each prod group
            cp = (
                input_pol[
                    np.where(conj[i], group_prod["input_b"], group_prod["input_a"])
                ]
                == inputmap[input_26m[i]].pol
            )
            xp = np.logical_not(cp)
            copol.append(cp)
            xpol.append(xp)
            # Move products to co/cross-based groups
            prod_groups_cox[0][cp] = pg[cp]
            prod_groups_cox[1][xp] = pg[xp]
            conj_cox[0][cp] = conj[i][cp]
            conj_cox[1][xp] = conj[i][xp]
        # Check for compeleteness
        consistent = np.all(copol[0] + copol[1] == np.ones(copol[0].shape)) and np.all(
            xpol[0] + xpol[1] == np.ones(xpol[0].shape)
        )
        if not consistent:
            msg = (
                "Products do not separate exclusively into co- and cross-polar groups."
            )
            self.log.error(msg)
            raise PipelineRuntimeError(msg)

        # Make new index map
        ra = data.attrs["cirs_ra"]
        phi = unwrap_lha(data.ra[:], ra)
        if "dec" not in data.attrs.keys():
            msg = (
                "Input stream must have a 'dec' attribute specifying "
                "declination of holography source."
            )
            self.log.error(msg)
            raise PipelineRuntimeError(msg)
        theta = np.ones_like(phi) * data.attrs["dec"]
        pol = np.array(["co", "cross"], dtype="S5")

        # Create new container and fill
        track = TrackBeam(
            theta=theta,
            phi=phi,
            track_type="drift",
            coords="celestial",
            input=inputs_sorted,
            pol=pol,
            freq=data.freq[:],
            attrs_from=data,
            distributed=data.distributed,
        )
        for ip in range(len(pol)):
            track.beam[:, ip, :, :] = data.vis[:, prod_groups_cox[ip], :]
            track.weight[:, ip, :, :] = data.weight[:, prod_groups_cox[ip], :]
            if np.any(conj_cox[ip]):
                track.beam[:, ip, conj_cox[ip], :] = track.beam[
                    :, ip, conj_cox[ip], :
                ].conj()

        # Store 26 m inputs
        track.attrs["26m_inputs"] = [list(isort).index(ii) for ii in input_26m]

        return track
    def beam(self, feed, freq, angpos=None):
        """Primary beam implementation for the CHIME/Pathfinder.

        This only supports normal CHIME cylinder antennas. Asking for the beams
        for other types of inputs will cause an exception to be thrown. The
        beams from this routine are rotated by `self.rotation_angle` to account
        for the CHIME/Pathfinder rotation.

        Parameters
        ----------
        feed : int
            Index for the feed.
        freq : int
            Index for the frequency.
        angpos : np.ndarray[nposition, 2], optional
            Angular position on the sky (in radians).
            If not provided, default to the _angpos
            class attribute.

        Returns
        -------
        beam : np.ndarray[nposition, 2]
            Amplitude vector of beam at each position on the sky.
        """
        # # Fetch beam parameters out of config database.

        feed_obj = self.feeds[feed]

        # Check that feed exists and is a CHIME cylinder antenna
        if feed_obj is None:
            raise ValueError(
                "Craziness. The requested feed doesn't seem to exist.")

        if not tools.is_array(feed_obj):
            raise ValueError("Requested feed is not a CHIME antenna.")

        # If the angular position was not provided, then use the values in the
        # class attribute.
        if angpos is None:
            angpos = self._angpos

        # Get the beam rotation parameters.
        yaw = -self.rotation_angle
        pitch = 0.0
        roll = 0.0

        rot = np.radians([yaw, pitch, roll])

        # We can only support feeds angled parallel or perp to the cylinder
        # axis. Check for these and throw exception for anything else.
        if tools.is_array_y(feed_obj):
            beam = cylbeam.beam_y(
                angpos,
                self.zenith,
                self.cylinder_width / self.wavelengths[freq],
                self.fwhm_ey[freq],
                self.fwhm_hy[freq],
                rot=rot,
            )
        elif tools.is_array_x(feed_obj):
            beam = cylbeam.beam_x(
                angpos,
                self.zenith,
                self.cylinder_width / self.wavelengths[freq],
                self.fwhm_ex[freq],
                self.fwhm_hx[freq],
                rot=rot,
            )
        else:
            raise RuntimeError(
                "Given polarisation (feed.pol=%s) not supported." %
                feed_obj.pol)

        # Normalize the beam
        if self._beam_normalization is not None:
            beam *= self._beam_normalization[freq, feed, np.newaxis, :]

        return beam