Physiological signal algorithms in Swift 6:
- heart-rate-variability metrics,
- RR artifact correction,
- R-peak detection,
- ECG-derived respiration, and
- step detection and step prediction from accelerometry. Ports of published methods, with every reference pinned in PROVENANCE.md.
Dependency-free — Apple Accelerate only, no packages.
Largely written by Claude. Not thoroughly reviewed by a human and no independent security review. Caveat emptor. Not a medical device, and not intended for diagnosis or treatment.
meanRR, rmssd, sdnn, standardDeviation |
time-domain HRV metrics over an RR series, via vDSP |
RRArtifactCorrector |
a corrector seam — one RR interval in, a value plus an artifact flag out |
PercentageJumpCorrector |
causal percentage-jump rejection with a local expectation |
RPeakDetector / RPeak |
a detector seam — samples in, absolute sample indices out |
PanTompkinsDetector |
causal Pan–Tompkins QRS detection, batch-fed, fiducial measured off the raw signal |
respiratoryRate |
breathing rate from R-wave amplitude modulation, with a measured noise guard |
magnitudes |
Euclidean norm over chosen axes of a flat row-major batch — 3-axis, 6-axis or scalar |
StepDetector / Step |
a detector seam — accelerometer batches in, foot-falls at absolute indices out |
PeakStepDetector |
causal peak detection with an adaptive threshold and a walking gate |
StepPredictor / StepPrediction |
a predictor seam — where the next foot-fall will be, plus gait phase |
AdaptiveOscillatorStepPredictor |
a pool of adaptive frequency oscillators entrained to the waveform |
PhaseLockedStepPredictor |
a two-term phase-locked loop on step events, the baseline to beat |
Swift 6.0, macOS 13+ / iOS 16+.
.package(url: "https://github.com/PhysiologyWorkbench/PhysioKit", from: "0.1.0")var detector = PanTompkinsDetector(sampleRate: 130)
var corrector = PercentageJumpCorrector()
for batch in ecgBatches { // [Double], microvolts
for peak in detector.accept(batch) {
// peak.sampleIndex is absolute since the last reset()
let rr = msBetween(peak, previous)
let corrected = corrector.accept(rr)
if !corrected.isArtifact {
rrSeries.append(corrected.valueMs)
}
}
}
let hrv = rmssd(rrSeries)
let breathsPerMinute = respiratoryRate(amplitudes: amplitudes, times: beatTimes)respiratoryRate returns nil when it cannot see a rate it believes — too short
a span, too few beats, or nothing standing clear of the noise. Treat nil as
unknown, never as unchanged.
Steps come in the same shape, from accelerometer frames rather than ECG:
var detector = PeakStepDetector(sampleRate: 200, dimensions: 3)
var predictor = AdaptiveOscillatorStepPredictor(sampleRate: 200, dimensions: 3)
for frame in accBatches { // [Double], milli-g, x,y,z interleaved
let steps = detector.accept(frame) // foot-falls that have happened
let next = predictor.accept(frame) // where the next one will be
for step in steps { predictor.observe(step) }
if let next, next.confidence > 0.5 {
schedule(at: next.sampleIndex) // absolute, fractional, still in the future
}
}The two are complements, not alternatives. A detector cannot drive an on-the-beat
actuation: filling a 36-sample accelerometer frame costs 176 ms, confirming a peak
another 60–100 ms, and an acknowledged BLE write to a device 40–120 ms more —
some 300–400 ms against a step period of 545 ms at 110 steps/min. Anything meant
to land on a foot-fall has to be scheduled before the previous one has been
confirmed, which is what StepPredictor is for.
swift build
swift test # 45 testsThe tests run against synthetic signals. The ECG algorithms have also been minimally validated against a Polar H10 over a 26-minute annotated protocol with a breathing metronome. The gait algorithms have not been validated on hardware at all — the walking protocol for doing so is written up in CLAUDE.md, and every constant in them is provisional until it has been run.
- Causal by default. The corrector and the detector run on a live stream and
may not look forward. Batch variants belong behind the same seams, named as
batch — the Lipponen–Tarvainen corrector is the planned one, already pinned in
PROVENANCE.md. - Detect on the processed signal, measure on the original. The QRS detector decides that there is a beat from a band-passed chain, then locates the peak in the raw buffer — otherwise every beat time carries the filter's group delay (6 samples, 46 ms at 130 Hz) and the amplitude is not in microvolts.
- Absolute indices. A peak is confirmed after its own samples arrive, so the batch that returns it is usually not the batch that contained it. They are also what lets a step detector hold candidates back until its gate is satisfied and then release them: a step emitted late still names the sample it happened on.
- Placement-agnostic by construction. The step algorithms read the Euclidean norm of the acceleration, so a sensor on a chest, wrist, waist or ankle differs in amplitude but not in kind, and no axis is assumed to be vertical.
MIT — see LICENSE. Referenced upstream implementations carry their own licences; see PROVENANCE.md.
Patches welcome: CONTRIBUTING.md. Releases are listed in CHANGELOG.md.