HeaderStorage

HeaderStorage is the base class for any object that contributes fields to an experiment’s CSV header file. The header is the human-readable, semicolon-delimited record of every parameter that defined an acquisition: hardware settings, RF and chirp configuration, digitizer setup, flow setpoints, validation thresholds, and so on. The file uses six columns — object key, array key, array index, key, value, unit — and HeaderStorage packs values into that schema on the way out and unpacks them back when an experiment is loaded from disk.

Experiment is the root of a tree of HeaderStorage nodes, with FtmwConfig, RfConfig, the digitizer, the pulse generator, the LIF config, and the validator as children, each of which may add its own grandchildren. The framework dispatches incoming rows to the correct subtree by matching the object key in column 0, and on the write side it walks the tree depth-first to produce the full set of rows. The on-disk layout of an experiment, including the header file, is described in Data Storage.

The two virtuals you must implement

Subclasses override two pure virtuals:

  • storeValues() runs just before the header is written. Inside it, call store() once per scalar field and storeArrayValue() once per cell of any array fields. Each row is buffered in this object’s cache.

  • retrieveValues() runs after the header has been parsed and all matching rows have been routed to this object. Inside it, call retrieve() and retrieveArrayValue() to extract the cached values into your own members.

Each call to retrieve() or retrieveArrayValue() removes the row from the cache. Asking for the same key twice yields the default the second time. arrayStoreSize() reports how many entries an array has before you start consuming it.

Composing a tree

A HeaderStorage may own children. Children are declared by overriding prepareChildren() and calling addChild() once per child:

void Experiment::prepareChildren()
{
    addChild(ps_ftmwConfig.get());
    addChild(ps_validator.get());
    for(auto &p : d_hwCfgs)
        addChild(p.get());
    addChild(ps_lifCfg.get());
}

The framework calls prepareChildren() at the start of every read or write pass, after wiping any prior child list, so the tree always reflects the current shape of the data. Children themselves do not call addChild on their parent — the parent owns the relationship in prepareChildren(). The framework then recurses into each child’s prepareChildren(), allowing arbitrary nesting.

removeChild() exists for the rare case of a parent that needs to detach a child mid-flight (Experiment uses it when the user disables an FTMW or LIF subsystem). It does not delete the child object; ownership lives with whoever holds the smart pointer.

Write flow

  1. A caller invokes getStrings() on the root (Experiment does this inside save() via BlackchirpCSV::writeHeader).

  2. Each node’s prepareToStore() runs, which calls prepareChildren() and recurses into each child.

  3. Each node’s storeValues() runs, populating its cache via store() and storeArrayValue().

  4. The framework converts cached entries to the six-column form, merges in each child’s getStrings() output, and clears the in-memory cache.

Therefore: never call store() outside storeValues(); the rows would be cleared on the next write.

Read flow

  1. The caller (Experiment’s reading constructor) calls prepareToStore() on the root once, so the child tree is built.

  2. The caller reads the CSV file line by line and hands each row to storeLine() on the root. Rows are dispatched to whichever node’s d_headerKey matches column 0 (children searched first if the root does not match).

  3. After all lines have been routed, the caller invokes readComplete() on the root, which calls retrieveValues() on every node depth-first.

  4. Each retrieveValues() implementation pulls rows back out via retrieve() / retrieveArrayValue() and assigns them to the object’s members.

Therefore: never call retrieve() outside retrieveValues() (or after readComplete() has run) — the cache is empty by then.

Enum cells

store() / storeArrayValue() template overloads wrap their argument with QVariant::fromValue, so a Q_ENUM-registered enumerator is written by name rather than as an opaque integer. On the read side retrieve() and retrieveArrayValue() dispatch to BC::CSV::enumFromVariant whenever the requested type carries Q_ENUM or Q_ENUM_NS, so historical fixtures whose cells held the integer form continue to round-trip back to the typed value without subclasses having to call the helper directly. The dual-form contract that motivates this is described under Enum cells: writing names, reading both.

Object-key conventions

  • Singleton-style objects (Experiment, RfConfig, ChirpConfig, etc.) pass a constant key from their BC::Store::* namespace.

  • Per-instance objects (PulseGenConfig and other hardware configs) pass the hardware key for the specific instance (e.g. "PulseGenerator.main"). This guarantees that experiments with several instances of the same hardware type produce distinguishable header rows.

The chosen key is stored in d_headerKey and cannot be changed afterward.

API Reference

class HeaderStorage

Base class for objects that contribute fields to an experiment’s CSV header file.

Each HeaderStorage is identified by an object key (column 0 of the six-column header CSV: object key, array key, array index, key, value, unit). Rows are dispatched between objects by matching column 0, so the same hardware-instance key (e.g. "PulseGenerator.main") that identifies a HardwareObject is also used as its HeaderStorage object key. The chosen key is stored in d_headerKey and cannot be changed afterward.

