Skip to content

Repository files navigation

PhysioKit

CI

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.

What is in it

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

Requirements

Swift 6.0, macOS 13+ / iOS 16+.

Use

.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.

Build and test

swift build
swift test        # 45 tests

The 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.

Design notes

  • 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.

Licence

MIT — see LICENSE. Referenced upstream implementations carry their own licences; see PROVENANCE.md.

Patches welcome: CONTRIBUTING.md. Releases are listed in CHANGELOG.md.

About

Physiological signal algorithms in Swift: HRV metrics, RR artifact correction, R-peak detection and ECG-derived respiration

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages