Ejemplo n.º 1
0
    def __init__(self, dec: Quantity, equinox: float, plane_id: int,
                 ra: Quantity):
        try:
            dec.to(u.degree)
        except u.UnitConversionError:
            raise ValueError("The declination must have an angular unit.")
        if dec.to_value(u.degree) < -90 or dec.to_value(u.degree) > 90:
            raise ValueError(
                "The declination must be between -90 and 90 degrees.")
        if 199.9 < equinox < 200.1:
            equinox = 2000
        if equinox < 1900:
            raise ValueError("The equinox must be 1900 or later.")
        try:
            ra.to(u.degree)
        except u.UnitConversionError:
            raise ValueError("The right ascension must have an angular unit.")
        if ra.to_value(u.degree) < 0 or ra.to_value(u.degree) >= 360:
            raise ValueError(
                "The right ascension must have a value between 0 degress "
                "(inclusive) and 360 degrees (exclusive).")

        self._dec = dec
        self._equinox = equinox
        self._plane_id = plane_id
        self._ra = ra
Ejemplo n.º 2
0
def make_bounds(
    origin: coord.UnitSphericalRepresentation,
    rot_lower: u.Quantity = _make_bounds_defaults["rot_lower"],
    rot_upper: u.Quantity = _make_bounds_defaults["rot_upper"],
    origin_lim: u.Quantity = _make_bounds_defaults["origin_lim"],
) -> T.Tuple[float, float]:
    """Make bounds on Rotation parameter.

    Parameters
    ----------
    rot_lower, rot_upper : |Quantity|, optional
        The lower and upper bounds in degrees.
    origin_lim : |Quantity|, optional
        The symmetric lower and upper bounds on origin in degrees.

    Returns
    -------
    bounds : ndarray
        Shape (3, 2)
        Rows are rotation_bounds, lon_bounds, lat_bounds

    """
    rotation_bounds = (rot_lower.to_value(u.deg), rot_upper.to_value(u.deg))
    # longitude bounds (ra in ICRS).
    lon_bounds = (origin.lon + (-1, 1) * origin_lim).to_value(u.deg)
    # latitude bounds (dec in ICRS).
    lat_bounds = (origin.lat + (-1, 1) * origin_lim).to_value(u.deg)

    # stack bounds so rows are bounds.
    bounds = np.c_[rotation_bounds, lon_bounds, lat_bounds].T

    return bounds
Ejemplo n.º 3
0
 def test_conversion(self):
     q_pv = Quantity(self.pv, self.pv_unit)
     q1 = q_pv.to(('AU', 'AU/day'))
     assert isinstance(q1, Quantity)
     assert q1['p'].unit == u.AU
     assert q1['v'].unit == u.AU / u.day
     assert np.all(q1['p'] == q_pv['p'].to(u.AU))
     assert np.all(q1['v'] == q_pv['v'].to(u.AU/u.day))
     q2 = q_pv.to(self.pv_unit)
     assert q2['p'].unit == self.p_unit
     assert q2['v'].unit == self.v_unit
     assert np.all(q2['p'].value == self.pv['p'])
     assert np.all(q2['v'].value == self.pv['v'])
     assert not np.may_share_memory(q2, q_pv)
     pv1 = q_pv.to_value(('AU', 'AU/day'))
     assert type(pv1) is np.ndarray
     assert np.all(pv1['p'] == q_pv['p'].to_value(u.AU))
     assert np.all(pv1['v'] == q_pv['v'].to_value(u.AU/u.day))
     pv11 = q_pv[1].to_value(('AU', 'AU/day'))
     assert type(pv11) is np.void
     assert pv11 == pv1[1]
     q_pv_t = Quantity(self.pv_t, self.pv_t_unit)
     q2 = q_pv_t.to((('kpc', 'kpc/Myr'), 'Myr'))
     assert q2['pv']['p'].unit == u.kpc
     assert q2['pv']['v'].unit == u.kpc / u.Myr
     assert q2['t'].unit == u.Myr
     assert np.all(q2['pv']['p'] == q_pv_t['pv']['p'].to(u.kpc))
     assert np.all(q2['pv']['v'] == q_pv_t['pv']['v'].to(u.kpc/u.Myr))
     assert np.all(q2['t'] == q_pv_t['t'].to(u.Myr))
