Architecture¶
This page is for contributors. It explains how the package is organized, and which rules keep the parts separate.
Repository layout¶
feature_extraction/ the ecg-records distribution (pyproject.toml, README.package.md, CHANGELOG.md)
ecgfeat/ the import package
__init__.py lazy public facade (__all__ = PUBLIC_API + LEGACY_API)
config.py, errors.py public configuration and error types
refinement.py RefinementConfig (experimental switches)
record/ L4: the ECG Record model, validation, query, serialization, sidecars
pipeline/ L3 (shell): extractor, record builder, file/segment front ends;
stages, policies and context are aliases of _engine.pipeline
availability.py availability states shared by the record and the engine
_engine/ L1/L2: private numerical engine, incl. _engine/pipeline (context, stages, policies)
viz/ plotting of (signal, record); Matplotlib only
compat/ the explicit legacy contract (removed no earlier than 0.3.0)
io.py WFDB MAT/header reading
cli.py the ecg-record command
schemas/ecg-record/1.0/ JSON Schema and validation-evidence registry (package data)
<old module names>.py deprecated aliases of moved modules
schemas/ecg-record/1.0/ repository copy of the schema, registry and the legacy crosswalk
tests/ unit, contract, property and fixture tests
benchmarks/golden/ golden regression corpus: manifests, frozen baselines, harness
tools/ release, documentation, registry and performance checks
docs/site/ this documentation (MkDocs)
Layers¶
ecgfeat (facade) cli viz (Matplotlib)
│ │ │
▼ ▼ ▼
L4 record ◄──────────────────────── pipeline ──► record builder
▲ (model, contract, query, │ L3: extractor (orders the stages)
│ serialize, sidecar) ▼
│ _engine.pipeline L2+: context, stages, policies
│ │
│ _engine L2: preprocess, quality, detection,
│ │ beats, delineation, atrial, measurement
│ ▼
│ _engine.foundation L1: numeric helpers, internal models
│
compat ──► legacy API over the same pipeline (lazy access to ecginterpret)
- L1
_engine.foundation: numeric helpers, internal data models and the optional Numba kernel. Imports no other engine package. - L2
_engine: the measurement algorithms. Never imports the record, pipeline, compatibility, plotting or interpretation code. - L3
pipeline: orchestration.extractor.pyholds the publicecg_*functions and the stage order, andrecord_builder.pyprojects measurements into a record. The eight stages, the four policy objects and thePipelineContextlive in the compiled engine (_engine/pipeline/, since 0.3.2): they carry measurement policy.ecgfeat.pipeline.stages.*,ecgfeat.pipeline.policies.*andecgfeat.pipeline.contextare silent aliases of the same module objects. The engine's pipeline reads the availability types from the dependency-freeecgfeat.availability. - L4
record: everything about the published format, with no dependency on the engine. Reading and querying a record never loads NumPy-heavy code. vizreads records (and, for the legacy helpers, legacy models) and draws them. It never measures.compatkeeps the legacy API working on top of the same pipeline. It is the only module that may reach the separateecginterpretdistribution, and it does so lazily.
Enforced rules¶
feature_extraction/.importlinter states seven contracts, checked in CI with
lint-imports:
ecgfeat.recordimports no engine, pipeline, legacy, plotting or interpretation code.ecgfeat._enginenever imports record, pipeline, compatibility, plotting or interpretation code.ecgfeat._engine.foundationimports no measurement-engine siblings.- The pipeline (shell and engine parts) never imports interpretation, plotting, the legacy exporter or the preserved legacy orchestration.
- Only
ecgfeat.compatreaches the interpretation distribution. ecgfeat.viznever reaches the pipeline or interpretation.ecgfeat.viznever imports the private engine directly.
Tests also check that import ecgfeat loads neither Matplotlib nor the
engine, and that import ecgfeat.viz does not load Matplotlib.
The pipeline¶
ecg_record = ecg_prepare → ecg_measure → ecg_emit.
ecg_measure runs the stage sequence in pipeline/extractor.py
(LEGACY_STAGE_SEQUENCE) over an immutable PipelineContext. Each stage
returns a new context, never mutating the previous one:
| Order | Stage (_engine/pipeline/stages/…) |
|---|---|
| 1 | input.run |
| 2 | quality.run |
| 3 | ventricular.run |
| 4 | beats.run |
| 5 | delineation.run |
| 6 | beats.refine_measurement_group |
| 7 | delineation.settle_qrs_tail_and_refine_t |
| 8 | atrial.run |
| 9 | measurement.run |
| 10 | finalize.run |
Contextual decisions live in four policy objects (pipeline/policies/):
pacing, qt (QT rescue), applicability and lead_integrity. Their
decisions accumulate as events in the context and are summarized in the
record's provenance (policy_execution).
ecg_emit calls record_builder.build_record_document. It maps engine
quantities to record fields through the reviewed
legacy crosswalk, applies the
invariants (withholding violating
cells as unmeasurable), encodes absence states, attaches validation status
from the registry, and validates the result.
Adding or changing a published field¶
A new record field is a schema change. It needs all of the following:
- a name, unit, axes and absence semantics, documented in The ECG Record;
- an entry in the validation-evidence registry
(
validation-evidence.json, both copies) with profiles, publication tier, validation status and ceiling, legacy source and known limitations; - a crosswalk entry when it comes from an existing engine quantity;
- size accounting: the
summaryprofile must stay within 30,000 bytes (schema 1.1); - a schema minor version (additive) and at least a package minor release;
- regenerated reference pages (
python tools/build_docs.py).
Changing a measured value (an algorithm change) is a behavioural change. It must show up as golden-corpus differences, be reviewed, and ship with a new frozen baseline; see Golden corpus.