NDCube Arithmetic Capabilities
Overload the following arithmetic operations for NDCube
which requires creating the following methods:
+
: __add__
, __radd__
-
: __sub__
, __rsub__
*
: __mul__
, __rmul__
/
: __truediv__
, __rtruediv__
**
: __pow__
(Should not support two NDCube
s or an NDCube
and NDData
.)
==
: __eq__
!=
: __ne__
=+
: __iadd__
=-
: __isub__
=*
: __imul__
=/
: __itruediv__
This will enable NDCube
s to be operated on by a scalar, array, astropy.units.Quantity
, numpy.ma.MaskedArray
, astropy.nddata.NDData
or NDCube
with an equivalent/compatible WCS object. These are defined as the secondary object, while the NDCube
is the primary object. The advantage of using NDData
or an NDCube
as the secondary object is that you can combine all or some of data, uncertainty, mask and unit as part of the operation whereas Quantity
and MaskedArray
instances can only give an inflexible subset of these components.
+
, -
, *
, /
, =+
, =-
, =*
, =/
The different attributes of NDCube
will be treated in the following ways by these operators:
Data, Unit and Mask
When the secondary object is a MaskedArray
, non-scalar Quantity
, NDData
and NDCube
secondary objects:
- If the secondary object is an
NDCube
or and NDData
with a WCS
, the WCS
will be compared to that of the primary NDCube
. If they are not equivalent or compatible, a ValueError
will be raised. WCS
s can be not equal but compatible if the secondary WCS
has fewer dimensions that are a subset of the primary, WCS.
- A primary
MaskedArray
will be formed from the primary NDCube
's data and mask attributes.
- If the second object is a
Quantity
, NDData
or NDCube
, a second Quantity
will be created, converted to the primary NDCube
's unit, and the values extracted as an array. This will then be converted to a MaskedArray
using the secondary object's mask attribute. If there is no mask all pixels will be set to unmasked.
- The primary and secondary
MaskedArray
s will then be combined as the operator dictates.
- The result will be be broken out into
new_data
and new_mask
variables. The new_unit
variable will be equivalent to the unit of the primary NDCube
as everything was converted to that unit at the start.
When the secondary object is a scalar, scalar Quantity
or array:
- If the secondary is a scalar
Quantity
, it is converted to the primary NDCube
unit and its value is extracted as a non-Quantity scalar.
- The primary
NDCube.data
is combined with the secondary object as dictated by the operator.
- The
new_mask
and new_unit
variables are the same as those from the primary NDCube
.
Uncertainty
Combining the uncertainties correctly may not always be the same depending on how the uncertainties were created. A more flexibly API may be required. See below for a discussion. However, for the purpose of overloading the operators, we can employ algorithms based on the standard propagation of errors.
When the secondary object is a scalar, array, Quantity
, MaskedArray
or NDData
or NDCube
without uncertainties or with uncertainties set to 0
will be treated as constants. Therefore the following behaviour holds:
+
, -
, =+
, =-
: uncertainties will not be changed;
*
, /
, =*
and =/
: uncertainties are simply scaled;
//
, =//
: uncertainties treated the same as /
and =/
it is equivalent to subtracting a scalar after the division.
When the secondary objects are an NDData
or NDCube
with non-zero uncertainties, they are combined along the standard propagation of error rules depending on the operator.
Meta
For all operations discussed in this section, the NDCube.meta
dictionaries will be combined into a new variable, new_meta
. For common keys will the same value, only one key will be retained. Their are a few possible ways of handling keys with differing values:
- Raise
ValueError
: The operation can be aborted. In order to proceed the user will have alter the two metadatas so all common keys agree or remove common keys from at least one metadata dictionary.
- Alter key names and raise
Warning
: If common keys have differing values, set the value to a tuple
where the value from the first NDCube
is the 0th entry and the value from the second NDCube
is in the 1st entry.
force_combine_metas
kwarg: Create and set this kwarg to None
, "both"
or "first"
.
None
: ValueError
raised as above.
"both"
: both values are kept as above.
"first"
: Only the value of the first NDCube
is kept.
Which of these strategies should be adopted or whether an amended strategy should be used is still open for debate. At time of writing, the force_combine_metas
option is slightly preferred.
Extra_coords
Extra coords only need to be handled in two NDCube
instances are being combined. For all operations in this case, the NDCube.extra_coords
will be combined into a new variable new_extra_coords_wcs_axis
so all coords are kept.
Similar to the Meta section, there are a few ways to handle common extra coords that do not have the same values:
- Raise
ValueError
.
force_combine_extra_coords
kwarg: Create and set this kwarg to
False
: ValueError
raised as above as for the None
option in the Meta section.
True
: Use the value from the first NDCube
only.
WCS and missing_axis
As above, WCS only needs to be handled if two NDCube
s are being combined. In this case is should be checked whether the NDCube
s have equivalent WCS object. This means the same WCS when units are accounted for. Or the second WCS has fewer dimensions than the first, but within those dimensions, the WCSs are equivalent. If not, a ValueError
should be raised. Otherwise, the operation can proceed and the new_wcs
and new_missing_axis
variable will be taken from the primary NDCube
.
NOTES
More Detailed Handling of Uncertainties
As there is not necessarily a single way to handle uncertainties. This may require non-hidden add
, sub
, etc. methods with an API for users to supply their own function to handle the uncertainties, e.g. a kwarg like uncertainty_handler=None
. Defaults can remain the same as used when overloading the operators as above. Perhaps these methods will also have a kwarg for altering how the mask is handled? See below.
Other Options for Handling Mask?
There are at least two ways in which the masks can be combined.
- Masks do not affect how the data are combined, only how the output mask. In this case
True
is dominant, i.e. if a pixel is masked (True
) in one NDCube
, but not the other (False
), the resultant NDCube.mask
value for that pixel will be True
. This will be done by always inverting the two masks, multiplying them, then reinverting them. This is required because True
implies masked, but True * False == False
.
- The mask of the primary
NDCube
remains unchanged. The data/uncertainty elements that are masked in the second object are simply not applied to the primary NDCube
.
On of these must be the default when operating on mask arrays. To do: determine which it is!
**
The **
should only accept a scalar. It will only affect the data
and the uncertainty
. The data array will be raised to that power (new_data = self.data ** x
) and the uncertainty will follow the standard propagation rule for powers.
==
, !=
==
can only be True
is both objects are NDCube
s. The WCS and extra_coords must all be equal when taking into account units. Meanwhile the data arrays must be equal within uncertainties when taking into account units. The uncertainties and unit can only be combined correctly if the uncertainty type is known. If the uncertainty type is not set and not all uncertainties are 0
, a warning should be raised saying that uncertainties will not be taken into account in the comparison. If the uncertainty attribute is None
or all uncertainties are 0
, then do not raise a warning and compare data without uncertainties.
The metadata and mask do not have to be the same.
!=
will simply be the Boolean inverse of the output of ==
and likely inherited, so required no redefinition.
Returned Result
Finally, a new NDCube
will be instantiated:
>>> new_cube = NDCube(new_data, wcs=new_wcs, uncertainty=new_uncertainty, mask=new_mask,
meta=new_meta, unit=new_unit, extra_coords=None,
missing_axis=new_missing_axis)
>>> new_cube._extra_coords_wcs_axis = new_extra_coords_wcs_axis # If the new_extra_coords_wcs_axis is already is the correct dictionary format, this way is more efficient than converting is to the input format only reconvert it back to this format.
>>> return new_cube
This the cases of =+
, +-
, =*
, =/
, the new_cube
will overwrite the primary cube. This should be done by resetting the attributed of the primary cube rather than instantiating a whole new new cube which is less memory efficient.
NDCubeSequence Arithmetic Capabilities
The above arithmetic capabilities must also be implemented for NDCubeSequence
. All the same secondary objects must be supported. Except in these cases, the NDCubeSequence
operators will simply be wrappers, passing the operation of to the constituent NDCube
s. Interpreting two other cases must be considered: combing of two NDCubeSequence
s and an NDCubeSequence
with an iterable of NDData
instances.
Combining an NDCubeSequence
with an NDCubeSequence
or iterable of NDData
with +
, -
, *
, /
, =+
, =-
, =*
, =/
For these operators to be applied, the NDCubeSequence
s and/or iterable of NDData
s must have the same number of NDCube
s/NDData
s. Each pair of corresponding NDCube
/NDData
will then be combined as the operator dictates. If any of these pairs are incompatible under the operation, an error is thrown.
Comparing Two NDCubeSequence
s with ==
This should simply pass on the ==
operator to the constituent NDCube
s. Just like the metadata of NDCube
s does not have to be identical, nor should the metadata of NDCubeSequence
s