Ejemplo n.º 4
0
    def __init__(
        self,
        content_checksum: str,
        content_length: Quantity,
        identifier: uuid.UUID,
        name: str,
        plane_id: int,
        paths: CalibrationLevelPaths,
        product_type: ProductType,
    ):
        if len(content_checksum) > 32:
            raise ValueError(
                "The content checksum must have at most 32 characters.")
        try:
            content_length.to(byte)
        except u.UnitConversionError:
            raise ValueError("The content length must have a file size unit.")
        if content_length.to_value(byte) <= 0:
            raise ValueError("The content length must be positive.")
        if len(name) > 200:
            raise ValueError(
                "The artifact name must have at most 200 characters.")
        if paths.raw is None and paths.reduced is None:
            raise ValueError("At least one of the paths must be non-None.")

        self._content_checksum = content_checksum
        self._content_length = content_length
        self._identifier = identifier
        self._name = name
        self._paths = paths
        self._plane_id = plane_id
        self._product_type = product_type
Ejemplo n.º 5
0
 def _prepare_samples(self, frequency: u.Quantity) -> np.ndarray:
     """Interpolate aperture plane to selected frequencies, and optionally rotate."""
     frequency_Hz = frequency.to_value(u.Hz).astype(np.float32,
                                                    copy=False,
                                                    casting='same_kind')
     samples = self._interp_samples(frequency_Hz)
     return samples
def rss_resolution_element(grating_frequency: Quantity,
                           grating_angle: Quantity,
                           slit_width: Quantity) -> Quantity:
    """
    Returns the resolution element for the given grating frequency, grating angle and slit width.

    Parameters
    ----------
    grating_frequency:
       The grating frequency
    grating_angle:
        The grating angle
    slit_width:
        The slit width

    Return
    ------
    resolution_element
        The resolution element

    """

    Lambda = 1 / grating_frequency
    # TODO some thing is not right below units were supposed to be arcsec but got arcsec * mm
    return (slit_width.to_value(u.arcsec) * Lambda * np.cos(grating_angle) *
            (FOCAL_LENGTH_TELESCOPE / FOCAL_LENGTH_RSS_COLLIMATOR))
Ejemplo n.º 7
0
    def column_to_values(self, colname, unit):

        # First make sure the column is a quantity
        quantity = Quantity(self.time_series[colname], copy=False)

        if quantity.unit.is_equivalent(unit):
            return quantity.to_value(unit)
        else:
            raise UnitsError(f"Cannot convert the units '{quantity.unit}' of "
                             f"column '{colname}' to the required units of "
                             f"'{unit}'")
Ejemplo n.º 8
0
 def __init__(self, step: float, frequency: u.Quantity, samples: np.ndarray,
              *, band: str) -> None:
     if len(samples) != len(frequency):
         raise ValueError('frequency and samples have inconsistent shape')
     self._step = step
     self._samples = samples
     self._frequency = frequency
     self._band = band
     self._interp_samples = scipy.interpolate.interp1d(frequency.to_value(
         u.Hz),
                                                       samples,
                                                       axis=0,
                                                       copy=False,
                                                       bounds_error=False,
                                                       fill_value=np.nan,
                                                       assume_sorted=True)
