NDData Arithmetic¶
Introduction¶
NDDataRef
implements the following arithmetic operations:
Addition:
add()
Subtraction:
subtract()
Multiplication:
multiply()
Division:
divide()
Using Basic Arithmetic Methods¶
Using the standard arithmetic methods requires that the first operand
is an NDDataRef
instance:
>>> from astropy.nddata import NDDataRef
>>> from astropy.wcs import WCS
>>> import numpy as np
>>> ndd1 = NDDataRef([1, 2, 3, 4])
While the requirement for the second operand is that it must be convertible to the first operand. It can be a number:
>>> ndd1.add(3)
NDDataRef([4, 5, 6, 7])
Or a list
:
>>> ndd1.subtract([1,1,1,1])
NDDataRef([0, 1, 2, 3])
Or a numpy.ndarray
:
>>> ndd1.multiply(np.arange(4, 8))
NDDataRef([ 4, 10, 18, 28])
>>> ndd1.divide(np.arange(1,13).reshape(3,4)) # a 3 x 4 numpy array
NDDataRef([[1. , 1. , 1. , 1. ],
[0.2 , 0.33333333, 0.42857143, 0.5 ],
[0.11111111, 0.2 , 0.27272727, 0.33333333]])
Here, broadcasting takes care of the different dimensions. Several other classes are also possible.
Using Arithmetic Classmethods¶
Here both operands do not need to be NDDataRef
-like:
>>> NDDataRef.add(1, 3)
NDDataRef(4)
To wrap the result of an arithmetic operation between two Quantities:
>>> import astropy.units as u
>>> ndd = NDDataRef.multiply([1,2] * u.m, [10, 20] * u.cm)
>>> ndd
NDDataRef([10., 40.], unit='cm m')
>>> ndd.unit
Unit("cm m")
Or take the inverse of an NDDataRef
object:
>>> NDDataRef.divide(1, ndd1)
NDDataRef([1. , 0.5 , 0.33333333, 0.25 ])
Possible Operands¶
The possible types of input for operands are:
Scalars of any type
Lists containing numbers (or nested lists)
numpy
arraysnumpy
masked arraysastropy
quantitiesOther
nddata
classes or subclasses
Advanced Options¶
The normal Python operators +
, -
, etc. are not implemented because
the methods provide several options on how to proceed with the additional
attributes.
Data and Unit¶
For data
and unit
there are no parameters. Every arithmetic
operation lets the astropy.units.Quantity
-framework evaluate the result
or fail and abort the operation.
Adding two NDData
objects with the same unit works:
>>> ndd1 = NDDataRef([1,2,3,4,5], unit='m')
>>> ndd2 = NDDataRef([100,150,200,50,500], unit='m')
>>> ndd = ndd1.add(ndd2)
>>> ndd.data
array([101., 152., 203., 54., 505.])
>>> ndd.unit
Unit("m")
Adding two NDData
objects with compatible units also works:
>>> ndd1 = NDDataRef(ndd1, unit='pc')
INFO: overwriting NDData's current unit with specified unit. [astropy.nddata.nddata]
>>> ndd2 = NDDataRef(ndd2, unit='lyr')
INFO: overwriting NDData's current unit with specified unit. [astropy.nddata.nddata]
>>> ndd = ndd1.subtract(ndd2)
>>> ndd.data
array([ -29.66013938, -43.99020907, -58.32027876, -11.33006969,
-148.30069689])
>>> ndd.unit
Unit("pc")
This will keep by default the unit of the first operand. However, units will not be decomposed during division:
>>> ndd = ndd2.divide(ndd1)
>>> ndd.data
array([100. , 75. , 66.66666667, 12.5 , 100. ])
>>> ndd.unit
Unit("lyr / pc")
Mask¶
The handle_mask
parameter for the arithmetic operations implements what the
resulting mask will be. There are several options.
None
, the result will have nomask
:>>> ndd1 = NDDataRef(1, mask=True) >>> ndd2 = NDDataRef(1, mask=False) >>> ndd1.add(ndd2, handle_mask=None).mask is None True
"first_found"
or"ff"
, the result will have themask
of the first operand or if that isNone
, themask
of the second operand:>>> ndd1 = NDDataRef(1, mask=True) >>> ndd2 = NDDataRef(1, mask=False) >>> ndd1.add(ndd2, handle_mask="first_found").mask True >>> ndd3 = NDDataRef(1) >>> ndd3.add(ndd2, handle_mask="first_found").mask False
A function (or an arbitrary callable) that takes at least two arguments. For example,
numpy.logical_or
is the default:>>> ndd1 = NDDataRef(1, mask=np.array([True, False, True, False])) >>> ndd2 = NDDataRef(1, mask=np.array([True, False, False, True])) >>> ndd1.add(ndd2).mask array([ True, False, True, True]...)
This defaults to
"first_found"
in case only onemask
is not None:>>> ndd1 = NDDataRef(1) >>> ndd2 = NDDataRef(1, mask=np.array([True, False, False, True])) >>> ndd1.add(ndd2).mask array([ True, False, False, True]...)
Custom functions are also possible:
>>> def take_alternating_values(mask1, mask2, start=0): ... result = np.zeros(mask1.shape, dtype=np.bool_) ... result[start::2] = mask1[start::2] ... result[start+1::2] = mask2[start+1::2] ... return result
This function is nonsense, but we can still see how it performs:
>>> ndd1 = NDDataRef(1, mask=np.array([True, False, True, False])) >>> ndd2 = NDDataRef(1, mask=np.array([True, False, False, True])) >>> ndd1.add(ndd2, handle_mask=take_alternating_values).mask array([ True, False, True, True]...)
Additional parameters can be given by prefixing them with
mask_
(which will be stripped before passing it to the function):>>> ndd1.add(ndd2, handle_mask=take_alternating_values, mask_start=1).mask array([False, False, False, False]...) >>> ndd1.add(ndd2, handle_mask=take_alternating_values, mask_start=2).mask array([False, False, True, True]...)
Meta¶
The handle_meta
parameter for the arithmetic operations implements what the
resulting meta
will be. The options are the same as for the mask
:
If
None
the resultingmeta
will be an emptycollections.OrderedDict
.>>> ndd1 = NDDataRef(1, meta={'object': 'sun'}) >>> ndd2 = NDDataRef(1, meta={'object': 'moon'}) >>> ndd1.add(ndd2, handle_meta=None).meta OrderedDict()
For
meta
this is the default so you do not need to pass it in this case:>>> ndd1.add(ndd2).meta OrderedDict()
If
"first_found"
or"ff"
, the resultingmeta
will be themeta
of the first operand or if that contains no keys, themeta
of the second operand is taken.>>> ndd1 = NDDataRef(1, meta={'object': 'sun'}) >>> ndd2 = NDDataRef(1, meta={'object': 'moon'}) >>> ndd1.add(ndd2, handle_meta='ff').meta {'object': 'sun'}
If it is a
callable
it must take at least two arguments. Bothmeta
attributes will be passed to this function (even if one or both of them are empty) and the callable evaluates the result’smeta
. For example, a function that merges these two:>>> # It's expected with arithmetics that the result is not a reference, >>> # so we need to copy >>> from copy import deepcopy >>> def combine_meta(meta1, meta2): ... if not meta1: ... return deepcopy(meta2) ... elif not meta2: ... return deepcopy(meta1) ... else: ... meta_final = deepcopy(meta1) ... meta_final.update(meta2) ... return meta_final >>> ndd1 = NDDataRef(1, meta={'time': 'today'}) >>> ndd2 = NDDataRef(1, meta={'object': 'moon'}) >>> ndd1.subtract(ndd2, handle_meta=combine_meta).meta {'object': 'moon', 'time': 'today'}
Here again additional arguments for the function can be passed in using the prefix
meta_
(which will be stripped away before passing it to this function). See the description for the mask-attribute for further details.
World Coordinate System (WCS)¶
The compare_wcs
argument will determine what the result’s wcs
will be
or if the operation should be forbidden. The possible values are identical to
mask
and meta
:
If
None
the resultingwcs
will be an emptyNone
.>>> ndd1 = NDDataRef(1, wcs=None) >>> ndd2 = NDDataRef(1, wcs=WCS()) >>> ndd1.add(ndd2, compare_wcs=None).wcs is None True
If
"first_found"
or"ff"
the resultingwcs
will be thewcs
of the first operand or if that isNone
, themeta
of the second operand is taken.>>> wcs = WCS() >>> ndd1 = NDDataRef(1, wcs=wcs) >>> ndd2 = NDDataRef(1, wcs=None) >>> str(ndd1.add(ndd2, compare_wcs='ff').wcs) == str(wcs) True
If it is a
callable
it must take at least two arguments. Bothwcs
attributes will be passed to this function (even if one or both of them areNone
) and the callable should returnTrue
if thesewcs
are identical (enough) to allow the arithmetic operation orFalse
if the arithmetic operation should be aborted with aValueError
. IfTrue
thewcs
are identical and the first one is used for the result:>>> def compare_wcs_scalar(wcs1, wcs2, allowed_deviation=0.1): ... if wcs1 is None and wcs2 is None: ... return True # both have no WCS so they are identical ... if wcs1 is None or wcs2 is None: ... return False # one has WCS, the other doesn't not possible ... else: ... # Consider wcs close if centers are close enough ... return all(abs(wcs1.wcs.crpix - wcs2.wcs.crpix) < allowed_deviation) >>> ndd1 = NDDataRef(1, wcs=None) >>> ndd2 = NDDataRef(1, wcs=None) >>> ndd1.subtract(ndd2, compare_wcs=compare_wcs_scalar).wcs
Additional arguments can be passed in prefixing them with
wcs_
(this prefix will be stripped away before passing it to the function):>>> ndd1 = NDDataRef(1, wcs=WCS()) >>> ndd1.wcs.wcs.crpix = [1, 1] >>> ndd2 = NDDataRef(1, wcs=WCS()) >>> ndd1.subtract(ndd2, compare_wcs=compare_wcs_scalar, wcs_allowed_deviation=2).wcs.wcs.crpix array([1., 1.])
If you are using
WCS
objects, a very handy function to use might be:>>> def wcs_compare(wcs1, wcs2, *args, **kwargs): ... return wcs1.wcs.compare(wcs2.wcs, *args, **kwargs)
See
astropy.wcs.Wcsprm.compare()
for the arguments this comparison allows.
Uncertainty¶
The propagate_uncertainties
argument can be used to turn the propagation
of uncertainties on or off.
If
None
the result will have no uncertainty:>>> from astropy.nddata import StdDevUncertainty >>> ndd1 = NDDataRef(1, uncertainty=StdDevUncertainty(0)) >>> ndd2 = NDDataRef(1, uncertainty=StdDevUncertainty(1)) >>> ndd1.add(ndd2, propagate_uncertainties=None).uncertainty is None True
If
False
the result will have the first found uncertainty.Note
Setting
propagate_uncertainties=False
is generally not recommended.If
True
both uncertainties must beNDUncertainty
subclasses that implement propagation. This is possible forStdDevUncertainty
:>>> ndd1 = NDDataRef(1, uncertainty=StdDevUncertainty([10])) >>> ndd2 = NDDataRef(1, uncertainty=StdDevUncertainty([10])) >>> ndd1.add(ndd2, propagate_uncertainties=True).uncertainty StdDevUncertainty([14.14213562])
Uncertainty with Correlation¶
If propagate_uncertainties
is True
you can also give an argument
for uncertainty_correlation
. StdDevUncertainty
cannot
keep track of its correlations by itself, but it can evaluate the correct
resulting uncertainty if the correct correlation
is given.
The default (0
) represents uncorrelated while 1
means correlated and
-1
anti-correlated. If given a numpy.ndarray
it should represent the
element-wise correlation coefficient.
Examples¶
Without correlation, subtracting an NDDataRef
instance from
itself results in a non-zero uncertainty:
>>> ndd1 = NDDataRef(1, uncertainty=StdDevUncertainty([10]))
>>> ndd1.subtract(ndd1, propagate_uncertainties=True).uncertainty
StdDevUncertainty([14.14213562])
Given a correlation of 1
(because they clearly correlate) gives the
correct uncertainty of 0
:
>>> ndd1 = NDDataRef(1, uncertainty=StdDevUncertainty([10]))
>>> ndd1.subtract(ndd1, propagate_uncertainties=True,
... uncertainty_correlation=1).uncertainty
StdDevUncertainty([0.])
Which would be consistent with the equivalent operation ndd1 * 0
:
>>> ndd1.multiply(0, propagate_uncertainties=True).uncertainty
StdDevUncertainty([0.])
Warning
The user needs to calculate or know the appropriate value or array manually
and pass it to uncertainty_correlation
. The implementation follows
general first order error propagation formulas. See, for example:
Wikipedia.
You can also give element-wise correlations:
>>> ndd1 = NDDataRef([1,1,1,1], uncertainty=StdDevUncertainty([1,1,1,1]))
>>> ndd2 = NDDataRef([2,2,2,2], uncertainty=StdDevUncertainty([2,2,2,2]))
>>> ndd1.add(ndd2,uncertainty_correlation=np.array([1,0.5,0,-1])).uncertainty
StdDevUncertainty([3. , 2.64575131, 2.23606798, 1. ])
The correlation np.array([1, 0.5, 0, -1])
would indicate that the first
element is fully correlated and the second element partially correlates, while
the third element is uncorrelated, and the fourth is anti-correlated.
Uncertainty with Unit¶
StdDevUncertainty
implements correct error propagation even
if the unit of the data differs from the unit of the uncertainty:
>>> ndd1 = NDDataRef([10], unit='m', uncertainty=StdDevUncertainty([10], unit='cm'))
>>> ndd2 = NDDataRef([20], unit='m', uncertainty=StdDevUncertainty([10]))
>>> ndd1.subtract(ndd2, propagate_uncertainties=True).uncertainty
StdDevUncertainty([10.00049999])
But it needs to be convertible to the unit for the data.