Skip to content

Support reading and writing multidimensional and object columns in ECSV - #11569

Merged
taldcroft merged 43 commits into
astropy:mainfrom
taldcroft:ascii-serialize-nd-cols-json
Apr 28, 2021
Merged

taldcroft merged 43 commits into
astropy:mainfrom
taldcroft:ascii-serialize-nd-cols-json

Conversation

@taldcroft

@taldcroft taldcroft commented Apr 14, 2021 •

Copy link
Copy Markdown
Member

Description

This is a second go at #11155 but using the ideas and specifications developed in #11368, in particular using JSON to serialize an array-valued element. This PR actually started from the code in #11155 since there was some useful infrastructure.

Key changes and features:

  • An object type column is serialized using JSON on each item.
    • This requires that the objects all be JSON-serializable. A reasonable error message is issued if this fails.
    • This is how one would handle a variable-length array (corresponding to the FITS variable length array).
  • A multidimensional column (with ndim > 1) is serialized using json.dumps(col[idx].tolist()) on each item.
    • A new shape attribute in the ECSV header is added in this case, where shape refers to the shape of each item (i.e. it is col.shape[1:]).
  • Values that are not multidim nor object are always represented using str(value), regardless of whether the column has a format set.
  • Overrode the key data conversion methods (strings to values and values to strings) within the ECSV classes. These are now tailored to ECSV and remove functionality that does not apply:
    • Fill values on input and output do not apply. ECSV has a fixed spec for representing null values.
    • Column format handling (e.g. formats when writing) does not apply since values are always now represented with str or via JSON.
    • Probably faster but I have not profiled.
  • Updating ECSV version to 1.0 in anticipation of a corresponding update to APE-6.
  • Consistent checking for multidim columns in all the io.ascii readers
  • Improved docstrings in the astropy.io.core to help me out.
  • Slightly out of scope:
    • Fixed NdArrayMixin and ArrayWrapper classes to work properly
    • Added a check in represent_mixins_as_columns() to ensure it actually did the job. It used to be that ArrayWrapper was not actually working. This means that the ECSV methods can assume Column subclasses when actually writing out the data.

TODO:

  • Fully handle variable length list.
    • Validate for read
    • Set shape parameter for write where the input corresponds to a variable length list.

Fixes #11368

@taldcroft taldcroft added this to the v4.3 milestone Apr 14, 2021
@github-actions github-actions Bot added the utils label Apr 14, 2021
@github-actions

Copy link
Copy Markdown
Contributor

👋 Thank you for your draft pull request! Do you know that you can use [ci skip] or [skip ci] in your commit messages to skip running continuous integration tests until you are ready?

@taldcroft
taldcroft force-pushed the ascii-serialize-nd-cols-json branch 3 times, most recently from 2deb55d to 8dea89b Compare April 16, 2021 21:18
@pep8speaks

pep8speaks commented Apr 17, 2021 •

Copy link
Copy Markdown

Hello @taldcroft 👋! It looks like you've made some changes in your pull request, so I've checked the code again for style.

There are no PEP8 style issues with this pull request - thanks! 🎉

Comment last updated at 2021-04-28 17:28:33 UTC

@taldcroft
taldcroft force-pushed the ascii-serialize-nd-cols-json branch 3 times, most recently from ba2a9ba to 47582b6 Compare April 17, 2021 17:40
@taldcroft
taldcroft marked this pull request as ready for review April 17, 2021 18:03
@taldcroft
taldcroft force-pushed the ascii-serialize-nd-cols-json branch from 47582b6 to bc6082a Compare April 17, 2021 18:03
@taldcroft
taldcroft requested review from hamogu and mhvk April 17, 2021 18:04

@taldcroft taldcroft left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@mhvk @hamogu @dhomeier @mbtaylor - this is pretty far along now and ready for review. I might do a bit more on the docs but as far as I know the main code changes are all there.

Next: work on corresponding updates to APE-6.

