ezmsg.nwb.convert#

Apply an NWB stream’s scaling downstream, instead of at the reader.

NWBSlicer never applies a file’s conversion – it hands back the stored integers and describes them, carrying the factors under nwb_scaling_* attrs (see scaling). This is what picks them up later in the graph, so a stage far from the file can do exactly what the reader would have.

Why defer at all? Reading raw is the only way to keep a broadband stream int16 through the parts of a graph that do not care about units. Scaling at the reader doubles every message to float32 immediately – ~80 GB per recording-hour going to ~160 – and a graph that decimates, selects a channel subset, or windows before it needs volts pays that on samples it is about to discard. Putting this transformer after those stages converts what survives.

The other use is a graph whose source is not always a file: the reader emits what the file declares, and one of these pinned to target_unit at the head of the processing chain makes every branch downstream agree on a scale regardless.

Everything per-stream is resolved once. A stream’s scaling does not change from message to message, so resolving overrides, parsing units, and building the output attrs on every message is pure waste – and at simulated-online sizes (30 samples) that waste was 60% of the call. NWBScalingTransformer caches a _Plan per stream key and does nothing per message but one multiply.

Classes

class NWBScalingSettings(conversion_dtype='float32', scale_override=None, unit_override=None, target_unit=None)[source]#

Bases: Settings

Settings for NWBScalingTransformer.

Deliberately the same names and forms as the corresponding NWBIteratorSettings fields, so moving the work down the graph is a matter of moving the settings, not rewriting them.

Parameters:
conversion_dtype: str = 'float32'#

Output dtype for a message that gets scaled (default float32).

scale_override: float | dict[str, float] | None = None#

Replace the gain the message reports, for a file whose recorded factors are wrong. Bare value or {stream_key: value}, matched against msg.key.

unit_override: str | dict[str, str] | None = None#

Replace the reported unit without changing the gain. Same form.

target_unit: str | VoltageUnit | dict[str, str | VoltageUnit] | None = None#

Deliver voltage streams in this unit. Same form. Unlike the other two this also applies to a message that was already scaled and carries only its unit, since a prefix change on top of a known unit is well defined.

__init__(conversion_dtype='float32', scale_override=None, unit_override=None, target_unit=None)#
Parameters:
Return type:

None

class NWBScalingState[source]#

Bases: object

plans: dict[str, _Plan] | None = None#
settings_ref: Any = None#
class NWBScalingTransformer(*args, **kwargs)[source]#

Bases: BaseStatefulTransformer[NWBScalingSettings, AxisArray, AxisArray, NWBScalingState]

Apply the scaling a message reports, and update what it reports.

Idempotent, and structurally so: applying a scaling consumes it. The output carries no nwb_scaling_* keys, so a second of these in a chain finds nothing pending and has nothing to double-apply. Never inferred from the dtype – “does this look like integers?” is exactly the kind of guess that makes wrong units silent.

What it emits is unit and nothing else from this module. gain and offset described stored counts, and the multiply consumed them; a filter or a decimation downstream makes them less true still, so carrying them would hand a later stage a factor that looks authoritative and reconstructs nothing. Re-digitizing is a genuine need, but it happens after transformations those numbers never saw and so takes its range and resolution from the caller.

A message with no nwb_scaling_* attrs and no unit passes through untouched: it did not come from an NWB reader, and inventing a scaling for it would be worse than doing nothing. One that kept its unit is still retargetable – see _scaling_from_unit().

State is a plan per stream key, not a single current plan, because one reader publishes every stream on one output: with a single plan, two interleaved streams would evict each other on every message and the cache would cost more than it saved.

class NWBScalingUnit(*args, settings=None, **kwargs)[source]#

Bases: BaseTransformerUnit[NWBScalingSettings, AxisArray, AxisArray, NWBScalingTransformer]

Parameters:

settings (Settings | None)

SETTINGS#

alias of NWBScalingSettings