Skip to content

Add support for ASDF serialisation and deserialisation #776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 80 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
ac36722
initial commit
ViciousEagle03 May 20, 2024
6be61e7
version alignment
ViciousEagle03 May 20, 2024
6e619fc
Remove method to remove any delay and
ViciousEagle03 May 22, 2024
f505703
Update tag and
ViciousEagle03 May 22, 2024
af2f9c7
enable schema test
ViciousEagle03 May 22, 2024
adfc6b3
revert small change
ViciousEagle03 May 22, 2024
78eeb53
Add the converters for ExtraCoords and TimeTableCoordinate class
ViciousEagle03 May 30, 2024
0804f1d
Add ExtraCoords and TimeTableCoord Schema
ViciousEagle03 May 30, 2024
0f4a86f
Update manifest and entry_point.py
ViciousEagle03 May 30, 2024
86bfac9
Add the QuantityTableCoordinate and SkyCoordTableCoordinate converters
ViciousEagle03 May 31, 2024
da1c14e
Add the schema for QuantityTableCoordinate and SkyCoordTableCoordinate
ViciousEagle03 May 31, 2024
dc24d40
Update manifest and entry_point.py
ViciousEagle03 May 31, 2024
ef208ed
Add validation for schema and manifests
ViciousEagle03 Jun 4, 2024
c3d2606
pre-commit
Cadair Jun 5, 2024
86c2b6c
Update the tox.ini and CI workflow
ViciousEagle03 Jun 5, 2024
3a875ab
Update schemas
ViciousEagle03 Jun 5, 2024
ce5a14b
Add the converter and schema for GlobalCoords
ViciousEagle03 Jun 17, 2024
9247b46
Update converters
ViciousEagle03 Jun 17, 2024
c8a1bc4
Update entry_points,schemas and manifest
ViciousEagle03 Jun 17, 2024
c681cb2
minor change
ViciousEagle03 Jun 19, 2024
42c590e
lowercase schema URIs and use wildcards
ViciousEagle03 Jun 21, 2024
7f3a81b
Add tests and GWCS objects
ViciousEagle03 Jul 1, 2024
7622950
apply suggestions from code review
ViciousEagle03 Jul 6, 2024
f89e195
Remove mesh as a property validator
ViciousEagle03 Jul 11, 2024
8b519ae
Update the dependencies version
ViciousEagle03 Jul 11, 2024
37bf563
Style changes and add warnings to NDCube converter
ViciousEagle03 Jul 19, 2024
693e63e
env name update
ViciousEagle03 Jul 19, 2024
de6944e
Add asdf as an optional dep
ViciousEagle03 Aug 4, 2024
22a839e
Remove asdf as an optional dependency
ViciousEagle03 Aug 5, 2024
ec863ad
Add support for meta in converter and schema
ViciousEagle03 Aug 6, 2024
b2fd5e1
Add mask as a validator property
ViciousEagle03 Aug 21, 2024
6d81ee9
Add unit support to asdf
Cadair Aug 22, 2024
a6a33be
Add the serialization support for the ResampledLowlevelWCS ReorderedL…
ViciousEagle03 Aug 16, 2024
95486b0
Apply suggestions from code review
ViciousEagle03 Aug 23, 2024
4fb5288
Use Tabular1D, Tabular2D instead of tabular_model
ViciousEagle03 Aug 25, 2024
6cf9602
Add comment and update logic
ViciousEagle03 Aug 25, 2024
ddc75be
Apply minor suggestions from code review
DanRyanIrish Nov 4, 2024
e7cf14f
More minor tweaks
Cadair Nov 6, 2024
7ee5048
Merge pull request #751 from ViciousEagle03/wcs_wrapper_asdf_support
Cadair Nov 6, 2024
8610805
Add validation for schema and manifests
ViciousEagle03 Jun 4, 2024
149944f
revert small change
ViciousEagle03 Jul 4, 2024
98e633c
Add asdf as an optional dep
ViciousEagle03 Aug 4, 2024
789e841
Remove asdf as an optional dependency
ViciousEagle03 Aug 5, 2024
24af793
Add serialization logic for the NDCubeSequence and NDCollection
ViciousEagle03 Aug 24, 2024
494e2c5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 6, 2024
db8f22d
Minor tweaks
Cadair Nov 6, 2024
3876924
Merge pull request #756 from ViciousEagle03/asdf_ndcube_sequence_and_…
Cadair Nov 7, 2024
1b9679a
Merge branch 'main' into asdf-support
Cadair Nov 7, 2024
082d8c6
Merge branch 'main' into asdf-support
Cadair Nov 13, 2024
9732fa9
Merge branch 'main' into asdf-support
Cadair May 15, 2025
46215b0
Update pytest.ini
Cadair May 15, 2025
1b72253
fix pre-commit
Cadair May 26, 2025
e7c4c04
Fix schema
Cadair May 26, 2025
61cc81d
Fix uncertainty, fits wcs etc
Cadair May 15, 2025
d9b4b80
Allow FITS WCS in more places
Cadair May 26, 2025
ee48a53
Fix tests while awaiting astropy/asdf-astropy#276
Cadair May 26, 2025
eea3e40
Merge pull request #842 from Cadair/add_uncertainty
Cadair May 26, 2025
675bf1d
Rough start at basic docs
SolarDrew May 22, 2025
1cf3b18
Update ndcube/asdf/converters/compoundwcs_converter.py
Cadair May 26, 2025
e70a736
More details on what's supported
Cadair May 26, 2025
1d92daf
Add support for MultipleTableCoordinate
Cadair May 26, 2025
50a3cdd
More docs and test fixes
Cadair May 26, 2025
ce0556d
Add support for MTC and tests
Cadair May 26, 2025
45f6d55
Merge pull request #843 from SolarDrew/asdf-support
Cadair May 26, 2025
350d7ce
Bump versions of all schemas etc
Cadair May 26, 2025
f1cec49
Add changelog
Cadair May 26, 2025
f430969
Bump various deps based on SPEC-0
Cadair May 26, 2025
38e2680
American I guess
Cadair May 26, 2025
da2b74f
Update 776.feature.rst
Cadair May 26, 2025
3754a61
Update docs/explaining_ndcube/asdf_serialization.rst
Cadair May 26, 2025
5fac69a
More test fixes
Cadair May 26, 2025
81677e9
Allow bool for mask
Cadair May 26, 2025
b90a2ff
Add support for NDMeta
Cadair May 26, 2025
ad258e5
Allow FITS WCS in ExtraCoords ASDF schema
Cadair May 26, 2025
e2c24f2
Change NDCollection to save a dict not kv pairs
Cadair May 27, 2025
85f93f7
Apply suggestions from code review
Cadair May 27, 2025
b56c702
Apply suggestions from code review
Cadair Jun 2, 2025
0762b8d
Only add meta, global and extra to ndcube if populated
Cadair Jun 2, 2025
eb0c6f3
More lax schemas
Cadair Jun 3, 2025
2559057
no property order
Cadair Jun 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
- windows: py311
- macos: py310
- linux: py310-oldestdeps
- linux: asdf_schemas
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Expand Down
9 changes: 9 additions & 0 deletions changelog/776.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The minimum supported version of some dependencies has increased:

