# Buggy code
def rotate(
    self,
    angle: u.deg = None,
    rmatrix=None,
    order=4,
    scale=1.0,
    recenter=False,
    missing=0.0,
    use_scipy=False,
):
    """
    Returns a new rotated and rescaled map.

    Specify either a rotation angle or a rotation matrix, but not both. If
    neither an angle or a rotation matrix are specified, the map will be
    rotated by the rotation angle in the metadata.

    The map will be rotated around the reference coordinate defined in the
    meta data.

    This method also updates the ``rotation_matrix`` attribute and any
    appropriate header data so that they correctly describe the new map.

    Parameters
    ----------
    angle : `~astropy.units.Quantity`
        The angle (degrees) to rotate counterclockwise.
    rmatrix : 2x2
        Linear transformation rotation matrix.
    order : int 0-5
        Interpolation order to be used. When using scikit-image this
        parameter is passed into :func:`skimage.transform.warp` (e.g., 4
        corresponds to bi-quartic interpolation).
        When using scipy it is passed into
        :func:`scipy.ndimage.interpolation.affine_transform` where it
        controls the order of the spline. Faster performance may be
        obtained at the cost of accuracy by using lower values.
        Default: 4
    scale : float
        A scale factor for the image, default is no scaling
    recenter : bool
        If True, position the axis of rotation at the center of the new map
        Default: False
    missing : float
        The numerical value to fill any missing points after rotation.
        Default: 0.0
    use_scipy : bool
        If True, forces the rotation to use
        :func:`scipy.ndimage.interpolation.affine_transform`, otherwise it
        uses the :func:`skimage.transform.warp`.
        Default: False, unless scikit-image can't be imported

    Returns
    -------
    out : `~sunpy.map.GenericMap` or subclass
        A new Map instance containing the rotated and rescaled data of the
        original map.

    See Also
    --------
    sunpy.image.transform.affine_transform : The routine this method calls
    for the rotation.

    Notes
    -----
    This function will remove old CROTA keywords from the header.
    This function will also convert a CDi_j matrix to a PCi_j matrix.

    See :func:`sunpy.image.transform.affine_transform` for details on the
    transformations, situations when the underlying data is modified prior
    to rotation, and differences from IDL's rot().
    """
    # Put the import here to reduce sunpy.map import time
    from sunpy.image.transform import affine_transform

    if angle is not None and rmatrix is not None:
        raise ValueError("You cannot specify both an angle and a rotation matrix.")
    elif angle is None and rmatrix is None:
        rmatrix = self.rotation_matrix

    if order not in range(6):
        raise ValueError("Order must be between 0 and 5.")

    # The FITS-WCS transform is by definition defined around the
    # reference coordinate in the header.
    lon, lat = self._get_lon_lat(self.reference_coordinate.frame)
    rotation_center = u.Quantity([lon, lat])

    # Copy meta data
    new_meta = self.meta.copy()
    if angle is not None:
        # Calculate the parameters for the affine_transform
        c = np.cos(np.deg2rad(angle))
        s = np.sin(np.deg2rad(angle))
        rmatrix = np.array([[c, -s], [s, c]])

    # Calculate the shape in pixels to contain all of the image data
    extent = np.max(
        np.abs(np.vstack((self.data.shape @ rmatrix, self.data.shape @ rmatrix.T))),
        axis=0,
    )

    # Calculate the needed padding or unpadding
    diff = np.asarray(np.ceil((extent - self.data.shape) / 2), dtype=int).ravel()
    # Pad the image array
    pad_x = int(np.max((diff[1], 0)))
    pad_y = int(np.max((diff[0], 0)))

    new_data = np.pad(
        self.data,
        ((pad_y, pad_y), (pad_x, pad_x)),
        mode="constant",
        constant_values=(missing, missing),
    )
    new_meta["crpix1"] += pad_x
    new_meta["crpix2"] += pad_y

    # All of the following pixel calculations use a pixel origin of 0

    pixel_array_center = (np.flipud(new_data.shape) - 1) / 2.0

    # Create a temporary map so we can use it for the data to pixel calculation.
    temp_map = self._new_instance(new_data, new_meta, self.plot_settings)

    # Convert the axis of rotation from data coordinates to pixel coordinates
    pixel_rotation_center = u.Quantity(
        temp_map.world_to_pixel(self.reference_coordinate, origin=0)
    ).value
    del temp_map

    if recenter:
        pixel_center = pixel_rotation_center
    else:
        pixel_center = pixel_array_center

    # Apply the rotation to the image data
    new_data = affine_transform(
        new_data.T,
        np.asarray(rmatrix),
        order=order,
        scale=scale,
        image_center=np.flipud(pixel_center),
        recenter=recenter,
        missing=missing,
        use_scipy=use_scipy,
    ).T

    if recenter:
        new_reference_pixel = pixel_array_center
    else:
        # Calculate new pixel coordinates for the rotation center
        new_reference_pixel = pixel_center + np.dot(
            rmatrix, pixel_rotation_center - pixel_center
        )
        new_reference_pixel = np.array(new_reference_pixel).ravel()

    # Define the new reference_pixel
    new_meta["crval1"] = rotation_center[0].value
    new_meta["crval2"] = rotation_center[1].value
    new_meta["crpix1"] = new_reference_pixel[0] + 1  # FITS pixel origin is 1
    new_meta["crpix2"] = new_reference_pixel[1] + 1  # FITS pixel origin is 1

    # Unpad the array if necessary
    unpad_x = -np.min((diff[1], 0))
    if unpad_x > 0:
        new_data = new_data[:, unpad_x:-unpad_x]
        new_meta["crpix1"] -= unpad_x
    unpad_y = -np.min((diff[0], 0))
    if unpad_y > 0:
        new_data = new_data[unpad_y:-unpad_y, :]
        new_meta["crpix2"] -= unpad_y

    # Calculate the new rotation matrix to store in the header by
    # "subtracting" the rotation matrix used in the rotate from the old one
    # That being calculate the dot product of the old header data with the
    # inverse of the rotation matrix.
    pc_C = np.dot(self.rotation_matrix, np.linalg.inv(rmatrix))
    new_meta["PC1_1"] = pc_C[0, 0]
    new_meta["PC1_2"] = pc_C[0, 1]
    new_meta["PC2_1"] = pc_C[1, 0]
    new_meta["PC2_2"] = pc_C[1, 1]

    # Update pixel size if image has been scaled.
    if scale != 1.0:
        new_meta["cdelt1"] = (self.scale[0] / scale).value
        new_meta["cdelt2"] = (self.scale[1] / scale).value

    # Remove old CROTA kwargs because we have saved a new PCi_j matrix.
    new_meta.pop("CROTA1", None)
    new_meta.pop("CROTA2", None)
    # Remove CDi_j header
    new_meta.pop("CD1_1", None)
    new_meta.pop("CD1_2", None)
    new_meta.pop("CD2_1", None)
    new_meta.pop("CD2_2", None)

    # Create new map with the modification
    new_map = self._new_instance(new_data, new_meta, self.plot_settings)

    return new_map


