Data Structures
Contents
Data Structures#
To keep this documentation generic we typically use dimensions x
or y
, but this should not be seen as a recommendation to use these labels for anything but actual positions or offsets in space.
Variable#
Basics#
scipp.Variable is a labeled multi-dimensional array. A variable has the following key properties:
values
: a multi-dimensional array of values, e.g., a numpy.ndarrayvariances
: a (optional) multi-dimensional array of variances for the array valuesdims
: a list of dimension labels (strings) for each axis of the arrayunit
: a (optional) physical unit of the values in the array
Note that variables, unlike DataArray and its eponym xarray.DataArray, variables do not have coordinate dicts.
[1]:
import numpy as np
import scipp as sc
Variables should generally be created using one of the available creation functions. For example, we can create a variable from a numpy array:
[2]:
var = sc.array(dims=['x', 'y'], values=np.random.rand(2, 4))
Note:
Internally scipp is not using numpy, so the above makes a copy of the numpy array of values into an internal buffer.
We can inspect the created variable as follows:
[3]:
sc.show(var)
[4]:
var
[4]:
- (x: 2, y: 4)float64𝟙0.997, 0.577, ..., 0.998, 0.369
Values:
array([[0.99727206, 0.57668688, 0.34120045, 0.60114721], [0.30937228, 0.56886347, 0.99751865, 0.36941612]])
[5]:
var.unit
[5]:
dimensionless
[6]:
var.values
[6]:
array([[0.99727206, 0.57668688, 0.34120045, 0.60114721],
[0.30937228, 0.56886347, 0.99751865, 0.36941612]])
[7]:
print(var.variances)
None
Variances must have the same shape as values, and units are specified using the scipp.units module or with a string:
[8]:
var = sc.array(dims=['x', 'y'],
unit='m/s',
values=np.random.rand(2, 4),
variances=np.random.rand(2, 4))
sc.show(var)
[9]:
var
[9]:
- (x: 2, y: 4)float64m/s0.505, 0.820, ..., 0.393, 0.711σ = 0.578, 0.486, ..., 0.814, 0.492
Values:
array([[0.50485189, 0.81955848, 0.01889153, 0.40100721], [0.18592517, 0.29878174, 0.39335719, 0.71123869]])
Variances (σ²):
array([[0.33444457, 0.23578495, 0.10863863, 0.18676937], [0.30317107, 0.42653791, 0.6623727 , 0.24168766]])
[10]:
var.variances
[10]:
array([[0.33444457, 0.23578495, 0.10863863, 0.18676937],
[0.30317107, 0.42653791, 0.6623727 , 0.24168766]])
0-D variables (scalars)#
A 0-dimensional variable contains a single value (and an optional variance). The most convenient way to create a scalar variable is by multiplying a value by a unit:
[11]:
scalar = 1.2 * sc.units.m
sc.show(scalar)
scalar
[11]:
- ()float64m1.2
Values:
array(1.2)
Singular versions of the values
and variances
properties are provided:
[12]:
print(scalar.value)
print(scalar.variance)
1.2
None
An exception is raised from the value
and variance
properties if the variable is not 0-dimensional. Note that a variable with one or more dimension extent(s) of 1 contains just a single value as well, but the value
property will nevertheless raise an exception.
Creating scalar variables with variances or with custom dtype
or variances is possible using scipp.scalar:
[13]:
var_0d = sc.scalar(value=1.0, variance=0.5, dtype=sc.DType.float32, unit='kg')
var_0d
[13]:
- ()float32kg1.0σ = 0.70710677
Values:
array(1., dtype=float32)
Variances (σ²):
array(0.5, dtype=float32)
[14]:
var_0d.value = 2.3
var_0d.variance
[14]:
0.5
DataArray#
Basics#
scipp.DataArray is a labeled array with associated coordinates. A data array is essentially a Variable object with attached dicts of coordinates, masks, and attributes.
A data array has the following key properties:
data
: the variable holding the array data.coords
: a dict-like container of coordinates for the array, accessed using a string as dict key.masks
: a dict-like container of masks for the array, accessed using a string as dict key.attrs
: a dict-like container of “attributes” for the array, accessed using a string as dict key.
The key distinction between coordinates (added via the coords
property) and attributes (added via the attrs
property) is that the former are required to match (“align”) in operations between data arrays whereas the latter are not.
masks
allows for storing boolean-valued masks alongside data. All four have items that are internally a Variable, i.e., they have a physical unit and optionally variances.
[15]:
a = sc.DataArray(
data = sc.array(dims=['y', 'x'], values=np.random.rand(2, 3)),
coords={
'y': sc.array(dims=['y'], values=np.arange(2.0), unit='m'),
'x': sc.array(dims=['x'], values=np.arange(3.0), unit='m')},
attrs={
'aux': sc.array(dims=['x'], values=np.random.rand(3))})
sc.show(a)
Note how the 'aux'
attribute is essentially a secondary coordinate for the x dimension. The dict-like coords
and masks
properties give access to the respective underlying variables:
[16]:
a.coords['x']
[16]:
- (x: 3)float64m0.0, 1.0, 2.0
Values:
array([0., 1., 2.])
[17]:
a.attrs['aux']
[17]:
- (x: 3)float64𝟙0.625, 0.633, 0.546
Values:
array([0.62511142, 0.6332681 , 0.54556003])
Access to coords and attrs in a unified manner is possible with the meta
property. Essentially this allows us to ignore whether a coordinate is aligned or not:
[18]:
a.meta['x']
[18]:
- (x: 3)float64m0.0, 1.0, 2.0
Values:
array([0., 1., 2.])
[19]:
a.meta['aux']
[19]:
- (x: 3)float64𝟙0.625, 0.633, 0.546
Values:
array([0.62511142, 0.6332681 , 0.54556003])
Unlike values
when creating a variable, data
as well as entries in the meta data dicts (coords
, masks
, and attrs
) are not deep-copied on insertion into a data array. To avoid unwanted sharing, call the copy()
method. Compare:
[20]:
x2 = sc.zeros(dims=['x'], shape=[3])
a.coords['x2_shared'] = x2
a.coords['x2_copied'] = x2.copy()
x2 += 123
a
[20]:
- y: 2
- x: 3
- x(x)float64m0.0, 1.0, 2.0
Values:
array([0., 1., 2.]) - x2_copied(x)float64𝟙0.0, 0.0, 0.0
Values:
array([0., 0., 0.]) - x2_shared(x)float64𝟙123.0, 123.0, 123.0
Values:
array([123., 123., 123.]) - y(y)float64m0.0, 1.0
Values:
array([0., 1.])
- (y, x)float64𝟙0.315, 0.481, ..., 0.833, 0.197
Values:
array([[0.31495581, 0.48141168, 0.16180807], [0.74403045, 0.83271888, 0.19725265]])
- aux(x)float64𝟙0.625, 0.633, 0.546
Values:
array([0.62511142, 0.6332681 , 0.54556003])
Meta data can be removed in the same way as in Python dicts:
[21]:
del a.attrs['aux']
Distinction between dimension coords and non-dimension coords, and coords and attrs#
When the name of a coord matches its dimension, e.g., if d.coord['x']
depends on dimension 'x'
as in the above example, we call this coord dimension coordinate. Otherwise it is called non-dimension coord. It is important to highlight that for practical purposes (such as matching in operations) dimension coords and non-dimension coords are handled equivalently. Essentially:
Non-dimension coordinates are coordinates.
There is at most one dimension coord for each dimension, but there can be multiple non-dimension coords.
Operations such as value-based slicing that accept an input dimension and require lookup of coordinate values will only consider dimension coordinates.
As mentioned above, the difference between coords and attrs is “alignment”, i.e., only the former are compared in operations. The concept of dimension coords is unrelated to the distinction between coords
or attrs
. In particular, dimension coords could be made attrs if desired, and non-dimension coords can (and often are) “aligned” coords.
Dataset#
scipp.Dataset is a dict-like container of data arrays. Individual items of a dataset (“data arrays”) are accessed using a string as a dict key.
In a dataset the coordinates of the sub-arrays are enforced to be aligned. That is, a dataset is not actually just a dict of data arrays. Instead, the individual arrays share their coordinates. It is therefore not possible to combine arbitrary data arrays into a dataset. If, e.g., the extents in a certain dimension mismatch, or if coordinate values mismatch, insertion of the mismatching data array will fail.
Often a dataset is not created from individual data arrays. Instead we may provide a dict of variables (the data of the items), and dicts for coords:
[22]:
d = sc.Dataset(
data={
'a': sc.array(dims=['y', 'x'], values=np.random.rand(2, 3)),
'b': sc.array(dims=['y'], values=np.random.rand(2)),
'c': sc.scalar(value=1.0)},
coords={
'x': sc.array(dims=['x'], values=np.arange(3.0), unit='m'),
'y': sc.array(dims=['y'], values=np.arange(2.0), unit='m'),
'aux': sc.array(dims=['x'], values=np.random.rand(3))})
sc.show(d)
[23]:
d
[23]:
- y: 2
- x: 3
- aux(x)float64𝟙0.345, 0.222, 0.976
Values:
array([0.34529777, 0.22154399, 0.97618648]) - x(x)float64m0.0, 1.0, 2.0
Values:
array([0., 1., 2.]) - y(y)float64m0.0, 1.0
Values:
array([0., 1.])
- a(y, x)float64𝟙0.849, 0.517, ..., 0.213, 0.108
Values:
array([[0.84934576, 0.51697298, 0.14131557], [0.99419974, 0.21297476, 0.10793064]]) - b(y)float64𝟙0.521, 0.884
Values:
array([0.52066519, 0.88404255]) - c()float64𝟙1.0
Values:
array(1.)
[24]:
d.coords['x'].values
[24]:
array([0., 1., 2.])
The name of a data item serves as a dict key. Item access returns a new data array which is a view onto the data in the dataset and its corresponding coordinates, i.e., no deep copy is made:
[25]:
sc.show(d['a'])
d['a']
[25]:
- y: 2
- x: 3
- aux(x)float64𝟙0.345, 0.222, 0.976
Values:
array([0.34529777, 0.22154399, 0.97618648]) - x(x)float64m0.0, 1.0, 2.0
Values:
array([0., 1., 2.]) - y(y)float64m0.0, 1.0
Values:
array([0., 1.])
- (y, x)float64𝟙0.849, 0.517, ..., 0.213, 0.108
Values:
array([[0.84934576, 0.51697298, 0.14131557], [0.99419974, 0.21297476, 0.10793064]])
Use the copy()
method to turn the view into an independent object:
[26]:
copy_of_a = d['a'].copy()
copy_of_a += 17 # does not change d['a']
d
[26]:
- y: 2
- x: 3
- aux(x)float64𝟙0.345, 0.222, 0.976
Values:
array([0.34529777, 0.22154399, 0.97618648]) - x(x)float64m0.0, 1.0, 2.0
Values:
array([0., 1., 2.]) - y(y)float64m0.0, 1.0
Values:
array([0., 1.])
- a(y, x)float64𝟙0.849, 0.517, ..., 0.213, 0.108
Values:
array([[0.84934576, 0.51697298, 0.14131557], [0.99419974, 0.21297476, 0.10793064]]) - b(y)float64𝟙0.521, 0.884
Values:
array([0.52066519, 0.88404255]) - c()float64𝟙1.0
Values:
array(1.)
Each data item is linked to its corresponding coordinates, masks, and attributes. These are accessed using the coords
, masks
, and attrs
properties. The variable holding the data of the dataset item is accessible via the data
property:
[27]:
d['a'].data
[27]:
- (y: 2, x: 3)float64𝟙0.849, 0.517, ..., 0.213, 0.108
Values:
array([[0.84934576, 0.51697298, 0.14131557], [0.99419974, 0.21297476, 0.10793064]])
For convenience, properties of the data variable are also properties of the data item:
[28]:
d['a'].values
[28]:
array([[0.84934576, 0.51697298, 0.14131557],
[0.99419974, 0.21297476, 0.10793064]])
[29]:
d['a'].variances
[30]:
d['a'].unit
[30]:
dimensionless
Coordinates of a data item include only those that are relevant to the item’s dimensions, all others are hidden. For example, when accessing 'b'
, which does not depend on the 'y'
dimension, the coord for 'y'
as well as the 'aux'
coord are not part of the item’s coords
:
[31]:
sc.show(d['b'])
Similarly, when accessing a 0-dimensional data item, it will have no coordinates:
[32]:
sc.show(d['c'])
All variables in a dataset must have consistent dimensions. Thanks to labeled dimensions, transposed data is supported:
[33]:
d['d'] = sc.array(dims=['x', 'y'], values=np.random.rand(3, 2))
sc.show(d)
d
[33]:
- y: 2
- x: 3
- aux(x)float64𝟙0.345, 0.222, 0.976
Values:
array([0.34529777, 0.22154399, 0.97618648]) - x(x)float64m0.0, 1.0, 2.0
Values:
array([0., 1., 2.]) - y(y)float64m0.0, 1.0
Values:
array([0., 1.])
- a(y, x)float64𝟙0.849, 0.517, ..., 0.213, 0.108
Values:
array([[0.84934576, 0.51697298, 0.14131557], [0.99419974, 0.21297476, 0.10793064]]) - b(y)float64𝟙0.521, 0.884
Values:
array([0.52066519, 0.88404255]) - c()float64𝟙1.0
Values:
array(1.) - d(x, y)float64𝟙0.301, 0.441, ..., 0.493, 0.598
Values:
array([[0.30081691, 0.44110324], [0.68139092, 0.90399448], [0.49313428, 0.59843404]])
When inserting a data array or variable into a dataset ownership is shared by default. Use the copy()
method to avoid this if undesirable:
[34]:
d['a_shared'] = a
d['a_copied'] = a.copy()
a += 1000
d
[34]:
- y: 2
- x: 3
- aux(x)float64𝟙0.345, 0.222, 0.976
Values:
array([0.34529777, 0.22154399, 0.97618648]) - x(x)float64m0.0, 1.0, 2.0
Values:
array([0., 1., 2.]) - x2_copied(x)float64𝟙0.0, 0.0, 0.0
Values:
array([0., 0., 0.]) - x2_shared(x)float64𝟙123.0, 123.0, 123.0
Values:
array([123., 123., 123.]) - y(y)float64m0.0, 1.0
Values:
array([0., 1.])
- a(y, x)float64𝟙0.849, 0.517, ..., 0.213, 0.108
Values:
array([[0.84934576, 0.51697298, 0.14131557], [0.99419974, 0.21297476, 0.10793064]]) - a_copied(y, x)float64𝟙0.315, 0.481, ..., 0.833, 0.197
Values:
array([[0.31495581, 0.48141168, 0.16180807], [0.74403045, 0.83271888, 0.19725265]]) - a_shared(y, x)float64𝟙1000.315, 1000.481, ..., 1000.833, 1000.197
Values:
array([[1000.31495581, 1000.48141168, 1000.16180807], [1000.74403045, 1000.83271888, 1000.19725265]]) - b(y)float64𝟙0.521, 0.884
Values:
array([0.52066519, 0.88404255]) - c()float64𝟙1.0
Values:
array(1.) - d(x, y)float64𝟙0.301, 0.441, ..., 0.493, 0.598
Values:
array([[0.30081691, 0.44110324], [0.68139092, 0.90399448], [0.49313428, 0.59843404]])
The usual dict
-like methods are available for Dataset
:
[35]:
for name in d:
print(name)
a_copied
a_shared
c
b
d
a
[36]:
'a' in d
[36]:
True
[37]:
'e' in d
[37]:
False