Cube Maths#

The section Navigating a Cube highlighted that every cube has a data attribute; this attribute can then be manipulated directly:

cube.data -= 273.15

The problem with manipulating the data directly is that other metadata may become inconsistent; in this case the units of the cube are no longer what was intended. This example could be rectified by changing the units attribute:

cube.units = 'celsius'

Note

iris.cube.Cube.convert_units() can be used to automatically convert a cube’s data and update its units attribute. So, the two steps above can be achieved by:

cube.convert_units('celsius')

In order to reduce the amount of metadata which becomes inconsistent, fundamental arithmetic operations such as addition, subtraction, division and multiplication can be applied directly to any cube.

Calculating the Difference Between Two Cubes#

Let’s load some air temperature which runs from 1860 to 2100:

filename = iris.sample_data_path('E1_north_america.nc')
air_temp = iris.load_cube(filename, 'air_temperature')

We can now get the first and last time slices using indexing (see Cube Indexing for a reminder):

t_first = air_temp[0, :, :]
t_last = air_temp[-1, :, :]

And finally we can subtract the two. The result is a cube of the same size as the original two time slices, but with the data representing their difference:

>>> print(t_last - t_first)
unknown / (K)                       (latitude: 37; longitude: 49)
    Dimension coordinates:
        latitude                             x              -
        longitude                            -              x
    Scalar coordinates:
        forecast_reference_time     1859-09-01 06:00:00
        height                      1.5 m
    Attributes:
        Conventions                 'CF-1.5'
        Model scenario              'E1'
        source                      'Data from Met Office Unified Model 6.05'

Note

Notice that the coordinates “time” and “forecast_period” have been removed from the resultant cube; this is because these coordinates differed between the two input cubes.

Calculating a Cube Anomaly#

In section Cube Statistics we discussed how the dimensionality of a cube can be reduced using the Cube.collapsed method to calculate a statistic over a dimension.

Let’s use that method to calculate a mean of our air temperature time-series, which we’ll then use to calculate a time mean anomaly and highlight the powerful benefits of cube broadcasting.

First, let’s remind ourselves of the shape of our air temperature time-series cube:

>>> print(air_temp.summary(True))
air_temperature / (K)               (time: 240; latitude: 37; longitude: 49)

Now, we’ll calculate the time-series mean using the Cube.collapsed method:

>>> air_temp_mean = air_temp.collapsed('time', iris.analysis.MEAN)
>>> print(air_temp_mean.summary(True))
air_temperature / (K)               (latitude: 37; longitude: 49)

As expected the time dimension has been collapsed, reducing the dimensionality of the resultant air_temp_mean cube. This time-series mean can now be used to calculate the time mean anomaly against the original time-series:

>>> anomaly = air_temp - air_temp_mean
>>> print(anomaly.summary(True))
unknown / (K)                       (time: 240; latitude: 37; longitude: 49)

Notice that the calculation of the anomaly involves subtracting a 2d cube from a 3d cube to yield a 3d result. This is only possible because cube broadcasting is performed during cube arithmetic operations.

Cube broadcasting follows similar broadcasting rules as NumPy, but the additional richness of Iris coordinate meta-data provides an enhanced capability beyond the basic broadcasting behaviour of NumPy.

As the coordinate meta-data of a cube uniquely describes each dimension, it is possible to leverage this knowledge to identify the similar dimensions involved in a cube arithmetic operation. This essentially means that we are no longer restricted to performing arithmetic on cubes with identical shapes.

This extended broadcasting behaviour is highlighted in the following examples. The first of these shows that it is possible to involve the transpose of the air temperature time-series in an arithmetic operation with itself.

Let’s first create the transpose of the air temperature time-series:

>>> air_temp_T = air_temp.copy()
>>> air_temp_T.transpose()
>>> print(air_temp_T.summary(True))
air_temperature / (K)               (longitude: 49; latitude: 37; time: 240)

Now add the transpose to the original time-series:

