File: arithmetic.rst

package info (click to toggle)
ndcube 2.4.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,356 kB
  • sloc: python: 9,335; makefile: 34
file content (392 lines) | stat: -rw-r--r-- 16,997 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
.. _arithmetic:

*********************
Arithmetic Operations
*********************

Arithmetic operations are a crucial tool in n-dimensional data analysis.
Applications include subtracting a background from a 1-D timeseries or spectrum, scaling an image by a vignetting function, and many more.
To this end, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with numbers, arrays, and `~astropy.units.Quantity`.
Raising an `~ndcube.NDCube` to a power is also supported.
These operations return a new `~ndcube.NDCube` with the data array (and, where appropriate, the uncertainties and unit) altered in accordance with the arithmetic operation.
Other attributes of the `~ndcube.NDCube` remain unchanged.

Arithmetic operations between `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` subclasses are also supported.
See the section below on :ref:`arithmetic_nddata` for further details.

.. _arithmetic_standard:

Arithmetic Operations with Numbers, Arrays and Quantities
=========================================================

Addition and Subtraction
------------------------

Numbers, arrays and `~astropy.units.Quantity` can be added to and subtracted from `~ndcube.NDCube` via the ``+`` and ``-`` operators.
Note that these only change the data values of the `~ndcube.NDCube` and units must be consistent with that of the `~ndcube.NDCube`.
Let's demonstrate with an example `~ndcube.NDCube`, ``cube``:

.. expanding-code-block:: python
  :summary: Expand to see cube instantiated.

  >>> import astropy.units as u
  >>> import astropy.wcs
  >>> import numpy as np
  >>> from astropy.nddata import StdDevUncertainty

  >>> from ndcube import NDCube

  >>> # Define data array.
  >>> data = np.arange(2*3).reshape((2, 3)) + 10

  >>> # Define WCS transformations in an astropy WCS object.
  >>> wcs = astropy.wcs.WCS(naxis=2)
  >>> wcs.wcs.ctype = 'HPLT-TAN', 'HPLN-TAN'
  >>> wcs.wcs.cunit = 'deg', 'deg'
  >>> wcs.wcs.cdelt = 0.5, 0.4
  >>> wcs.wcs.crpix = 2, 2
  >>> wcs.wcs.crval = 0.5, 1

  >>> # Define mask. Initially set all elements unmasked.
  >>> mask = np.zeros_like(data, dtype=bool)
  >>> mask[0, :] = True  # Now mask some values.
  >>> # Define uncertainty, metadata and unit.
  >>> uncertainty = StdDevUncertainty(np.abs(data) * 0.1)
  >>> meta = {"Description": "This is example NDCube metadata."}
  >>> unit = u.ct

  >>> # Instantiate NDCube with supporting data.
  >>> cube = NDCube(data, wcs=wcs, uncertainty=uncertainty, mask=mask, meta=meta)

.. code-block:: python

  >>> cube.data
  array([[10, 11, 12],
         [13, 14, 15]])

  >>> new_cube = cube + 1
  >>> new_cube.data
  array([[11, 12, 13],
         [14, 15, 16]])

Note that all the data values have been increased by 1.
We can also use an array if we want to add a different number to each data element:

.. code-block:: python

  >>> import numpy as np
  >>> arr = np.arange(cube.data.size).reshape(cube.data.shape)
  >>> arr
  array([[0, 1, 2],
         [3, 4, 5]])

  >>> new_cube = cube + arr
  >>> new_cube.data
  array([[10, 12, 14],
         [16, 18, 20]])

Subtraction works in the same way.

.. code-block:: python

  >>> new_cube = cube - 1
  >>> new_cube.data
  array([[ 9, 10, 11],
         [12, 13, 14]])

  >>> new_cube = cube - arr
  >>> new_cube.data
  array([[10, 10, 10],
         [10, 10, 10]])

Note that in the above examples, ``cube`` has no unit.
This is why we are able to add and subtract numbers and arrays.
If, however, we have an `~ndcube.NDCube` with a unit assigned,

.. code-block:: python

  >>> cube_with_unit = NDCube(cube, unit=u.ct)

then adding or subtracting an array or unitless number will raise an error.
In such cases, we must use a `~astropy.units.Quantity` with a compatible unit:

