The ECG Toolkit is a Rust library for reading, writing, converting, and processing electrocardiogram (ECG) medical data files. It is a conversion of the C# ECG Conversion Toolkit originally written by Maarten JB van Ettinger.
- License: Apache License, Version 2.0
- Original author: Maarten JB van Ettinger
- Original source: van Ettinger Information Technology / Thoraxcentrum, Erasmus MC
The crate is organized into seven modules:
ecgtoolkit/
├── types/ Core type definitions (LeadType, Date, enums)
├── signal/ ECG signal data (Signal, Signals, QrsZone)
├── demographics/ Patient demographic data (Demographic trait, Drug, AcquiringDeviceId)
├── diagnostic/ Diagnostic statements (Statements, DiagnosticStatements trait)
├── measurements/ ECG measurements (GlobalMeasurement, LeadMeasurement, Spike, Morphology)
├── dsp/ Digital signal processing (Butterworth filters, FIR, IIR)
├── converter/ Format conversion interfaces (EcgFormat trait, EcgConfig)
└── tools/ Utility functions (lead calculations, resampling, anonymization)
Core type definitions used throughout the library.
Enumeration of 150+ ECG lead types. Matches the C# LeadType enum values exactly.
use ecgtoolkit::types::LeadType;
let lead = LeadType::V1;
let lead_from_byte = LeadType::from_u8(3); // V1
assert_eq!(lead, lead_from_byte);Standard 12-lead types: I, II, III, AVR, AVL, AVF, V1-V6.
Extended types include: V7-V9, V2R-V7R, X, Y, Z, Frank leads,
derived leads (prefixed with D), and many more.
The from_u8 method safely converts a byte to a LeadType, returning
Unknown for out-of-range values.
MDC (Medical Device Communication) reference IDs for lead types. Maps to
standard vital signs identifiers (e.g., MdcEcgLeadI = 1, MdcEcgLeadIII = 61).
Simple date type with year, month, and day fields, matching the SCP format.
use ecgtoolkit::types::Date;
let date = Date::new(2024, 6, 15);
assert!(date.is_existing_date());
// Leap year validation
assert!(Date::new(2024, 2, 29).is_existing_date());
assert!(!Date::new(2023, 2, 29).is_existing_date());| Enum | Variants |
|---|---|
Sex |
Unspecified, Male, Female, Null |
Race |
Unspecified, Caucasian, Black, Oriental, Null |
AgeDefinition |
Unspecified, Years, Months, Weeks, Days, Hours |
HeightDefinition |
Unspecified, Centimeters, Inches, Millimeters |
WeightDefinition |
Unspecified, Kilogram, Gram, Pound, Ounce |
DeviceType |
Cart, System |
DeviceManufacturer |
Unknown, Burdick, Cambridge, ... Other (21 variants) |
ElectrodeConfigCodeTwelveLead |
Unspecified, StandardTwelveLead, ... (7 variants) |
ElectrodeConfigCodeXyz |
Unspecified, Frank, ... (7 variants) |
ECG signal data structures.
Represents one ECG lead/channel.
use ecgtoolkit::signal::Signal;
use ecgtoolkit::types::LeadType;
let sig = Signal {
lead_type: LeadType::I,
rhythm_start: 0,
rhythm_end: 1000,
rhythm: Some(vec![0i16; 1000]),
median: None,
};Key methods:
apply_filter(rhythm_filter, median_filter)— Apply DSP filters, returning a new SignalSignal::is_normal(leads)— Check if first 8 leads are I, II, V1-V6Signal::nr_simultaneously(leads)— Count leads recorded at the same time (within 5-sample tolerance)Signal::sort_on_type(leads)— Sort leads by type (quicksort)
Container for all ECG signal leads with metadata.
use ecgtoolkit::signal::Signals;
let mut sigs = Signals::with_nr_leads(12);
sigs.rhythm_avm = 2.5; // Analog value multiplier in mV
sigs.rhythm_samples_per_second = 500;Key methods:
| Method | Description |
|---|---|
nr_leads() / set_nr_leads() |
Get/set number of leads |
get(i) / get_mut(i) |
Access lead by index |
set_leads(leads) |
Set all leads (max 255) |
is_normal() |
Check standard 8-lead order |
is_twelve_leads() |
Detect 12-lead configuration |
is_fifteen_leads() |
Detect 15-lead configuration (both variants) |
calculate_twelve_leads() |
Derive 12 leads from 8 (I, II, V1-V6) |
calculate_start_and_end() |
Get signal time boundaries |
nr_simultaneously() |
Count simultaneous leads |
sort_on_type() |
Sort leads by type |
trim_signals(val) |
Remove leading/trailing padding |
resample(sps) |
Resample all leads to new rate |
set_avm(avm) |
Change analog value multiplier |
apply_bandpass_filter(lo, hi) |
Apply Butterworth bandpass filter |
apply_lowpass_filter(cutoff) |
Apply Butterworth lowpass filter |
apply_highpass_filter(cutoff) |
Apply Butterworth highpass filter |
Describes a QRS complex zone with start, fiducial point, and end positions.
Patient and device identification data.
Defines 40+ getter/setter methods for patient demographics. Implementations provide access to:
- Patient identity: last name, first name, patient ID, prefix/suffix
- Physical data: age (with units), height, weight, sex, race
- Device IDs: acquiring and analyzing machine identifiers
- Acquisition info: timestamp, filters, blood pressure
- Institutional: institution, department, physicians, technician
- Clinical: drugs, referral indication, room description, stat code
Device identification with institution number, department, device ID, manufacturer, capabilities, and model description.
use ecgtoolkit::demographics::AcquiringDeviceId;
let device = AcquiringDeviceId::no_device();
assert_eq!(&device.model_description[..5], b"MCONV");Simple drug record with class, code, and text description.
Diagnostic interpretation statements for an ECG.
use ecgtoolkit::diagnostic::Statements;
let stmt = Statements {
confirmed: true,
interpreted: true,
time: 1700000000,
statement: Some(vec!["Normal sinus rhythm".to_string()]),
};Interface for getting and setting diagnostic statements.
ECG measurement data at both global and per-lead levels.
One wave measurement (SCP/UNIPRO defined) containing P/QRS/T onset/offset values and axis measurements.
use ecgtoolkit::measurements::GlobalMeasurement;
let mut m = GlobalMeasurement::default();
m.set_p_dur(80); // P-wave duration: 80ms
m.set_qrs_dur(100); // QRS duration: 100ms
m.set_qt_dur(400); // QT duration: 400ms
// Calculate corrected QT (Bazett formula)
use ecgtoolkit::measurements::QTcCalcType;
let qtc = m.calc_qtc(800, 75, QTcCalcType::Bazett); // ~447msSentinel values:
NO_VALUE = 29999— indicates no measurement available (u16)NO_AXIS_VALUE = 29999— indicates no axis measurement (i16)
QTc calculation types: Bazett, Hodges, Fridericia, Framingham.
Collection of global measurements including ventricular rate, average RR/PP intervals, spike data, and QTc configuration.
Per-lead measurement storage using a BTreeMap keyed by MeasurementType.
use ecgtoolkit::measurements::{LeadMeasurement, MeasurementType};
use ecgtoolkit::types::LeadType;
let mut lm = LeadMeasurement::with_lead_type(LeadType::V1);
lm.set(MeasurementType::Ramp, 1500);
lm.set(MeasurementType::Samp, -500);
assert_eq!(lm.count(), 2);44 measurement types including durations (Pdur, QRSdur, QTint), amplitudes (Qamp, Ramp, Samp), ST measurements, morphology indices, and wave offsets.
Aliases: RONSET = Qoffset, SONSET = Roffset, etc.
Wave morphology classification: Positive, Negative, PositiveNegative, NegativePositive, NotchedMShaped, NotchedWShaped, etc.
Pacemaker spike data with time and amplitude.
Digital signal processing filters for ECG data.
pub trait Filter {
fn compute(&mut self, input: f64) -> f64;
}All filters implement this trait, allowing runtime polymorphism via
Box<dyn Filter>.
Multi-section Butterworth filter implementations for ECG signal processing:
| Filter | Usage |
|---|---|
LowpassFilterButterworth |
Remove high-frequency noise |
HighpassFilterButterworth |
Remove baseline wander |
BandpassFilterButterworth |
Isolate frequency band (cascade of lowpass + highpass) |
use ecgtoolkit::dsp::{Filter, BandpassFilterButterworth};
let mut filter = BandpassFilterButterworth::new(
0.5, // bottom frequency (Hz)
40.0, // top frequency (Hz)
2, // number of sections
500.0, // sample rate (Hz)
);
let output = filter.compute(input_sample);Each filter is composed of sections (LowpassFilterButterworthSection,
HighpassFilterButterworthSection), which internally use FIR and IIR
filter building blocks.
Low-level Finite Impulse Response and Infinite Impulse Response filter implementations used by the Butterworth filter sections.
Core interfaces for ECG file format conversion.
Abstract interface that all ECG file format implementations must satisfy. Defines methods for:
- Reading from files, streams, or byte buffers
- Writing to files, streams, or byte buffers
- Format detection (
check_format_*) - Access to demographics, diagnostics, signals, and measurements
- Anonymization
- Format validation
Configuration management for format-specific settings. Supports mandatory and optional configuration keys with validation.
use ecgtoolkit::converter::EcgConfig;
let mut cfg = EcgConfig::new(
&["sample_rate"], // mandatory
&["encoding", "compression"], // optional
None, // no custom check function
);
cfg.set("sample_rate", Some("500"));
assert!(cfg.configuration_works());Utility functions for ECG signal processing.
Calculate derived ECG leads from primary leads using standard formulas:
| Function | Formula |
|---|---|
calculate_lead_iii |
III = II - I |
calculate_lead_avr |
aVR = -(I + II) / 2 |
calculate_lead_avl |
aVL = (2I - II) / 2 or (I - III) / 2 |
calculate_lead_avf |
aVF = (2II - I) / 2 or (II + III) / 2 |
calculate_leads_from_two |
All four from I and II |
calculate_leads_from_three |
aVR, aVL, aVF from I, II, III |
These satisfy Einthoven's law: I + III = II.
| Function | Description |
|---|---|
lead_subtract(a, b) |
Subtract lead B from A in-place |
lead_add(a, b) |
Add lead B to A in-place |
change_multiplier(src, old, new) |
Scale signal values |
copy_signal(src, dst, ...) |
Copy signal data between buffers |
shift_signal(src, shift) |
DC offset removal |
Polynomial interpolation (Neville's algorithm) for sample rate conversion:
use ecgtoolkit::tools::ecg_tool;
let src = vec![100i16; 500]; // 500 samples at 250 Hz
let dst = ecg_tool::resample_lead(&src, 250, 500); // upsample to 500 Hzuse ecgtoolkit::tools::ecg_tool;
// Replaces patient names, IDs, and birth date with anonymized values
ecg_tool::anonymous(&mut demographics, '*');The standard 12-lead ECG consists of:
- Limb leads: I, II, III (Einthoven triangle)
- Augmented leads: aVR, aVL, aVF (Goldberger)
- Precordial leads: V1, V2, V3, V4, V5, V6 (Wilson)
The toolkit can derive the full 12 leads from 8 independent leads (I, II, V1-V6) using Einthoven's law and augmented lead formulas.
Two standard 15-lead configurations are supported:
- Standard 12 + V3R, V4R, V7 (right-sided and posterior)
- Standard 12 + V7, V8, V9 (posterior leads)
The crate includes 184 comprehensive tests across 7 test modules:
cargo testTests cover all modules including:
- Type conversions and enum values
- Signal lead detection and sorting
- All four QTc formulas with clinical ranges
- Einthoven's law mathematical verification
- DSP filter frequency response characteristics
- Resampling accuracy
- Configuration management
- 12-lead and 15-lead detection
Format implementations (Phase 7) are planned for:
- SCP — Standardized Communications Protocol
- HL7 aECG — XML-based annotated ECG
- DICOM — Medical imaging standard
- ISHNE — Holter format
- MUSE XML — GE Healthcare format
- Omron ECG — Consumer device format
- CSV — Comma-separated values export
- PDF — PDF rendering
- Raw — Raw binary format