* astropy >= 5.3
* gwcs >= 0.20
* numpy >= 1.25
* scipy >= 1.11
* matplotlib >= 3.8
* mpl_animators >= 1.1
* reproject >= 0.11
1 change: 1 addition & 0 deletions changelog/776.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for serialization of most `ndcube` objects to ASDF files.
3 changes: 2 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
'sunpy': ('https://docs.sunpy.org/en/stable/', None),
'mpl_animators': ('https://docs.sunpy.org/projects/mpl-animators/en/stable/', None),
'gwcs': ('https://gwcs.readthedocs.io/en/stable/', None),
'reproject': ("https://reproject.readthedocs.io/en/stable/", None)
'reproject': ("https://reproject.readthedocs.io/en/stable/", None),
'asdf': ("https://www.asdf-format.org/projects/asdf/en/stable/", None),
}

# -- Options for HTML output -------------------------------------------------
Expand Down
67 changes: 67 additions & 0 deletions docs/explaining_ndcube/asdf_serialization.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
.. _asdf_serialization:

*************************
Saving ND objects to ASDF
*************************

:ref:`asdf` is an extensible format for validating and saving complex scientific data along with its metadata.
`ndcube` provides schemas and converters for all the ND objects (`~ndcube.NDCube`, `~ndcube.NDCubeSequence` and `~ndcube.NDCollection`) as well as for various WCS and table objects required by them.
To make use of these, simply save an ND object to an ASDF file and it will be correctly serialized.
ASDF files save a "tree" which is a `dict`.
You can save any number of cubes in your ASDF by adding them to the dictionary.

.. expanding-code-block:: python
:summary: Click to reveal/hide instantiation of the NDCube.