.. code-block:: python

  >>> cube_with_unit.data
  array([[10, 11, 12],
         [13, 14, 15]])

  >>> new_cube = cube_with_unit + 1 * u.ct  # Adding a scalar quantity
  >>> new_cube.data
  array([[11., 12., 13.],
         [14., 15., 16.]])

  >>> new_cube = cube_with_unit - 1 * u.ct  # Subtracting a scalar quantity
  >>> new_cube.data
  array([[ 9., 10., 11.],
         [12., 13., 14.]])

  >>> new_cube = cube_with_unit + arr * u.ct  # Adding an array-like quantity
  >>> new_cube.data
  array([[10., 12., 14.],
         [16., 18., 20.]])

  >>> new_cube = cube_with_unit - arr * u.ct  # Subtracting an array-like quantity
  >>> new_cube.data
  array([[10., 10., 10.],
         [10., 10., 10.]])

Multiplication and Division
---------------------------

An `~ndcube.NDCube` can be multiplied and divided by numbers, arrays, and `~astropy.units.Quantity` via the ``*`` and ``-`` operators.
These work similarly to addition and subtraction with a few minor differences:

- The uncertainties of the resulting `~ndcube.NDCube` are scaled by the same factor as the data.
- Classes with non-equivalent units can be combined.

  * e.g. an `~ndcube.NDCube` with a unit of ``ct`` divided by an `~astropy.units.Quantity` with a unit of ``s`` will result in an `~ndcube.NDCube` with a unit of ``ct / s``.
  * This also holds for cases were unitful and unitless classes can be combined.  In such cases, the unit of the resulting `~ndcube.NDCube` will be the same as that of the unitful object.

Below are some examples.

.. code-block:: python

  >>> # See attributes of original cube.
  >>> cube_with_unit.data
  array([[10, 11, 12],
         [13, 14, 15]])
  >>> cube_with_unit.unit
  Unit("ct")
  >>> cube_with_unit.uncertainty
  StdDevUncertainty([[1. , 1.1, 1.2],
                     [1.3, 1.4, 1.5]])

  >>> # Multiply by a unitless array.
  >>> arr = 1 + np.arange(cube_with_unit.data.size).reshape(cube_with_unit.data.shape)
  >>> arr
  array([[1, 2, 3],
         [4, 5, 6]])

  >>> new_cube = cube_with_unit * arr

  >>> # Inspect attributes of resultant cube.
  >>> new_cube.data
  array([[10, 22, 36],
         [52, 70, 90]])
  >>> new_cube.unit
  Unit("ct")
  >>> new_cube.uncertainty
  StdDevUncertainty([[1. , 2.2, 3.6],
                     [5.2, 7. , 9. ]])

  >>> # Divide by a scalar astropy Quantity.
  >>> new_cube = cube_with_unit / (2 * u.s)

  >>> # Inspect attributes of resultant cube.
  >>> new_cube.data
  array([[5. , 5.5, 6. ],
         [6.5, 7. , 7.5]])
  >>> new_cube.unit
  Unit("ct / s")
  >>> new_cube.uncertainty
  StdDevUncertainty([[0.5 , 0.55, 0.6 ],
                     [0.65, 0.7 , 0.75]])

Note that when performing arithmetic operations with `~ndcube.NDCube` and array-like objects, their shapes only have to be broadcastable, not necessarily the same.
For example:

.. code-block:: python

  >>> cube.data
  array([[10, 11, 12],
         [13, 14, 15]])
  >>> arr[0]
  array([1, 2, 3])

  >>> new_cube = cube + arr[0]
  >>> new_cube.data
  array([[11, 13, 15],
         [14, 16, 18]])

Raising NDCube to a Power
-------------------------

.. code-block:: python

  >>> cube_with_unit.data
  array([[10, 11, 12],
         [13, 14, 15]])
  >>> new_cube = cube_with_unit**2  # doctest: +SHOW_WARNINGS
  NDCubeUserWarning: <class 'astropy.nddata.nduncertainty.StdDevUncertainty'> does not support propagation of uncertainties for power. Setting uncertainties to None.
  >>> new_cube.data
  array([[100, 121, 144],
         [169, 196, 225]])
  >>> new_cube.unit
  Unit("ct2")
  >>> (new_cube.mask == cube_with_unit.mask).all()
  np.True_

Note that error propagation is delegated to the ``cube.uncertainty`` object.
Therefore, if this class supports error propagation by power, then ``new_cube`` will include uncertainty.
Otherwise, ``new_cube.uncertainty`` will be set to ``None``, and a warning shown.