Comment thread astropy/table/tests/test_serialize.py Resolved
Comment thread astropy/table/tests/test_serialize.py Resolved
Comment thread astropy/table/tests/test_serialize.py Resolved
Comment thread astropy/table/tests/test_serialize.py Resolved
Comment thread astropy/table/tests/test_serialize.py Resolved
Comment thread astropy/table/tests/test_serialize.py Resolved
Comment thread astropy/table/tests/test_serialize.py Resolved
Comment thread astropy/utils/data_info.py Resolved

@hamogu hamogu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd by lying if I claimed to understand all of this. The tests look reasonable, and I keep learning, just be reading the code.

Taking a step back though, I'm slightly worried that we move into this direction: https://xkcd.com/927/

The current ECSV is essentially a CSV with some added metadata, but most normal table readers (e.g. Excel) can still make sense of the data, even if some information (say, the unit of a column) is lost. With every addition, we are moving away from that. It may be possible to read this new thing into Excel, at least for fixed-length columns; you just get a lot of extra columns, some of which have only [ or ] in them. For variable length columns, it's getting even harder.

So, we have to ask: What is the purpose of ECSV? Is it a new universal storage format that -unlike fits or hdf5- is human-readable because the data is stored as ASCII (even if it's not human writable)? Or is a generally supported CSV format that essentially any CSV reader can read with some extra bells and whistles, so that, if I read is back into astropy, I get the column units automatically as opposed to reading the "meta-info" by hand and taking care of the units "by hand" in my following calculations (the way essentially anyone did it before QTable and all the non-astropy world still does it)?

That's a question that's somewhat bigger than this PR. Those two visions are also not mutually exclusive, e.g. the docs could say "ESCV can write any astropy data in any dimension, but if you want to read it back in outside of astropy, it might be best to limit yourself to scalar columns with numbers or strings in them and not arbitrary objects. Here is an example how to make a 2d column into three 1d colums or a coordinate into separate RA, DEC by hand before writing".

As a real-world example: I've adopted ECSV for most the "CALDB - prototype" files (I'm aware that an actual CALDB has a strict format definition, but there are reasons we don't go there quite yet) for ARCUS. The problem is now that I'm the only one in the collaboration that can edit these files, because every by-hand edit brakes the YAML somewhere (white space, commas, line breaks). It's convenient that I can rely on Astropy reading it back in and using the correct units for, e.g. the dimensions of a CCD, but it also caused significant extra work. If I did it again, I would opt for a normal CSV that gets read by io.ascii indenpended of the exact character separator etc, because guessing usually "just works", and just hope that it's all good or check for consistency (e.g. CCDs need to have roughly this size in mm, if they are > 1 m, then someone probably put the info in in the wrong unit). So, for my case, ECSV is on the cusp of being too complicated to be useful already. Again, this is not mutually exclusive, as I can continue to only write simple 1D columns, but it's something to consider.

Comment thread astropy/io/ascii/core.py Outdated
FAST_CLASSES = {}


def _check_multidim_table(table, max_ndim=1):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I almost wonder if this should live the table not in io.ascii, since it's applicable more generally. I don't know the internals of votable, fits etc, but I can imagine that non-ascii formats might have situations that also warrant this check.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

On the other hand, it's really only one lines of check, so it can be re-implemented there, if needed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Makes sense, this could go in table_helpers.

Comment thread astropy/io/ascii/ecsv.py Resolved
@hamogu

hamogu commented Apr 17, 2021

Copy link
Copy Markdown
Member

@aaryapatil You asked for review experience. If you want to look through a complex, gold-standard PR to see how everything looks when it's great, but that might still benefit from general thoughts then this one is for you! At least I learned new stuff about astropy and Python by just reading through the code.

Comment thread astropy/io/ascii/core.py Outdated
FAST_CLASSES = {}


def _check_multidim_table(table, max_ndim=1):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Makes sense, this could go in table_helpers.

Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/ecsv.py Resolved
Comment thread astropy/table/ndarray_mixin.py Resolved
Comment thread astropy/table/table_helpers.py Resolved
@taldcroft

Copy link
Copy Markdown
Member Author

@hamogu - your concerns are well-founded. Version one of this #11155 was closer to the idea of all the data being readable as simple CSV. It basically flattened an N-d column into the required number of 1-d columns and then used a column naming convention. After the good discussion in #11368 I came to the opinion that the JSON approach is better.

But backing up, the first branch point is whether to just throw an exception at N-d columns ("can't do it") or "find a way". I think that "finding a way" is not quite an example of that famous XKCD since we are extending an existing format. It has been encouraging to see ECSV getting some traction.

Part the driver for #11368 and this PR is the possibility that the GAIA data processing consortium would use ECSV as an ASCII table format that is compatible with both TOPCAT and astropy. That really made me want to "find a way"! 😄

A distinct need there is to support variable length arrays (ala FITS), and for that the flattening approach just doesn't work. So we came to JSON encoding, at which point it makes sense to do the N-d arrays and object columns using the same JSON encoding. JSON is simple so the encode/decode tends to be fast, and of course it is supported in every imaginable language.

@taldcroft

Copy link
Copy Markdown
Member Author

p.s. @hamogu - thanks for the super-quick review!

@hamogu

hamogu commented Apr 17, 2021

Copy link
Copy Markdown
Member

GAIA is a good use case and certainly much more high-profile than me. How would this get into TOPCAT? IS there coordinated TOPCAT development?
I think some of my concerns can be alleviated by documentation (as suggested above), just so the people don't accidentally write new ECSV that they can't read in other programs any longer. It seems that this scheme is back-wards compatible as in "if you don't have multi-d columns, then I nothing changes". That's good! Also, that means that we don't need (to write and maintain) version-specific separate writers (tab.write(format='ascii.ecsv', ECSV_version=0.9)).

@nstarman nstarman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm really looking forward to this. I'm not familiar much with this portion of the astropy codebase... so while I poke around, I mostly limited myself here to docs comments. I've had docstrings on my mind recently 😅 .

Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/ecsv.py Resolved
Comment thread astropy/io/ascii/ecsv.py Outdated
col_vals = [json.loads(val) for val in col.str_vals]
# Make a numpy object array of col_vals to look for None
data = np.array(col_vals, dtype=object)
mask = (data == None) # noqa

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To mask only the == None comparison warning, I think it's

Suggested change
mask = (data == None) # noqa
mask = (data == None) # noqa: E711

Comment thread astropy/io/ascii/ecsv.py Outdated
kind = np.dtype(col.dtype).kind
data[mask] = {'U': '', 'S': b''}.get(kind, 0)
# Finally make a MaskedArray with the filled data + mask
col.data = np.ma.array(data.astype(col.dtype), mask=mask)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does @mhvk 's Masked class offer an alternative ? or is this for a serialization reason.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right now the Masked class is fully experimental and not meant for any "production" code.

Comment thread astropy/io/ascii/tests/test_ecsv.py Resolved
Comment thread docs/io/ascii/write.rst Outdated
- `astropy.coordinates.EarthLocation`
- `astropy.coordinates.SkyCoord`
- `astropy.table.NdarrayMixin`
- Coordinate representation types such as `astropy.coordinates.SphericalRepresentation`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be great to have an example in the docs of putting a Representation in a Table.
Also, when did general CoordinateFrame objects become allowed in Tables?!? I thought it was only SkyCoord.

@mbtaylor

Copy link
Copy Markdown
Contributor

GAIA is a good use case and certainly much more high-profile than me. How would this get into TOPCAT? IS there coordinated TOPCAT development?

@hamogu, the STIL java table I/O library has ECSV support since June 2020 (somewhat stimulated by internal Gaia/DPAC suggestions), and STIL is used for table I/O both in TOPCAT and in GACS, ESA's Gaia archive service software. TOPCAT can therefore read/write ECSV already, and ECSV is going to be be provided as an output option in the next GACS release (soon).

Up till now ECSV has only been capable of scalar-valued columns, while Gaia EDR3 (and later releases) will contain a number of array-valued columns. That discrepancy led the Gaia Archive Scientist to ask about the possibility of an array-valued enhancement to ECSV, and I (Gaia/DPAC member, STIL/TOPCAT author, someone with a small amount of Astropy contact) raised #11368 accordingly.

Regarding future development: once this is written up in the APE, I will implement corresponding functionality in STIL, so it will make its way into TOPCAT and GACS. If there are other documented and language-neutral enhancements to the ECSV format in the future, I'd aim to continue to track them in STIL.

@taldcroft

taldcroft commented Apr 19, 2021 •

Copy link
Copy Markdown
Member Author

@mbtaylor et al. - here is a view of the output ECSV that comes out of the new code. It is a little unfortunate that ECSV uses the double quote for quoting and that JSON does the same. The ECSV rule is that a single " within a quoted string needs to be represented as "". This results in double-double quoting for multidimensional string arrays. I think that from a parser perspective this is OK since the rules are simple and consistent, but it does impact aesthetics for a human-readable file.

Space delimited

# %ECSV 1.0
# ---
# datatype:
# - {name: a, datatype: string}
# - name: b
#   datatype: float64
#   shape: [2, null]
# - name: c
#   datatype: int64
#   shape: [null]
# - name: d
#   datatype: string
#   shape: [null]
# - name: e
#   datatype: int8
#   shape: [2]
# - name: f
#   datatype: float32
#   shape: [2, 2]
# schema: astropy-2.0
a b c d e f
x [[1,2],[3,4]] [1,2] "[""a"",""b""]" [1,2] [[0.0,1.0],[2.0,3.0]]
y [[1,2,3],[4,5.25,6]] [3,4,5] "[""c"",""d"",""e""]" [3,4] [[4.0,5.0],[6.0,7.0]]

Comma delimited

# %ECSV 1.0
# ---
# datatype:
# - {name: a, datatype: string}
# - name: b
#   datatype: float64
#   shape: [2, null]
# - name: c
#   datatype: int64
#   shape: [null]
# - name: d
#   datatype: string
#   shape: [null]
# - name: e
#   datatype: int8
#   shape: [2]
# - name: f
#   datatype: float32
#   shape: [2, 2]
# delimiter: ','
# schema: astropy-2.0
a,b,c,d,e,f
x,"[[1,2],[3,4]]","[1,2]","[""a"",""b""]","[1,2]","[[0.0,1.0],[2.0,3.0]]"
y,"[[1,2,3],[4,5.25,6]]","[3,4,5]","[""c"",""d"",""e""]","[3,4]","[[4.0,5.0],[6.0,7.0]]"

Source code

t = Table()
t['a'] = ['x', 'y']
t['b'] = np.empty(shape=(2,), dtype=object)
t['b'][0] = [[1, 2], [3, 4]]
t['b'][1] = [[1, 2, 3], [4, 5.25, 6]]
t['c'] = np.array([[1, 2], [3, 4, 5]], dtype=object)
t['d'] = np.array([['a', 'b'], ['c', 'd', 'e']], dtype=object)
t['e'] = np.array([[1, 2], [3, 4]], dtype=np.int8)
t['f'] = np.arange(8, dtype=np.float32).reshape(2, 2, 2)

out = StringIO()
t.write(out, format='ascii.ecsv', delimiter=' ')
print(out.getvalue())

out = StringIO()
t.write(out, format='ascii.ecsv', delimiter=',')
print(out.getvalue())

t2 = Table.read(out.getvalue(), format='ascii.ecsv')

Python implementation detail

When a typed variable length array gets read back in, there are a couple of options:

  1. Object-type column of the pure-Python lists that came from json.loads
  2. Object-type column of the numpy typed arrays based on type specified by datatype

Right now it does (2), so you get an array of ndarray and typing is strictly maintained. If the ECSV says the datatype=int8 then that's what you see. I think this makes sense but I wanted to note this. Working with arrays of ndarray can be a little fiddly:

In [6]: x = np.array([np.array([1]), np.array([2,3])], dtype=object)
In [8]: x[0].dtype
Out[8]: dtype('int64')   # Expected

In [9]: x = np.array([np.array([1]), np.array([2])], dtype=object)
In [10]: x[0].dtype
Out[10]: dtype('O')  # Huh?

@taldcroft
taldcroft force-pushed the ascii-serialize-nd-cols-json branch from 0301eca to c97bee7 Compare April 27, 2021 17:27
@mhvk

mhvk commented Apr 27, 2021

Copy link
Copy Markdown
Contributor

@taldcroft - definitely see your logic, but of course for FITS we also know that in reality there are corrupt headers (e.g., with units not complying with the standard) and we still parse them. But that is the headers, not the data, so perhaps not quite the same.

In any case, quite happy to go with exceptions now - changing from those to warnings if forced to is easier than realizing warnings was a mistake and then trying to change to exceptions!

Separately: ready for a last review?

@taldcroft

taldcroft commented Apr 27, 2021 •

Copy link
Copy Markdown
Member Author

Sounds good, we'll leave as an exception. I agree it is easier to remove an exception than go the other way.

I see that all tests are passing, so this is indeed ready for final review.

@taldcroft

Copy link
Copy Markdown
Member Author

I do plan to squash this down at the end, there was a fair bit of meandering and once this is done I don't see too much value in all the intermediate commits.

@aaryapatil aaryapatil left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is definitely a gold-standard PR ;-) I got a chance to look through this final (?) version, and it looks great. Especially like the sub-type format. I won't say I understood everything (I learnt a lot about Python), but the documentation is super clear in terms of letting the user know that ECSV is recommended for lossless round-tripping and supports multi-dimensionality!

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some leftover small nits here and there, but otherwise looks great! Tests are very nice too!

Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/core.py Outdated
def _check_multidim_table(self, table, max_ndim=1):
"""Check that ``table`` has only 1-d columns.

If a reader class supports N-d columns then override this method.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, that makes sense. Note that the docstring still needs to be updated - if a reader supports N-d, it should increase max_ndim.

Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/core.py Resolved
Comment thread astropy/io/ascii/ecsv.py Resolved
Comment thread astropy/io/ascii/ecsv.py Outdated
out = serialize.represent_mixins_as_columns(table)
return out

def _check_multidim_table(self, table, max_ndim=1):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This method can now be removed, since the check will pass already.

Comment thread astropy/table/ndarray_mixin.py Outdated
ndarrays within a Table. This is a subclass of numpy.ndarray
and has the same initialization options as ndarray().
"""
__module__ = 'astropy.table.table'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My guess would be that this happens because the ndarray_mixin module is not included in the table API sections.