>>> import numpy as np
>>> import asdf
>>> import astropy.wcs
>>> from ndcube import NDCube

>>> # Define data array.
>>> data = np.random.rand(4, 4, 5)

>>> # Define WCS transformations in an astropy WCS object.
>>> wcs = astropy.wcs.WCS(naxis=3)
>>> wcs.wcs.ctype = 'WAVE', 'HPLT-TAN', 'HPLN-TAN'
>>> wcs.wcs.cunit = 'Angstrom', 'deg', 'deg'
>>> wcs.wcs.cdelt = 0.2, 0.5, 0.4
>>> wcs.wcs.crpix = 0, 2, 2
>>> wcs.wcs.crval = 10, 0.5, 1
>>> wcs.wcs.cname = 'wavelength', 'HPC lat', 'HPC lon'

>>> # Now instantiate the NDCube
>>> my_cube = NDCube(data, wcs=wcs)


.. code-block:: python

>>> my_tree = {"mycube": my_cube}
>>> with asdf.AsdfFile(tree=my_tree) as f: # doctest: +SKIP
... f.write_to("somefile.asdf") # doctest: +SKIP


What's Supported and What Isn't
===============================

We aim to support all features of `ndcube` when saving and loading to ASDF.
However, because it is possible to create `ndcube` objects with many different components (for example dask arrays) which aren't part of the `ndcube` package these may not be supported.
Many common components of `ndcube` classes are supported in the `asdf_astropy <https://asdf-astropy.readthedocs.io/en/stable/>`__ package, such as `astropy.wcs.WCS`, `astropy.wcs.wcsapi.SlicedLowLevelWCS` and uncertainty objects.

The only component of the `ndcube.NDCube` class which is never saved is the ``.psf`` attribute.

`ndcube` implements converters and schemas for the following objects:

* `~ndcube.NDCube`
* `~ndcube.NDCubeSequence`
* `~ndcube.NDCollection`
* `~ndcube.NDMeta`
* `~ndcube.GlobalCoords`
* `~ndcube.ExtraCoords`
* `~ndcube.extra_coords.TimeTableCoordinate`
* `~ndcube.extra_coords.QuantityTableCoordinate`
* `~ndcube.extra_coords.SkyCoordTableCoordinate`
* `~ndcube.extra_coords.MultipleTableCoordinate`
* `~ndcube.wcs.wrappers.ReorderedLowLevelWCS`
* `~ndcube.wcs.wrappers.ResampledLowLevelWCS`
* `~ndcube.wcs.wrappers.CompoundLowLevelWCS`
1 change: 1 addition & 0 deletions docs/explaining_ndcube/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ Explaining ``ndcube``
tabular_coordinates
reproject
visualization
asdf_serialization
Empty file added ndcube/asdf/__init__.py
Empty file.
Empty file.
18 changes: 18 additions & 0 deletions ndcube/asdf/converters/compoundwcs_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from asdf.extension import Converter