Ejemplo n.º 9
0
    def evaluate(self, method=None, **kwargs):
        """Evaluate NDData Array

        This function provides a uniform interface to several interpolators.
        The evaluation nodes are given as ``kwargs``.

        Currently available:
        `~scipy.interpolate.RegularGridInterpolator`, methods: linear, nearest

        Parameters
        ----------
        method : str {'linear', 'nearest'}, optional
            Interpolation method
        kwargs : dict
            Keys are the axis names, Values the evaluation points

        Returns
        -------
        array : `~astropy.units.Quantity`
            Interpolated values, axis order is the same as for the NDData array
        """
        values = []
        for idx, axis in enumerate(self.axes):
            # Extract values for each axis, default: nodes
            shape = np.ones(len(self.axes), dtype=int)
            shape[idx] = -1
            default = axis.nodes.reshape(shape)
            temp = Quantity(kwargs.pop(axis.name, default))
            # Transform to correct unit
            temp = temp.to_value(axis.unit)
            # Transform to match interpolation behaviour of axis
            values.append(np.atleast_1d(axis._interp_values(temp)))

        # This is to catch e.g. typos in axis names
        if kwargs != {}:
            raise ValueError("Input given for unknown axis: {}".format(kwargs))

        if self._regular_grid_interp is None:
            self._add_regular_grid_interp()

        return self._regular_grid_interp(values, method=method, **kwargs)
Ejemplo n.º 10
0
 def sample(self,
            l: ArrayLike,
            m: ArrayLike,
            frequency: u.Quantity,
            frame: Union[AltAzFrame, RADecFrame],
            output_type: OutputType,
            *,
            out: Optional[np.ndarray] = None) -> np.ndarray:
     l_ = np.asarray(l)
     m_ = np.asarray(m)
     if output_type != OutputType.UNPOLARIZED_POWER:
         raise NotImplementedError(
             'Only UNPOLARIZED_POWER is currently implemented')
     in_shape = np.broadcast_shapes(l_.shape, m_.shape, frame.shape)
     out_shape = frequency.shape + in_shape
     if out is not None:
         if out.shape != out_shape:
             raise ValueError(
                 f'out must have shape {out_shape}, not {out.shape}')
         if out.dtype != np.float32:
             raise TypeError(
                 f'out must have dtype float32, not {out.dtype}')
         if not out.flags.c_contiguous:
             raise ValueError('out must be C contiguous')
     else:
         out = np.empty(out_shape, np.float32)
     frequency_Hz = frequency.to_value(u.Hz).astype(np.float32,
                                                    copy=False,
                                                    casting='same_kind')
     samples = self._interp_samples(frequency_Hz)
     # Create view with l/m axis flattened to 1D for benefit of numba
     l_view = np.broadcast_to(l_, in_shape).ravel()
     m_view = np.broadcast_to(m_, in_shape).ravel()
     out_view = out.view()
     out_view.shape = frequency.shape + l_view.shape
     _sample_impl(l_view, m_view, samples, self._step, out_view)
     return out
