Pendulum

Restless legs, measured at the ankle

The amplitude decays.
The period holds.

Pendulum records raw acceleration from a Wear OS watch worn at the ankle and reports whether your legs move periodically while you sleep — and how regular that rhythm is. It tracks the interval between movements, not their number.

Huygens, 1656: a pendulum's period does not depend on how far it swings. The swing above decays; the tick marks below it — one per crossing — stay evenly spaced. The same split runs through this project.

What this is

A real measurement. The accelerometer readings are real, the processing chain follows published scoring rules, and the numbers mean something. Used over several nights, it can give you a well-founded reason to book an appointment — or a well-founded reason not to worry.

What this is not

Not a medical device, an official health application, or a diagnosis. Not reviewed or approved by any health authority, not affiliated with any medical body, and never validated against a sleep study. Its numbers are on a different scale from the ones a laboratory produces.

So Take the result to a physician. Do not take a treatment decision from it, and do not let a reassuring number stop you from seeing someone if you have symptoms.

Pre-alpha All four modules build; 195 unit tests on the two pure-JVM modules. No accelerometer has yet been worn at an ankle by this software.

It takes two devices, and that cannot be engineered away

The clinical index is movements per hour of sleep. If the movements and the sleep both come from the same accelerometer, the metric becomes circular: processing that suppresses movements lowers the numerator and, in the same gesture, raises the denominator. A treatment with no effect can then display as a clear improvement.

Ankle

A Wear OS watch

The only class of consumer device whose raw accelerometer a third-party app can read at 50 Hz for eight hours. Fitbit, Oura, Whoop and Garmin expose aggregated vendor metrics, never the sample stream.

Wrist, finger, or under the mattress

Any source that writes sleep to Health Connect

It provides the denominator, independently. Consumer wearables estimate total sleep time well and sleep stages poorly — and total sleep time is all the index needs.

An accelerometer does the work, not a gyroscope. A movement lasting 0.5 to 10 seconds is fully described by acceleration, and a gyroscope costs 3 to 40 times the current for nothing.

The thesis

Why the rhythm, and not the count

The clinical convention is the PLM index — movements per hour of sleep. Pendulum computes it and puts it in the report a physician would read, because that is the number a sleep specialist reads. It is not the number the application follows from one night to the next, for three reasons.

Stability

Night-to-night variability of the hourly count is 43.2 % ± 37.1; of the mean log inter-movement interval, 3.6 % ± 3.7. Twelve times less. Skeba et al., Sleep Med 2016, PMID 26847989

Hourly count 43.2 %
Mean log interval 3.6 %

No denominator

The inter-movement interval and the periodicity index are computed from the movement onsets alone. The hourly count needs hours of sleep — and when that estimate comes from the same accelerometer that supplies the movements, the metric is circular and self-amplifying.

Scale

39 % of movements scored on EMG produce no detectable motion at an ankle-worn sensor. An accelerometric count is a different quantity, not a noisy estimate of the EMG one, so the 15/h clinical threshold does not transfer. Terrill et al., EMBC 2013, PMID 24111321

Interactive · move the slider

What a miss rate does to each quantity

Suppose the detector misses a fraction p of the movements, independently. The count falls in direct proportion. The rhythm does not: a missed movement merges two 21 s intervals into one 42 s interval, which is a harmonic of the fundamental, not noise. The information is displaced, not destroyed — and the mixture model that separates the harmonics recovers the fundamental and measures p at the same time.

0 %

Hourly count · collapses

25.2 /h

From a true 25.2 /h — the nominal synthetic night of the regression suite: 168 in-series movements over 400 min of analysable sleep.

Raw mean log interval · biased upward

21.0 s

Bias +0.000 nats · interval ×1.00. Published night-to-night variability is 0.11 nats at a 21 s interval.

Fundamental, deconvolved · holds

21.0 s

Recovered by the harmonic mixture model, together with an estimate of p itself — which doubles as a night-comparability check.

Observed distribution of intervals (synthetic, log-normal with σ = 0.3, mixed over harmonics with weights wk ∝ pk−1(1−p)) Fundamental μ — solid, stays at 21 s Raw mean of the log distribution — dashed, drifts right

At a zero miss rate the two agree. Raise the slider and watch them separate.

Values are computed live from the published relations: E[ln N] = Σ pk−1(1−p)·ln k for the bias, and a proportional fall for the count. At p = 0.39 the bias is +0.357 nats, an interval multiplied by 1.43 — a true 21 s rhythm reads as 30.0 s. That is 3.3 times the night-to-night variability the whole method rests on, which is why deconvolution is not a refinement but the condition for the metric to exist.