Comment thread astropy/table/serialize.py Outdated
@@ -37,6 +37,7 @@
'astropy.coordinates.earth.EarthLocation',
'astropy.coordinates.sky_coordinate.SkyCoord',
'astropy.table.table.NdarrayMixin',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Might it make sense to move the location here to where it actually is? (Maybe only possible if one does not define __module__ - which still feels like a hack best avoided...)

return table


class ArrayWrapperInfo(ParentDtypeInfo):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, sounds good. Maybe add a comment to that extent? (or raise an issue)

Comment thread astropy/io/ascii/tests/test_ecsv.py Resolved
@taldcroft taldcroft changed the title Support reading and writing multidimensional columns in ECSV format (take 2) Support reading and writing multidimensional and object columns in ECSV Apr 27, 2021
@taldcroft

Copy link
Copy Markdown
Member Author

FYI see astropy/astropy-APEs#68.

@taldcroft

Copy link
Copy Markdown
Member Author

@mhvk - I think I have addressed the last round of comments. 🤞 again on docs and tests (passing locally).

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, looks good! Let's get this in - a really nice addition!!

@mhvk mhvk added the zzz 💤 merge-when-ci-passes Do not use: We have auto-merge option now. label Apr 28, 2021
@taldcroft
taldcroft merged commit 734f8df into astropy:main Apr 28, 2021
@taldcroft
taldcroft deleted the ascii-serialize-nd-cols-json branch April 28, 2021 19:20
@taldcroft

Copy link
Copy Markdown
Member Author

Thanks for the really useful and timely review @mhvk!

@taldcroft

Copy link
Copy Markdown
Member Author

And I'll get to work on the What's New.

@pllim

pllim commented Jun 1, 2021

Copy link
Copy Markdown
Member

Might have broken backward compatibility? See #11803

eteq pushed a commit that referenced this pull request Jun 11, 2021
Fix problem with multidim Table repr in Jupyter introduced in #11569
@astrofrog

Copy link
Copy Markdown
Member

Just for the record, I had to backport one commit from here (6544aa8) to LTS to fix CI failures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ECSV array/object-valued column support

9 participants