Cache invariants: store() / storeArrayValue() may only be called inside storeValues(), and retrieve() / retrieveArrayValue() may only be called inside retrieveValues(). The cache is cleared at the start of each write pass, and each retrieve removes its row, so calls outside those hooks yield silently wrong results. Tree composition is declared by overriding prepareChildren() and calling addChild() once per child; the framework rebuilds the child list at the start of every read or write pass and recurses automatically.

Subclassed by ChirpConfig, DigitizerConfig, Experiment, ExperimentValidator, FlowConfig, FtmwConfig, LifConfig, PressureControllerConfig, PulseGenConfig, RfConfig, TemperatureControllerConfig

Public Types

using ValueUnit = std::pair<QVariant, QString>

Alias for storing a value and unit

using HeaderMap = std::map<QString, ValueUnit, std::less<>>

Alias for a map of key-value+unit pairs

using HeaderArray = std::vector<HeaderMap>

Alias for a list of HeaderMap values

using HeaderStrings = std::multimap<QString, std::tuple<QString, QString, QString, QString, QString>>

Alias for a set of strings representing header data, together with the object key

Public Functions

HeaderStorage(QAnyStringView objKey)

Constructor. Sets the object key, which should be unique to the most derived class.

Parameters:

objKey – The object key written into the first column of every header line produced by this object. Used by storeLine() to dispatch incoming lines to the correct HeaderStorage in the parent/child tree.

inline virtual ~HeaderStorage()

Destructor. Does nothing.

inline QString headerKey() const

This node’s object key (column 0 of the CSV).

Returns:

The key set in the constructor.

HeaderStrings getStrings()

Drive the write pass: build the child tree, populate caches, collect every node’s rows, and clear the caches.

Called by the experiment writer (BlackchirpCSV::writeHeader) on the root HeaderStorage. Internally:

  1. calls prepareToStore() to (re)build the child tree;

  2. calls this object’s storeValues() to fill its cache;

  3. packs every cached row into the six-column form (object key, array key, array index, key, value, unit);

  4. recursively merges every child’s getStrings() result;

  5. clears this object’s cache so a subsequent write starts clean.

Subclasses normally never call this directly; only the writer does. They participate by overriding storeValues() and prepareChildren().

Returns:

Multimap of cached rows keyed by the producing object’s d_headerKey. Each value is a five-string tuple (array key, array index, key, value, unit), any of which may be empty.

void prepareToStore()

Build the child tree (recursive).

Clears this node’s child list, then calls prepareChildren() so the subclass can repopulate it via addChild(), then recurses into every newly added child. Called automatically at the start of a read pass and from getStrings() at the start of a write pass.

bool storeLine(const QVariantList l)

Route one parsed CSV row into the appropriate node’s cache.

Called by the experiment reader once per CSV line, after the line has been parsed into six QVariants. If l ‘s first entry matches d_headerKey, the row is added to this object’s cache; otherwise children are searched depth-first. Empty key or value cells cause the row to be dropped silently.

The caller (Experiment’s reading constructor) is responsible for verifying l has exactly six entries before invoking this function — there is no length check inside.

Parameters:

l – Six-element list parsed from a CSV row:

  1. Object Key

  2. Array Key (empty for scalar rows)

  3. Array Index (empty for scalar rows)

  4. Key

  5. Value

  6. Unit (may be empty)

Returns:

True if the row was claimed by this node or any of its descendants; false if no matching object key was found.

void readComplete()

Drive the read pass: invoke retrieveValues() depth-first.

Called by the experiment reader on the root HeaderStorage after every line has been routed via storeLine(). This object’s retrieveValues() runs first, then every child’s readComplete() is called in turn. By the time control returns to the reader, each subclass has consumed its cached rows back into its own members.

void addChild(HeaderStorage *other)

Register a child for this node. Call from prepareChildren().

Children are stored as raw pointers; ownership is not transferred. The caller (typically the parent’s prepareChildren()) must guarantee that the child outlives the read or write pass currently in progress. Passing nullptr is safe and ignored.

Parameters:

other – Pointer to the child HeaderStorage.

HeaderStorage *removeChild(HeaderStorage *child)

Detach a child without deleting it.

Used by parents that disable a subsystem during the lifetime of the parent (Experiment uses it when an FTMW or LIF subsystem is removed mid-flight). After removal the child no longer participates in subsequent read/write passes; ownership of the child object is unaffected.

Parameters:

child – Child to detach.

Returns:

The detached child pointer, or nullptr if not found.

inline virtual void prepareChildren()

Declare this node’s children. Override in subclasses with children.

Called every time prepareToStore() runs, after the prior child list has been wiped. Inside, call addChild() once per child. The default implementation declares no children.

Repopulating the list every pass (rather than once at construction) means children that come and go with user choices — a freshly disabled hardware subsystem, an optional validator — are reflected automatically.

Protected Functions

virtual void storeValues() = 0

Populate this object’s cache with rows to be written.

Subclasses must implement. Inside, call store() once per scalar field and storeArrayValue() once per cell of any array fields. Do not call store() / storeArrayValue() from anywhere else; rows added outside this function are cleared at the next getStrings() call.

virtual void retrieveValues() = 0