# Traceback (ValueError)
Traceback (most recent call last):
File "/Users/dstansby/github/sunpy/test.py", line 27, in <module>
print(m.bottom_left_coord)
File "/Users/dstansby/github/sunpy/sunpy/map/mapbase.py", line 719, in bottom_left_coord
return self.pixel_to_world(0*u.pix, 0*u.pix)
File "/Users/dstansby/miniconda3/envs/dev/lib/python3.9/site-packages/astropy/units/decorators.py", line 251, in wrapper
return_ = wrapped_function(*func_args, **func_kwargs)
File "/Users/dstansby/github/sunpy/sunpy/map/mapbase.py", line 1146, in pixel_to_world
return SkyCoord(x, y, frame=self.coordinate_frame)
File "/Users/dstansby/miniconda3/envs/dev/lib/python3.9/site-packages/astropy/coordinates/sky_coordinate.py", line 314, in __init__
skycoord_kwargs, components, info = _parse_coordinate_data(
File "/Users/dstansby/miniconda3/envs/dev/lib/python3.9/site-packages/astropy/coordinates/sky_coordinate_parsers.py", line 301, in _parse_coordinate_data
_components[frame_attr_name] = attr_class(arg, unit=unit)
File "/Users/dstansby/miniconda3/envs/dev/lib/python3.9/site-packages/astropy/coordinates/angles.py", line 536, in __new__
self._validate_angles()
File "/Users/dstansby/miniconda3/envs/dev/lib/python3.9/site-packages/astropy/coordinates/angles.py", line 557, in _validate_angles
raise ValueError('Latitude angle(s) must be within -90 deg <= angle <= 90 deg, '
ValueError: Latitude angle(s) must be within -90 deg <= angle <= 90 deg, got 359.9997222222222 deg

# Fixed code
def rotate(
    self,
    angle: u.deg = None,
    rmatrix=None,
    order=4,
    scale=1.0,
    recenter=False,
    missing=0.0,
    use_scipy=False,
):
    """
    Returns a new rotated and rescaled map.

    Specify either a rotation angle or a rotation matrix, but not both. If
    neither an angle or a rotation matrix are specified, the map will be
    rotated by the rotation angle in the metadata.

    The map will be rotated around the reference coordinate defined in the
    meta data.

    This method also updates the ``rotation_matrix`` attribute and any
    appropriate header data so that they correctly describe the new map.

    Parameters
    ----------
    angle : `~astropy.units.Quantity`
        The angle (degrees) to rotate counterclockwise.
    rmatrix : 2x2
        Linear transformation rotation matrix.
    order : int 0-5
        Interpolation order to be used. When using scikit-image this
        parameter is passed into :func:`skimage.transform.warp` (e.g., 4
        corresponds to bi-quartic interpolation).
        When using scipy it is passed into
        :func:`scipy.ndimage.interpolation.affine_transform` where it
        controls the order of the spline. Faster performance may be
        obtained at the cost of accuracy by using lower values.
        Default: 4
    scale : float
        A scale factor for the image, default is no scaling
    recenter : bool
        If True, position the axis of rotation at the center of the new map
        Default: False
    missing : float
        The numerical value to fill any missing points after rotation.
        Default: 0.0
    use_scipy : bool
        If True, forces the rotation to use
        :func:`scipy.ndimage.interpolation.affine_transform`, otherwise it
        uses the :func:`skimage.transform.warp`.
        Default: False, unless scikit-image can't be imported

    Returns
    -------
    out : `~sunpy.map.GenericMap` or subclass
        A new Map instance containing the rotated and rescaled data of the
        original map.

    See Also
    --------
    sunpy.image.transform.affine_transform : The routine this method calls
    for the rotation.

    Notes
    -----
    This function will remove old CROTA keywords from the header.
    This function will also convert a CDi_j matrix to a PCi_j matrix.

    See :func:`sunpy.image.transform.affine_transform` for details on the
    transformations, situations when the underlying data is modified prior
    to rotation, and differences from IDL's rot().
    """
    # Put the import here to reduce sunpy.map import time
    from sunpy.image.transform import affine_transform

    if angle is not None and rmatrix is not None:
        raise ValueError("You cannot specify both an angle and a rotation matrix.")
    elif angle is None and rmatrix is None:
        rmatrix = self.rotation_matrix

    if order not in range(6):
        raise ValueError("Order must be between 0 and 5.")

    # The FITS-WCS transform is by definition defined around the
    # reference coordinate in the header.
    lon, lat = self._get_lon_lat(self.reference_coordinate.frame)
    rotation_center = u.Quantity([lon, lat])

    # Copy meta data
    new_meta = self.meta.copy()
    if angle is not None:
        # Calculate the parameters for the affine_transform
        c = np.cos(np.deg2rad(angle))
        s = np.sin(np.deg2rad(angle))
        rmatrix = np.array([[c, -s], [s, c]])

    # Calculate the shape in pixels to contain all of the image data
    extent = np.max(
        np.abs(np.vstack((self.data.shape @ rmatrix, self.data.shape @ rmatrix.T))),
        axis=0,
    )

    # Calculate the needed padding or unpadding
    diff = np.asarray(np.ceil((extent - self.data.shape) / 2), dtype=int).ravel()
    # Pad the image array
    pad_x = int(np.max((diff[1], 0)))
    pad_y = int(np.max((diff[0], 0)))

    new_data = np.pad(
        self.data,
        ((pad_y, pad_y), (pad_x, pad_x)),
        mode="constant",
        constant_values=(missing, missing),
    )
    new_meta["crpix1"] += pad_x
    new_meta["crpix2"] += pad_y

    # All of the following pixel calculations use a pixel origin of 0

    pixel_array_center = (np.flipud(new_data.shape) - 1) / 2.0

    # Create a temporary map so we can use it for the data to pixel calculation.
    temp_map = self._new_instance(new_data, new_meta, self.plot_settings)

    # Convert the axis of rotation from data coordinates to pixel coordinates
    pixel_rotation_center = u.Quantity(
        temp_map.world_to_pixel(self.reference_coordinate)
    ).value
    del temp_map

    if recenter:
        pixel_center = pixel_rotation_center
    else:
        pixel_center = pixel_array_center

    # Apply the rotation to the image data
    new_data = affine_transform(
        new_data.T,
        np.asarray(rmatrix),
        order=order,
        scale=scale,
        image_center=np.flipud(pixel_center),
        recenter=recenter,
        missing=missing,
        use_scipy=use_scipy,
    ).T

    if recenter:
        new_reference_pixel = pixel_array_center
    else:
        # Calculate new pixel coordinates for the rotation center
        new_reference_pixel = pixel_center + np.dot(
            rmatrix, pixel_rotation_center - pixel_center
        )
        new_reference_pixel = np.array(new_reference_pixel).ravel()

    # Define the new reference_pixel
    new_meta["crval1"] = rotation_center[0].value
    new_meta["crval2"] = rotation_center[1].value
    new_meta["crpix1"] = new_reference_pixel[0] + 1  # FITS pixel origin is 1
    new_meta["crpix2"] = new_reference_pixel[1] + 1  # FITS pixel origin is 1

    # Unpad the array if necessary
    unpad_x = -np.min((diff[1], 0))
    if unpad_x > 0:
        new_data = new_data[:, unpad_x:-unpad_x]
        new_meta["crpix1"] -= unpad_x
    unpad_y = -np.min((diff[0], 0))
    if unpad_y > 0:
        new_data = new_data[unpad_y:-unpad_y, :]
        new_meta["crpix2"] -= unpad_y

    # Calculate the new rotation matrix to store in the header by
    # "subtracting" the rotation matrix used in the rotate from the old one
    # That being calculate the dot product of the old header data with the
    # inverse of the rotation matrix.
    pc_C = np.dot(self.rotation_matrix, np.linalg.inv(rmatrix))
    new_meta["PC1_1"] = pc_C[0, 0]
    new_meta["PC1_2"] = pc_C[0, 1]
    new_meta["PC2_1"] = pc_C[1, 0]
    new_meta["PC2_2"] = pc_C[1, 1]

    # Update pixel size if image has been scaled.
    if scale != 1.0:
        new_meta["cdelt1"] = (self.scale[0] / scale).value
        new_meta["cdelt2"] = (self.scale[1] / scale).value

    # Remove old CROTA kwargs because we have saved a new PCi_j matrix.
    new_meta.pop("CROTA1", None)
    new_meta.pop("CROTA2", None)
    # Remove CDi_j header
    new_meta.pop("CD1_1", None)
    new_meta.pop("CD1_2", None)
    new_meta.pop("CD2_1", None)
    new_meta.pop("CD2_2", None)

    # Create new map with the modification
    new_map = self._new_instance(new_data, new_meta, self.plot_settings)

    return new_map