Seven minutes of it

What the detector actually sees

The vertical axis is amplitude divided by that night's own noise floor, on a log₂ scale — which is what makes the ×8 onset threshold a straight horizontal line and the detector's logic readable at a glance. The envelope is drawn as minimum-to-maximum per pixel column and never averaged, which is why it looks like a picket fence rather than a smooth curve.

Envelope, in multiples of the noise floor Θon = ×8 — solid Θoff = ×2.5 — dotted Counted movement Isolated — outside any series ×Excluded: gross body movement, ≥ ×40 Series of four or more

This signal is synthetic. No accelerometer has been worn at an ankle by this software. It is generated in the browser from the parameters of the worked example in docs/03-algorithm.md §7: a noise floor of 4.0 mg, an onset threshold at ×8 of it (32 mg) and an offset at ×2.5 (10 mg), movements 21 s apart with plateau amplitudes in the 45–150 mg range and durations between 1.7 and 3.3 s, and a gross-body threshold at ×40 of the effective floor. Four consecutive movements make a series; a movement above ×40 is excluded and imposes a 2 s refractory window on either side.

Raw acceleration to published indices

The processing chain

Nine stages, in order. Two edges are drawn dashed because they are the only places where the chain is not strictly feed-forward: posture boundaries cut the noise-floor windows, and detected movements are fed back into the sleep mask so that the mask does not read them as evidence of wakefulness.

−1

Integrity

Eleven O(n) checks on data the on-wire CRC does not cover: rate, monotonicity, overlap, impossible jerk, saturation, gravity plausibility. Above 1 % of blocks rejected, the night is void.

0

Timeline

The sampling rate is re-estimated from the timestamps and the signal resampled to 50.000 Hz — a rate wrong by 5.2 % falsifies every duration by 5.2 %. Gaps are classified; the first 5 s of each segment leaves the denominator.

1

Gravity / movement split

Two parallel paths, not a subtraction: a 0.15 Hz low-pass gives the gravity vector ĝ as a first-class signal, a 0.5–8 Hz band-pass gives the movement channel. Stateful biquads, one pass per segment — restarting them per block injects a periodic artefact that looks exactly like a perfect movement series.

From ĝ · off-chain

Posture detector

A change of more than 20° over ±2 s, held within 10° for 10 s. A turn moves gravity by up to 1 g — fifty times a real movement — and rings for the exact duration that would be counted.

↓ cuts the floor windows · guards detection

2

Dual envelope

RMS over 0.50 s for the decision — long enough to null the ripple that fragments one movement into three — and RMS over 0.15 s for edges and the WASM morphology criterion.

3

Adaptive noise floor

p25 over a bilateral 120 s window, then mask everything above ×4, then the median of the survivors. The floor is mechanical, not thermal: it steps at every posture change, so windows never cross a boundary.

4

Thresholds

Θ_on = max(8·floor, 0.020 g, 0.12·gainCal), with the offset at 0.3125 of the same three terms. Which term dominates is recorded per sample: two nights in different regimes are not comparable.

5

Candidate detection

A state machine on the coarse envelope, edges refined on the fine one. Morphology, duration 0.5–10 s, gross body movement, posture guard, blind zones. Rejection reasons are exclusive; flags are cumulative.

Three layers · off-chain

Sleep mask

A sustained-inactivity rule of the van Hees type — the only family showing no significant wrist-versus-ankle difference. Blind to periodic movements by construction, a bounded fixed point of exactly two iterations, then fused with Health Connect where it exists.

↓ supplies the denominator and its independence level

6

Series building

AASM v3 and WASM 2016 implemented literally and separately, never as one parameterised rule — the difference that matters is not the interval bound but whether a short interval breaks the sequence.

7

Indices

aPLM-i and aPLM-i/SPT, the Periodicity Index, and the fundamental rhythm μ by harmonic deconvolution — plus a publication gate that can refuse to emit a number at all. The rhythm and the periodicity index survive that refusal: they have no temporal denominator.

The published quantity is deliberately not called PLMI. It is called aPLM-i — ankle periodic limb movement index, estimated, not validated — in the code, the database and the export, because calling it PLMI would guarantee it is read as a laboratory PLMI.

Real screenshots, from emulators

The application

