# Licensed under a 3-clause BSD style license - see LICENSE.rst
from abc import ABC
from astropy import units
from copy import deepcopy
import importlib
import numpy as np
from sofia_redux.scan.coordinate_systems import \
coordinate_systems_numba_functions as csnf
from sofia_redux.scan.utilities.class_provider import (
to_module_name, to_class_name)
__all__ = ['Coordinate']
[docs]
class Coordinate(ABC):
default_dimensions = 1
def __init__(self, coordinates=None, unit=None, copy=True):
"""
Initialize a coordinate.
The Coordinate class is a generic container to store and operate on
a set of coordinates.
Parameters
----------
coordinates : list or tuple or array-like or units.Quantity, optional
The coordinates used to populate the object during initialization.
unit : str or units.Unit or units.Quantity, optional
The units of the internal coordinates.
copy : bool, optional
If `True`, populate these coordinates with a copy of `coordinates`
rather than the actual coordinates.
"""
self.coordinates = None
if unit is None:
self.unit = None
else:
self.unit = units.Unit(unit)
if coordinates is None:
return
coordinates, original = self.check_coordinate_units(coordinates)
if not original:
copy = False
if isinstance(coordinates, Coordinate):
self.copy_coordinates(coordinates)
else:
self.set(coordinates, copy=copy)
[docs]
def empty_copy(self):
"""
Return an unpopulated instance of the coordinates.
Returns
-------
Coordinate
"""
new = self.__class__()
for attribute, value in self.__dict__.items():
if attribute not in self.empty_copy_skip_attributes:
setattr(new, attribute, value)
else:
setattr(new, attribute, None)
return new
[docs]
def copy(self):
"""
Return a copy of the Coordinate.
Returns
-------
Coordinate
"""
return deepcopy(self)
@property
def empty_copy_skip_attributes(self):
"""
Return attributes that are set to None on an empty copy.
Returns
-------
attributes : set (str)
"""
return {'coordinates'}
@property
def ndim(self):
"""
Return the number of dimensions for the coordinate.
Returns
-------
int
"""
if self.coordinates is None:
return 0
elif not isinstance(self.coordinates, np.ndarray):
return 1
elif self.coordinates.ndim == 1:
return 1
else:
return self.coordinates.shape[0]
@property
def shape(self):
"""
Return the shape of the coordinates.
Returns
-------
tuple (int)
"""
if self.coordinates is None or self.singular:
return ()
return self.coordinates.shape[1:]
@shape.setter
def shape(self, new_shape):
"""
Set a new shape for the coordinates
Parameters
----------
new_shape : int or tuple (int)
Returns
-------
None
"""
self.set_shape(new_shape)
@property
def size(self):
"""
Return the number of coordinates.
Returns
-------
int
"""
if self.coordinates is None:
return 0
elif self.singular:
return 1
else:
return int(np.prod(self.coordinates.shape[1:]))
@property
def singular(self):
"""
Return if the coordinates are scalar in nature (not an array).
Returns
-------
bool
"""
if self.coordinates is None:
return True
if self.coordinates.ndim == 1:
return self.coordinates.size == 1
if self.coordinates.ndim > 1:
return np.prod(self.coordinates.shape[1:]) <= 1
return True # pragma: no cover
@property
def length(self):
"""
Return the Euclidean distance of the coordinates from (0, 0).
Returns
-------
distance : float or numpy.ndarray or astropy.units.Quantity
"""
if self.ndim == 0:
return np.nan if self.unit is None else np.nan * self.unit
elif self.ndim == 1:
return np.abs(self.coordinates)
else:
return np.linalg.norm(self.coordinates, axis=0)
def __eq__(self, other):
"""
Test if these coordinates are equal to another.
Parameters
----------
other : Coordinate
Returns
-------
bool
"""
if other is self:
return True
if self.__class__ != other.__class__:
return False
if self.coordinates is None:
return other.coordinates is None
elif other.coordinates is None:
return self.coordinates is None
if self.shape != other.shape:
return False
try:
return np.allclose(self.coordinates, other.coordinates,
equal_nan=True)
except units.UnitConversionError:
return False
def __len__(self):
"""
Return the number of stored coordinates.
Returns
-------
int
"""
return self.size
def __getitem__(self, indices):
"""
Return a section of the coordinates
Parameters
----------
indices : int or numpy.ndarray or slice
Returns
-------
Coordinate
"""
return self.get_indices(indices)
def __setitem__(self, indices, value):
"""
Set the coordinates for given indices.
Parameters
----------
indices : slice or int or numpy.ndarray (int or bool)
value : Coordinate
Returns
-------
None
"""
self.paste(value, indices)
[docs]
def get_indices(self, indices):
"""
Return selected data for given indices.
Parameters
----------
indices : slice or list or int or numpy.ndarray (int)
The indices to extract.
Returns
-------
Coordinate
"""
new = self.empty_copy()
if self.coordinates is None:
return new
if self.singular:
raise KeyError("Cannot retrieve indices for singular coordinates.")
if isinstance(indices, np.ndarray) and indices.shape == ():
indices = int(indices)
all_indices = slice(None), # dimensions
if not isinstance(indices, tuple):
all_indices += indices,
else:
all_indices += indices
coordinates = self.coordinates[all_indices]
if new.ndim == 0 and self.ndim > 0 and coordinates.ndim <= 1:
# For the case of base coordinates and multi-dimensional
coordinates = coordinates[..., None]
new.coordinates = coordinates
return new
[docs]
def set_shape(self, shape, empty=False):
"""
Set the shape of the coordinates.
If the current coordinates are blank, dimensionality will be inferred
from the input shape. If a single integer value or 1-tuple is passed
in, the number of dimensions will be set to 1. Otherwise, the
dimensionality will be permanently set to the first element of `shape`.
Parameters
----------
shape : int or tuple (int)
empty : bool, optional
If `True`, create an empty array. Otherwise, create a zeroed
array.
Returns
-------
None
"""
if isinstance(shape, int):
shape = shape,
shape_dim = len(shape)
if self.ndim == 0:
# Dimensionality is inferred from this shape if not set.
if shape_dim == 1:
new_shape = (1,) + shape
else:
new_shape = shape
else:
new_shape = (self.ndim,) + shape
if empty:
self.coordinates = np.empty(new_shape, dtype=float)
else:
self.coordinates = np.zeros(new_shape, dtype=float)
if self.unit is not None:
self.coordinates = self.coordinates * self.unit
[docs]
def set_singular(self, empty=False):
"""
Create a single coordinate.
Parameters
----------
empty : bool, optional
If `True`, create an empty coordinate array. Otherwise, create a
zeroed array.
Returns
-------
None
"""
if self.ndim <= 1:
shape = (1,)
else:
shape = (self.ndim, 1)
if empty:
coordinates = np.empty(shape, dtype=float)
else:
coordinates = np.zeros(shape, dtype=float)
if self.unit is not None:
coordinates = coordinates * self.unit
self.coordinates = coordinates
[docs]
def set_shape_from_coordinates(self, coordinates, single_dimension=False,
empty=False, change_unit=True):
"""
Set the new shape and units from the given coordinates.
The dimensionality of this coordinate will be determined from the input
`coordinates` if not previously defined. Otherwise, any previously
determined dimensionality will remain fixed and cannot be altered.
Parameters
----------
coordinates : numpy.ndarray (float) or astropy.units.Quantity
single_dimension : bool, optional
If `True`, the coordinates consist of data from only one of the
dimensions. Note that this is only applicable if the current
dimensionality has been determined. If `False`, the shape of the
coordinates will be determined from `coordinates[0]`. In the case
where coordinates.ndim <= 1, the new shape will be set to a
singular value.
empty : bool, optional
If `True` and a new array should be created, that array will be
empty. Otherwise, a zero array will be created.
change_unit : bool, optional
If `True`, allow the unit to be updated.
Returns
-------
None
"""
if isinstance(coordinates, units.Quantity):
new_unit = coordinates.unit
else:
new_unit = None
coordinates = np.asarray(coordinates)
if single_dimension or self.ndim == 0:
if coordinates.shape == ():
singular = True
shape = ()
else:
singular = False
shape = coordinates.shape
else:
if coordinates.ndim <= 1:
singular = True
shape = ()
else:
singular = False
shape = coordinates[0].shape
if change_unit and self.unit is None and new_unit is not None:
self.unit = new_unit
if singular:
if not self.singular or self.coordinates is None:
self.set_singular(empty=empty)
else:
if self.shape != shape or self.coordinates is None:
self.set_shape(shape, empty=empty)
[docs]
def check_coordinate_units(self, coordinates):
"""
Check the coordinate units and update parameters if necessary.
This method takes in a set of coordinates and returns a more
standardized version consistent with these coordinates. Coordinate
units will be converted to these coordinates, or if no units exist for
these coordinates, they will be inferred from the input `coordinates`.
If the given coordinates are a Coordinate subclass, they will be
converted to the current units, and any other array like input will
be converted to an numpy array or units.Quantity depending on whether
units for these coordinates have been defined (or inferred from
`coordinates`).
Parameters
----------
coordinates : list or tuple or numpy.ndarray or units.Quantity or None
The coordinates to check.
Returns
-------
coordinates, original : numpy.ndarray or units.Quantity or Coordinate
Returns the coordinates in the same unit as the coordinates, and
whether the coordinates are the original coordinates.
"""
if coordinates is None:
return None, True
original = True
coordinate_unit = None
if isinstance(coordinates, (Coordinate, units.Quantity)):
coordinate_unit = coordinates.unit
else: # In the case that coordinates have been supplied weirdly
if isinstance(coordinates, np.ndarray):
n = 0 if coordinates.shape == () else coordinates.shape[0]
elif hasattr(coordinates, '__len__'):
n = len(coordinates)
else:
n = 0
if n > 0:
if isinstance(coordinates[0], units.Quantity):
coordinate_unit = coordinates[0].unit
if coordinates[0].shape != ():
shape = (len(coordinates),) + coordinates[0].shape
else:
shape = len(coordinates),
new = np.empty(shape, dtype=float) * coordinate_unit
for dimension in range(shape[0]):
new[dimension] = coordinates[dimension]
coordinates = new
original = False
elif not isinstance(coordinates, np.ndarray):
coordinates = np.asarray(coordinates)
original = False
# Convert string values to floats
if isinstance(coordinates, np.ndarray):
if coordinates.dtype.kind in ['S', 'U']:
coordinates = coordinates.astype(float)
if coordinate_unit == units.dimensionless_unscaled:
coordinate_unit = None
if isinstance(coordinates, Coordinate):
coordinates = coordinates.copy()
original = False
coordinates.unit = None
coordinates.coordinates = coordinates.coordinates.value
elif isinstance(coordinates, units.Quantity):
coordinates = coordinates.value # Still references data
if self.unit is None and coordinate_unit is not None:
self.unit = coordinate_unit
if self.unit is not None:
if coordinate_unit is None:
if isinstance(coordinates, Coordinate):
coordinates = coordinates.copy()
coordinates.coordinates = (
coordinates.coordinates * self.unit)
coordinates.unit = self.unit
else:
coordinates = coordinates * self.unit
original = False
elif coordinate_unit != self.unit:
if isinstance(coordinates, Coordinate):
coordinates = coordinates.copy()
coordinates.change_unit(self.unit)
elif isinstance(coordinates, units.Quantity):
coordinates = coordinates.to(self.unit)
original = False
return coordinates, original
[docs]
def change_unit(self, unit):
"""
Change the coordinate units.
Parameters
----------
unit : str or units.Unit
Returns
-------
None
"""
unit = units.Unit(unit)
if unit == self.unit:
return
self.unit = unit
if self.coordinates is None:
return
if isinstance(self.coordinates, units.Quantity):
self.coordinates = self.coordinates.to(unit)
elif isinstance(self.coordinates, (np.ndarray, int, float)):
self.coordinates = self.coordinates * unit
[docs]
def broadcast_to(self, thing):
"""
Broadcast to a new shape if possible.
If the coordinates are singular (a single coordinate value in each
dimension), broadcasts that single value to a new shape in the internal
coordinates.
Parameters
----------
thing : numpy.ndarray or tuple (int)
An array from which to determine the broadcast shape, or a tuple
containing the broadcast shape.
Returns
-------
None
"""
if not self.singular:
return
if isinstance(thing, np.ndarray):
shape = thing.shape
elif isinstance(thing, tuple):
shape = thing
else:
return
if shape == ():
return
singular_coordinates = self.coordinates
n_dimensions = self.ndim
real_shape = (self.ndim,) + shape
self.coordinates = np.empty_like(self.coordinates, shape=real_shape)
for dimension in range(n_dimensions):
self.coordinates[dimension] = singular_coordinates[dimension]
[docs]
def add(self, coordinates):
"""
Add other coordinates to these.
Parameters
----------
coordinates : Coordinate
Returns
-------
None
"""
self.broadcast_to(coordinates.shape)
self.coordinates += coordinates.coordinates
[docs]
def subtract(self, coordinates):
"""
Subtract other coordinates from these.
Parameters
----------
coordinates : Coordinate
Returns
-------
None
"""
self.broadcast_to(coordinates.shape)
self.coordinates -= coordinates.coordinates
[docs]
def scale(self, factor):
"""
Scale the coordinates by a factor.
Parameters
----------
factor : int or float or Coordinate or iterable or np.ndarray
If a Coordinate is supplied, it must be singular.
Returns
-------
None
"""
if isinstance(factor, Coordinate):
self.broadcast_to(factor.shape)
self.coordinates *= factor.coordinates
else:
if not hasattr(factor, '__len__'):
factor = self.convert_factor(factor)
self.coordinates *= factor
return
factor, _ = self.check_coordinate_units(factor)
factor = self.convert_factor(factor)
if factor.size < 1:
return
if factor.size == 1:
self.coordinates *= np.atleast_1d(factor.ravel())[0]
elif factor.ndim <= 1 and factor.size == self.ndim:
for i in range(factor.size):
self.coordinates[i] *= factor[i]
else:
self.broadcast_to(factor.shape[1:])
self.coordinates *= factor
[docs]
def convert_factor(self, factor):
"""
Returns a float factor in the correct units for multiplication.
If the current coordinates are not quantities, they will be converted
to such if the factor is a quantity. Otherwise, the factor scaled to
units of the coordinates will be returned.
Parameters
----------
factor : float or numpy.ndarray or units.Quantity
Returns
-------
factor : float or numpy.ndarray (float)
"""
if not isinstance(factor, units.Quantity):
return factor
if factor.unit == units.dimensionless_unscaled:
return factor.value
if self.unit is None or self.unit == units.dimensionless_unscaled:
self.unit = factor.unit
if self.coordinates is not None: # Change coordinates
self.coordinates = self.coordinates * factor.unit
factor_value = factor.value
else:
factor_value = factor.to(self.unit).value
return factor_value
[docs]
def fill(self, value, indices=None):
"""
Fill the coordinates with the given value.
Parameters
----------
value : int or float or units.Quantity
The value with which to fill the coordinates.
indices : slice or numpy.ndarray (int or bool), optional
The indices to set to NaN.
Returns
-------
None
"""
if self.size == 0:
return
if (isinstance(value, units.Quantity)
and value.unit == units.dimensionless_unscaled):
value = value.value
if not isinstance(value, units.Quantity):
if self.unit is not None:
value = value * self.unit
elif self.unit is None:
self.change_unit(value.unit)
if (not isinstance(self.coordinates, np.ndarray)
or self.coordinates.shape == ()):
if isinstance(value, units.Quantity):
value = value.to(self.unit)
self.coordinates = value
return
if indices is None or self.singular:
self.coordinates.fill(value)
elif self.ndim == 1 and self.coordinates.ndim == 1:
self.coordinates[indices] = value
else:
self.coordinates[:, indices] = value
[docs]
def nan(self, indices=None):
"""
Set all coordinates to NaN.
Parameters
----------
indices : slice or numpy.ndarray (int or bool), optional
The indices to set to NaN.
Returns
-------
None
"""
if self.coordinates is None:
return
if (not isinstance(self.coordinates, np.ndarray)
or self.coordinates.shape == ()):
if self.unit is not None:
self.coordinates = np.nan * self.unit
else:
self.coordinates = np.nan
return
if indices is None:
self.coordinates.fill(np.nan)
elif self.coordinates.ndim == 1:
self.coordinates[indices] = np.nan
else:
self.coordinates[:, indices] = np.nan
[docs]
def zero(self, indices=None):
"""
Set all coordinates to zero.
Parameters
----------
indices : slice or numpy.ndarray (int or bool), optional
The indices to set to zero.
Returns
-------
None
"""
if self.coordinates is None:
return
if (not isinstance(self.coordinates, np.ndarray)
or self.coordinates.shape == ()):
if self.unit is not None:
self.coordinates = 0.0 * self.unit
else:
self.coordinates = 0.0
return
if indices is None:
self.coordinates.fill(0.0)
elif self.coordinates.ndim == 1:
self.coordinates[indices] = 0.0
else:
self.coordinates[:, indices] = 0.0
[docs]
@staticmethod
def apply_coordinate_mask_function(coordinates, func):
"""
Apply a masking function to a given set of coordinates.
Parameters
----------
coordinates : units.Quantity or numpy.ndarray
The coordinates to check. Must consist of an array with 2 or
more dimensions. I.e., of shape (ndim, x1, x2, ...).
func : function
A function that returns a boolean mask given a numpy array. For
these purposes, it should take in a two dimensional array of shape
(ndim, n) where n = product(x1, x2, ...).
Returns
-------
mask : numpy.ndarray (bool)
The boolean mask output by `func` of shape (x1, x2, ...).
"""
if isinstance(coordinates, units.Quantity):
data = coordinates.value
else:
data = coordinates
if data.ndim > 2:
shape = data.shape
shape2d = shape[0], int(np.prod(shape[1:]))
mask = func(data.reshape(shape2d)).reshape(shape[1:])
else:
mask = func(data)
return mask
[docs]
def is_null(self):
"""
Check whether coordinates are zero.
Returns
-------
bool or numpy.ndarray (bool)
"""
if self.coordinates is None:
return False
elif self.singular:
return np.all(self.coordinates == 0)
elif self.coordinates.ndim == 1:
return self.coordinates == 0
else:
return self.apply_coordinate_mask_function(
self.coordinates, csnf.check_null)
[docs]
def is_nan(self):
"""
Check whether coordinates are NaN.
Returns
-------
bool or numpy.ndarray (bool)
"""
if self.coordinates is None:
return False
elif self.singular:
return np.all(np.isnan(self.coordinates))
elif self.coordinates.ndim == 1:
return np.isnan(self.coordinates)
else:
return self.apply_coordinate_mask_function(
self.coordinates, csnf.check_nan)
[docs]
def is_finite(self):
"""
Check whether coordinates are finite.
Returns
-------
bool or numpy.ndarray (bool)
"""
if self.coordinates is None:
return False
elif self.singular:
return np.all(np.isfinite(self.coordinates))
elif self.coordinates.ndim == 1:
return np.isfinite(self.coordinates)
else:
return self.apply_coordinate_mask_function(
self.coordinates, csnf.check_finite)
[docs]
def is_infinite(self):
"""
Check whether coordinates are infinite.
Returns
-------
bool or numpy.ndarray (bool)
"""
if self.coordinates is None:
return False
elif self.singular:
return np.all(np.isinf(self.coordinates))
elif self.coordinates.ndim == 1:
return np.isinf(self.coordinates)
else:
return self.apply_coordinate_mask_function(
self.coordinates, csnf.check_infinite)
[docs]
def convert_from(self, coordinates):
"""
Convert coordinates from another or same system to this.
Parameters
----------
coordinates : Coordinate
Returns
-------
None
"""
self.copy_coordinates(coordinates)
[docs]
def convert_to(self, coordinates):
"""
Convert these coordinates to another coordinate system.
Parameters
----------
coordinates : Coordinate
Returns
-------
None
"""
coordinates.convert_from(self)
[docs]
@staticmethod
def correct_factor_dimensions(factor, array):
"""
Corrects the factor dimensionality prior to an array +-/* etc.
Frame operations are frequently of the form result = factor op array
where factor is of shape (n_frames,) and array is of shape
(n_frames, ...). This procedure updates the factor shape so that
array operations are possible. E.g., if factor is of shape (5,) and
array is of shape (5, 10), then the output factor will be of shape
(5, 1) and allow the two arrays to operate with each other.
Parameters
----------
factor : int or float or numpy.ndarray or astropy.units.Quantity
The factor to check.
array : numpy.ndarray or astropy.units.Quantity or Coordinate
The array to check against
Returns
-------
working_factor : numpy.ndarray or astropy.units.Quantity
"""
if not isinstance(factor, np.ndarray):
return factor
if factor.shape == ():
return factor
if isinstance(array, Coordinate):
array_ndim = len(array.shape)
else:
array_ndim = array.ndim
add_dimensions = array_ndim - factor.ndim
if add_dimensions == 0:
return factor
elif add_dimensions > 0:
for i in range(add_dimensions):
factor = factor[..., None]
return factor
[docs]
@classmethod
def get_class(cls, class_name=None):
"""
Return a coordinate class for a given name.
Parameters
----------
class_name : str, optional
The name of the class not including the "Coordinates" suffix. The
default is *this* class.
Returns
-------
coordinate_class : class``
"""
if class_name is None:
return cls
base_module_name = '.'.join(Coordinate.__module__.split('.')[:-1])
# Determine if it's a class from whether the class name is mixed case
is_class = not (class_name.isupper() or class_name.islower())
user_class_name = class_name
if is_class:
module_name = to_module_name(class_name)
else:
module_name = class_name.lower()
if module_name.endswith('_d'):
module_name = module_name[:-2] + 'd'
possibilities = ['%s', 'coordinate_%s', '%s_coordinates']
for possibility in possibilities:
module_basename = possibility % module_name
try_module = f'{base_module_name}.{module_basename}'
try:
module = importlib.import_module(try_module)
class_name = to_class_name(module_basename)
if class_name[-1] == 'd' and class_name[-2].isdigit():
class_name = class_name[:-1] + class_name[-1].upper()
break
except ModuleNotFoundError:
pass
else:
module = class_name = None
if module is None:
raise ValueError(f"Could not find {user_class_name} class module.")
coordinate_class = getattr(module, class_name)
if coordinate_class is None: # pragma: no cover
# If this gets hit it's because modules/classes weren't named well.
raise ValueError(f"Could not find {class_name} in {module} given "
f"user input {user_class_name}.")
if not issubclass(coordinate_class, Coordinate):
raise ValueError(f"Retrieved class {coordinate_class} is not a "
f"{Coordinate} sub class.")
return coordinate_class
[docs]
@classmethod
def get_instance(cls, class_name=None):
"""
Return a coordinate instance for a given name.
Parameters
----------
class_name : str, optional
The name of the class not including the "Coordinates" suffix. The
default is *this* class.
Returns
-------
object : Coordinate2D or SphericalCoordinates or CelestialCoordinates
"""
return cls.get_class(class_name=class_name)()
[docs]
def insert_blanks(self, insert_indices):
"""
Insert blank (NaN) values at the requested indices.
Follows the logic of :func:`numpy.insert`.
Parameters
----------
insert_indices : numpy.ndarray (int)
Returns
-------
None
"""
if self.coordinates is None or self.singular:
return
if self.coordinates.ndim == 1:
self.coordinates = np.insert(self.coordinates, insert_indices,
np.nan)
else:
self.coordinates = np.insert(
self.coordinates, insert_indices, np.nan, axis=1)
[docs]
def merge(self, other):
"""
Append other coordinates to the end of these.
Parameters
----------
other : Coordinate
Returns
-------
None
"""
if other.coordinates is None:
return
if self.coordinates is None:
self.copy_coordinates(other)
return
if other.ndim != self.ndim:
raise ValueError("Coordinate dimensions do not match.")
coordinates = self.coordinates
if self.ndim == 1 and coordinates.ndim == 1:
coordinates = coordinates[None]
elif self.ndim > 1 and coordinates.ndim == 1:
coordinates = coordinates[:, None]
other_c = other.coordinates
if other.ndim == 1 and other_c.ndim == 1:
other_c = other_c[None]
elif other.ndim > 1 and other_c.ndim == 1:
other_c = other_c[:, None]
self.coordinates = np.concatenate((coordinates, other_c), axis=1)
[docs]
def paste(self, coordinates, indices):
"""
Paste new coordinate values at the given indices.
Parameters
----------
coordinates : Coordinate
indices : numpy.ndarray (int)
Returns
-------
None
"""
if self.singular or self.coordinates is None:
raise ValueError("Cannot paste onto singular "
"or empty coordinates.")
elif coordinates.coordinates is None:
raise ValueError("Cannot paste empty coordinates.")
elif self.ndim != coordinates.ndim:
raise ValueError("Coordinate dimensions do not match.")
if self.coordinates.ndim == 1:
self.coordinates[indices] = coordinates.coordinates
else:
self.coordinates[:, indices] = coordinates.coordinates
[docs]
def shift(self, n, fill_value=np.nan):
"""
Shift the coordinates by a given number of elements.
Parameters
----------
n : int
fill_value : float or int or units.Quantity, optional
Returns
-------
None
"""
if self.singular:
return # Can't roll for singular coordinates
elif n == 0:
return
if (self.unit is not None
and not isinstance(fill_value, units.Quantity)):
fill_value = fill_value * self.unit
if self.coordinates.ndim == 1:
self.coordinates = np.roll(self.coordinates, n)
else:
self.coordinates = np.roll(self.coordinates, n, axis=1)
blank = slice(0, n) if n > 0 else slice(n, None)
if self.coordinates.ndim > 1:
blank = slice(None), blank
self.coordinates[blank] = fill_value
[docs]
def mean(self):
"""
Return the mean coordinates.
Returns
-------
mean_coordinates : Coordinate
"""
new = self.empty_copy()
if self.coordinates is None:
return new
if self.singular:
if isinstance(self.coordinates, np.ndarray):
mean_coordinates = self.coordinates.copy()
else:
mean_coordinates = self.coordinates
elif self.ndim == 1:
mean_coordinates = np.atleast_1d(np.nanmean(self.coordinates))
else:
mean_coordinates = np.nanmean(self.coordinates, axis=1)
if new.ndim == 0:
mean_coordinates = mean_coordinates[:, None]
new.set(mean_coordinates, copy=False)
return new
[docs]
def copy_coordinates(self, coordinates):
"""
Copy the coordinates from another system to this system.
Parameters
----------
coordinates : Coordinate
Returns
-------
None
"""
self.set(coordinates.coordinates)
[docs]
def set(self, coordinates, copy=True):
"""
Set the coordinates.
Parameters
----------
coordinates : numpy.ndarray or list
copy : bool, optional
If `True`, copy the coordinates. Otherwise do a reference.
Returns
-------
None
"""
coordinates, original = self.check_coordinate_units(coordinates)
copy &= original
if self.coordinates is not None:
if self.coordinates.ndim > 1 and coordinates.ndim == 1:
coordinates = coordinates[:, None]
if copy and isinstance(coordinates, np.ndarray):
self.coordinates = coordinates.copy()
else:
self.coordinates = coordinates