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

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 tt is

A(t)=S(t)+C(t)W(t)A(t) = S(t) + C(t) - W(t)

where higher values mean more alert. The score a scheduler cares about is the minimum of A(t)A(t) 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 A(t)A(t) 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.

Alertness curve across a night duty Predicted alertness plotted against time of day for a night duty. The homeostatic process declines steadily as time awake grows, while the circadian process oscillates, reaching its trough near 05:00. Their combination produces a minimum alertness in the pre-dawn hours, which is the fatigue score the scheduler evaluates. alert tired 22:00 02:00 05:00 07:00 fatigue score = min alertness A(t) C S
Alertness is the combination of a declining homeostatic drive and an oscillating circadian drive; their sum troughs before dawn, and that minimum is the fatigue score a night duty must be judged on.

Failure modes and troubleshooting

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.

Back to Fatigue Risk Scoring Models.