Ejemplo n.º 11
0
def format_duration(duration: u.Quantity) -> str:
    seconds = int(round(duration.to_value(u.s)))
    return '{}:{:02}:{:02}'.format(seconds // 3600, seconds // 60 % 60, seconds % 60)
Ejemplo n.º 12
0
class BinnedDataAxis(DataAxis):
    """Data axis for binned data

    Parameters
    ----------
    lo : `~astropy.units.Quantity`
        Lower bin edges
    hi : `~astropy.units.Quantity`
        Upper bin edges
    name : str, optional
        Axis name, default: 'Default'
    interpolation_mode : str {'linear', 'log'}
        Interpolation behaviour, default: 'linear'
    """
    def __init__(self, lo, hi, **kwargs):
        self.lo = Quantity(lo)
        self.hi = Quantity(hi)

        if ((self.lo < 0).any() or
            (self.hi < 0).any()) and kwargs.get("interpolation_mode") == "log":
            raise ValueError(
                "Interpolation scaling 'log' only support for positive node values."
            )

        super().__init__(None, **kwargs)

    @classmethod
    def logspace(cls, emin, emax, nbins, unit=None, **kwargs):
        # TODO: splitout log space into a helper function
        vals = DataAxis.logspace(emin, emax, nbins + 1, unit)._data
        return cls(vals[:-1], vals[1:], **kwargs)

    def __str__(self):
        ss = super().__str__()
        ss += "\nLower bounds {}".format(self.lo)
        ss += "\nUpper bounds {}".format(self.hi)

        return ss

    @property
    def bins(self):
        """Bin edges"""
        unit = self.lo.unit
        val = np.append(self.lo.value, self.hi.to_value(unit)[-1])
        return val * unit

    @property
    def bin_width(self):
        """Bin width"""
        return self.hi - self.lo

    @property
    def nodes(self):
        """Evaluation nodes.

        Depending on the interpolation mode, either log or lin center are
        returned
        """
        if self.interpolation_mode == "log":
            return self.log_center()
        else:
            return self.lin_center()

    def lin_center(self):
        """Linear bin centers"""
        return (self.lo + self.hi) / 2

    def log_center(self):
        """Logarithmic bin centers"""
        return np.sqrt(self.lo * self.hi)
Ejemplo n.º 13
0
    def __init__(
        self,
        object_id: str,
        start_time: datetime,
        end_time: datetime,
        stepsize: Quantity,
    ):
        SALT_OBSERVATORY_ID = "B31"

        # enforce timezones
        if start_time.tzinfo is None or end_time.tzinfo is None:
            raise ValueError("The start and end time must be timezone-aware.")

        # avoid overly excessive queries
        self.stepsize = stepsize
        if self.stepsize < 5 * u.minute:
            raise ValueError(
                "The sampling interval must be at least 5 minutes.")

        # query Horizons
        self.object_id = object_id
        start = start_time.astimezone(tzutc()).strftime("%Y-%m-%d %H:%M:%S")
        # Make sure the whole time interval is covered by the queried ephemerides
        end_time_with_margin = end_time + timedelta(
            seconds=stepsize.to_value(u.second))
        stop = end_time_with_margin.astimezone(
            tzutc()).strftime("%Y-%m-%d %H:%M:%S")
        # Horizons requires an int for the step size. As round() might call NumPy's
        # round method and thus produce a float, we have to round "manually" using
        # the int function.
        step = f"{int(0.5 + stepsize.to_value(u.minute))}m"
        obj = Horizons(
            id=self.object_id,
            location=SALT_OBSERVATORY_ID,
            epochs={
                "start": start,
                "stop": stop,
                "step": step
            },
        )
        ephemerides = obj.ephemerides()

        # store the ephemerides in the format we need
        self._ephemerides = []
        for row in range(len(ephemerides)):
            epoch = parse(
                ephemerides["datetime_str"][row]).replace(tzinfo=tzutc())
            ra = float(ephemerides["RA"][row]) * u.deg
            dec = float(ephemerides["DEC"][row]) * u.deg
            ra_rate = float(ephemerides["RA_rate"][row]) * u.arcsec / u.hour
            dec_rate = ephemerides["DEC_rate"][row] * u.arcsec / u.hour
            magnitude = ephemerides["V"][row] if "V" in ephemerides.keys(
            ) else 0
            magnitude_range = MagnitudeRange(min_magnitude=magnitude,
                                             max_magnitude=magnitude,
                                             bandpass="******")
            self._ephemerides.append(
                Ephemeris(
                    ra=ra,
                    dec=dec,
                    ra_rate=ra_rate,
                    dec_rate=dec_rate,
                    magnitude_range=magnitude_range,
                    epoch=epoch,
                ))
Ejemplo n.º 14
0
 def image(self, ra: Quantity, dec: Quantity) -> pyfits.HDUList:
     # grab 10' x 10' image from server and pull it into pyfits
     if self.survey in SurveyImageService.STSCI_SURVEYS:
         survey_identifiers = {
             Survey.POSS2UKSTU_RED: "poss2ukstu_red",
             Survey.POSS2UKSTU_BLUE: "poss2ukstu_blue",
             Survey.POSS2UKSTU_IR: "poss2ukstu_ir",
             Survey.POSS1_RED: "poss1_red",
             Survey.POSS1_BLUE: "poss1_blue",
         }
         url = "https://archive.stsci.edu/cgi-bin/dss_search"
         params = urllib.parse.urlencode({
             "v":
             survey_identifiers[self.survey],
             "r":
             "%f" % ra.to_value(u.deg),
             "d":
             "%f" % dec.to_value(u.deg),
             "e":
             "J2000",
             "h":
             10.0,
             "w":
             10.0,
             "f":
             "fits",
             "c":
             "none",
         }).encode("utf-8")
     elif self.survey in SurveyImageService.SKY_VIEW_SURVEYS:
         survey_identifiers = {
             Survey.TWO_MASS_J: "2mass-j",
             Survey.TWO_MASS_H: "2mass-h",
             Survey.TWO_MASS_K: "2mass-k",
         }
         ra = Angle(ra)
         dec = Angle(dec)
         url = "https://skyview.gsfc.nasa.gov/current/cgi/runquery.pl"
         params = urllib.parse.urlencode({
             "Position":
             "'%d %d %f, %d %d %f'" % (
                 round(ra.hms[0]),
                 ra.hms[1],
                 ra.hms[2],
                 round(dec.dms[0]),
                 abs(dec.dms[1]),
                 abs(dec.dms[2]),
             ),
             "Survey":
             survey_identifiers[self.survey],
             "Coordinates":
             "J2000",
             "Return":
             "FITS",
             "Pixels":
             700,
             "Size":
             0.1667,
         }).encode("utf-8")
     else:
         raise Exception(f"Unsupported survey: {self.survey}")
     fits_data = io.BytesIO()
     data = urllib.request.urlopen(url, params).read()
     fits_data.write(data)
     fits_data.seek(0)
     return pyfits.open(fits_data)
Ejemplo n.º 15
0
def deproject(projection: np.ndarray,
              projection_azimuth: u.Quantity,
              spectral_order: int,
              cube_shape: typ.Tuple[int, ...] = None,
              cube_spatial_offset: typ.Tuple[int, int] = (0, 0),
              projection_spatial_offset: typ.Tuple[int, int] = (0, 0),
              x_axis: int = ~2,
              y_axis: int = ~1,
              w_axis: int = ~0,
              rotation_kwargs: typ.Dict = None) -> np.ndarray:

    if rotation_kwargs is None:
        rotation_kwargs = {
            'reshape': False,
            'prefilter': False,
            'order': 3,
            'mode': 'nearest',
        }

    csh = list(projection.shape)
    # csh = list(cube_shape)
    csh[w_axis] = cube_shape[w_axis]

    projection = np.broadcast_to(projection, csh, subok=True)

    shifted_projection = np.zeros(cube_shape)

    px, py = cube_spatial_offset
    qx, qy = projection_spatial_offset

    out_sl = [slice(None)] * shifted_projection.ndim
    in_sl = [slice(None)] * projection.ndim
    tx = px - qx
    ty = py - qy

    out_sl[x_axis] = slice(
        max(0, tx), min(shifted_projection.shape[x_axis], tx + csh[x_axis]))
    out_sl[y_axis] = slice(
        max(0, ty), min(shifted_projection.shape[y_axis], ty + csh[y_axis]))
    in_sl[x_axis] = slice(
        max(0, -tx), min(csh[x_axis], -tx + shifted_projection.shape[x_axis]))
    in_sl[y_axis] = slice(
        max(0, -ty), min(csh[y_axis], -ty + shifted_projection.shape[y_axis]))

    shifted_projection[tuple(out_sl)] = projection[in_sl]
    backprojected_cube = np.zeros_like(shifted_projection)

    ssh = list(cube_shape)
    x, y, l = np.meshgrid(np.arange(ssh[x_axis]),
                          np.arange(ssh[y_axis]),
                          np.arange(ssh[w_axis]),
                          copy=False,
                          sparse=False)

    x, y, l = x.flatten(), y.flatten(), l.flatten()

    out_sl = [slice(None)] * shifted_projection.ndim
    in_sl = [slice(None)] * shifted_projection.ndim
    out_sl[x_axis], out_sl[y_axis], out_sl[w_axis] = x - spectral_order * (
        l), y, l
    in_sl[x_axis], in_sl[y_axis], in_sl[w_axis] = x, y, l

    backprojected_cube[tuple(out_sl)] = shifted_projection[in_sl]
    del shifted_projection
    del x, y, l

    az = -1 * projection_azimuth.to_value(u.deg)

    # backprojected_cube = scipy.ndimage.rotate(
    #     input=backprojected_cube,
    #     angle=az,
    #     axes=(x_axis, y_axis),
    #     **rotation_kwargs
    # )

    backprojected_cube = rotate(
        image=backprojected_cube,
        angle=az,
    )

    return backprojected_cube
Ejemplo n.º 16
0
def model(cube: np.ndarray,
          projection_azimuth: u.Quantity,
          spectral_order: int,
          projection_shape: typ.Tuple[int, ...] = None,
          cube_spatial_offset: typ.Tuple[int, int] = (0, 0),
          projection_spatial_offset: typ.Tuple[int, int] = (0, 0),
          x_axis: int = ~2,
          y_axis: int = ~1,
          w_axis: int = ~0,
          rotation_kwargs: typ.Dict = None) -> np.ndarray:
    """
    Model is a basic forward model of CT imaging spectrograph.
    :param cube: 'slabs' which is a `np.ndarray` which have at least an x, y, and wavelength axis.
    :param projection_azimuth: scalar angle describing dispersion direction in the `data` array.
    :param spectral_order: number of indices the array shifted per wavelength bin.
    :param projection_shape: Desired shape of output array, as tuple of integers
    :param projection_spatial_offset:
    :param cube_spatial_offset:
    :param x_axis: axis of the data slabs representing x-axis
    :param y_axis: axis of the data slabs representing y-axis
    :param w_axis: axis of the data slabs representing wavelength axis
    :param rotation_kwargs: kwargs for `scipy.ndimage.rotate` to be used during rotation portion of forward model
    :return: list of arrays to which the forward model has been applied
    """

    if rotation_kwargs is None:
        rotation_kwargs = {
            'reshape': False,
            'prefilter': False,
            'order': 3,
            'mode': 'nearest',
        }

    cube = cube.copy()
    az = projection_azimuth.to_value(u.deg)

    tsh = min_projection_shape(cube, spectral_order, x_axis, w_axis)
    if projection_shape is None:
        projection_shape = tsh

    p_data = np.zeros(projection_shape)

    px, py = projection_spatial_offset
    qx, qy = cube_spatial_offset

    ssh = list(cube.shape)
    x, y, l = np.meshgrid(np.arange(ssh[x_axis]), np.arange(ssh[y_axis]),
                          np.arange(ssh[w_axis]))
    x, y, l = x.flatten(), y.flatten(), l.flatten()

    # rotated_cube = scipy.ndimage.rotate(
    #     input=cube.copy(),
    #     angle=az,
    #     axes=(x_axis, y_axis),
    #     **rotation_kwargs
    # )
    rotated_cube = rotate(
        image=cube.copy(),
        angle=az,
    )

    ssh[x_axis] += np.abs(spectral_order) * ssh[w_axis]
    shifted_cube = np.zeros(ssh)

    out_sl = [slice(None)] * shifted_cube.ndim
    in_sl = [slice(None)] * rotated_cube.ndim
    out_sl[x_axis], out_sl[y_axis], out_sl[
        w_axis] = x + spectral_order * l, y, l
    in_sl[x_axis], in_sl[y_axis], in_sl[w_axis] = x, y, l

    # Making a little bit future proof:
    out_sl = tuple(out_sl)
    in_sl = tuple(in_sl)

    shifted_cube[out_sl] = rotated_cube[in_sl]

    out_sl = [slice(None)] * p_data.ndim
    in_sl = [slice(None)] * shifted_cube.ndim
    tx = px - qx
    ty = py - qy

    out_sl[x_axis] = slice(max(0, tx),
                           min(p_data.shape[x_axis], tx + ssh[x_axis]))
    out_sl[y_axis] = slice(max(0, ty),
                           min(p_data.shape[y_axis], ty + ssh[y_axis]))
    in_sl[x_axis] = slice(max(0, -tx),
                          min(ssh[x_axis], -tx + p_data.shape[x_axis]))
    in_sl[y_axis] = slice(max(0, -ty),
                          min(ssh[y_axis], -ty + p_data.shape[y_axis]))

    # Making a little bit future proof:
    # Using the tuple loophole
    out_sl = tuple(out_sl)
    in_sl = tuple(in_sl)

    p_data[out_sl] = np.sum(shifted_cube, axis=w_axis, keepdims=True)[in_sl]
    return p_data
Ejemplo n.º 17
0
    def smooth(self, width, kernel="gauss"):
        """Smooth the map.

        Iterates over 2D image planes, processing one at a time.

        Parameters
        ----------
        width : `~astropy.units.Quantity`, str or float
            Smoothing width given as quantity or float. If a float is given it
            interpreted as smoothing width in pixels. If an (angular) quantity
            is given it converted to pixels using ``healpy.nside2resol``.
            It corresponds to the standard deviation in case of a Gaussian kernel,
            and the radius in case of a disk kernel.
        kernel : {'gauss', 'disk'}
            Kernel shape

        Returns
        -------
        image : `HpxNDMap`
            Smoothed image (a copy, the original object is unchanged).
        """
        import healpy as hp

        if not self.geom.is_allsky:
            raise NotImplementedError(
                "Smoothing is only possible for all-sky maps")

        nside = self.geom.nside
        lmax = 3 * nside - 1  # maximum l of the power spectrum
        nest = self.geom.nest

        # The smoothing width is expected by healpy in radians
        if isinstance(width, (Quantity, str)):
            width = Quantity(width)
            width = width.to_value("rad")
        else:
            binsz = np.degrees(hp.nside2resol(nside))
            width = width * binsz
            width = np.deg2rad(width)

        smoothed_data = np.empty(self.data.shape, dtype=float)

        for img, idx in self.iter_by_image():
            img = img.astype(float)

            if nest:
                # reorder to ring to do the smoothing
                img = hp.pixelfunc.reorder(img, n2r=True)

            if kernel == "gauss":
                data = hp.sphtfunc.smoothing(img,
                                             sigma=width,
                                             pol=False,
                                             verbose=False,
                                             lmax=lmax)
            elif kernel == "disk":
                # create the step function in angular space
                theta = np.linspace(0, width)
                beam = np.ones(len(theta))
                beam[theta > width] = 0
                # convert to the spherical harmonics space
                window_beam = hp.sphtfunc.beam2bl(beam, theta, lmax)
                # normalize the window beam
                window_beam = window_beam / window_beam.max()
                data = hp.sphtfunc.smoothing(img,
                                             beam_window=window_beam,
                                             pol=False,
                                             verbose=False,
                                             lmax=lmax)
            else:
                raise ValueError(f"Invalid kernel: {kernel!r}")

            if nest:
                # reorder back to nest after the smoothing
                data = hp.pixelfunc.reorder(data, r2n=True)
            smoothed_data[idx] = data

        return self._init_copy(data=smoothed_data)