Saving and loading¶
Canonical JSON¶
Records serialize to canonical JSON: UTF-8, keys sorted, no insignificant
whitespace, no NaN or Infinity. The same record always gives the same
bytes, so you can hash, deduplicate and diff records.
import numpy as np
from ecgfeat import ecg_record, dumps_record, loads_record
signal = np.load("ecg.npy")
leads = ["I", "II", "III", "aVR", "aVL", "aVF", "V1", "V2", "V3", "V4", "V5", "V6"]
record = ecg_record(signal, sampling_rate=500, lead_names=leads)
data = dumps_record(record) # bytes
assert dumps_record(loads_record(data)) == data
pretty = dumps_record(record, indent=2) # human-readable, same content
| Function | Purpose |
|---|---|
dumps_record(record, *, profile=None, indent=None) -> bytes |
canonical bytes; profile must be the record's own profile or a smaller one |
loads_record(data, *, validate="strict") -> ECGRecord |
parse bytes or str |
ecg_dump(record, destination, *, indent=None) |
write JSON to a path or text stream |
load_record(source, *, validate="strict", sidecar=None) |
read from a path, stream or mapping; resolves sidecars |
record_to_dict(record) -> dict |
detached plain-Python mapping (same as record.as_dict()) |
A summary record of a 10 s, 12-lead recording with 10 beats stays within
30,000 bytes in schema 1.1 (24,000 in schema 1.0). The test suite enforces this
contract.
Dense sidecars¶
Per-beat, per-lead matrices dominate record size. serialize_record can move
them into a companion NPZ sidecar next to the JSON. The JSON keeps every
other member, plus a descriptor that binds the sidecar to it:
import numpy as np
from ecgfeat import ecg_record, serialize_record, load_record, query_measurement
signal = np.load("ecg.npy")
leads = ["I", "II", "III", "aVR", "aVL", "aVF", "V1", "V2", "V3", "V4", "V5", "V6"]
record = ecg_record(signal, sampling_rate=500, lead_names=leads, profile="all")
encoded = serialize_record(record, sidecar_uri="ecg.dense.npz")
with open("ecg.record.json", "wb") as handle:
handle.write(encoded.json_bytes)
for name, data in encoded.sidecars.items(): # {"ecg.dense.npz": b"PK..."}
with open(name, "wb") as handle:
handle.write(data)
restored = load_record("ecg.record.json") # finds ecg.dense.npz next to it
print(query_measurement(restored, "qrs_duration_ms", lead="V1", beat=0).value)
Properties of the sidecar:
- Same directory.
sidecar_uriis a plain file name, resolved next to the JSON file. Passsidecar=(a path or the NPZ bytes) toload_recordwhen the JSON does not come from a file. - Integrity-bound in both directions. The JSON records the sidecar's
SHA-256, and the sidecar records the record id and schema version. Each
matrix carries a
uint8state mask that must agree with the JSON absence encoding. Any mismatch raisesSidecarError(aRecordValidationError). - Deterministic. The same record gives the same NPZ bytes.
- Transparent. Queries read sidecar-backed values exactly like inline
ones.
strictloading verifies the sidecar immediately;schemaandnoneverify it on first access.
To get an ordinary, self-contained record back:
from ecgfeat import load_record, materialize_record, dumps_record
inline = materialize_record(load_record("ecg.record.json"))
with open("ecg.inline.json", "wb") as handle:
handle.write(dumps_record(inline))
Write the sidecar before (or together with) the JSON, so that a record file
never points at a sidecar that does not exist yet. The command line does this
for you (ecg-record measure --sidecar).
Storing the raw signal¶
The record identifies its signal but does not contain it. Keep both, and check that they still belong together:
import hashlib
import numpy as np
from ecgfeat import load_record
record = load_record("ecg.record.json")
signal = np.load("ecg.npy")
digest = "sha256:" + hashlib.sha256(np.ascontiguousarray(signal, dtype="<f8").tobytes()).hexdigest()
assert digest == record.artifacts["raw_signal"]["sha256"]
ecgfeat.viz performs this check automatically before plotting.
Reading records in other languages¶
A record is plain JSON validated by a standard JSON Schema (draft 2020-12)
shipped at ecgfeat/schemas/ecg-record/1.0/schema.json. Any JSON library can
read it. To resolve an absence state without Python, apply the rules in
Absence states. The strict reader's
cross-field invariants are documented in
The ECG Record.