Consume cached rows back into this object’s members.

Subclasses must implement. Called once per node, depth-first, after every header row has been routed by storeLine(). Inside, call retrieve() / retrieveArrayValue() (each call removes the row from the cache) and assign the values back to your own data members. Use arrayStoreSize() to learn how many entries an array contains before iterating it.

template<typename T>
inline void store(QAnyStringView key, const T &val, QAnyStringView unit = {})

Cache one scalar row for writing. Templated overload.

Call from storeValues(). Equivalent to the QVariant overload but accepts any type that QVariant can wrap.

Parameters:
  • key – Row key (column 4 of the CSV).

  • val – Value (column 5).

  • unit – Optional unit (column 6).

void store(QAnyStringView key, const QVariant val, QAnyStringView unit = {})

Cache one scalar row for writing.

Call from storeValues(). The row is added to this object’s cache and emitted by the next getStrings() call. Repeated calls with the same key overwrite.

Parameters:
  • key – Row key (column 4 of the CSV).

  • val – Value (column 5).

  • unit – Optional unit (column 6).

template<typename T>
inline void storeArrayValue(QAnyStringView arrayKey, std::size_t index, QAnyStringView key, const T &val, QAnyStringView unit = {})

Cache one cell of an array for writing. Templated overload.

Equivalent to the QVariant overload but accepts any type that QVariant can wrap.

Parameters:
  • arrayKey – Array name (column 2 of the CSV).

  • index – Row index within the array (column 3).

  • key – Cell key (column 4).

  • val – Cell value (column 5).

  • unit – Optional unit (column 6).

void storeArrayValue(QAnyStringView arrayKey, std::size_t index, QAnyStringView key, const QVariant val, QAnyStringView unit = {})

Cache one cell of an array for writing.

Call from storeValues() once per cell. Use array storage for any list-like collection (e.g. one row per pulse-generator channel). If arrayKey is new, the array is created; if index exceeds the current size, the array grows to fit. A typical loop:

for(std::size_t i = 0; i < d_channels.size(); ++i) {
    const auto &c = d_channels[i];
    storeArrayValue(channelArr, i, name,    c.name);
    storeArrayValue(channelArr, i, delay,   c.delay,   "us"_L1);
    storeArrayValue(channelArr, i, enabled, c.enabled);
}

Parameters:
  • arrayKey – Array name (column 2 of the CSV).

  • index – Row index within the array (column 3).

  • key – Cell key (column 4).

  • val – Cell value (column 5).

  • unit – Optional unit (column 6).

template<typename T>
inline T retrieve(QAnyStringView key, const T &defaultValue = QVariant().value<T>())

Pull one scalar row out of the cache.

Call from retrieveValues(). Removes the row from the cache; a second retrieve with the same key returns defaultValue. Convert from QVariant via QVariant::value, so any default value type that round-trips through QVariant is allowed.

Parameters:
  • key – The key matched against column 4 of the cached row.

  • defaultValue – Returned if the key is missing or has already been retrieved. Defaults to a default-constructed T.

Returns:

The retrieved value, or defaultValue.

QString peekUnit(QAnyStringView key) const

Read the unit cell of a cached scalar row without consuming it.

Returns column 6 of the row matching key. Does not erase the cache entry, so a subsequent retrieve() with the same key still returns the value as usual. Returns an empty QString if the key is absent or its unit cell was empty on disk.

QString peekValueString(QAnyStringView key) const

Read the literal value-cell string of a cached scalar row without consuming it.

Returns column 5 of the row matching key as the QString form the CSV reader stuffed into the QVariant. Useful for inspecting the on-disk numeric formatting (e.g., counting fractional digits) before letting retrieve() convert to a typed value. Returns an empty QString if the key is absent or the value cell was empty.

std::size_t arrayStoreSize(QAnyStringView key) const

Number of entries in a cached array.

Use this from retrieveValues() to size your loop before pulling values out with retrieveArrayValue().

Parameters:

key – Array name to look up.

Returns:

Number of entries, or 0 if the array was not stored.

template<typename T>
inline T retrieveArrayValue(QAnyStringView arrayKey, std::size_t index, QAnyStringView key, const T &defaultValue = QVariant().value<T>())

Pull one cell out of a cached array.

Call from retrieveValues(). Removes the cell from the cache; the containing entry’s outer slot is preserved (so arrayStoreSize() does not change), but a second retrieve of the same cell returns defaultValue.

Parameters:
  • arrayKey – Array name.

  • index – Row index within the array.

  • key – Cell key.

  • defaultValue – Returned if the array is missing, the index is out of range, or the cell has already been retrieved.

Returns:

The retrieved value, or defaultValue.

Protected Attributes

QString d_headerKey

Object key (column 0 of the CSV); set in the constructor and treated as immutable.

Private Members

HeaderMap d_values

Map containing key-value pairs

std::map<QString, HeaderArray, std::less<>> d_arrayValues

Map containing lists of key-value pairs

std::vector<HeaderStorage*> d_children

List containing pointers to children