.. _arithmetic_nddata:

Arithmetic Operations Between NDCubes
=====================================

Why Arithmetic Operations between NDCubes Are Not Supported Directly (but are indirectly)
-----------------------------------------------------------------------------------------

Arithmetic operations between two `~ndcube.NDCube` instances are not supported directly.
(However, as we shall see, they are supported indirectly.)
This is because of the wide scope for enabling non-sensical coordinate-aware operations.
For example, what does it mean to multiply a spectrum and an image?
Getting the difference between two images may make physical sense in certain circumstances.
For example, subtracting two sequential images of the same region of the Sun is a common step in many solar image analyses.
But subtracting images of different parts of the sky, e.g. the Crab and Horseshoe Nebulae, does not produce a physically meaningful result.
Even when subtracting two images of the Sun, drift in the telescope's pointing may result in corresponding pixels representing different points on the Sun.
In this case, it is questionable whether even this operation makes physical sense.
Moreover, in all of these cases, it is not clear what the resulting WCS object should be.

One way to ensure physically meaningful, coordinate-aware arithmetic operations between `~ndcube.NDCube` instances would be to compare their WCS objects are the same within a certain tolerance.
Alternatively, the arithmetic operation could attempt to reproject one `~ndcube.NDCube` to the other's WCS.
However, these operations can be prohibitively slow and resource-hungry.
Despite this, arithmetic operations between two `~ndcube.NDCube` instances is supported, provided the coordinate-awareness of one is dropped.
Below we shall outline two ways of doing this.

Performing Arithmetic Operations between NDCubes Indirectly
-----------------------------------------------------------

Extracting One NDCube's Data and Unit
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The simplest way to perform arithmetic operations between `~ndcube.NDCube` instances is to directly combine one with the data (an optionally the unit) of the other.
Thus, the operation can be performed as already described in the above section on :ref:`arithmetic_standard`:

.. expanding-code-block:: python
  :summary: Expand to see definition of cube1 and cube2.

  >>> cube1 = cube_with_unit
  >>> cube2 = cube_with_unit / 4

.. code-block:: python

  >>> new_cube = cube1 - cube2.data * cube2.unit

Enabling Arithmetic Operations between NDCubes with NDCube.to_nddata
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Sometimes, however, more advanced arithmetic operations are required for which numbers, arrays and `~astropy.units.Quantity` are insufficient.
These include cases where both operands have:

- uncertainties which need to be propagated, and/or;
- masks that need to be combined, and/or;
- units and non-`numpy` data arrays unsuitable for representation as an `~astropy.units.Quantity`, e.g. ``dask.array``.

To achieve these operations, it would be preferable to perform arithmetic operations directly between the `~ndcube.NDCube` instances.
While this is not supported, as already outlined, the same result can be achieved by first dropping the coordinate-awareness of one `~ndcube.NDCube` via the `ndcube.NDCube.to_nddata` method.
The two datasets can then be combined using the standard arithmetic operators.
`ndcube.NDCube.to_nddata` enables the conversion of the `~ndcube.NDCube` instance to any `~astropy.nddata.NDData` subclass, while also enabling the values of specific attributes to be altered during the conversion.
Therefore, arithmetic operations between `~ndcube.NDCube` instances via:

.. code-block:: python

  >>> new_cube = cube1 + cube2.to_nddata(wcs=None)

where addition, subtraction, multiplication and division are all enabled by the ``+``, ``-``, ``*``, and ``/`` operators, respectively.

Requiring users to explicitly remove coordinate-awareness makes it clear that coordinates are not combined as part of arithmetic operations.
It also makes it unambiguous which operand's coordinates are maintained through the operation.

Handling of Data, Units and Meta
""""""""""""""""""""""""""""""""
The treatment of the ``data`` and ``unit`` attributes in operations between `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` subclasses are the same as for arrays and `~astropy.units.Quantity`.
However, only the metadata from the `~ndcube.NDCube` is retained.
This can be updated after the operation, if desired.
For example:

.. code-block:: python

  >>> cube1.meta
  {'Description': 'This is example NDCube metadata.'}

  >>> cube2.meta["more"] = True
  >>> cube2.meta
  {'Description': 'This is example NDCube metadata.', 'more': True}

  >>> new_cube = cube1 + cube2.to_nddata(wcs=None)
  >>> new_cube.meta
  {'Description': 'This is example NDCube metadata.'}

  >>> new_cube.meta.update(cube2.meta)
  >>> new_cube.meta
  {'Description': 'This is example NDCube metadata.', 'more': True}

