Implementing a Three-Process Fatigue Model in Python
The exact task this guide solves is turning a crew member’s sleep and duty history into a predicted alertness score across a proposed duty period, using the well-established three-process structure: a homeostatic process that tracks accumulating sleep pressure, a circadian process that tracks the body clock, and a sleep-inertia term that captures grogginess just after waking. A duty that is perfectly legal on duty-time limits can still schedule a landing at the circadian trough after a short sleep, and only a fatigue model surfaces that risk. This page implements the three processes, combines them into a score, and evaluates it over a duty. It is the quantitative core of the fatigue risk scoring models section and consumes the rest quality produced by the rest period compliance checks.
Prerequisites
- Python 3.11+ with
numpyfor the vectorised evaluation. - A sleep/wake history — the crew member’s recent sleep episodes with UTC start and end.
- A circadian phase estimate — the acclimatised body-clock phase, from the EASA acclimatisation state or a phase model.
- Model parameters — the homeostatic time constants and circadian amplitude, treated as configurable data.
The three processes
Alertness is modelled as the sum of two oscillating drives plus a transient. Process S is the homeostatic sleep pressure: it rises while awake and falls during sleep, both exponentially. Process C is the circadian rhythm: a roughly sinusoidal drive peaking in the early evening and troughing in the small hours. Process W is sleep inertia: a short-lived penalty in the first minutes after waking. The predicted alertness at time is
where higher values mean more alert. The score a scheduler cares about is the minimum of across the duty, because that is the moment of greatest risk — typically a night landing.
Step 1 — Model the homeostatic process S
While awake, sleep pressure rises toward an upper asymptote; during sleep it decays toward a lower one. Both are exponential, governed by time constants.
import math
def process_s_awake(s0: float, hours_awake: float, tau_rise: float = 18.2) -> float:
"""Sleep pressure rising while awake toward the upper asymptote (1.0)."""
return 1.0 - (1.0 - s0) * math.exp(-hours_awake / tau_rise)
def process_s_asleep(s0: float, hours_asleep: float, tau_fall: float = 4.2) -> float:
"""Sleep pressure decaying during sleep toward the lower asymptote (0.0)."""
return s0 * math.exp(-hours_asleep / tau_fall)
Verify: starting from s0 = 0.3, eight hours of wake raises S toward the upper bound (process_s_awake(0.3, 8) ≈ 0.55), and eight hours of sleep from s0 = 0.9 decays it sharply (process_s_asleep(0.9, 8) ≈ 0.13) — the fast fall constant reflects how quickly sleep discharges pressure.
Step 2 — Model the circadian process C
The circadian drive is a cosine keyed on the acclimatised body-clock hour, peaking in the early evening and troughing near 05:00. Expressing it against acclimatised phase, not station local time, is what ties it to the crew member’s actual physiology.
def process_c(clock_hour: float, amplitude: float = 0.5, peak_hour: float = 17.0) -> float:
"""Circadian alertness drive; peaks near 17:00 and troughs ~12 h opposite."""
radians = 2.0 * math.pi * (clock_hour - peak_hour) / 24.0
return amplitude * math.cos(radians)
Verify: process_c(17.0) returns the full positive amplitude (0.5) at the early-evening peak, and process_c(5.0) returns close to the negative trough — the small-hours minimum where a landing is most fatiguing.
Step 3 — Combine into an alertness score across the duty
Sampling across the duty and taking its minimum gives a single risk score, with sleep inertia applied only in the minutes after the last wake.
import numpy as np
def alertness_curve(duty_hours: np.ndarray, s0: float, clock_at_report: float,
minutes_since_wake: float) -> np.ndarray:
s = 1.0 - (1.0 - s0) * np.exp(-duty_hours / 18.2) # process S over duty
clock = (clock_at_report + duty_hours) % 24.0
c = 0.5 * np.cos(2.0 * np.pi * (clock - 17.0) / 24.0) # process C
inertia = 0.25 * np.exp(-(minutes_since_wake + duty_hours * 60) / 30.0) # process W
return s * -1.0 + c - inertia + 1.0 # invert S so high = alert, normalise to ~[0,1.5]
def fatigue_score(curve: np.ndarray) -> float:
"""Lowest predicted alertness across the duty — the risk-defining moment."""
return float(curve.min())
Verify: a duty reporting at 22:00 acclimatised with a short prior sleep produces a curve whose minimum falls in the pre-dawn hours; the same duty reporting at 09:00 has a higher minimum, so the model ranks the night duty as the more fatiguing even when both are within duty-time limits.
Failure modes and troubleshooting
- Scoring the mean instead of the minimum. Averaging alertness across the duty hides the pre-dawn trough where risk peaks. Remediation: score the minimum of the curve.
- Circadian phase on station time. Keying process C on departure-station local time rather than acclimatised phase mislocates the trough for a jet-lagged crew. Remediation: drive C from the acclimatised body-clock hour.
- Ignoring sleep inertia after a nap. Omitting process W overstates alertness in the first minutes after a rest break. Remediation: apply the inertia term following the last wake.
- Hardcoded time constants. The homeostatic constants vary with the model calibration; embedding them prevents recalibration. Remediation: source parameters from configuration.
- Treating the score as a hard limit. A fatigue score is advisory input, not a regulatory ceiling; blocking on it alone conflates two different controls. Remediation: route the score through the alerting tiers, not the hard rule gate.
Frequently Asked Questions
Is a fatigue score a regulatory limit?
No. Duty-time limits are the hard regulatory control; the fatigue score is a predictive, advisory signal that flags duties which are legal but risky. It feeds the alerting layer, which decides whether to warn or escalate.
Why model three processes rather than one?
Because fatigue is driven by distinct mechanisms: accumulating sleep pressure, the body clock, and post-wake grogginess. A single curve cannot capture a duty that is short but lands at the circadian trough. Separating the processes lets each be calibrated and reasoned about independently.
What makes the pre-dawn hours the worst?
The circadian trough sits in the small hours, so alertness is lowest there even for a well-rested crew, and it compounds with homeostatic pressure late in a night duty. A landing scheduled at 05:00 after a long duty is the canonical high-fatigue moment.
How does acclimatisation change the score?
Acclimatisation shifts the circadian phase, moving the trough in body-clock time. A crew acclimatised to a different theatre experiences the trough at a different station-local hour, which is why the model keys process C on acclimatised phase.
Related
- Fatigue Risk Scoring Models — the parent section and scoring pipeline.
- Scoring Sleep Debt from Duty History with Pandas — the sleep-history input to process S.
- Modeling EASA Acclimatisation State Transitions — the phase estimate that drives process C.
- Rest Period Compliance Checks — the rest quality feeding the model.
Back to Fatigue Risk Scoring Models.