Saving arrays

Beyond the basic data types of dictionaries, lists, strings and numbers, the most important thing ASDF can save is arrays. It’s as simple as putting a numpy array somewhere in the tree. Here, we save an 8x8 array of random floating-point numbers (using numpy.random.rand). Note that the resulting YAML output contains information about the structure (size and data type) of the array, but the actual array content is in a binary block.

from asdf import AsdfFile
import numpy as np

tree = {'my_array': np.random.rand(8, 8)}
ff = AsdfFile(tree)
ff.write_to("test.asdf")

Note

In the file examples below, the first YAML part appears as it appears in the file. The BLOCK sections are stored as binary data in the file, but are presented in human-readable form on this page.

test.asdf

#ASDF 1.0.0
#ASDF_STANDARD 1.5.0
%YAML 1.1
%TAG ! tag:stsci.edu:asdf/
--- !core/asdf-1.1.0
asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf',
  name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
history:
  extensions:
  - !core/extension_metadata-1.0.0
    extension_class: asdf.extension._manifest.ManifestExtension
    extension_uri: asdf://asdf-format.org/core/extensions/core-1.5.0
    software: !core/software-1.0.0 {name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
my_array: !core/ndarray-1.0.0
  source: 0
  datatype: float64
  byteorder: little
  shape: [8, 8]
...
BLOCK 0:
    allocated_size: 512
    used_size: 512
    data_size: 512
    data: b'4a08a116b3c4d93fc65f74c86eb3d43f3bc6e308...'
#ASDF BLOCK INDEX
%YAML 1.1
---
- 639
...

Sharing of data

Arrays that are views on the same data automatically share the same data in the file. In this example an array and a subview on that same array are saved to the same file, resulting in only a single block of data being saved.

from asdf import AsdfFile
import numpy as np

my_array = np.random.rand(8, 8)
subset = my_array[2:4,3:6]
tree = {
    'my_array': my_array,
    'subset':   subset
}
ff = AsdfFile(tree)
ff.write_to("test.asdf")

test.asdf

#ASDF 1.0.0
#ASDF_STANDARD 1.5.0
%YAML 1.1
%TAG ! tag:stsci.edu:asdf/
--- !core/asdf-1.1.0
asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf',
  name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
history:
  extensions:
  - !core/extension_metadata-1.0.0
    extension_class: asdf.extension._manifest.ManifestExtension
    extension_uri: asdf://asdf-format.org/core/extensions/core-1.5.0
    software: !core/software-1.0.0 {name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
my_array: !core/ndarray-1.0.0
  source: 0
  datatype: float64
  byteorder: little
  shape: [8, 8]
subset: !core/ndarray-1.0.0
  source: 0
  datatype: float64
  byteorder: little
  shape: [2, 3]
  offset: 152
  strides: [64, 8]
...
BLOCK 0:
    allocated_size: 512
    used_size: 512
    data_size: 512
    data: b'9ca352bc61e3c53f2801708a3fb1b83f3cd8e900...'
#ASDF BLOCK INDEX
%YAML 1.1
---
- 768
...

Saving inline arrays

For small arrays, you may not care about the efficiency of a binary representation and just want to save the array contents directly in the YAML tree. The set_array_storage method can be used to set the storage type of the associated data. The allowed values are internal, external, and inline.

  • internal: The default. The array data will be stored in a binary block in the same ASDF file.

  • external: Store the data in a binary block in a separate ASDF file (also known as “exploded” format, which discussed below in Saving external arrays).

  • inline: Store the data as YAML inline in the tree.

from asdf import AsdfFile
import numpy as np

my_array = np.random.rand(8, 8)
tree = {'my_array': my_array}
ff = AsdfFile(tree)
ff.set_array_storage(my_array, 'inline')
ff.write_to("test.asdf")

test.asdf

#ASDF 1.0.0
#ASDF_STANDARD 1.5.0
%YAML 1.1
%TAG ! tag:stsci.edu:asdf/
--- !core/asdf-1.1.0
asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf',
  name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
history:
  extensions:
  - !core/extension_metadata-1.0.0
    extension_class: asdf.extension._manifest.ManifestExtension
    extension_uri: asdf://asdf-format.org/core/extensions/core-1.5.0
    software: !core/software-1.0.0 {name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
my_array: !core/ndarray-1.0.0
  data:
  - [0.2827418677864709, 0.24328874263104638, 0.3832615995853801, 0.4876856179355231,
    0.9138599922953354, 0.6955135399532499, 0.5086620814032372, 0.22653607349205873]
  - [0.5818456883430602, 0.8871081098479454, 0.14166823556836527, 0.44499112479704694,
    0.0926880274357007, 0.17941898541320234, 0.19230616480915497, 0.2593207818300205]
  - [0.13188541364004103, 0.14186226766985288, 0.4470787714264438, 0.8790766133283316,
    0.15715923306476876, 0.9126043434090306, 0.5900262973305109, 0.04771275218290616]
  - [0.2131971000382965, 0.6740942742699377, 0.9827883687989025, 0.2803142102076336,
    0.5929920955679859, 0.36792170046531236, 0.6027679441046925, 0.08534322251686943]
  - [0.0992181342586469, 0.5504409213915363, 0.3759786605832516, 0.14774908551526755,
    0.8340836815794264, 0.8128650102890015, 0.8983821616627404, 0.5644454375851116]
  - [0.04742060384447866, 0.4794178547413649, 0.9838832990090164, 0.5413121955715712,
    0.47143886840674465, 0.6906004401802281, 0.1421931674064868, 0.2523618244223965]
  - [0.6001901331178287, 0.34625444163625807, 0.7428795534822507, 0.22141236601699965,
    0.44248482983334614, 0.608802510864367, 0.9297443206602461, 0.88278011317428]
  - [0.5852246542166063, 0.6630740000631068, 0.8655102338264197, 0.6070039806831004,
    0.46485689019910237, 0.12487752978106337, 0.31460792795541426, 0.11055715794986598]
  datatype: float64
  shape: [8, 8]
...

Alternatively, it is possible to use the all_array_storage parameter of AsdfFile.write_to and AsdfFile.update to control the storage format of all arrays in the file.

# This controls the output format of all arrays in the file
ff.write_to("test.asdf", all_array_storage='inline')

For automatic management of the array storage type based on number of elements, see array_inline_threshold.

Saving external arrays

ASDF files may also be saved in “exploded form”, which creates multiple files corresponding to the following data items:

  • One ASDF file containing only the header and tree.

  • n ASDF files, each containing a single array data block.

Exploded form is useful in the following scenarios:

  • Over a network protocol, such as HTTP, a client may only need to access some of the blocks. While reading a subset of the file can be done using HTTP Range headers, it still requires one (small) request per block to “jump” through the file to determine the start location of each block. This can become time-consuming over a high-latency network if there are many blocks. Exploded form allows each block to be requested directly by a specific URI.

  • An ASDF writer may stream a table to disk, when the size of the table is not known at the outset. Using exploded form simplifies this, since a standalone file containing a single table can be iteratively appended to without worrying about any blocks that may follow it.

To save a block in an external file, set its block type to 'external'.

from asdf import AsdfFile
import numpy as np

my_array = np.random.rand(8, 8)
tree = {'my_array': my_array}
ff = AsdfFile(tree)

# On an individual block basis:
ff.set_array_storage(my_array, 'external')
ff.write_to("test.asdf")

# Or for every block:
ff.write_to("test.asdf", all_array_storage='external')

test.asdf

#ASDF 1.0.0
#ASDF_STANDARD 1.5.0
%YAML 1.1
%TAG ! tag:stsci.edu:asdf/
--- !core/asdf-1.1.0
asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf',
  name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
history:
  extensions:
  - !core/extension_metadata-1.0.0
    extension_class: asdf.extension._manifest.ManifestExtension
    extension_uri: asdf://asdf-format.org/core/extensions/core-1.5.0
    software: !core/software-1.0.0 {name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
my_array: !core/ndarray-1.0.0
  source: test0000.asdf
  datatype: float64
  byteorder: little
  shape: [8, 8]
...

test0000.asdf

#ASDF 1.0.0
#ASDF_STANDARD 1.5.0
%YAML 1.1
%TAG ! tag:stsci.edu:asdf/
--- !core/asdf-1.1.0
asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf',
  name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
history:
  extensions:
  - !core/extension_metadata-1.0.0
    extension_class: asdf.extension._manifest.ManifestExtension
    extension_uri: asdf://asdf-format.org/core/extensions/core-1.5.0
    software: !core/software-1.0.0 {name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
...
BLOCK 0:
    allocated_size: 512
    used_size: 512
    data_size: 512
    data: b'796c7784c166e43fb83d2be5c5eee03f45f43d89...'
#ASDF BLOCK INDEX
%YAML 1.1
---
- 541
...

Streaming array data

In certain scenarios, you may want to stream data to disk, rather than writing an entire array of data at once. For example, it may not be possible to fit the entire array in memory, or you may want to save data from a device as it comes in to prevent data loss. The ASDF Standard allows exactly one streaming block per file where the size of the block isn’t included in the block header, but instead is implicitly determined to include all of the remaining contents of the file. By definition, it must be the last block in the file.

To use streaming, rather than including a Numpy array object in the tree, you include a asdf.tags.core.Stream object which sets up the structure of the streamed data, but will not write out the actual content. The file handle’s write method is then used to manually write out the binary data.

from asdf import AsdfFile
from asdf.tags.core import Stream
import numpy as np

tree = {
    # Each "row" of data will have 128 entries.
    'my_stream': Stream([128], np.float64)
}

ff = AsdfFile(tree)
with open('test.asdf', 'wb') as fd:
    ff.write_to(fd)
    # Write 100 rows of data, one row at a time.  ``write``
    # expects the raw binary bytes, not an array, so we use
    # ``tobytes()``.
    for i in range(100):
        fd.write(np.array([i] * 128, np.float64).tobytes())

test.asdf

#ASDF 1.0.0
#ASDF_STANDARD 1.5.0
%YAML 1.1
%TAG ! tag:stsci.edu:asdf/
--- !core/asdf-1.1.0
asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf',
  name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
history:
  extensions:
  - !core/extension_metadata-1.0.0
    extension_class: asdf.extension._manifest.ManifestExtension
    extension_uri: asdf://asdf-format.org/core/extensions/core-1.5.0
    software: !core/software-1.0.0 {name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
my_stream: !core/ndarray-1.0.0
  source: -1
  datatype: float64
  byteorder: little
  shape: ['*', 128]
...
BLOCK 0:
    flags: BLOCK_FLAG_STREAMED
    allocated_size: 0
    used_size: 0
    data_size: 0
    data: b'0000000000000000000000000000000000000000...'

A case where streaming may be useful is when converting large data sets from a different format into ASDF. In these cases it would be impractical to hold all of the data in memory as an intermediate step. Consider the following example that streams a large CSV file containing rows of integer data and converts it to numpy arrays stored in ASDF:

import csv
import numpy as np
from asdf import AsdfFile
from asdf.tags.core import Stream

tree = {
    # We happen to know in advance that each row in the CSV has 100 ints
    'data': Stream([100], np.int64)
}

ff = AsdfFile(tree)
# open the output file handle
with open('new_file.asdf', 'wb') as fd:
    ff.write_to(fd)
    # open the CSV file to be converted
    with open('large_file.csv', 'r') as cfd:
        # read each line of the CSV file
        reader = csv.reader(cfd)
        for row in reader:
            # convert each row to a numpy array
            array = np.array([int(x) for x in row], np.int64)
            # write the array to the output file handle
            fd.write(array.tobytes())

Compression

Individual blocks in an ASDF file may be compressed.

You can easily zlib or bzip2 compress all blocks:

from asdf import AsdfFile
import numpy as np

tree = {
    'a': np.random.rand(32, 32),
    'b': np.random.rand(64, 64)
}

target = AsdfFile(tree)
target.write_to('target.asdf', all_array_compression='zlib')
target.write_to('target.asdf', all_array_compression='bzp2')

target.asdf

#ASDF 1.0.0
#ASDF_STANDARD 1.5.0
%YAML 1.1
%TAG ! tag:stsci.edu:asdf/
--- !core/asdf-1.1.0
asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf',
  name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
history:
  extensions:
  - !core/extension_metadata-1.0.0
    extension_class: asdf.extension._manifest.ManifestExtension
    extension_uri: asdf://asdf-format.org/core/extensions/core-1.5.0
    software: !core/software-1.0.0 {name: asdf, version: 3.1.1.dev0+gb499251.d20240227}
a: !core/ndarray-1.0.0
  source: 0
  datatype: float64
  byteorder: little
  shape: [32, 32]
b: !core/ndarray-1.0.0
  source: 1
  datatype: float64
  byteorder: little
  shape: [64, 64]
...
BLOCK 0:
    compression: b'bzp2'
    allocated_size: 8307
    used_size: 8307
    data_size: 8192
    data: b'204bc9f70f09b33feea982dc12a9d53f99ab074a...'
BLOCK 1:
    compression: b'bzp2'
    allocated_size: 31763
    used_size: 31763
    data_size: 32768
    data: b'0f8b329e8354ec3fec7a94cf39a5cd3fefa20207...'
#ASDF BLOCK INDEX
%YAML 1.1
---
- 727
- 9088
...

The lz4 compression algorithm is also supported, but requires the optional lz4 package in order to work.

When reading a file with compressed blocks, the blocks will be automatically decompressed when accessed. If a file with compressed blocks is read and then written out again, by default the new file will use the same compression as the original file. This behavior can be overridden by explicitly providing a different compression algorithm when writing the file out again.

import asdf

# Open a file with some compression
af = asdf.open('compressed.asdf')

# Use the same compression when writing out a new file
af.write_to('same.asdf')

# Or specify the (possibly different) algorithm to use when writing out
af.write_to('different.asdf', all_array_compression='lz4')

Memory mapping

By default, all internal array data is memory mapped using numpy.memmap. This allows for the efficient use of memory even when reading files with very large arrays. The use of memory mapping means that the following usage pattern is not permitted:

import asdf

with asdf.open('my_data.asdf') as af:
    ...

af.tree

Specifically, if an ASDF file has been opened using a with context, it is not possible to access the file contents outside of the scope of that context, because any memory mapped arrays will no longer be available.

It may sometimes be useful to copy array data into memory instead of using memory maps. This can be controlled by passing the copy_arrays parameter to either the AsdfFile constructor or asdf.open. By default, copy_arrays=False.