Crisis, form and advice
The crisis risk, the form drop risk, today's form, alerts and “What should I do now?”.
Each algorithm is described with what it reads, its steps, the values it uses (with their place in the code), what it shows and its limits.
Crisis risk alerts and notification limit
If you turned it on, Trévor writes to you every morning around 8 a.m. when your crisis risk reaches 70% on fresh measurements. An optional soft alert exists from 40%. It never sends more than one alert message per day, and it holds the message during quiet hours.
Where you see it: A bot message received in the morning (a neutral message with an « Open » button on the lock screen, then the content), followed by a second message « Two tips for now ». The buttons « Turn on the morning alert (if risk ≥ 70) » and « Warn me from moderate risk (40%) » are in the « My crisis risk » screen. Caregivers also see an alert line « Crisis risk calculated X% » in the dashboard when the score is 70 or more.
What the algorithm reads
- Today's crisis risk (score, freshness, explanations), computed as on the public crisis risk page (watch, self-report, health record)
- Your choice to turn on the morning alert, and your choice to turn on the soft alert from 40% (setting)
- Date of the last alert sent (Trévor’s state)
- Quiet hours (22:00 to 07:00 local time by default) and time zone (setting)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
CRISIS_ALERT_THRESHOLD | 70 | Score from which the « high » alert is sent | src/trevor/wearables/service.py:511 |
CRISIS_SOFT_THRESHOLD | 40 | Score from which the soft alert is sent, if you turned it on | src/trevor/wearables/service.py:512 |
send time | 8 am local time | The scheduler launches the risk alerts when the local time of the time-zone group is 8 | src/trevor/conversation/reminders.py:808-810 |
last_alert_date | once per day | If the last alert date equals the person's current date, no new send | src/trevor/wearables/service.py:1257, 1272 |
freshness | latest dated measurement at most 2 days old | With no fresh measurement, no message. Scores computed without a watch (record only) count as fresh | src/trevor/wearables/service.py:736, 763, 1263 |
alerts off by default | alerts_enabled false, soft_alert false | The morning alert must be turned on, then the soft alert separately | src/trevor/wearables/service.py:1179-1184 ; src/trevor/navigator/reco_flow.py:52-55 |
number of messages | at most 2 | The alert message and the two-tips message (omitted if no gesture is available) | src/trevor/conversation/reminders.py:1544-1551 |
default quiet hours | 10 pm (included) to 7 am (excluded), local time | Window during which proactive messages are held. You can change it or turn it off | src/trevor/localtime.py:182, 214-226 |
QUIET_DAILY | includes crisis_alerts | An alert held at night is dropped if the local day has changed on waking | src/trevor/conversation/reminders.py:99-103 |
attention budget | crisis_alerts exempt | The shared limit of proactive messages per day (3 by default, if the feature is on) neither holds nor counts crisis alerts | src/trevor/conversation/attention_budget.py:26, 44-48 |
study silent mode | no alerts during an LRH study | During the TREVOR-PILOTE study, the risk is computed and stored but never shown or alerted | src/trevor/wearables/service.py:1186-1190 |
dashboard row | score ≥ 70 and fresh | For caregivers: « Crisis risk calculated X% (model …) » with at most 3 explanations | src/trevor/dashboard/data.py:653-655 |
What is shown
At most one alert message per day. « High » alert (score of 70 or more): « Crisis risk this morning: X% (high). Signals: … », then gestures (drink about 1.5 to 2 liters, rest in the warmth, avoid intense effort) and the cases where to contact the care team or call 144 (rising pain, fever at the threshold, shortness of breath). Soft alert (40 to 69, if on): « Your crisis risk is rising a little: X% this morning (moderate). Signals: … », with the sentence « Nothing alarming at this stage ». Both messages end with « This feature is experimental and is not medical advice ». The second message gives the first two gestures of « What should I do now? ».
A worked example
Morning score at 72%, measurements dated yesterday, morning alert on, no alert sent today: the score reaches 70, the « high » message is sent, and today's date is saved. At 8:30, no second message is sent even if the score rises to 85. Another case: 45% with the soft alert off, no message. With the soft alert on, the soft message is sent. Another case: 75% but measurements 4 days old, nothing is sent. No numeric result was run: the cases come from the conditions in due_alerts.
What this algorithm cannot do
- Today's date is saved before the message is delivered. If sending fails or the message is dropped (night, then the local day changes), you receive no alert that day, and no other is sent.
- The check only happens at 8 a.m. local time. A score that passes 70 at 3 p.m. waits until the next morning.
- The alert thresholds (70 and 40) are not those of the displayed levels: the risk screen writes « moderate » from 30 and « high » from 60. A score of 65 is therefore « high » on the screen but does not trigger the 70 alert. The alert text nevertheless says « (high) » for any score of 70 or more.
- The alert is optional and off by default. Without turning it on, you receive nothing, whatever the score.
- If the measurements are more than 2 days old, no message is sent, even with a high score, because the score is judged out of date. The absence of an alert therefore does not prove the absence of risk.
- A low score never rules out a crisis. The messages remind you of this in « What should I do now? ».
- At night, the alert waits for the end of quiet hours. A crisis building during the night is not signaled earlier by Trévor: in an emergency, call 144 without waiting.
Code : src/trevor/wearables/service.py (CRISIS_ALERT_THRESHOLD, CRISIS_SOFT_THRESHOLD, due_alerts, alerts_enabled, set_alerts, silent_mode, interpretation_source), src/trevor/conversation/reminders.py (crisis_alerts, _deliver, QUIET_DAILY, tick à 8 h), src/trevor/conversation/push_gate.py (hold_if_quiet, push_gated), src/trevor/conversation/attention_budget.py (EXEMPT), src/trevor/localtime.py (is_quiet, QUIET_DEFAULT), src/trevor/navigator/reco_flow.py (soft_alert_enabled, alert_lines), src/trevor/dashboard/data.py (wearable_alerts), data/i18n/fr.json (textCrisisAlertMessage, textCrisisAlertSoft, textCrisisAlertsEnabled, textCrisisAlertsSoftEnabled)
Learning the personal model and the cohort model
Each night, Trévor can train a logistic regression model on your own days, then a model shared by everyone being followed. The starting weights come from a clinical prior, and training moves them away only if your data justify it. At calculation time, the models that have proven themselves are blended with the prior.
Where you see it: In the « My crisis risk » screen, the « Model reliability » block says whether the calculation starts from clinical markers, the cohort or your own data (number of days, number of crisis episodes, AUROC in temporal validation).
What the algorithm reads
- Gaps from your normal and daily markers, for each past day that has a baseline (see « Baselines and deviations ») (watch, self-report)
- Crisis labels: reported pain of 7/10 or more, hospitalization, a conversation observation of intensity 7 or more that mentions a crisis or the emergency room, days of the crisis log episodes (self-report)
- Treatments as they were on each training day: time since the transfusion, number of units, hydroxyurea regularity (health record, self-report)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
training days | 180 | History re-read for training (training_rows, days=180) | src/trevor/wearables/service.py:617 |
crisis pain threshold | pain_intensity ≥ 7 | A day is labeled crisis when the reported pain from the daily follow-up reaches 7/10, or when a hospitalization is reported | src/trevor/wearables/service.py:553 |
conversation observation | intensity ≥ 7 and a word among crisis, emergency, hospital, cvo, vaso | A conversation observation counts as a crisis if its intensity is 7 or more and its text contains one of these words | src/trevor/wearables/service.py:513, 568-572 |
crisis log | each day of a declared episode | Each day between the start and end of a declared episode (until today if ongoing) is a certain crisis | src/trevor/wearables/service.py:573-587 |
HORIZON_DAYS | 2 | A day is positive if a crisis occurs today, tomorrow or the day after (loop over 0 to 2 days inclusive) | src/trevor/wearables/crisis.py:123, 413-419 |
personal model minimum | 60 days and 4 crises | Below this, train_user produces nothing | src/trevor/wearables/service.py:662 |
cohort model minimum | 60 days and 5 crises | Below this, train_cohort produces nothing | src/trevor/wearables/service.py:677 |
l2 | 0.5 | Strength of the pull of each weight toward its prior value (the bias is not pulled) | src/trevor/wearables/crisis.py:433, 453 |
epochs / lr | 400 epochs; step 0.05 (200 epochs in validation) | Gradient descent over all days, with no external library | src/trevor/wearables/crisis.py:433, 482 |
pos_weight | (days without crisis) / (crisis days) | Crisis days count more in the gradient to compensate for their rarity | src/trevor/wearables/crisis.py:442-448 |
cross-validation | 4 blocks, at least 30 days and 3 crises | Contiguous temporal blocks; without these minimums, the AUROC stays empty | src/trevor/wearables/crisis.py:470-485 |
personal model usage threshold | n_events ≥ 4 and AUROC ≥ 0.65 | With no AUROC computed, the model is not used | src/trevor/wearables/crisis.py:508 |
cohort model usage threshold | n_events ≥ 5 and AUROC ≥ 0.6 | Cohort AUROC threshold, lower than the personal model's | src/trevor/wearables/crisis.py:506 |
blend weight | prior 1; cohort min(4; crises/5); personal min(6; crises/2) | The more crises observed, the more the model weighs, up to a cap | src/trevor/wearables/crisis.py:505-509 |
RISK_GAIN | 1.6 | Multiplier applied to all weights in the calculation, except hu_protect, dmt_other, tx_recent and tx_protect | src/trevor/wearables/crisis.py:494-500 |
PRIOR_BIAS | −2.2 | About 10% baseline risk when everything is normal | src/trevor/wearables/crisis.py:120 |
RISK_MAX | 0.99 | The risk never exceeds 99% | src/trevor/wearables/crisis.py:493 |
The weights
A positive weight raises the result, a negative weight lowers it.
| Signal | Weight | Where |
|---|---|---|
| Recent reported pain (prior) | +0.9 | src/trevor/wearables/crisis.py:83 |
| Reported fever (prior) | +0.8 | src/trevor/wearables/crisis.py:90 |
| Reported breathing difficulty (prior) | +1.0 | src/trevor/wearables/crisis.py:90 |
| SpO2 below 92% (prior) | +0.8 | src/trevor/wearables/crisis.py:83 |
| SpO2 vs your normal (prior) | −0.6 | src/trevor/wearables/crisis.py:81 |
| Resting HR vs your normal (prior) | +0.55 | src/trevor/wearables/crisis.py:81 |
| HRV vs your normal (prior) | −0.45 | src/trevor/wearables/crisis.py:81 |
| Crisis in the last 7 days (prior) | +0.6 | src/trevor/wearables/crisis.py:83 |
| Recent transfusion (prior, not amplified) | −3.0 | src/trevor/wearables/crisis.py:78 |
| Transfusion still holding (prior, not amplified) | −2.0 | src/trevor/wearables/crisis.py:78 |
| Fading transfusion effect (prior) | +1.4 | src/trevor/wearables/crisis.py:78 |
| Overdue transfusion (prior) | +0.8 | src/trevor/wearables/crisis.py:93 |
| Absolute night oxygen low (prior) | +0.7 | src/trevor/wearables/crisis.py:97 |
| Low hemoglobin (prior) | +0.5 | src/trevor/wearables/crisis.py:93 |
What is shown
Two stored models (yours and the cohort's), each with its weights, bias, number of days, number of crises and cross-validated temporal AUROC. At calculation time, the list of retained models and their blend weights. The screen shows where the calculation comes from: « clinical markers » (prior only), « fitted on the cohort » or « fitted on your data », with days, episodes and AUROC.
A worked example
A personal model with 8 crises and an AUROC of 0.70, and a cohort model with 10 crises and an AUROC of 0.62. Both pass their threshold (4 crises and 0.65; 5 crises and 0.6). The blend weights are prior 1.0, cohort min(4; 10/5) = 2.0 and personal min(6; 8/2) = 4.0, so the personal model has a share of 4/7. If the personal AUROC were only 0.60, the personal model would be set aside and only the prior (1.0) and the cohort (2.0) would remain. Verified by running blend() from the repository (result: prior 1.0, cohort 2.0, personal 4.0).
What this algorithm cannot do
- The model learns to predict a day labeled crisis within 3 days (today and the next 2), while the texts speak of 48 hours. The label is broad: today, tomorrow and the day after.
- The crisis label comes largely from what you report. An unreported crisis counts as a calm day, and a pain of 7/10 without a real crisis counts as a crisis.
- With 4 to 10 crises, the model is fragile: the AUROC is estimated on very few cases, and a single multi-day episode accounts for many positive days.
- The displayed AUROC comes from cross-validation, while the stored model is trained on all days. Consecutive days of the same crisis episode can also fall on both sides of a block boundary.
- In training, vasoconstriction is 0 and treatments are reconstructed day by day. Live, these values are read at the time of calculation: the model has therefore never learned the weight of vasoconstriction from your data.
- The learned weights are then multiplied by 1.6 with no new validation of this gain, which was calibrated on one reported case.
- The cohort model groups very different people, and its gaps are already normalized per person. It may be trained on few people.
Code : src/trevor/wearables/crisis.py (build_features, train_logistic, auc_score, evaluate_blocked, blend, amplified, PRIOR_WEIGHTS), src/trevor/wearables/service.py (day_records, training_rows, _treatment_history, train_user, train_cohort, train_everyone, _stored_model)
Daily readiness (recovery)
The daily readiness is a recovery marker out of 100, in the manner of an Oura score. Trévor shows your device's own score as is when one exists. Otherwise it computes its own from your sleep, heart rate variability (HRV) and resting heart rate, compared with your normal.
Where you see it: « My daily readiness » screen (command /forme, or My health > Readiness, risk, hemoglobin), the « Daily readiness » tile of My health, the Measurements and estimates screen, and the My health screen on the watch. The text gives a percentage, a level (good, medium, low), the source (device score or Trévor estimate), up to 2 factors, advice for the day and a note that it is not medical advice.
What the algorithm reads
- Recovery score supplied by the device (Oura readiness, Garmin Body Battery, etc.) (watch or ring)
- Sleep minutes, HRV, resting heart rate for the previous 30 days and the day (watch or ring)
- Night reported in « My night »: good, average or bad (self-report)
- Interpretation turned on by the person (warning accepted, global setting or test program) (setting)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
WEIGHTS | sleep 0.40; HRV 0.35; resting HR 0.25 | Weights of the three components of the computed score (sum 1) | src/trevor/wearables/readiness.py:27 |
BASELINE_DAYS | 30 | Previous days that form the baseline | src/trevor/wearables/readiness.py:23 |
FRESH_DAYS | 2 | Maximum age of a computed score to be shown; an older device score stays available but not « fresh » | src/trevor/wearables/readiness.py:24, 88 |
series read | 35 days | Length of the series passed to compute_readiness | src/trevor/wearables/service.py:1199 |
z bounds | −2 to +2 | Each gap is limited to ±2 before the average | src/trevor/wearables/readiness.py:86 |
score slope | 50 + mean × 15 | An average of +2 gives 80; −2 gives 20 | src/trevor/wearables/readiness.py:87 |
good / medium / low level | ≥ 66 / 40 to 65 / < 40 | Level thresholds | src/trevor/wearables/readiness.py:36-37 |
minimum number of components | 2 | With fewer than two measurements that day, the day is skipped (fixed in 2.200.2: before, a day with no measurement gave 50%) | src/trevor/wearables/readiness.py:55-57, 83 |
MIN_BASELINE | 7 | Minimum days before the chosen day, and minimum values per measurement | src/trevor/wearables/crisis.py:122 |
SELF_SLEEP_Z | good +1.0; average 0.0; bad −1.0 | Value given to the reported night when the watch did not measure sleep | src/trevor/navigator/sleep.py:26 |
factor display threshold | |z| ≥ 0.5, at most 2 factors | A weaker factor is not named | src/trevor/wearables/readiness.py:96-101 |
The weights
A positive weight raises the result, a negative weight lowers it.
| Signal | Weight | Where |
|---|---|---|
| Sleep longer than your normal | +0.40 | src/trevor/wearables/readiness.py:27 |
| HRV higher than your normal | +0.35 | src/trevor/wearables/readiness.py:27 |
| Resting HR lower than your normal | +0.25 | src/trevor/wearables/readiness.py:27, 80 |
What is shown
A score from 0 to 100 (as a percentage), a level (good from 66, medium 40 to 65, low below 40), a source (« device » or « computed »), up to 2 factors as sentences (for example « Shorter night than usual », « Your resting heart is a bit high »), the date and the « fresh » flag. The advice depends on the level: good (great form, keep your good habits), medium (at your pace, hydrate, keep a margin), low (rest, hydrate, spare your efforts, avoid the cold). The text says it is not medical advice and replaces neither how you feel nor your care team. When nothing is shown, a sentence gives the reason: no watch, silent mode, warning to accept, fewer than 7 reference days, measurements too old or too few.
A worked example
Twenty reference days: sleep around 430 min (median 430, spread 30 from the floor), HRV median 51.5 (spread 4), resting HR median 60.5 (spread 2). Today: sleep 360 min, HRV 44, HR 65. The z-scores are −2.33 (clipped to −2), −1.875 and +2.25 for HR, inverted to −2.25 (clipped to −2). The weighted average is (0.40 × −2 + 0.35 × −1.875 + 0.25 × −2) / 1 = −1.96. Score = 50 + (−1.96 × 15) = 20.7, rounded to 21: low level. The two factors shown are sleep (|z| 2.33) then HR (|z| 2.25): « Shorter night than usual » and « Your resting heart is a bit high ». Result verified by running compute_readiness from the repository.
What this algorithm cannot do
- The device score is shown as is, with no check or explanation from Trévor. Only the computed score is explained.
- The computed score uses only three measurements. It ignores pain, mood, oxygen, temperature and what you report in the daily follow-up. A day of strong pain can therefore give a « good » readiness if you slept well.
- When only one measurement is present, there is no score. With only two measurements, the score rests on those two with weights scaled back to 1.
- The reported night replaces sleep only on the day the watch did not measure sleep, with the sleep weight (0.40).
- The score is bounded by z-scores of ±2, so a computed score stays between 20 and 80; it never reaches 0 or 100. Only a device score can go to the extremes.
- An old device score (more than 2 days) is no longer shown as the daily readiness, and an old computed score is dropped. A day with neither device nor calculation has no readiness.
- The daily readiness is distinct from the risk of a dip in form and from the « form » of « What should I do now? ». They are three different notions with different calculations.
Code : src/trevor/wearables/readiness.py (compute_readiness, _usable, _level, WEIGHTS, DRIVER_LABEL), src/trevor/wearables/service.py (readiness_today), src/trevor/conversation/wearables_screen.py (_readiness_lines, screen_readiness), src/trevor/lifecourse/health_view.py (_form_reason, _form_tile, EMPTY_NOTE), src/trevor/signals.py (build), src/trevor/wearables/watch_summary.py (_health), data/i18n/fr.json (textReadiness*)
Baselines and deviations from your normal
Trévor compares each measurement from your watch or ring with what is usual for you, rather than with a general norm. It computes a median and a median absolute deviation (MAD) over your previous days, then turns today's gap into a robust z-score and a 3-day trend. These gaps are the raw material of the crisis risk, the dip-in-form risk and the daily readiness.
Where you see it: Never shown as a raw number. The gaps appear in the sentences of the « My crisis risk » and « My daily readiness » screens (for example « Your sleep is shorter than usual »), in the list of signals that weigh the most, and in the sentence explaining why a daily readiness is missing.
What the algorithm reads
- Resting heart rate, heart rate variability (HRV), SpO2, sleep minutes, steps, skin temperature deviation, stress score, respiratory rate, ambient noise, lowest heart rate of the night (watch or ring, file import)
- Reported pain (0 to 10) and crisis days (self-report (daily check-in, conversation, crisis log))
- Night oxygen and detailed sleep (efficiency, awakenings, deep sleep, bedtime) (watch, ring, oximeter or “My night” entry)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
BASELINE_DAYS | 30 | Number of previous records that form the baseline | src/trevor/wearables/crisis.py:121 |
MIN_BASELINE | 7 | Minimum values of a measurement to compute its median, and minimum records before a day for it to have gaps | src/trevor/wearables/crisis.py:122 |
clip (z-score bound) | 4.0 | A gap never counts for more than 4 spreads, in either direction | src/trevor/wearables/crisis.py:179 |
MAD factor | 1.4826 | Converts the MAD into a standard-deviation equivalent for a normal distribution | src/trevor/wearables/crisis.py:176 |
MIN_SD resting HR | 2.0 bpm | Spread floor: prevents a very stable baseline from turning a small gap into an alert | src/trevor/wearables/crisis.py:164 |
MIN_SD HRV | 4.0 ms | Spread floor for heart rate variability | src/trevor/wearables/crisis.py:164 |
MIN_SD SpO2 | 0.7 point | Spread floor for oxygen saturation | src/trevor/wearables/crisis.py:164 |
MIN_SD sleep | 30 min | Spread floor for sleep duration | src/trevor/wearables/crisis.py:164 |
MIN_SD steps | 1000 steps | Spread floor for activity | src/trevor/wearables/crisis.py:164 |
MIN_SD temperature, stress, breathing, noise | 0.15 °C; 5; 1.0 /min; 5 dB | Spread floors for the other daily measurements | src/trevor/wearables/crisis.py:164-165 |
MIN_SD lowest HR of the night | 2.0 | Spread floor for the lowest night heart rate | src/trevor/wearables/crisis_signaux.py:19 |
MIN_SD nights | mean oxygen 0.7; trough 1.5; time below 90 % 1.0; drops/h 2.0; efficiency 3.0; awakenings 10; deep sleep 12; bedtime 30 min | Spread floors for night measurements | src/trevor/wearables/crisis.py:166-167 |
default dispersion | 1.0 | Floor used for a measurement missing from the MIN_SD table | src/trevor/wearables/crisis.py:176 |
temperature without baseline | z = deviation / 0.25, capped at 4 | When temperature lacks reference nights, the device's own deviation is divided by 0.25 °C | src/trevor/wearables/crisis.py:382 |
recent pain | max of the last 3 days / 10 | Highest reported pain over today and the 2 previous days | src/trevor/wearables/crisis.py:397-398 |
crisis_last7 | 1 if a crisis in the previous 7 entries | Binary marker of a recent crisis | src/trevor/wearables/crisis.py:399 |
pain_days7 | 0.5 per pain day of 3/10 or more, maximum 2 | Pain days in the last 7 records, today included | src/trevor/wearables/crisis.py:306-308 |
low_spo2 | SpO2 of the day < 92 | Binary marker of low saturation | src/trevor/wearables/crisis.py:401 |
fever_flag | temperature deviation ≥ 0.5 | Binary marker of a possible fever sign | src/trevor/wearables/crisis.py:402 |
DROP_Z | −1.5 | A night is « down » when its mean oxygen is at least 1.5 spreads below your normal | src/trevor/wearables/crisis.py:189 |
ABS_LOW_NIGHT | 92 | Night mean below which a night counts as low, no baseline needed | src/trevor/wearables/crisis.py:190 |
NADIR_ABS_REF / NADIR_ABS_STEP | 90 ; 4 | Below a 90% low, severity rises by one point per 4 points (86%: 1, 82%: 2, 78%: 3), capped at 3, halved if the measurement quality is low | src/trevor/wearables/crisis.py:186-187, 311-318 |
bedtime tolerance | 0.5 h, cap 4 | Bedtime irregularity counts from a half-hour gap from your usual bedtime | src/trevor/wearables/crisis.py:234 |
What is shown
A feature dictionary per day: z-scores clipped from −4 to +4 (resting HR, HRV, SpO2, sleep, steps, temperature, stress, breathing, noise, lowest night HR), 3-day trends (resting HR, HRV, sleep, breathing), recent pain, pain days, recent crisis, low SpO2, fever sign and night features. A positive z means « above your normal ». Whether that is favorable or unfavorable is only decided later, by the model weights.
A worked example
Eight days of resting HR: 60, 61, 59, 62, 60, 58, 61, 60. The median is 60. The distances to 60 are 0, 1, 1, 2, 0, 2, 1, 0, whose median (the MAD) is 1. 1.4826 × 1 = 1.48 is below the 2.0 bpm floor, so the spread is 2.0. A day at 66 bpm gives z = (66 − 60) / 2.0 = +3.0. A day at 68 bpm gives +4.0, the maximum, and a day at 75 bpm also gives +4.0. Verified by running _robust and _z from the repository (median 60.0, spread 2.0, z for 68 bpm = 4.0).
What this algorithm cannot do
- The window of 30 counts records, not calendar days: a gap of several weeks without measurements pushes the baseline further back in time.
- Seven values are enough to set a normal. With so few days, the median and MAD are not very stable, and the spread floor often decides on its own.
- A very unusual day that has already passed (a crisis, an illness) enters the baseline of the following days. The median and MAD resist this better than a mean, but a long abnormal period gradually becomes your new normal.
- A missing measurement counts as a gap of 0, which is neutral. A model that reads few measurements cannot tell « everything is fine » from « nothing was measured ».
- The z-score is clipped at 4: a huge gap and a very large gap weigh the same.
- Wrist and finger measurements are noisy (movement, irregular wear). Trévor does not filter these values before the median, except for rejecting low-quality nights for night measurements.
Code : src/trevor/wearables/crisis.py (_robust, _z, MIN_SD, build_features, night_features, nadir_abs, pain_days), src/trevor/wearables/crisis_signaux.py (features), src/trevor/wearables/readiness.py (même méthode pour 3 mesures)
What should I do now?
When your crisis risk rises, your form is medium or low, or pain starts, Trévor suggests 3 to 5 concrete gestures. It always starts with the safety instruction if it sees an emergency sign, then puts forward the gestures that already helped you, the general markers, and finally what other people observe if there are enough of them. These are general self-care gestures, never a medication dose.
Where you see it: « What should I do now? » screen (command /quefaire or /action, button offered from 40% risk), cards in the companion app (GET /api/phone/reco), and the first two gestures in the second message of the morning alert. Each gesture has a reason (« Why »), sometimes evidence (« In your case » or « Among people who use Trévor »), and an « I did it » button.
What the algorithm reads
- Score and level of today's crisis risk (calcul Trévor (watch, self-report, health record))
- Daily readiness from the device or computed, mood of the day (well 8, average 5, not well 3), « Not OK » in the daily follow-up: the worst of these counts (watch, self-report)
- Pain of the day (the highest between observations and the daily follow-up) (self-report)
- Emergency signs: reported fever or measured temperature of 38.3 °C or more, shortness of breath, chest pain, confusion, neurological deficit (chosen in the app), pain rising despite treatment (self-report, calcul)
- Measured saturation (92% or less), night oxygen lower than your normal for several nights in a row, more fragmented night (watch, ring, oximeter)
- Feeling of cold or chills in the daily follow-up (self-report)
- Care plan: oxygen planned, rescue pain medicine planned (carte d’urgence, health record)
- Gestures you logged and their effect on your pain or form the next day (self-report (« Je l’ai fait »))
- Effects observed among other people who use Trévor (anonymous, aggregated) (cohort data)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
RISK_MODERATE | 40 | Score from which « What should I do now? » is offered, and « moderate » level for gestures | src/trevor/navigator/reco.py:52 |
LEVEL_OF_SCORE | 70 high; 40 moderate; otherwise low | Level thresholds used here to order the gestures. They differ from those of the risk screen (30 and 60) | src/trevor/navigator/reco.py:263-269 |
FEVER_C | 38.3 °C (read from data/emergency_thresholds.json) | Measured temperature that counts as a fever to report | src/trevor/navigator/reco.py:53 |
SPO2_LOW | 92 | A measured saturation of 92% or less (within the last 2 days) triggers the safety instruction. The code notes: noisy measurement, threshold to be clinically validated | src/trevor/navigator/reco.py:54 ; src/trevor/navigator/reco_flow.py:124-136, 232 |
PAIN_ONSET | 3 | Pain of 3/10 or more: « pain is starting » | src/trevor/navigator/reco.py:55 |
MIN_ACTIONS / MAX_ACTIONS | 3 ; 5 | Number of gestures shown (5 at most; the minimum is not enforced in the code) | src/trevor/navigator/reco.py:48-49, 162-190 |
MAX_PERSONAL / MAX_PEERS | 2 ; 1 | Gestures « for you » and « other people » at most | src/trevor/navigator/reco.py:50-51 |
RISING_POINTS / RISING_MIN_LEVEL | 2 points; 4/10 | Pain rising despite treatment: a rescue pain medicine dose in the last 24 h AND a logged pain of at least 4/10 that exceeds the lowest of the last 24 h by 2 points or more | src/trevor/navigator/reco_flow.py:28-29, 108-118 |
MIN_USES / MIN_PAIRED / MIN_USUAL | 5 ; 5 ; 10 | Gesture uses, measured next days and other measured days needed before mentioning an effect « for you » | src/trevor/navigator/reco.py:57-59 |
NOTABLE_PAIN / NOTABLE_FORM | 1 point; 8 points | Minimum favorable gap in next-day pain (0 to 10) or form (0 to 100) compared with the average of other days | src/trevor/navigator/reco.py:60-61 |
PEER_MIN_DIFF | 0.5 point | Median pain gap to present another people's gesture as associated with less pain | src/trevor/navigator/reco.py:62 |
DEFAULT_MIN_PEOPLE / MIN_PER_GESTURE | 20 ; 5 | Minimum people to show what others observe, and per gesture | src/trevor/navigator/reco_peers.py:27-28 |
FOLLOWUP_DELAY / FOLLOWUP_STALE / ASKED_VALID | 2 h; 8 h; 12 h | Delay before « Feeling better? », time before an unsent question is dropped, validity of the answer | src/trevor/navigator/reco_flow.py:23-25 |
eau | 1.5 to 2 liters per day | Hydration marker shown in the « water » gesture and in the alert (converted to the person's preferred unit) | src/trevor/navigator/reco.py:17-18 ; src/trevor/units.py:523 |
What is shown
A list of at most 5 gestures. Each gesture carries a source: « securite » (emergency instruction or care team), « perso » (« In your case »), « plan » (« Your plan »: rescue pain medicine, planned oxygen), « general » (general marker) or « autres » (others). Catalog gestures: emergency, care team at night, water, rest in the warmth, avoid the cold, avoid intense effort, avoid alcohol, local heat, breathing and relaxation, plan pain medicine (no dose), rescue medicine ready, plan oxygen, sleep, regular meals, tell someone close. The screen ends with « These gestures do not replace your care team. If you feel worse, contact them. » Call 144 without waiting if there is a serious sign.
A worked example
Context: score 75 (high), medium form, pain 4/10, reported fever, plan with a named rescue pain medicine, no oxygen. Fever is an emergency sign, so the first card is the safety instruction. Four places remain. Since pain is 3 or more, the order is water, rest in the warmth, local heat, plan pain medicine, then breathing. Result of reco.build: emergency (securite), water (general), rest in the warmth (general), heat (general), plan pain medicine (plan). Second case: score 45 (moderate), good form, no pain, no sign: water, rest in the warmth, avoid the cold, sleep, regular meals. Verified by running reco.build from the repository.
What this algorithm cannot do
- The level used here (high from 70, moderate from 40 to 69) differs from the risk screen's (moderate from 30, high from 60). A score of 65 is « high » on the risk screen but « moderate » for ordering the gestures.
- Emergency signs come from what you report, the daily follow-up, a measured saturation and a rising-pain calculation. If you report nothing and no measurement exists, no safety instruction appears. A low score never rules out a crisis.
- The 92% saturation threshold comes from a noisy watch or ring measurement. The code states it must be clinically validated. The proposed gesture is always to contact the care team, never to give yourself oxygen.
- The gestures « for you » rest on an association between a gesture and the next day's pain or form, with no proof of cause. You often do a gesture because you are in pain, which blurs the link. An unfavorable effect is never shown as advice.
- The gestures of other people require at least 20 people. With fewer, nothing is shown. The calculation is cached for 6 hours.
- The gesture texts are general. No medication dose is given, and rescue pain medicine does not enter the effects « for you ».
- The minimum of 3 gestures is stated in the code but not enforced: a shorter list is possible, for example when few general gestures remain available.
Code : src/trevor/navigator/reco.py (build, _general_order, emergency_signs, personal_effects, gesture_effect, favourable, level_of, triggered, form_of_mood, worst_form), src/trevor/navigator/reco_flow.py (context_for, compute, present, pain_rising, plan_of, record_done, alert_lines, api_payload), src/trevor/navigator/reco_peers.py (aggregate, peer_actions), src/trevor/conversation/reco_screen.py, src/trevor/conversation/reminders.py (crisis_alerts : deuxième message), data/i18n/fr.json (textReco*)
Risk of a dip in form
The risk of a dip in form is a second score, softer and more frequent than the crisis risk. It estimates the probability of a drop in your daily functioning (sleep, energy, pain) within 48 hours. It is meant to help you act early, for example with rest, hydration and a calmer pace, before a possible crisis.
Where you see it: In the bot message « My crisis risk » (My watch screen), right below the crisis risk: a line « Risk of a dip in form: X% · level » with a green, orange or red icon, an explanatory sentence, then one bullet per signal that weighs. No other screen displays this score: not the app, the watch or the dashboard (the code only sends it to this message).
What the algorithm reads
- Gaps from your normal in sleep, activity (steps), HRV, resting HR, measured stress, skin temperature and SpO2, plus the 3-day trends (see « Baselines and deviations ») (watch or ring)
- Reported pain over the last 3 days, pain days of the week, crisis in the last 7 days (self-report)
- Low hemoglobin, overdue transfusion, fading transfusion effect, recent or holding transfusion, hydroxyurea followed or irregular (health record, self-report, lab import)
- Consecutive nights with low oxygen (watch, oximeter or “My night” entry)
- Cold, heat and PM2.5 fine particles at your location (local weather and air quality)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
FORM_BIAS | −1.4 | Starting point of the calculation: a day with all signals at 0 gives 20% (1 / (1 + e^1.4) = 0.198), versus about 10% for crisis | src/trevor/wearables/crisis.py:605 |
moderate threshold | score ≥ 30 | The thresholds are hard-coded in form_risk, with the same values as the crisis risk | src/trevor/wearables/crisis.py:615 |
high threshold | score ≥ 60 | « High » level | src/trevor/wearables/crisis.py:615 |
contribution display threshold | 0.15 | A signal whose |weight × value| is below 0.15 is not explained | src/trevor/wearables/crisis.py:616 |
number of explanations | 4 | At most four signals shown | src/trevor/wearables/crisis.py:617 |
STALE_SHARE | 0.6 | Share erased from pre-transfusion signals on the day itself (× e^(−days/6) afterward) | src/trevor/wearables/crisis.py:529 |
keys dampened after transfusion | z_spo2, z_spo2_night, low_spo2, night_nadir, night_nadir_abs, night_abs_low, night_time_low, night_odi, night_drop_streak, hb_low, transfusion_overdue, crisis_last7, pain_days7 | Among them, only z_spo2, hb_low, transfusion_overdue, crisis_last7, pain_days7 and night_abs_low weigh in the dip in form | src/trevor/wearables/crisis.py:524-528 |
TX_RECENT_DAYS / TX_RECENT_WINDOW | 6 days; 21 days | tx_recent = e^(−days/6) for 21 days after the transfusion, then 0 | src/trevor/wearables/crisis.py:270-271, 292-293 |
hold_days | 5 days per unit, from 14 to 56 days; 28 without a unit count | Holding time of the transfusion, which sets tx_protect and tx_loss | src/trevor/wearables/crisis.py:268-279 |
hb_low | (9.5 − Hb) / 2, between 0 and 4 | Hemoglobin below 9.5 g/dL; 7.5 g/dL gives 1 | src/trevor/wearables/crisis.py:651-660 |
transfusion_overdue | delay (fraction of interval) × 2, between 0 and 4 | Only for a person on a transfusion program | src/trevor/wearables/crisis.py:658-659 |
cold_ambient | (10 °C − tmin) / 10, between 0 and 4 | Cold below 10 °C | src/trevor/wearables/crisis.py:104, 251 |
heat_ambient | (tmax − 28 °C) / 6, between 0 and 4 | Heat above 28 °C | src/trevor/wearables/crisis.py:105, 252 |
air_pm25 | (PM2.5 − 15) / 15, between 0 and 4 | Fine particles above the WHO guideline (15 µg/m³ over 24 h) | src/trevor/wearables/crisis.py:107, 255 |
The weights
A positive weight raises the result, a negative weight lowers it.
| Signal | Weight | Where |
|---|---|---|
| Recent reported pain (highest of the last 3 days / 10) | +0.8 | src/trevor/wearables/crisis.py:599 |
| Fading effect of the last transfusion | +0.8 | src/trevor/wearables/crisis.py:602 |
| Sleep shorter than your normal (negative z) | −0.6 | src/trevor/wearables/crisis.py:597 |
| Activity lower than your normal (negative z) | −0.5 | src/trevor/wearables/crisis.py:597 |
| Low hemoglobin | +0.5 | src/trevor/wearables/crisis.py:599 |
| Measured stress higher than your normal | +0.45 | src/trevor/wearables/crisis.py:597 |
| HRV lower than your normal (negative z) | −0.4 | src/trevor/wearables/crisis.py:597 |
| Resting HR faster than your normal | +0.4 | src/trevor/wearables/crisis.py:597 |
| Crisis in the last 7 days | +0.4 | src/trevor/wearables/crisis.py:599 |
| Pain days in the week (0 to 2) | +0.4 | src/trevor/wearables/crisis.py:600 |
| Sleep over 3 days (negative z) | −0.35 | src/trevor/wearables/crisis.py:598 |
| Overdue transfusion | +0.35 | src/trevor/wearables/crisis.py:599 |
| SpO2 vs your normal (negative z) | −0.3 | src/trevor/wearables/crisis.py:597 |
| Hydroxyurea followed irregularly | +0.3 | src/trevor/wearables/crisis.py:602 |
| Consecutive nights with low oxygen (0 to 4) | +0.25 | src/trevor/wearables/crisis.py:600 |
| Skin temperature vs your normal | +0.2 | src/trevor/wearables/crisis.py:597 |
| HRV over 3 days (negative z); resting HR over 3 days | −0.2 ; +0.2 | src/trevor/wearables/crisis.py:598 |
| Outdoor cold; outdoor heat; PM2.5 | +0.2 each | src/trevor/wearables/crisis.py:603 |
| Hydroxyurea taken regularly | −0.2 | src/trevor/wearables/crisis.py:602 |
| Recent transfusion; transfusion still holding | −1.5 each | src/trevor/wearables/crisis.py:601 |
What is shown
A percentage from 0 to 100, a level and up to 4 explanations. Levels: low below 30% (green icon), moderate 30 to 59% (orange), high from 60% (red). The bot writes « Risk of a dip in form: X% · level », then « A possible drop in your form (sleep, energy, pain) within 48 h, lighter than a crisis », then one bullet per explanation, for example « Your sleep is shorter than usual: this raises your risk of a dip in form. ». The direction of the signals for form is carried by the weights: a negative sleep z multiplied by −0.6 gives a positive contribution.
A worked example
With no gap at all, every signal is 0 and the score is 20% (low level). Take a day where sleep is at −2 spreads, activity at −1.5, HRV at −1, resting HR at +1, stress at +1 and recent pain at 0.5 (5/10). The logit is −1.4 + 1.2 (sleep) + 0.75 (steps) + 0.4 (HRV) + 0.4 (HR) + 0.45 (stress) + 0.4 (pain) = 2.2. The probability is 1 / (1 + e^−2.2) = 0.90, i.e. 90% (high level). The four explanations kept are sleep (1.2), steps (0.75), stress (0.45), then HRV (0.4), which ties with HR and pain at 0.4 and comes first in the weight table order. Result obtained by running form_risk from the repository (score 90, level high, contributions z_sleep 1.2; z_steps 0.75; z_stress 0.45; z_hrv 0.4).
What this algorithm cannot do
- The weights are fixed. Unlike the crisis risk, the dip-in-form risk is never trained on your data or the cohort's, and no AUROC exists for it. Nothing says this score truly predicts a dip in form.
- No definition of a « dip in form » is written in the code: there is no label and no measured target. The « 48 h » in the text is an intention, not a verified quantity.
- A calm day starts at 20%: the « low » level is therefore barely held with a single moderate signal. A single signal of weight 0.8 at a value of 1 moves the score to 35%, « moderate » level.
- The score has no 99% cap and no post-transfusion ceiling. A very loaded day can exceed 99%.
- The symptoms you report (fever, breathing difficulty, fatigue, bad night, feeling unwell) do not enter this score, although they enter the crisis risk. Your reported fatigue therefore does not raise the risk of a dip in form.
- When night oxygen is measured, it replaces the daytime SpO2 in the gap calculation, but the dip-in-form score does not read the night mean oxygen. SpO2 then weighs 0 in this score, and only consecutive nights with low oxygen (0.25) remain.
- A mean oxygen better than usual can lower the score even if the night had a dip, because the crisis risk's no-reassurance rule is not applied here.
- Without a watch or ring, the gaps are 0. The score then rests only on pain, medical record and environment, and can look reassuring by default.
- The score is not computed on the phone: the companion app does not receive its weights. It only exists in the bot message.
Code : src/trevor/wearables/crisis.py (FORM_WEIGHTS, FORM_BIAS, form_risk, after_transfusion, explain, reason_label, EXPLAIN_UP, EXPLAIN_DOWN, FEATURE_LABELS), src/trevor/wearables/service.py (crisis_risk, lignes 793-796 ; crisis_text, lignes 1019-1025), data/i18n/fr.json (textCrisisFormHead)
Watch signals and temperature and heart sources
Trévor reads three families of signals from your watch, ring or files. The factual signals are simple markers (SpO2 below 92%, resting HR 10 bpm higher, short sleep, etc.) shown with their date. Three temperature and heart features enter the crisis risk with modest weights, and the signals module compares temperature and heart with your baseline for its own screens.
Where you see it: The factual signals appear in the watch summary (« My last 7 days ») and in the caregivers' dashboard as « Connected device: night SpO2 91% ». The three risk features are read in the sentences of the crisis risk, with below them the line « where the temperature and heart come from » (source, type, method, quality). The Signals screen shows temperature and heart against your baseline.
What the algorithm reads
- Mean night SpO2, resting heart rate, HRV, sleep, skin temperature deviation, respiratory rate (watch or ring, file import)
- Lowest night heart rate and skin temperature deviation supplied by other sources (imported files, entry, hospital)
- The previous 30 days of each measurement (watch or ring)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
Low nighttime SpO2 | < 92 | Common alert threshold of consumer oximeters, according to the code | src/trevor/wearables/service.py:486 |
High resting HR | ≥ median of the previous 30 days + 10 bpm (at least 5 values) | Signal of a faster resting heart | src/trevor/wearables/service.py:488 |
Low HRV | ≤ 0.75 × median (at least 5 values) | Drop of 25% or more | src/trevor/wearables/service.py:490 |
high skin temperature | deviation ≥ +0.5 °C | Possible fever signal (Oura ring, according to the code) | src/trevor/wearables/service.py:492 |
high respiratory rate | ≥ 20 per minute | Usual adult polypnea threshold, according to the code | src/trevor/wearables/service.py:494 |
short sleep | < 5 h three nights in a row; a night < 2 h is ignored | A night under 2 h is probably a device not worn | src/trevor/wearables/service.py:497-506 |
ELEVATED_TEMP_DELTA | 0.3 °C | Skin deviation threshold to count a night as elevated in temp_nights (same value as in data/signaux_sources.json) | src/trevor/wearables/crisis_signaux.py:20 |
MAX_NIGHTS | 4 | Cap on the number of consecutive nights counted | src/trevor/wearables/crisis_signaux.py:21 |
temp_hr_coupling | min(3; max(0; z_temp)) × min(3; max(0; z resting HR, z night HR)) / 3 | Computed if at least one elevated skin night or a positive temperature z; otherwise 0 | src/trevor/wearables/crisis_signaux.py:62-63 |
NEW_PRIOR_WEIGHTS | z_night_hr 0.3; temp_nights 0.25; temp_hr_coupling 0.3 | Prior weights in the crisis risk (then multiplied by 1.6) | src/trevor/wearables/crisis_signaux.py:18 |
signals module baseline | 30-day window; 3 nights minimum; +0.3 °C; +8 bpm; MAD floor 3 bpm; resting tachycardia 100 bpm; resting jump 20 bpm | Values of the Signals module for its own screens. They are not used for the risk score | data/signaux_sources.json (baseline) ; src/trevor/signaux/baseline.py:15-24 |
measured fever | threshold from the emergency table (38.3 °C), thermometer only | A sensor's skin temperature never triggers the emergency alone | src/trevor/signaux/temperature.py:1-9, 32-34 |
The weights
A positive weight raises the result, a negative weight lowers it.
| Signal | Weight | Where |
|---|---|---|
| Lowest night HR vs your normal | +0.3 | src/trevor/wearables/crisis_signaux.py:18 |
| High skin temperature for several nights | +0.25 | src/trevor/wearables/crisis_signaux.py:18 |
| Temperature and HR rising together | +0.3 | src/trevor/wearables/crisis_signaux.py:18 |
| Skin temperature vs your normal (crisis risk) | +0.5 | src/trevor/wearables/crisis.py:81 |
| Fever sign (skin deviation ≥ 0.5 °C) | +0.9 | src/trevor/wearables/crisis.py:83 |
| Breathing vs your normal | +0.4 | src/trevor/wearables/crisis.py:85 |
What is shown
For the factual signals: a list of dated lines, for example « Night SpO2 91% », « Resting HR 72 bpm (baseline 60) », « HRV 38 ms (baseline 52, drop of 25% or more) », « Skin temperature +0.6 °C », « Respiratory rate 21 /min », « Less than 5 h of sleep 3 nights in a row », from most recent to oldest. For the crisis risk: three numbers (z of lowest night HR, consecutive nights from 0 to 4, coupling from 0 to 3) added to the calculation, and the list of sources (up to 6). For the Signals screen: the gaps of temperature and heart from your normal. ECGs do not enter the score: they only trigger guidance messages.
A worked example
Resting HR: median of the previous 30 days at 60 bpm. A day at 70 bpm gives the signal « Resting HR 70 bpm (baseline 60) », because 70 ≥ 60 + 10. A day at 69 bpm does not. For the coupling, with a temperature z of +2 and a night HR z of +3: min(3; 2) × min(3; 3) / 3 = 2. Contribution to the crisis risk before amplification: 0.3 × 2 = 0.6; after the 1.6 gain: 0.96 on the model scale. Calculated by hand from the code formulas, not executed.
What this algorithm cannot do
- The factual signals are fixed thresholds on wrist or finger values, measured by a consumer sensor. Night SpO2 and skin temperature values are sensitive to movement and how the device is worn.
- The short sleep signal ignores a night under 2 h, so a truly very short night may not count.
- There are two different baselines for the heart: the crisis risk's (median and MAD over 30 records, minimum 7) and that of the factual signals and the Signals module (30 days, minimum 5 or 3 nights, thresholds in bpm). The thresholds (+10 bpm, +8 bpm) are not the same.
- Skin temperature is not body temperature: a sensor never triggers the emergency, and the coupling between temperature and heart invites you to take your temperature without saying you have a fever.
- The temperature deviation supplied by the device is already a deviation from its own baseline. When Trévor's baseline is missing, it is simply divided by 0.25 °C.
- Other sources only complete the days that the watch already measured: a night HR or skin deviation imported for a day without a watch is ignored.
- The factual signals and the list of sources disappear if interpretation is turned off.
Code : src/trevor/wearables/service.py (signals, summary, day_records, crisis_risk), src/trevor/wearables/crisis_signaux.py (night_streak, features, NEW_PRIOR_WEIGHTS), src/trevor/signaux/service.py (crisis_extras, crisis_sources, crisis_sources_text, build), src/trevor/signaux/baseline.py (against_baseline, consecutive_elevated, robust), src/trevor/signaux/temperature.py (analyse, is_fever), src/trevor/signaux/heart.py (analyse), src/trevor/signals.py (vue « Mesures et estimations »), data/signaux_sources.json (baseline)
Vasoconstriction trend (experimental)
This indicative index spots skin that cools more than usual during the night, a possible sign of vasoconstriction. It compares your skin temperature, resting heart rate, HRV, oxygen and breathing with your previous 14 days. It stays silent when the measurement is fragile, and it is only computed if you gave your consent.
Where you see it: On the watch (My health screen), as codes only (state, level, factor names, quality), and as a « Skin cooling down (experimental) » factor in the crisis risk explanations when it weighs. This index displays no health value.
What the algorithm reads
- Night skin temperature (skin value, or deviation given by the device) (watch or ring)
- Resting heart rate, HRV, SpO2, respiratory rate (watch or ring)
- Measurement quality over the last 14 days (calcul Trévor)
- Your « My context » consent for vasoconstriction, and the indicator turned on on Trévor's side (setting)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
BASE_DAYS | 14 | Previous days that form the comparison base (median) | src/trevor/wearables/vasoconstriction.py:33 |
MIN_BASE | 5 | Minimum values in the base, for each measurement | src/trevor/wearables/vasoconstriction.py:34 |
TEMP_DROP_C | 0.3 °C | Skin drop from the median (in addition to at least 1 spread) | src/trevor/wearables/vasoconstriction.py:35 |
TEMP_MIN_SD | 0.15 °C | Spread floor for temperature | src/trevor/wearables/vasoconstriction.py:36 |
HR_RISE_BPM / HR_MIN_SD | 5 bpm ; 2 bpm | Rise of resting heart rate and spread floor | src/trevor/wearables/vasoconstriction.py:37-38 |
HRV_RATIO | 0.75 | HRV at most 75% of the median (25% below it) | src/trevor/wearables/vasoconstriction.py:39 |
SPO2_DROP | 1.5 points | Oxygen drop below the base (spread floor 0.7) | src/trevor/wearables/vasoconstriction.py:40, 113 |
RESP_RISE | 2 per minute | Rise of respiratory rate (spread floor 1.0) | src/trevor/wearables/vasoconstriction.py:41, 115 |
Z_MIN | 1.0 | Each gap must also reach one robust spread | src/trevor/wearables/vasoconstriction.py:42 |
WINDOW_DAYS / GOOD_DAYS / MEDIUM_DAYS | 14 ; 10 ; 6 | Usable days to judge measurement quality | src/trevor/wearables/mesure.py:16-20 |
MIN_NIGHT_COVERAGE_MIN | 120 | A known sleep of under 2 h makes the day unusable | src/trevor/wearables/mesure.py:17, 30 |
weight in crisis risk | +0.2 (× 1.6 = +0.32 per level) | Modest weight: the code speaks of a described association (Mohan 1998, Veluswamy 2020), not a validated prediction | src/trevor/wearables/crisis.py:92 |
consentement | ctx_vasoconstriction_enabled and consent "My context" | Without either, the result is empty (None) and the factor is 0 | src/trevor/wearables/vasoconstriction.py:134-141 |
The weights
A positive weight raises the result, a negative weight lowers it.
| Signal | Weight | Where |
|---|---|---|
| Skin cooling down (level 0, 1 or 2) | +0.2 | src/trevor/wearables/crisis.py:92 |
What is shown
A result with a state (silence, calm, slight or clear), a level from 0 to 2, the names of the factors present (temp, hr, hrv, spo2, resp), the measurement quality (good, medium, insufficient) and the number of usable days. Only labels come out, never a health value or series, and nothing is logged. For the crisis risk, only the level counts: 0 for silence and calm, 1 for slight, 2 for clear. In the app and on the watch, the codes are silent, calm, slight and clear.
A worked example
Fourteen reference days: skin around 34.0 °C, resting heart rate 58 to 59 bpm, HRV 50 to 52, SpO2 96, breathing 14, sleep 420 min. Today: skin 33.5 °C and heart 66 bpm. Skin dropped by 0.5 °C (at least 0.3) and by more than one spread, and the heart rises by more than 5 bpm: two factors (temp, hr) with good quality, so state « clear », level 2, i.e. +0.64 on the model scale (2 × 0.32). If the heart stays at 59 bpm, only « temp » is present: state « slight », level 1. Result verified by running compute() from the repository (clear, level 2, factors temp and hr; slight, level 1, factor temp).
What this algorithm cannot do
- The central signal is skin temperature measured at the wrist or finger, which varies with room temperature, bedding, how the device is worn and movement. A drop of 0.3 °C can come from a cooler bedroom.
- The perfusion index (PPG) and daytime distal temperature are not collected by Trévor: they do not count, and the text does not promise them.
- Without skin cooling, the other signs are set aside: a faster heart or low HRV alone say nothing here about vasoconstriction.
- The base counts only 14 days and only 5 values, so it is fragile. « Insufficient » quality makes the index silent, but « medium » is enough for a « slight » state.
- The « clear » state requires « good » quality and at least two factors. The factors are not weighted against each other.
- When the model is trained, the index is always 0: its weight has never been learned from your data.
- Skin that cools down is not a diagnosis. The index text remains an aid to caution (cold, lack of water, stress, infection).
Code : src/trevor/wearables/vasoconstriction.py (compute, level_feature, for_user), src/trevor/wearables/mesure.py (assess, usable), src/trevor/wearables/service.py (_vaso_feature), src/trevor/wearables/crisis.py (PRIOR_WEIGHTS vasoconstriction), src/trevor/wearables/watch_summary.py (_vasoconstriction)