Handling of Uncertainties
"""""""""""""""""""""""""
How uncertainties are handled depends on the uncertainty types of the operands:

- ``NDCube.uncertainty`` and ``NDData.uncertainty`` are both ``None`` => ``new_cube.uncertainty`` is ``None``;
- ``NDCube`` or ``NDData`` have uncertainty, but not both => the existing uncertainty is assigned to ``new_cube`` as is;
- ``NDCube`` and ``NDData`` both have uncertainty => uncertainty propagation is delegated to the ``NDCube.uncertainty.propagate`` method.

  * Note that not all uncertainty classes support error propagation, e.g. `~astropy.nddata.UnknownUncertainty`.  In such cases, uncertainties are dropped altogether and ``new_cube.uncertainty`` is set to ``None``.

If users would like to remove uncertainty from one of the operands in order to propagate the other without alteration, this can be done by casting the `~ndcube.NDCube` to a new instance with the uncertainty set to ``None`` via the `ndcube.NDCube.to_nddata` method before the operation:

.. code-block:: python

  >>> # Remove uncertainty from NDCube
  >>> new_cube = cube1.to_nddata(uncertainty=None, nddata_type=NDCube) + cube2.to_nddata(wcs=None)

Handling of Masks and NDCube.fill_masked Method
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The mask resulting from an arithmetic operation between an `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` subclass depends on the mask types of the operands:

- ``NDCube.mask`` and ``NDData.mask`` are both ``None`` => ``new_cube.mask`` is ``None``;
- ``NDCube`` or ``NDData`` have a mask, but not both => the existing mask is assigned to ``new_cube`` as is;
- ``NDCube`` and ``NDData`` both have masks => The masks are combined via `numpy.logical_or`.

The mask values do not affect the ``data`` values output by the operation.
However, in some cases, the mask may be used to identify regions of unreliable data that should not be included in the operation.
This can be achieved by altering the masked data values before the operation via the `ndcube.NDCube.fill_masked` method.

The NDCube.fill_masked Method
"""""""""""""""""""""""""""""

The `ndcube.NDCube.fill_masked` method returns a new `~ndcube.NDCube` instance with masked data elements (and optionally uncertainty elements) replaced with a user-defined ``fill_value``.
This can be used to effectively exclude masked values from an arithmetic operation by replacing masked values with the identity value for that operation.
For example, in the case of addition and subtraction, the identity ``fill_value`` is ``0``.

.. code-block:: python

  >>> new_cube = cube1.fill_masked(0) + cube2.to_nddata(wcs=None)

In this example, both operands have uncertainties, which means masked uncertainties are propagated through the operation, even though the masked data values have been set to ``0``.
Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``uncertainty_fill_value`` to ``0``.

By default, the mask of the filled `~ndcube.NDCube` cube is not changed, and therefore is incorporated into the mask of ``new_cube``.
However, mask propagation can also be suppressed by unmasking the filled `~ndcube.NDCube`.
This can be done by setting the optional kwarg, ``unmask=True``, in `ndcube.NDCube.fill_masked`, which sets the mask of the filled `~ndcube.NDCube` to ``False``.

In the case of multiplication and division, the identity ``fill_value`` is ``1``.  (Note that in the below example we show the optional use of the ``uncertainty_fill_value`` and ``unmask`` kwargs.)

.. code-block:: python

  >>> cube_filled = cube1.fill_masked(1, uncertainty_fill_value=0, unmask=True)
  >>> new_cube = cube_filled * cube2.to_nddata(wcs=None)

Note that irrespective of the arithmetic operation, the ``uncertainty_fill_value`` should always be set to ``0`` to avoid propagating masked uncertainties.

By default, `ndcube.NDCube.fill_masked` returns a new `~ndcube.NDCube` instance.
However, in some cases it may be preferable to fill the masked values in-place, for example, because the data are very large and users want to control the number of copies in RAM.
In this case, the ``fill_in_place`` kwarg can be used.

.. code-block:: python

  >>> cube1.fill_masked(0, fill_in_place=True)
  >>> new_cube = cube1 + cube2.to_nddata(wcs=None)