NeoMatlabIO: round-trip quantity, array and nested annotations - #1894
Conversation
_get_matlab_value flattens an annotation dict for MATLAB by splitting a quantity into a magnitude plus a companion <key>_units field, mirroring a nested mapping as a nested struct and standing None up as a sentinel string. The read side undid none of that. It copied every field of the struct straight into the annotations dict, so units came back as a separate key, nested mappings came back as scipy mat_struct objects, and the comparison against the sentinel was done with `value == PY_NONE`, which on an array value yields an array and raises "The truth value of an array with more than one element is ambiguous". That made any file holding an array-valued annotation unreadable. Decoding now mirrors the encoding: a <key>_units field is folded back into the quantity it belongs to, a nested struct is decoded recursively, and the sentinel is tested only on values that are actually strings. On the write side None inside a nested annotation dict was being dropped, because the guard naming the annotations attribute did not survive the recursion, and annotations are the only mapping-valued attribute Neo has. Fixes NeuralEnsemble#852
There was a problem hiding this comment.
This is nice implementation, and well tested. Many thanks for this PR! It just needs a couple of docstring changes for clarity.
For future reference, I should note that I found the PR description too long and difficult to understand, which slowed down the review.
Some general principles for future PRs:
- Begin by stating the problem it solves. Where there is an existing issue, you can just refer to the issue, and briefly summarise it.
- Next state how this PR solves the problem. It is not necessary to explain every code change in words, or to give the history of how you solved it, just give a high-level view; anything that needs a detailed commentary should already have code comments to that effect. Use present tense, not past tense, when referring to the state of the code in the master branch (since the PR has not been merged yet).
- Next explain briefly how the code was tested/validated.
- Any other comments, such as changes unrelated to the underlying issue.
Applying this to the current PR would give something like this:
Problem
Fixes #852.
On master, NeoMatlabIO writes a quantity annotation as a plain magnitude plus a companion <key>_units field, and a nested annotation dict as a nested struct, but the read path reverses neither. An annotation yop=[3, 4, 5] * pq.ms comes back as {'yop': array([3., 4., 5.]), 'yop_units': 'ms'}, and a nested dict comes back as a raw scipy.io.matlab.mat_struct.
While fixing that I hit a second, more serious bug in the same branch of create_ob_from_struct. The None sentinel is tested with if value == PY_NONE, which evaluates to an array when the annotation is array-valued and raises ValueError: The truth value of an array with more than one element is ambiguous. Any object carrying an array annotation is therefore written without complaint and can never be read back.
Solution
Decoding now mirrors encoding, in a new create_dict_from_struct alongside the existing create_ob_from_struct: <key>_units is folded back into the value it belongs to, nested structs are decoded recursively, and the sentinel is tested only on strings.
One change on the write side: None inside a nested annotation dict is currently dropped, because the guard identifying the annotations attribute does not survive the recursion into the nested mapping. Annotations are the only mapping-valued attribute any Neo class declares, so the guard can be dropped.
Testing
neo/test/iotest/test_neomatlabio.py goes from 6 failed / 11 passed to 17 passed, and neo/test/coretest is unaffected. The new parametrized test round-trips a range of value types, and checks that no companion field leaks into the annotations. The yop annotation from #852 is also added to test_write_read_single_spike, as the issue suggests.
Other comments
A quantity and its units are two separate fields in the .mat file, so plain annotations a and a_units are indistinguishable on disk from a single quantity annotation a, and now read back as the latter. The ambiguity is inherent to the storage convention the write side already uses but it is a behaviour change for that case.
|
|
||
| That method flattens a quantity into a plain magnitude plus a companion | ||
| ``<key>_units`` field, mirrors nested mappings as nested structs, and stores | ||
| `None` as a sentinel string because MATLAB has no equivalent. This undoes all |
There was a problem hiding this comment.
"Undoes" is a bit confusing. Maybe "This reverses that flattening, transforming the string "Py_None" into Python None and the magnitude/units pair into a Quantity scalar or array.
Also, the sentinal string is "Py_None" not "None", isn't it?
|
Thanks, and you are right on both counts. The sentinel is I also rewrote the PR description to your four-part structure. Noted on length for future PRs. One correction to my own first draft of that description, in case you read it before I fixed it. I wrote that the tests cover "an array annotation alongside |
|
Thanks, that is useful and I have rewritten the description to your version. I checked the numbers before adopting them. Without datalad the file goes 5 failed / 3 passed to 8 passed with the other 9 skipped, which lines up with your 6 / 11 to 17 once the datalad tests run, since I had also left the write-side guard change out of the description entirely, so that is in now too. |
|
Thanks for running the CI. The red is the data host rather than this branch, so flagging what it does and does not tell us. The step runs Every one of the 96 is that same The job stopped there, so
Locally the file is 8 passed and 9 skipped, where the 9 skips are exactly the datalad cases this run would have covered. |
Problem
Fixes #852.
On master,
NeoMatlabIOwrites a quantity annotation as a plain magnitude plus a companion<key>_unitsfield, and a nested annotation dict as a nested struct, but the read path reverses neither. An annotationyop=[3, 4, 5] * pq.mscomes back as{'yop': array([3., 4., 5.]), 'yop_units': 'ms'}, and a nested dict comes back as a rawscipy.io.matlab.mat_struct.While fixing that I hit a second, more serious bug in the same branch of
create_ob_from_struct. TheNonesentinel is tested withif value == PY_NONE, which evaluates to an array when the annotation is array-valued and raisesValueError: The truth value of an array with more than one element is ambiguous. Any object carrying an array annotation is therefore written without complaint and can never be read back.Solution
Decoding now mirrors encoding, in a new
create_dict_from_structalongside the existingcreate_ob_from_struct:<key>_unitsis folded back into the value it belongs to, nested structs are decoded recursively, and the sentinel is tested only on strings.One change on the write side:
Noneinside a nested annotation dict is currently dropped, because the guard identifying the annotations attribute does not survive the recursion into the nested mapping. Annotations are the only mapping-valued attribute any Neo class declares, so the guard can be dropped.Testing
neo/test/iotest/test_neomatlabio.pygoes from 6 failed / 11 passed to 17 passed. The new parametrized test round-trips a range of value types, and checks that no companion field leaks into the annotations. It writes to a temporary file rather than the downloaded test data, so it also runs without datalad, where the counts are 5 failed / 3 passed to 8 passed with the other 9 skipped. Theyopannotation from #852 is also added totest_write_read_single_spike, as the issue suggests.Other comments
A quantity and its units are two separate fields in the
.matfile, so plain annotationsaanda_unitsare indistinguishable on disk from a single quantity annotationa, and now read back as the latter. The ambiguity is inherent to the storage convention the write side already uses but it is a behaviour change for that case.