Refactor the dataset reader surface - #27
Conversation
Loading every register of a session is now written as a comprehension over the registers that have files, calling read for each, rather than through a single call on the reader.
A register now renders as its name and address rather than as <class '...'>, so a register map prints legibly. The repr sits on the metaclass of RegisterBase, which is what reaches structured-payload registers too, since those do not derive from the scalar bases. A base declaring no address keeps the default. Calling a register that already declares an address raises TypeError instead of returning a copy at the new address. Declaring one from a base, RegisterU32(0x08), is unchanged.
open_dataset replaces create_dataset_reader and takes an optional device module as its second argument, so a folder with its own device.yml and a pre-generated package both reach the same reader. Passing schema= or converters= beside a module now raises TypeError. Identity is checked only when the module was not built from the folder schema. read accepts a register name alongside a class and an address, resolved through the device register map rather than the module namespace. contents maps register name to address for every register with data in the folder, so what a dataset holds and what read takes are the same key. The registers property is gone, since it only repeated device_module.REGISTER_MAP, and files becomes paths. The default resolver now returns addresses in numeric order.
|
Looks good! One thing I realized but probably we can discuss later: the current design may allow us to return an empty, correctly structured (i.e. expected column names) dataframe if no file exists in the directory. I am not sure this is something we want, but this is probably one of the few packages I have seen around where the infrastructure affords it. |
The number of columns a sub-array field renders is taken from the dtype rather than inferred from the data, so a buffer carrying no frames now returns an empty DataFrame with the full column set. Previously parse_to_dataframe would fail on any register with a sub-array field when given no frames, since numpy cannot infer a dimension from a zero-length array.
|
@bruno-f-cruz Agreed, I think we do want to return typed empty frames. The reason why generic loaders have to raise on a missing file is because a missing CSV says nothing about what the columns or the types should have been, so there is no empty result available to return. In harp-data the register set comes from the folder Most of it is already in place, and I have pushed the one gap to this PR. |
A register declared in the device register map with no data in the folder now reads as an empty DataFrame carrying the same columns, rather than raising FileNotFoundError. The schema describes the structure of the data regardless of whether anything was recorded, so contents is what distinguishes a register that was never logged from one the device does not declare. read now takes timestamp as a bool defaulting to True, matching parse_to_dataframe, in place of a tri-state that inferred it from the payload-type bit of the first frame.
The
harp-datareader was shaped around callers holding a generated device package, where every register is reachable as a typed class. Someone with only a recorded folder had a harder path: addresses to look up, no way to see what a session actually contains, and aread_allthat pulled in everything by default. This branch reshapes the surface so both audiences are served by the same reader, taking it from eight public members to six.Removals are outright, with no deprecation shims, since the packages are pre-release and every downstream device package is regenerated on publication.
Construction
create_dataset_readeris replaced byopen_dataset, which takes the folder first and an optional device module second, mirroringopen_serial_devicein the serial package.schemaandconvertersboth describe how to build a module, so passing either beside one now raisesTypeErrorrather than silently ignoring it.DatasetReader(module, root)remains public and constructible, exactly asDeviceis besideopen_serial_device.Reading and discovery
readaccepts a register name alongside a class and an address. Names resolve through the device register map rather than the module namespace, which excludes the common registers, soread("WhoAmI")still reaches a dataset file that is on disk.contentsis the new starting point for an unfamiliar dataset. It maps register name to address for every register with data in the folder, keyed exactly asreadaccepts:read_allis removed. Most devices log every register by default anyway, and the previous method also silently skipped files whose address the schema does not describe. The comprehension above replaces it and keeps the per-register options ofreadin reach.Surface reduction
registersis gone, since it only repeateddevice_module.REGISTER_MAP.filesbecomespaths, which are the actual objects held by the collection, and is now documented as the physical debug view besideresolver. The default resolver now returns addresses in numeric order, where previously address 8 would sort after address 34 because the file names sort lexicographically.Register repr
A register now renders as e.g.
<AnalogData @33>rather than<class 'harp.device.behavior.AnalogData'>. This lives inharp-protocoland belongs on this branch because it is what keepsREGISTER_MAPreadable onceregistersis gone from the reader.The repr sits on the metaclass of
RegisterBase, which is what reaches structured-payload registers such asOperationControl. Those derive fromRegisterBasedirectly and never pass through the scalar metaclass, so putting it lower would cover only half the register set. Calling a register that already declares an address now raisesTypeErrorinstead of returning a copy at the new address, soRegisterU32(0x08)still declares one whileWhoAmI(44)no longer clones it.Empty buffer parsing
parse_to_dataframewould fail on any register with a sub-array payload field when given a buffer with no frames, because the column count was inferred from the data and numpy cannot infer a dimension from a zero-length array. It is now taken from the dtype, so an empty buffer returns an empty frame with the full column set. This affectsAnalogDataandVersionin the test schema, 2 of 30 registers, so every register can now produce a correctly structured empty frame.Registers with no data
A register declared in the device register map with no data in the folder now reads as an empty DataFrame carrying the same columns, rather than raising
FileNotFoundError. The schema describes the structure of the data regardless of whether anything was recorded. Naming an absent chunk throughsuffix=still raises, since that is a mistake about the request rather than data that was never recorded.readalso takestimestampas abooldefaulting toTrue, matchingparse_to_dataframe, in place of a tri-state that inferred it from the payload-type bit of the first frame.