The ECG Record¶
An ECG Record is one JSON object: the published, versioned result of measuring
one recording. This page describes schema 1.1.0 (written by ecg-records
0.2) member by member; schema 1.0.0 (written by 0.1) is the same minus the
additions marked 1.1. The machine-readable definitions ship in the package
(ecgfeat/schemas/ecg-record/<1.0|1.1>/schema.json), and the schema
references (1.1,
1.0) list every published field with
its profiles, validation status and evidence.
In Python a record is an immutable ECGRecord. Its top-level members are
attributes (record.acquisition, record.measurements, …; nested mappings are
read-only), and record.as_dict() returns a detached, mutable copy.
The record and the raw signal¶
The durable product is the pair (raw signal, ECG Record). The record never
embeds samples. /artifacts/raw_signal carries the SHA-256 of the signal
exactly as measured (little-endian float64, C order, shape
[n_leads, n_samples]), so a record can always be checked against the signal
it claims to describe. Every fiducial is a zero-based sample index into that
signal at its native sampling rate.
Top-level members¶
| Member | Required | Content |
|---|---|---|
schema_version |
yes | "1.1.0" (0.2) or "1.0.0" (0.1): the version of this format, independent of the package version |
profile |
yes | "summary", "all" or "debug" (profiles) |
record_id |
yes | identifier used in every address; any non-empty string without @, # or line breaks |
intended_use |
no | always "research_and_engineering_only" in records this package writes |
acquisition |
yes | how the signal was recorded (below) |
axes |
yes | the beat and lead axes that index every matrix |
quality |
yes | record and per-lead signal quality |
provenance |
yes | library, configuration, methods and policies that produced the values |
delineation |
yes | fiducial points (/delineation/fiducials/*) |
measurements |
yes | intervals, amplitudes, areas, record-level quantities, flags |
artifacts |
yes | identity of the raw signal; optional dense-matrix sidecar |
metadata |
no | published patient metadata (/metadata/patient) when supplied |
debug |
no | engine diagnostics in the debug profile only; no compatibility guarantee |
extensions |
no | reserved for namespaced third-party additions |
Readers must ignore members they do not understand: later 1.x schema versions
only add optional members (see Compatibility).
acquisition¶
| Key | Example | Meaning |
|---|---|---|
sample_rate_hz |
500.0 |
native sampling rate of the measured signal |
sample_count |
5000 |
samples per lead |
input_mode |
"standard_12" |
standard_12 or limited |
amplitude_unit |
"mV" |
unit of the input samples |
leads |
["I", "II", …] |
lead names in row order |
capabilities |
see below | what the input supports as a whole |
capabilities states, for axis, formal_qt and diagnosis, whether the
record supports them ({"status": "available"}) or why not
({"status": "not_applicable", "reason": "input_mode_limited"}). diagnosis
is always not_applicable with reason measurement_package_boundary: this
package measures and never diagnoses.
axes¶
axes.leads: the lead names; the second index of every per-lead matrix.axes.beats: one object per detected beat,{"id": "b0001", "r_sample": 250}; the first index of every per-beat matrix.r_sampleis the beat's reference R sample. Beat ids are stable within a record and are used in absence overrides. 1.1: beats also carrygroup(the engine's morphology group),dominant(member of the dominant group) andpaced(captured by a pacing spike).- 1.1,
all/debugprofiles:axes.atrial_events, one object per detected atrial activation,{"id": "a0001", "sample": 116, "association": "conducted", "beat": "b0001", "confidence": 1.0}(associationconducted,blockedorretrograde), andaxes.pacing_spikes,{"sample": …}.
quality¶
{"record": {"status": "usable", "grade": "Q0"},
"leads": {"II": {"status": "usable"}, "V1": {"status": "limited", "reason": "quality_gate"}}}
quality.record.gradeis the record signal-quality grade fromQ0(best) toQ3.statusisusableforQ0/Q1andlimitedotherwise.quality.leads.<lead>.statusisusableorlimited. Alimitedlead failed the per-lead quality gate; its values are still published, but treat them with care.- 1.1: every lead also has its
grade; theall/debugprofiles addreason_codes,reliable_for(p,qrs,t,qt) and noisemetrics(baseline wander, muscle noise, powerline, clipping, flatline; 0 clean, 1 worst).
provenance¶
| Key | Meaning |
|---|---|
library_version |
package version that produced the record |
schema_version |
schema version written |
input_fingerprint |
SHA-256 over the samples, rate, lead names and unit |
config, config_hash |
config.resolved is the fully resolved configuration; config.id and config_hash are its SHA-256 |
method_requested, method_resolved |
the requested method (default) and the algorithm that ran (legacy_dxl_inspired) |
fallback_path |
fallbacks taken, in order (empty when none) |
experimental |
names of enabled experimental refinements (empty by default) |
internal_sample_rate_hz, mains_frequency_hz |
internal analysis rate and mains-interference setting |
fiducial_sample_domain, coordinate_conversion |
how internal positions were mapped to native sample indices |
algorithm_paths, policy_execution |
which algorithm paths and policies ran |
fields |
named provenance entries that fields point to with provenance_ref |
Fields¶
Every value in delineation and measurements lives in a field object.
This is /measurements/intervals/qrs_duration_ms of the reference record,
showing the first two of its ten beats:
{
"type": "integer",
"unit": "ms",
"axes": ["beat", "lead"],
"values": [[78, 78, 70, 94, 88, 70, 154, 168, 204, 116, 80, 76],
[76, 78, 70, 94, 108, 70, 158, 148, 234, 116, 78, 76]],
"nullable": true,
"validation": {"status": "unvalidated", "evidence": []},
"provenance_ref": "/provenance/fields/measurement_default"
}
| Key | Meaning |
|---|---|
type |
JSON type of each value: integer, or number for fractions such as pnn50 (1.1), boolean for flags |
unit |
sample, ms, uV, uV_ms, 1/min, deg, 1 (fraction) or count |
axes |
["beat", "lead"] for a matrix indexed like values[beat][lead], ["lead"] for one value per lead (median beat, 1.1), ["beat"] for one value per beat (1.1), or [] for a single record-level value |
values |
the numbers; null where there is no value |
nullable |
whether null may appear |
validation |
the field's validation status and evidence (Validation) |
provenance_ref |
JSON pointer to the provenance entry for this field |
absence |
why values are missing (below); omitted when nothing needs explaining |
feature_states |
the feature state of every cell, as a default plus exceptions or a reference into /provenance/feature_state_tables |
Units and rounding¶
| Unit | Used for | Convention |
|---|---|---|
sample |
fiducials | zero-based index into the raw signal at sample_rate_hz |
ms |
intervals | rounded half-to-even to whole milliseconds |
uV |
amplitudes | microvolts, rounded half-to-even; sign preserved (negative S and ST values) |
uV_ms |
QRS signed area | microvolt-milliseconds |
1/min |
heart rate | beats per minute |
deg |
frontal QRS axis | degrees |
Published fields¶
| Group | Field | Axes | Profiles |
|---|---|---|---|
delineation/fiducials |
p_onset, p_offset, qrs_onset, r_peak, qrs_offset, j_point, t_offset |
beat × lead | all |
measurements/intervals |
p_duration_ms, pr_interval_ms, qrs_duration_ms, qt_interval_ms, tpeak_tend_ms |
beat × lead | all |
measurements/intervals |
jt_interval_ms |
beat × lead | all, debug |
measurements/amplitudes |
p_amplitude_uv, r_amplitude_uv, s_amplitude_uv, st_80ms_uv, t_amplitude_uv |
beat × lead | all |
measurements/amplitudes |
u_amplitude_uv |
beat × lead | all, debug |
measurements/areas |
qrs_signed_area_uv_ms |
beat × lead | all |
measurements/global |
heart_rate_bpm, frontal_qrs_axis_deg |
record | all |
measurements/global |
qtc_bazett_ms, qrs_wide_ms |
record | all, debug |
"all" in the Profiles column means summary, all and debug.
measurements/flags is reserved and empty in schema 1.0.
Schema 1.1 adds, in the same five groups:
| Group | Field | Axes | Profiles |
|---|---|---|---|
measurements/global |
p_duration_ms, pr_interval_ms, qrs_duration_ms, qt_interval_ms, qtc_bazett_ms, qtc_fridericia_ms, p_axis_deg, t_axis_deg, atrial_rate_bpm |
record | all |
measurements/global |
qtc_framingham_ms, qtc_hodges_ms, qrs_t_angle_deg, heart_rate_min_bpm, heart_rate_max_bpm, rr_mean_ms, rr_sd_ms, rmssd_ms, pnn50, rr_cv, qt_dispersion_ms, ptf_v1_uv_ms, sokolow_lyon_uv, cornell_voltage_uv, paced_beat_fraction, pacing_spike_count, organized_p_fraction, atrial_residual_rms_uv, atrial_dominant_cycle_ms |
record | all, debug |
measurements/amplitudes |
median_q_amplitude_uv, median_r_amplitude_uv, median_s_amplitude_uv, median_st_j_uv, median_st_60ms_uv, median_t_amplitude_uv |
lead | all |
measurements/amplitudes |
median_p_amplitude_uv, median_r_prime_amplitude_uv, median_st_40ms_uv, median_st_80ms_uv |
lead | all, debug |
measurements/intervals |
median_p_duration_ms, median_pr_interval_ms, median_qrs_duration_ms, median_qt_interval_ms, median_q_duration_ms |
lead | all, debug |
measurements/areas |
median_qrs_signed_area_uv_ms |
lead | all, debug |
measurements/intervals |
rr_interval_ms (first beat not_applicable) |
beat | all |
measurements/flags |
p_present, qrs_present, q_present, r_present, s_present, r_prime_present, s_prime_present, t_present, u_present |
beat × lead | all, debug |
Record-level and median-beat values are the engine's global (multilead)
measurement on the dominant non-paced beats. They are not averages of the
per-beat cells and can differ from them. Formal 12-lead quantities (axes, QT,
QTc, voltage sums) are not_applicable(input_mode_limited) in limited mode.
Four record-level names exist at two levels (p_duration_ms,
pr_interval_ms, qrs_duration_ms, qt_interval_ms); see
Querying.
Invariants that every record written by this package satisfies:
- within each wave, landmarks are ordered:
p_onset ≤ p_offsetandqrs_onset ≤ r_peak ≤ qrs_offsetfor every beat and lead where both are present (the strict reader checks this for any record); - every fiducial lies inside the signal (
0 ≤ sample < sample_count); - no published interval is negative.
A cell that would violate one of these is published as
unmeasurable(fiducial_order_violation), unmeasurable(sample_outside_acquisition)
or unmeasurable(negative_interval), and the dependent duration is withheld
with it. The rest of the record is still published.
Absence states¶
A missing value is never a bare "no number". Each null cell has one of three
states:
| State | Meaning | Example reasons |
|---|---|---|
plain null |
no stronger claim is made | a value absent in imported data |
unmeasurable(reason) |
the quantity applies to this beat and lead, but no trustworthy value exists | measurement_unavailable, fiducial_order_violation, negative_interval, sample_outside_acquisition |
not_applicable(reason) |
the input contract excludes the quantity | input_mode_limited |
Reasons are an open vocabulary: any non-empty string is valid. The reasons
listed are the ones this package writes; records from other producers (like
the example at the end of this page, with low_snr) may carry others.
The absence member of a field encodes the reasons compactly, in one of these
forms:
{"kind": "not_applicable", "reason": "input_mode_limited"}
Whole field: every null in the field has this state (typical for
record-level values).
{"default": {"kind": "unmeasurable", "reason": "measurement_unavailable"},
"unmeasurable": {"b0003|V1": "fiducial_order_violation"}}
Default plus overrides. Each null cell whose coordinate is listed under
unmeasurable or not_applicable takes that reason. Every other null cell
takes the default. Cells that hold a value are unaffected.
{"unmeasurable": {"b0001|II": "low_snr"}}
Overrides only. The listed cells have a reason. An unlisted null cell is
a plain null.
Coordinates are <beat id>|<lead name>, which is why lead names may not
contain |. You rarely need to decode this yourself: query_measurement
returns the resolved state of any cell as result.absence
(Querying).
Feature states¶
Besides the absence state of a missing value, every cell carries a feature
state: observed, absent, not_detected, not_applicable,
unassessable, not_computed or not_supported, with a reason. A value can
be present and still unassessable: the engine measured it but it failed a
reportability gate (for example the QT gate). Read the state before using a
value. query_measurement returns it as feature_state, the tables as
state and state_reason, and ECGRecord.summary() marks such values with
*.
Validation status¶
Each field carries validation.status, taken from the validation-evidence
registry shipped for the record's schema family, and never higher than that
registry allows. In schema 1.0.0 every published field is unvalidated. In
schema 1.1.0 the record-level p_duration_ms, pr_interval_ms,
qrs_duration_ms, qt_interval_ms and the per-lead t_offset are
benchmark_validated, heart_rate_bpm, p_axis_deg, frontal_qrs_axis_deg
and t_axis_deg are indirectly_validated (0.3.2), each citing its
evidence; every other field is unvalidated. See
Validation and benchmarks.
Complete example¶
A complete, valid summary record (schema 1.0.0; a 1.1.0 record has the same
shape plus the additions above) for a short two-lead recording in limited
mode. It shows all three absence forms: low_snr overrides on lead II, a
plain null in st_80ms_uv, and a whole-field not_applicable frontal axis.
The repository's test suite validates this exact example against the JSON
Schema and the strict reader.
{
"schema_version": "1.0.0",
"profile": "summary",
"record_id": "example-short-001",
"acquisition": {
"sample_rate_hz": 500,
"sample_count": 1000,
"input_mode": "limited",
"amplitude_unit": "mV",
"leads": ["I", "II"],
"capabilities": {
"diagnosis": {
"status": "not_applicable",
"reason": "input_mode_limited"
},
"axis": {
"status": "not_applicable",
"reason": "input_mode_limited"
},
"formal_qt": {
"status": "not_applicable",
"reason": "input_mode_limited"
}
}
},
"axes": {
"beats": [
{
"id": "b0001",
"r_sample": 500
}
],
"leads": ["I", "II"]
},
"quality": {
"record": {
"status": "usable"
},
"leads": {
"I": {
"status": "usable"
},
"II": {
"status": "limited",
"reason": "low_snr"
}
}
},
"provenance": {
"library_version": "0.0.0-example",
"schema_version": "1.0.0",
"input_fingerprint": "sha256:example-input",
"config": {
"id": "default-example",
"sha256": "sha256:example-config"
},
"policies": {
"pacing": {
"name": "default_pacing_policy",
"version": "1"
},
"qt_rescue": {
"name": "default_qt_rescue_policy",
"version": "1"
}
},
"algorithm_paths": {
"delineation": "default",
"st": "default",
"qt": "default"
},
"fields": {
"delineation_default": {
"method": "selected_consensus"
},
"measurement_default": {
"method": "published_core"
}
}
},
"delineation": {
"fiducials": {
"p_onset": {
"type": "integer",
"unit": "sample",
"axes": ["beat", "lead"],
"values": [[430, null]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/delineation_default",
"absence": {
"unmeasurable": {
"b0001|II": "low_snr"
}
}
},
"p_offset": {
"type": "integer",
"unit": "sample",
"axes": ["beat", "lead"],
"values": [[470, null]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/delineation_default",
"absence": {
"unmeasurable": {
"b0001|II": "low_snr"
}
}
},
"qrs_onset": {
"type": "integer",
"unit": "sample",
"axes": ["beat", "lead"],
"values": [[482, 483]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/delineation_default"
},
"r_peak": {
"type": "integer",
"unit": "sample",
"axes": ["beat", "lead"],
"values": [[500, 500]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/delineation_default"
},
"qrs_offset": {
"type": "integer",
"unit": "sample",
"axes": ["beat", "lead"],
"values": [[526, 527]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/delineation_default"
},
"j_point": {
"type": "integer",
"unit": "sample",
"axes": ["beat", "lead"],
"values": [[526, 527]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/delineation_default"
},
"t_offset": {
"type": "integer",
"unit": "sample",
"axes": ["beat", "lead"],
"values": [[690, 694]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/delineation_default"
}
}
},
"measurements": {
"intervals": {
"p_duration_ms": {
"type": "integer",
"unit": "ms",
"axes": ["beat", "lead"],
"values": [[80, null]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default",
"absence": {
"unmeasurable": {
"b0001|II": "low_snr"
}
}
},
"pr_interval_ms": {
"type": "integer",
"unit": "ms",
"axes": ["beat", "lead"],
"values": [[104, null]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default",
"absence": {
"unmeasurable": {
"b0001|II": "low_snr"
}
}
},
"qrs_duration_ms": {
"type": "integer",
"unit": "ms",
"axes": ["beat", "lead"],
"values": [[88, 88]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
},
"qt_interval_ms": {
"type": "integer",
"unit": "ms",
"axes": ["beat", "lead"],
"values": [[416, 422]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
},
"tpeak_tend_ms": {
"type": "integer",
"unit": "ms",
"axes": ["beat", "lead"],
"values": [[78, 82]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
}
},
"amplitudes": {
"p_amplitude_uv": {
"type": "integer",
"unit": "uV",
"axes": ["beat", "lead"],
"values": [[112, null]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default",
"absence": {
"unmeasurable": {
"b0001|II": "low_snr"
}
}
},
"r_amplitude_uv": {
"type": "integer",
"unit": "uV",
"axes": ["beat", "lead"],
"values": [[1040, 1265]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
},
"s_amplitude_uv": {
"type": "integer",
"unit": "uV",
"axes": ["beat", "lead"],
"values": [[-310, -420]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
},
"st_80ms_uv": {
"type": "integer",
"unit": "uV",
"axes": ["beat", "lead"],
"values": [[24, null]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
},
"t_amplitude_uv": {
"type": "integer",
"unit": "uV",
"axes": ["beat", "lead"],
"values": [[345, 318]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
}
},
"areas": {
"qrs_signed_area_uv_ms": {
"type": "integer",
"unit": "uV_ms",
"axes": ["beat", "lead"],
"values": [[18200, 19750]],
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
}
},
"flags": {},
"global": {
"heart_rate_bpm": {
"type": "integer",
"unit": "1/min",
"axes": [],
"values": 60,
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default"
},
"frontal_qrs_axis_deg": {
"type": "integer",
"unit": "deg",
"axes": [],
"values": null,
"nullable": true,
"validation": {
"status": "unvalidated",
"evidence": []
},
"provenance_ref": "/provenance/fields/measurement_default",
"absence": {
"kind": "not_applicable",
"reason": "input_mode_limited"
}
}
}
},
"artifacts": {
"raw_signal": {
"uri": "example-short-001.npy",
"media_type": "application/x-npy",
"sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"dense_measurements": null
}
}