class CompoundConverter(Converter):
tags = ["tag:sunpy.org:ndcube/compoundwcs-*"]
types = ["ndcube.wcs.wrappers.compound_wcs.CompoundLowLevelWCS"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.wcs.wrappers import CompoundLowLevelWCS

return CompoundLowLevelWCS(*node["wcs"], mapping=node.get("mapping"), pixel_atol=node.get("atol"))

def to_yaml_tree(self, compoundwcs, tag, ctx):
node = {}
node["wcs"] = compoundwcs._wcs
node["mapping"] = compoundwcs.mapping.mapping
node["atol"] = compoundwcs.atol
return node
28 changes: 28 additions & 0 deletions ndcube/asdf/converters/extracoords_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from asdf.extension import Converter


class ExtraCoordsConverter(Converter):
tags = ["tag:sunpy.org:ndcube/extra_coords/extra_coords/extracoords-*"]
types = ["ndcube.extra_coords.extra_coords.ExtraCoords"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.extra_coords.extra_coords import ExtraCoords
extra_coords = ExtraCoords()
extra_coords._wcs = node.get("wcs")
extra_coords._mapping = node.get("mapping")
extra_coords._lookup_tables = node.get("lookup_tables", [])
extra_coords._dropped_tables = node.get("dropped_tables")
extra_coords._ndcube = node.get("ndcube")
return extra_coords

def to_yaml_tree(self, extracoords, tag, ctx):
node = {}
if extracoords._wcs is not None:
node["wcs"] = extracoords._wcs

Check warning on line 21 in ndcube/asdf/converters/extracoords_converter.py

View check run for this annotation

Codecov / codecov/patch

ndcube/asdf/converters/extracoords_converter.py#L21

Added line #L21 was not covered by tests
if extracoords._mapping is not None:
node["mapping"] = extracoords._mapping

Check warning on line 23 in ndcube/asdf/converters/extracoords_converter.py

View check run for this annotation

Codecov / codecov/patch

ndcube/asdf/converters/extracoords_converter.py#L23

Added line #L23 was not covered by tests
if extracoords._lookup_tables:
node["lookup_tables"] = extracoords._lookup_tables
node["dropped_tables"] = extracoords._dropped_tables
node["ndcube"] = extracoords._ndcube
return node
24 changes: 24 additions & 0 deletions ndcube/asdf/converters/globalcoords_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from asdf.extension import Converter


class GlobalCoordsConverter(Converter):
tags = ["tag:sunpy.org:ndcube/global_coords/globalcoords-*"]
types = ["ndcube.global_coords.GlobalCoords"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.global_coords import GlobalCoords

globalcoords = GlobalCoords()
if "internal_coords" in node:
globalcoords._internal_coords = node["internal_coords"]
globalcoords._ndcube = node["ndcube"]

return globalcoords

def to_yaml_tree(self, globalcoords, tag, ctx):
node = {}
node["ndcube"] = globalcoords._ndcube
if globalcoords._internal_coords:
node["internal_coords"] = globalcoords._internal_coords

return node
25 changes: 25 additions & 0 deletions ndcube/asdf/converters/ndcollection_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from asdf.extension import Converter


class NDCollectionConverter(Converter):
tags = ["tag:sunpy.org:ndcube/ndcube/ndcollection-*"]
types = ["ndcube.ndcollection.NDCollection"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.ndcollection import NDCollection

key_value_pairs = list(zip(node["keys"], node["value"]))
aligned_axes = list(node.get("aligned_axes").values())
aligned_axes = tuple(tuple(lst) for lst in aligned_axes)
return NDCollection(key_value_pairs, meta=node.get("meta"), aligned_axes=aligned_axes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that every future metadata object needs its own schema to be saved and read?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well yes and no, anything custom will need one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also goes for unsaveable values in meta. i.e. if you put a dask array in your meta dict it will break.


def to_yaml_tree(self, ndcollection, tag, ctx):
node = {}
node["keys"] = tuple(ndcollection.keys())
node["value"] = tuple(ndcollection.values())
if ndcollection.meta is not None:
node["meta"] = ndcollection.meta

Check warning on line 21 in ndcube/asdf/converters/ndcollection_converter.py

View check run for this annotation

Codecov / codecov/patch

ndcube/asdf/converters/ndcollection_converter.py#L21

Added line #L21 was not covered by tests
if ndcollection._aligned_axes is not None:
node["aligned_axes"] = ndcollection._aligned_axes

return node
65 changes: 65 additions & 0 deletions ndcube/asdf/converters/ndcube_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import warnings

from asdf.extension import Converter


class NDCubeConverter(Converter):
tags = ["tag:sunpy.org:ndcube/ndcube/ndcube-*"]
types = ["ndcube.ndcube.NDCube"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.ndcube import NDCube

ndcube = NDCube(
node["data"],
node["wcs"],
meta=node.get("meta"),
mask=node.get("mask"),
unit=node.get("unit"),
uncertainty=node.get("uncertainty"),
)
if "extra_coords" in node:
ndcube._extra_coords = node["extra_coords"]
if "global_coords" in node:
ndcube._global_coords = node["global_coords"]

return ndcube

def to_yaml_tree(self, ndcube, tag, ctx):
"""
Notes
-----
This methods serializes the primary components of the NDCube object,
including the `data`, `wcs`, `extra_coords`, and `global_coords` attributes.
Issues a warning if unsupported attributes are present.

Warnings
--------
UserWarning
Warns if the NDCube object has a 'psf' attribute that will not be
saved in the ASDF serialization.
This ensures that users are aware of potentially important information
that is not included in the serialized output.
"""
from astropy.wcs.wcsapi import BaseHighLevelWCS

node = {}
node["data"] = ndcube.data
if isinstance(ndcube.wcs, BaseHighLevelWCS):
node["wcs"] = ndcube.wcs.low_level_wcs
else:
node["wcs"] = ndcube.wcs

Check warning on line 51 in ndcube/asdf/converters/ndcube_converter.py

View check run for this annotation

Codecov / codecov/patch

ndcube/asdf/converters/ndcube_converter.py#L51

Added line #L51 was not covered by tests
node["extra_coords"] = ndcube.extra_coords
node["global_coords"] = ndcube.global_coords
node["meta"] = ndcube.meta
if ndcube.mask is not None:
node["mask"] = ndcube.mask
if ndcube.unit is not None:
node["unit"] = ndcube.unit
if ndcube.uncertainty is not None:
node["uncertainty"] = ndcube.uncertainty

if getattr(ndcube, 'psf') is not None:
warnings.warn("Attribute 'psf' is present but not being saved in ASDF serialization.", UserWarning)

Check warning on line 63 in ndcube/asdf/converters/ndcube_converter.py

View check run for this annotation

Codecov / codecov/patch

ndcube/asdf/converters/ndcube_converter.py#L63

Added line #L63 was not covered by tests

return node
23 changes: 23 additions & 0 deletions ndcube/asdf/converters/ndcubesequence_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from asdf.extension import Converter


class NDCubeSequenceConverter(Converter):
tags = ["tag:sunpy.org:ndcube/ndcube/ndcube_sequence-*"]
types = ["ndcube.ndcube_sequence.NDCubeSequence"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.ndcube_sequence import NDCubeSequence

return NDCubeSequence(node["data"],
meta=node.get("meta"),
common_axis=node.get("common_axis"))

def to_yaml_tree(self, ndcseq, tag, ctx):
node = {}
node["data"] = ndcseq.data
if ndcseq.meta is not None:
node["meta"] = ndcseq.meta

Check warning on line 19 in ndcube/asdf/converters/ndcubesequence_converter.py

View check run for this annotation

Codecov / codecov/patch

ndcube/asdf/converters/ndcubesequence_converter.py#L19

Added line #L19 was not covered by tests
if ndcseq._common_axis is not None:
node["common_axis"] = ndcseq._common_axis

return node
24 changes: 24 additions & 0 deletions ndcube/asdf/converters/ndmeta_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import numpy as np

from asdf.extension import Converter


class NDMetaConverter(Converter):
tags = ["tag:sunpy.org:ndcube/meta/ndmeta-*"]
types = ["ndcube.meta.NDMeta"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.meta import NDMeta
axes = {k: np.array(v) for k, v in node["axes"].items()}
meta = NDMeta(node["meta"], node["key_comments"], axes, node["data_shape"])
meta._original_meta = node["original_meta"]
return meta

def to_yaml_tree(self, meta, tag, ctx):
node = {}
node["meta"] = dict(meta)
node["key_comments"] = meta.key_comments
node["axes"] = meta.axes
node["data_shape"] = meta.data_shape
node["original_meta"] = meta._original_meta # not the MappingProxy object
return node
22 changes: 22 additions & 0 deletions ndcube/asdf/converters/reorderedwcs_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from asdf.extension import Converter


class ReorderedConverter(Converter):
tags = ["tag:sunpy.org:ndcube/reorderedwcs-*"]
types = ["ndcube.wcs.wrappers.reordered_wcs.ReorderedLowLevelWCS"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.wcs.wrappers import ReorderedLowLevelWCS

return ReorderedLowLevelWCS(
wcs=node["wcs"],
pixel_order=node.get("pixel_order"),
world_order=node.get("world_order"),
)

def to_yaml_tree(self, reorderedwcs, tag, ctx):
node = {}
node["wcs"] = reorderedwcs._wcs
node["pixel_order"] = reorderedwcs._pixel_order
node["world_order"] = reorderedwcs._world_order
return node
23 changes: 23 additions & 0 deletions ndcube/asdf/converters/resampled_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from asdf.extension import Converter


class ResampledConverter(Converter):
tags = ["tag:sunpy.org:ndcube/resampledwcs-*"]
types = ["ndcube.wcs.wrappers.resampled_wcs.ResampledLowLevelWCS"]

def from_yaml_tree(self, node, tag, ctx):
from ndcube.wcs.wrappers import ResampledLowLevelWCS

return ResampledLowLevelWCS(
wcs=node["wcs"],
offset=node.get("offset"),
factor=node.get("factor"),
)

def to_yaml_tree(self, resampledwcs, tag, ctx):
node = {}
node["wcs"] = resampledwcs._wcs
node["factor"] = resampledwcs._factor
node["offset"] = resampledwcs._offset

return node
Loading