-
-
Notifications
You must be signed in to change notification settings - Fork 53
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
base: main
Are you sure you want to change the base?
Changes from 90 commits
ac36722
6be61e7
6e619fc
f505703
af2f9c7
adfc6b3
78eeb53
0804f1d
0f4a86f
86bfac9
da1c14e
dc24d40
ef208ed
c3d2606
86c2b6c
3a875ab
ce5a14b
9247b46
c8a1bc4
c681cb2
42c590e
7f3a81b
7622950
f89e195
8b519ae
37bf563
693e63e
de6944e
22a839e
ec863ad
b2fd5e1
6d81ee9
a6a33be
95486b0
4fb5288
6cf9602
ddc75be
e7cf14f
7ee5048
8610805
149944f
98e633c
789e841
24af793
494e2c5
db8f22d
3876924
1b9679a
082d8c6
9732fa9
46215b0
1b72253
e7c4c04
61cc81d
d9b4b80
ee48a53
eea3e40
675bf1d
1cf3b18
e70a736
1d92daf
50a3cdd
ce0556d
45f6d55
350d7ce
f1cec49
f430969
38e2680
da2b74f
3754a61
5fac69a
81677e9
b90a2ff
ad258e5
e2c24f2
85f93f7
b56c702
0762b8d
eb0c6f3
2559057
c9b4611
ff4b79f
fa41ede
3ead8ad
b467dab
771c1a2
a59c901
a66756e
cc8d915
504f230
ba8e495
b720b8a
b9988e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add support for serialization of most `ndcube` objects to ASDF files. |
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 | ||||||||
|
>>> with asdf.AsdfFile(tree=my_tree) as f: # doctest: +SKIP | |
... f.write_to("somefile.asdf") # doctest: +SKIP | |
>>> asdf.AsdfFile(tree=my_tree).write_to("somefile.asdf"): # doctest: +SKIP |
No need for the with
here since the AsdfFile
instance isn't holding onto a file.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,3 +14,4 @@ Explaining ``ndcube`` | |
tabular_coordinates | ||
reproject | ||
visualization | ||
asdf_serialization |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are imports inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because these files are imported at There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
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 |
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 | ||
if extracoords._mapping is not None: | ||
node["mapping"] = extracoords._mapping | ||
if extracoords._lookup_tables: | ||
node["lookup_tables"] = extracoords._lookup_tables | ||
node["dropped_tables"] = extracoords._dropped_tables | ||
node["ndcube"] = extracoords._ndcube | ||
return node |
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 |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,23 @@ | ||||||
from asdf.extension import Converter | ||||||
|
||||||
|
||||||
class NDCollectionConverter(Converter): | ||||||
tags = ["tag:sunpy.org:ndcube/ndcollection-*"] | ||||||
types = ["ndcube.ndcollection.NDCollection"] | ||||||
|
||||||
def from_yaml_tree(self, node, tag, ctx): | ||||||
from ndcube.ndcollection import NDCollection | ||||||
|
||||||
aligned_axes = list(node.get("aligned_axes").values()) | ||||||
|
aligned_axes = list(node.get("aligned_axes").values()) | |
aligned_axes = list(node.get("aligned_axes").values()) |
Won't this raise an exception if "aligned_axes" is missing?
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import warnings | ||
|
||
from asdf.extension import Converter | ||
|
||
|
||
class NDCubeConverter(Converter): | ||
tags = ["tag:sunpy.org: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. | ||
""" | ||
node = {} | ||
node["data"] = ndcube.data | ||
# NDData always has .wcs as a high level wcs | ||
node["wcs"] = ndcube.wcs.low_level_wcs | ||
if not ndcube.extra_coords.is_empty: | ||
node["extra_coords"] = ndcube.extra_coords | ||
if ndcube.global_coords._all_coords: | ||
node["global_coords"] = ndcube.global_coords | ||
if ndcube.meta: | ||
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) | ||
|
||
return node |
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/ndcubesequence-*"] | ||
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 | ||
if ndcseq._common_axis is not None: | ||
node["common_axis"] = ndcseq._common_axis | ||
|
||
return node |
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 |
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 |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.