xarray structure how to

Tip

Checkout the interactive notebook in your web browser to try this functionality yourself

An xarray instance is meant to be the primary data and metadata storage of one CTD cast. In Sea-Bird terms you can think of it to be the python internal equivalent of a .cnv file.


input parsing

A cf-compliant xarray can be retrieved by conversion or by parsing a file, but the usage is the same:

from ctdam import parse

ds = parse('sbs_data/cnv/EMB356_11-1.cnv')
ds = parse('sbs_data/hex/EMB356_11-1.hex')
ds = parse("sbs_data/other/IB051044.TOB")

output parsing

At the moment, you can parse an xarray to Sea-Birds .cnv format

ds.export.to_cnv()

as well as NetCDF

ds.to_netcdf()

Information to access

>>> print(ds)
<xarray.Dataset> Size: 380kB
Dimensions:                          (sensor: 2, scan: 3067)
Coordinates:
* sensor                           (sensor) <U9 72B 'primary' 'secondary'
    time                             (scan) float64 25kB 1.707e+09 ... 1.707e+09
Dimensions without coordinates: scan
Data variables: (12/17)
    pressure                         (scan) float64 25kB 0.648 0.696 ... 23.06
    pressure_qc                      (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    temperature                      (scan, sensor) float64 49kB 3.562 ... 3.744
    temperature_qc                   (scan, sensor) int8 6kB 0 0 0 0 ... 0 0 0 0
    conductivity                     (scan, sensor) float64 49kB 15.33 ... 17.36
    conductivity_qc                  (scan, sensor) int8 6kB 0 0 0 0 ... 0 0 0 0
    ...                               ...
    turbidity_qc                     (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    par_biosphericallicorchelsea     (scan) float64 25kB 9.24 9.267 ... 0.04539
    par_biosphericallicorchelsea_qc  (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    salinity                         (scan, sensor) float64 49kB 15.55 ... 17.69
    salinity_qc                      (scan, sensor) int8 6kB 0 0 0 0 ... 0 0 0 0
    flag                             (scan) float64 25kB 0.0 0.0 0.0 ... 0.0 0.0
Attributes:
    start_time:           2024-02-08 08:31:25
    position:             (54.155166666666666, 11.293333333333333)
    cruise:               EMB356
    station:              EMB356_11-1
    path_to_source_file:  /home/emil/Projects/ctdam/sbs_data/cnv/EMB356_11-1.cnv
    sample_rate:
    instrument_metadata:  Sea-Bird SBE 9 Data File:\nFileName = C:\CTD\CTD_Da...
    custom_metadata:      Cruise = EMB356\nStation = EMB356_11-1\nPlatform = ...
    sensor_metadata:      <Sensors count="15" >\n  <sensor Channel="1" >\n   ...
    provenance_metadata:  datcnv_date = Nov 17 2025 13:21:17, 7.26.7.129 [dat...

You can display all kinds of information from inside the source .hex or .cnv files, like header, custom metadata, file_name and much more:

>>> ds.attrs['instrument_metadata']
['* Sea-Bird SBE 9 Data File:\n', '* FileName = C:\\CTD\\CTD_Data\\EMB356\\E
MB356_011-01_CTD_0010.hex\n', '* Software Version Seasave V 7.26.7.121\n',
....]

>>> ds.meta.custom
{'Cruise': 'EMB356', 'Station': 'EMB356_11-1', 'Platform': 'CTD', 'Cast': '0
010', 'Operator': 'Johann Ruickoldt', 'GPS_Time': '08.02.2024 08:30:10', 'GP
S_Lat': '54  9.308 N', 'GPS_Lon': '11 17.587 E', 'Echo_Depth': '25.7 m', 'Ai
r_Pressure': '1000.8 hPa', 'WsStartID': '251', 'Pos_Alias': 'TF0021'}

>>> ds.access.path
Path('sbs_data/hex/EMB356_11-1.hex')

>>> ds.meta.provenance.keys()
[hex2py, wildedit, wfilter, alignctd, celltm, binning]

>>> ds.meta.provenance['binning']
{'metainfo': '2026.02.19 11:35:18, ctdam python package, v1.4.1', 'bin_varia
ble': 'pressure', 'bin_size': '1', 'cast_type': 'down'}

Functionality

As CTD is often measured using a dual-sensor setup, a few handy functions have been written to work with dual-sensor data. In general, the data of two sensors is saved inside the same xarray variable.

>>> ds.temperature
<xarray.DataArray 'temperature' (scan: 3067, sensor: 2)> Size: 49kB
 array([[3.5616, 3.561 ],
     [3.5614, 3.561 ],
     [3.5617, 3.5609],
     ...,
     [3.7476, 3.7436],
     [3.7475, 3.7433],
     [3.7473, 3.7436]], shape=(3067, 2))
 Coordinates:
     time     (scan) float64 25kB 1.707e+09 1.707e+09 ... 1.707e+09 1.707e+09
 * sensor   (sensor) <U9 72B 'primary' 'secondary'
 Dimensions without coordinates: scan
 Attributes:
     standard_name:        sea_water_temperature
     units:                degree_C
     ancillary_variables:  temperature_qc

You can specificly access only the primary sensor data via:

>>> ds.temperature.sel(sensor='primary')
<xarray.DataArray 'temperature' (scan: 3067)> Size: 25kB
array([3.5616, 3.5614, 3.5617, ..., 3.7476, 3.7475, 3.7473], shape=(3067,))
Coordinates:
    time     (scan) float64 25kB 1.707e+09 1.707e+09 ... 1.707e+09 1.707e+09
    sensor   <U9 36B 'primary'
Dimensions without coordinates: scan
Attributes:
    standard_name:        sea_water_temperature
    units:                degree_C
    ancillary_variables:  temperature_qc

If you want to access the whole secondary sensor strand, you can also do so:

>>> ds.access.sensor_strand('secondary')
<xarray.Dataset> Size: 270kB
Dimensions:                          (scan: 3067)
Coordinates:
    time                             (scan) float64 25kB 1.707e+09 ... 1.707e+09
Dimensions without coordinates: scan
Data variables: (12/17)
    pressure                         (scan) float64 25kB 0.648 0.696 ... 23.06
    pressure_qc                      (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    temperature                      (scan) float64 25kB 3.561 3.561 ... 3.744
    temperature_qc                   (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    conductivity                     (scan) float64 25kB 15.33 15.33 ... 17.36
    conductivity_qc                  (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    ...                               ...
    turbidity_qc                     (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    par_biosphericallicorchelsea     (scan) float64 25kB 9.24 9.267 ... 0.04539
    par_biosphericallicorchelsea_qc  (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    salinity                         (scan) float64 25kB 15.55 15.55 ... 17.69
    salinity_qc                      (scan) int8 3kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
    flag                             (scan) float64 25kB 0.0 0.0 0.0 ... 0.0 0.0

>>> ds.access.sensor_strand('secondary').salinity
<xarray.DataArray 'salinity' (scan: 3067)> Size: 25kB
array([15.5515, 15.5517, 15.5517, ..., 17.694 , 17.6903, 17.6891],
    shape=(3067,))
Coordinates:
    time     (scan) float64 25kB 1.707e+09 1.707e+09 ... 1.707e+09 1.707e+09
Dimensions without coordinates: scan
Attributes:
    standard_name:        sea_water_practical_salinity
    units:                PSU
    ancillary_variables:  salinity_qc

Apart from this custom functionality, there is a ton of features you can do out of the box on xarrays, so its worth checking out their documentation. Additionally, you can use all gsw functions on-top of your arrays, like so:

ds.gsw.sigma0()

And through cf-compliance, the correct variables will be picked automatically. This is made possible by gsw-xarray, which is an amazing python package and is included in ctdam. For ease of use, you can calculate the base TEOS-10 variables, absolute salinity, conservative temperature and density, via this handy shortcut:

ds.add.teos10_vars()