>>> result = air_temp + air_temp_T
>>> print(result.summary(True))
unknown / (K)                       (time: 240; latitude: 37; longitude: 49)

Notice that the result is the same dimensionality and shape as air_temp. Let’s check that the arithmetic operation has calculated a result that we would intuitively expect:

>>> result == 2 * air_temp
True

Let’s extend this example slightly, by taking a slice from the middle latitude dimension of the transpose cube:

>>> air_temp_T_slice = air_temp_T[:, 0, :]
>>> print(air_temp_T_slice.summary(True))
air_temperature / (K)               (longitude: 49; time: 240)

Compared to our original time-series, the air_temp_T_slice cube has one less dimension and its shape is different. However, this doesn’t prevent us from performing cube arithmetic with it, thanks to the extended cube broadcasting behaviour:

>>> result = air_temp - air_temp_T_slice
>>> print(result.summary(True))
unknown / (K)                       (time: 240; latitude: 37; longitude: 49)

See also

Relevant gallery example: Colouring Anomaly Data With Logarithmic Scaling (Anomaly)

Combining Multiple Phenomena to Form a New One#

Combining cubes of potential-temperature and pressure we can calculate the associated temperature using the equation:

\[T = \theta (\frac{p}{p_0}) ^ {(287.05 / 1005)}\]

Where \(p\) is pressure, \(\theta\) is potential temperature, \(p_0\) is the potential temperature reference pressure and \(T\) is temperature.

First, let’s load pressure and potential temperature cubes:

filename = iris.sample_data_path('colpex.pp')
phenomenon_names = ['air_potential_temperature', 'air_pressure']
pot_temperature, pressure = iris.load_cubes(filename, phenomenon_names)

In order to calculate \(\frac{p}{p_0}\) we can define a coordinate which represents the standard reference pressure of 1000 hPa:

import iris.coords
p0 = iris.coords.AuxCoord(1000.0,
                          long_name='reference_pressure',
                          units='hPa')

We must ensure that the units of pressure and p0 are the same, so convert the newly created coordinate using the iris.coords.Coord.convert_units() method:

p0.convert_units(pressure.units)

Now we can combine all of this information to calculate the air temperature using the equation above:

temperature = pot_temperature * ( (pressure / p0) ** (287.05 / 1005) )

Finally, the cube we have created needs to be given a suitable name:

temperature.rename('air_temperature')

The result could now be plotted using the guidance provided in the Plotting a Cube section.

A very similar example to this can be found in Deriving Exner Pressure and Air Temperature.

Combining Units#

It should be noted that when combining cubes by multiplication, division or power operations, the resulting cube will have a unit which is an appropriate combination of the constituent units. In the above example, since pressure and p0 have the same unit, then pressure / p0 has a dimensionless unit of '1'. Since (pressure / p0) has a unit of '1', this does not change under power operations and so ( (pressure / p0) ** (287.05 / 1005) ) also has unit 1. Multiplying by a cube with unit '1' will preserve units, so the cube temperature will be given the same units as are in pot_temperature. It should be noted that some combinations of units, particularly those involving power operations, will not result in a valid unit and will cause the calculation to fail. For example, if a cube a had units 'm' then a ** 0.5 would result in an error since the square root of a meter has no meaningful unit (if a had units 'm2' then a ** 0.5 would result in a cube with units 'm').

Iris inherits units from cf_units which in turn inherits from UDUNITS. As well as the units UDUNITS provides, cf units also provides the units 'no-unit' and 'unknown'. A unit of 'no-unit' means that the associated data is not suitable for describing with a unit, cf units considers 'no-unit' unsuitable for combining and therefore any arithmetic done on a cube with 'no-unit' will fail. A unit of 'unknown' means that the unit describing the associated data cannot be determined. cf units and Iris will allow arithmetic on cubes with a unit of 'unknown', but the resulting cube will always have a unit of 'unknown'. If a calculation is prevented because it would result in inappropriate units, it may be forced by setting the units of the original cubes to be 'unknown'.