Repository: asdf-format/asdf

Issue:
masked arrays do not roundtrip with all false masks
# Description of the problem

Attempting to save a masked array with an all false mask results in no errors. Yet when loaded the array is no longer masked.

See:
https://github.com/asdf-format/asdf/blob/4a0064aa48d6360d27ab588b2bb85255016e3876/asdf/_core/_converters/ndarray.py#L121-L125

I believe that removing `np.any(data.mask)` would fix the issue.

# Example of the problem

# System information

asdf version: main
python version: python 3.10
operating system: mac


Discussion:
braingram: Would you add a changelog for this that documents the fixed bug?

Also, would you either move the test near:
https://github.com/asdf-format/asdf/blob/main/asdf/_tests/tags/core/tests/test_ndarray.py#L381
or update that test to the nicely parametrized one added in this PR?

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 3a625fe3..acf53aaf 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,7 +1,7 @@
 3.4.0 (unreleased)
 ------------------
 
--
+- Fix issue where roundtripping a masked array with no masked values removes the mask [#1803]
 
 3.3.0 (2024-07-12)
 ------------------
diff --git a/asdf/_core/_converters/ndarray.py b/asdf/_core/_converters/ndarray.py
index d5b312f3..5a59b9c8 100644
--- a/asdf/_core/_converters/ndarray.py
+++ b/asdf/_core/_converters/ndarray.py
@@ -125,7 +125,7 @@ class NDArrayConverter(Converter):
             if strides is not None:
                 result["strides"] = list(strides)
 
-        if isinstance(data, ma.MaskedArray) and np.any(data.mask):
+        if isinstance(data, ma.MaskedArray):
             if options.storage_type == "inline":
                 ctx._blocks._set_array_storage(data.mask, "inline")
 


Test patch:
diff --git a/asdf/_tests/tags/core/tests/test_ndarray.py b/asdf/_tests/tags/core/tests/test_ndarray.py
index ea830fca..2661cb3a 100644
--- a/asdf/_tests/tags/core/tests/test_ndarray.py
+++ b/asdf/_tests/tags/core/tests/test_ndarray.py
@@ -378,17 +378,27 @@ def test_inline_bare():
         assert_array_equal(ff.tree["arr"], [[1, 2, 3, 4], [5, 6, 7, 8]])
 
 
-def test_mask_roundtrip(tmp_path):
-    x = np.arange(0, 10, dtype=float)
-    m = ma.array(x, mask=x > 5)
-    tree = {"masked_array": m, "unmasked_array": x}
+@pytest.mark.parametrize(
+    "mask",
+    [
+        [[False, False, True], [False, True, False], [False, False, False]],
+        True,
+        False,
+    ],
+)
+def test_mask_roundtrip(mask, tmp_path):
+    array = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0]])
+    tree = {
+        "unmasked": array,
+        "masked": np.ma.array(array, mask=mask),
+    }
 
     with roundtrip(tree) as af:
-        tree = af.tree
-
-        m = tree["masked_array"]
-
-        assert np.all(m.mask[6:])
+        # assert_array_equal ignores the mask, so use equality here
+        assert (tree["masked"].data == af["masked"].data).all()
+        assert (tree["masked"].mask == af["masked"].mask).all()
+        # ensure tree validity
+        assert (af["unmasked"] == af["masked"].data).all()
         assert len(af._blocks.blocks) == 2
 
 
@@ -520,7 +530,8 @@ def test_inline_masked_array(tmp_path):
 
     with asdf.open(testfile) as f2:
         assert len(list(f2._blocks.blocks)) == 0
-        assert_array_equal(f.tree["test"], f2.tree["test"])
+        # assert_array_equal ignores the mask, so use equality here
+        assert (f.tree["test"] == f2.tree["test"]).all()
 
     with open(testfile, "rb") as fd:
         assert b"null" in fd.read()