Every number on these screens was produced by the application's own preview dataset flowing through the real Compose code, and every chart was drawn by the same routines the production build uses. What they are not is a recorded night: the plotted signal is synthetic preview data. The screens are real; the night is not.

Phone screen titled Night of 12 March, provisional result, explaining that sleep stages have not reached Health Connect yet, with a Try again now button, above a card showing 21 s with a 95 % confidence interval of 18 to 25 over 6 eligible nights.
Waking, provisional The hypnogram arrives hours after waking because the vendor's sync follows its own battery policy. This is the normal case, so nothing on the screen is red and there is no error code: if a missing hypnogram were presented as an anomaly, the user would conclude the application was broken every single morning and stop believing the one flag that means something. The retry ladder — 30 min, 1 h, 2 h, 4 h, 8 h — runs whether the button is pressed or not.
Trend screen: a boxed sentence saying the interval spans the 15 per hour threshold, above a chart of seven nights plotted as unjoined points with a dashed minimum-detectable-change band around a 21 second line.
The trend, and its refusals Points are not joined: a line between nightly values would assert a continuity the measurement does not have. The dashed band is the minimum detectable change — below it, a difference is not a difference. One night is a hollow circle: excluded, still visible, with its reason. The boxed sentence is the one the specification demanded — the interval spans the 15/h threshold, so the application says it cannot tell which side you are on.
Lower half of the trend screen listing counting rule AASM v3, sleep mask Health Connect on five of six nights, movements while awake nine per hour, high periodicity, and an estimated missed rate of 31 percent.
What produced the number Which counting rule, which sleep mask and on how many nights, and the estimated missed rate — the by-product of the mixture model, shown because a rate that jumps between nights is precisely the non-comparability flag. Periodicity is described in words rather than as its index: a bare 0.58 compares to nothing you can picture.
List of recorded nights, each with its state and quality flags; one night is struck through with the criterion that excluded it, and a card at the bottom explains why no manual exclusion button exists.
The nights Each night carries its state and its quality flags. The excluded night keeps its value visible but struck through, with the criterion that excluded it. There is no button to exclude a night by hand, and the card at the bottom says why: a night removed after its figure has been seen would manufacture the trend.
One night in detail: envelope on a log-2 axis with event markers and series bands, a hypnogram aligned below on the same time axis, then counts including 412 movements detected, 278 in sleep, 57 excluded for posture, and an estimated fundamental rhythm of 19 seconds.
One night, in detail The envelope on a log₂ axis with the adaptive threshold, event markers on their own band rather than overlaid on the curve, series bars underneath, and the hypnogram aligned below on the same time axis with a lane of hatching where the two disagree. Below it, the counts — including the ones that undermine the headline figure, such as the 57 movements discarded for posture.
Round watch screen on pure black showing elapsed time, sample count, measured rate, gaps, megabytes written, battery and FIFO mode, with recording blocked until the evening record is sealed.
The watch One screen. No navigation, no animation, and no result of any kind, ever. Everything on it is a correctness check, read at most twice a night. Pure black because on OLED black does not light the pixel. Recording refuses to start until the evening record is sealed on the phone — an interface that invites you to look at your ankle invites you to move the leg being measured.

Not disclaimers added for form

What this cannot do

These are the reasons the output must not be read as a diagnosis. They are stated here rather than at the bottom of the page because three of them are properties of the sensor and its position, not defects to be fixed in a later version.

  1. A leg sensor cannot diagnose restless legs syndrome

    The diagnosis is clinical — five IRLSSG criteria based on waking symptoms. Periodic limb movements are a supporting criterion, nothing more.

  2. The AASM recommends against actigraphy for this, strongly

    As a replacement for EMG in diagnosing periodic limb movement disorder. This project does not dispute that recommendation. Smith et al., JCSM 2018

  3. No respiratory channel

    Respiratory-related leg movements cannot be excluded, because both published exclusion rules require a respiratory signal. In the presence of sleep apnoea the index is structurally overestimated. Periodicity does not rescue this: the movement interval mode is 22–26 s and the apnoeic cycle is typically 25–45 s, and respiratory-related movements are themselves periodic.

  4. One leg only

    Movements of the opposite leg are missed, biasing the count downward. This does not cancel the bias above — different subjects, different mechanisms, and their variances add.

  5. A single night means nothing

    In confirmed patients the 15/h threshold is exceeded on only about a third of individual nights. The interface refuses to draw a trend below three nights — by construction, not by warning.

  6. Never validated against a sleep study

    Nothing here has been checked against polysomnography, and there is no plan that would make that possible for an individual.