Source code for ndcube.utils.misc
import copy
import numbers
from collections.abc import Sequence
from types import EllipsisType
try:
from numpy.lib.array_utils import normalize_axis_index
except ImportError: # numpy < 2
from numpy.core.multiarray import normalize_axis_index
import astropy.units as u
from astropy.utils import minversion
__all__ = ["convert_quantities_to_units", "unique_sorted"]
[docs]
def unique_sorted(iterable):
"""
Return unique values in the order they are first encountered in the iterable.
"""
lookup = set() # a temporary lookup set
return [ele for ele in iterable if ele not in lookup and lookup.add(ele) is None]
[docs]
def convert_quantities_to_units(coords, units):
"""Converts a sequence of Quantities to units used in the WCS.
Non-Quantity types in the sequence are allowed and ignored.
Parameters
----------
coords: iterable of `astropy.units.Quantity` or `None`
The coordinates to be converted.
units: iterable of `astropy.units.Unit` or `str`
The units to which the coordinates should be converted.
Returns
-------
converted_coords: iterable of `astropy.units.Quantity` or `None`
The coordinates converted to the units.
Non-quantity types remain.
"""
return [coord.to(unit) if isinstance(coord, u.Quantity) else coord
for coord, unit in zip(coords, units)]
def _deepcopy_without_parent(obj, memo):
"""
Deep copy an object without copying the parent cube it links to through ``_ndcube``.
Assign this as ``__deepcopy__`` on the class.
"""
new = copy.copy(obj)
memo[id(obj)] = new
for key, value in vars(obj).items():
if key != "_ndcube":
setattr(new, key, copy.deepcopy(value, memo))
# A plotter subclass that skips BasePlotter.__init__ has no _ndcube.
if hasattr(obj, "_ndcube"):
# Use the cube's copy if deepcopy reached the cube first, otherwise keep the original cube.
new._ndcube = memo.get(id(obj._ndcube), obj._ndcube)
return new
if minversion("astropy", "8.0.2"):
from astropy.utils.shapes import simplify_basic_index as _simplify_basic_index
else:
def _simplify_basic_index(
basic_index: int | slice | Sequence[int | slice | EllipsisType | None],
*,
shape: Sequence[int | None],
) -> tuple[int | slice, ...]:
"""
Given a Numpy basic index, return a tuple of integers and slice objects
with no default values (`None`) if possible.
If one of the dimensions has a slice and the step is negative and the stop
value of the slice was originally `None`, the new stop value of the slice
may still be set to `None`.
For more information on valid basic indices, see
https://numpy.org/doc/stable/user/basics.indexing.html#basic-indexing
Parameters
----------
basic_index
A valid Numpy basic index
shape
The shape of the array being indexed. Entries can be `None` if the
size along a dimension is not known, in which case the index for that
dimension is returned unchanged, except that a negative integer for
slice start raises an error since it cannot be interpreted.
"""
ndim = len(shape)
if not isinstance(basic_index, (tuple, list)): # We just have a single int
basic_index = (basic_index,)
new_index = list(basic_index)
# Identity rather than equality checks are used here since the latter
# would fail if any of the elements were arrays.
ellipsis = [i for i, idx in enumerate(new_index) if idx is Ellipsis]
if len(ellipsis) > 1:
raise IndexError("an index can only have a single ellipsis ('...')")
if ellipsis:
# Replace the Ellipsis with the correct number of slice(None)s
e_ind = ellipsis[0]
new_index[e_ind : e_ind + 1] = [slice(None)] * (ndim - len(new_index) + 1)
if len(new_index) > ndim:
raise ValueError(
f"The dimensionality of the basic index {basic_index} can not be greater "
f"than the dimensionality ({ndim}) of the data."
)
new_index += [slice(None)] * (ndim - len(new_index))
for i, (slc, size) in enumerate(zip(new_index, shape)):
if isinstance(slc, slice):
if size is None:
if slc.start is not None and slc.start < 0:
raise ValueError(
f"Cannot use a negative slice start for dimension {i} "
"since its size is not known"
)
else:
indices = list(slc.indices(size))
# The following case is the only one where slice(*indices) does
# not give the 'correct' answer because it will set stop to -1
# which means the last element in the array.
if indices[1] == -1:
indices[1] = None
new_index[i] = slice(*indices)
elif isinstance(slc, numbers.Integral):
if size is None:
if slc < 0:
raise ValueError(
f"Cannot use a negative index for dimension {i} since "
"its size is not known"
)
new_index[i] = int(slc)
else:
new_index[i] = normalize_axis_index(int(slc), size)
else:
raise ValueError(f"Unexpected index element in basic index: {slc}")
return tuple(new_index)