Rest Period Compliance Checks

Rest period validation answers a deceptively narrow question — did this crew member get enough time off between duties? — that turns out to hide most of the temporal complexity in a scheduling system. A rest period is not a single number to compare against a minimum; it is a window bounded by the end of one duty and the start of the next, measured in the local time of the place the crew rests, discounted for interruptions, and simultaneously subject to a rolling weekly requirement that no single-window check can see. Within the Duty Time Validation & Rule Engines domain, rest validation is the constraint layer that a pairing engine hits most often and trusts most completely, because a mis-computed rest window silently propagates into every downstream flight duty period the engine then believes is legal. This page covers how rest windows are modelled, which regulatory provisions drive the schema, how the checks are implemented in Python, and how they connect to the rest of the validation pipeline.

The Rest Validation Problem Behind a Simple Minimum

The scoped engineering challenge is precise: given a proposed sequence of duties for a crew member, confirm that every gap between consecutive duties satisfies the applicable minimum rest, that any rest taken inside a duty under split-duty provisions is credited correctly, and that the crew member has received a qualifying long rest somewhere in the preceding rolling window. Three properties make this genuinely hard, and none of them are captured by a naive subtraction of two timestamps.

First, rest is measured in the local time of the rest facility, not in UTC and not in the crew member’s home base time. A ten-hour rest that begins at 23:00 local and ends at 09:00 local is a very different recovery opportunity from one that runs 11:00 to 21:00, and the regulations that govern its duration are unforgiving about which clock is authoritative. Because the rest window is bounded by segment end and segment start times that arrive from operational feeds in UTC, the validator must convert to the facility zone, apply the duration rule there, and convert back — and it must do so across daylight-saving transitions where a nominal ten-hour local window is nine or eleven real hours.

Second, rest is not always a clean gap between duties. Split-duty provisions let an operator interrupt a flight duty period with a rest opportunity in a suitable accommodation, crediting some of that break back against the duty limit. A validator that only inspects the gaps between duties will miss the rest taken within one, and will either reject a legal split-duty pairing or, worse, fail to extend the flight duty period the split rest legitimately unlocks.

Third, rest compliance is simultaneously an instantaneous check and a rolling-window check. The minimum-rest-before-duty rule is local to a single gap, but the weekly-rest rule — a required block of consecutive hours free from all duty within a rolling 168-hour window — can only be evaluated by scanning history. A pairing that satisfies every individual gap can still be illegal because no single gap in the preceding week was long enough to qualify as the weekly rest.

The consequence is that rest validation is not one comparison but a small family of related checks that share a normalized event stream and must agree with each other. Building them as independent conditionals is how engines end up passing a roster on the gap check while silently violating the weekly-rest requirement.

Schema and Data Structure Design

The data model separates three concerns that flat implementations collapse into a single duty table: the raw duty and segment history that bounds each rest window, the derived rest_period rows the validator actually reasons about, and the effective-dated rule parameters that supply the minimum thresholds. Keeping the thresholds in their own effective-dated table is what lets a contractual buffer or a regulatory revision be diffed and rolled back without touching validation code.

The core entities are the crew_member; the duty_period rows with their bounding report and release instants; the flight_segment rows that compose each duty; the derived rest_period that spans the gap between one duty’s release and the next duty’s report, carrying the facility IANA zone, the raw and restorative durations, and an interruption flag; the split_rest rows that record a qualifying in-duty break and the accommodation class that makes it eligible; and the effective-dated rest_rule supplying the minimum-rest value, the weekly-rest block length, the rolling-window length, and the jurisdiction key. Every temporal column is stored in UTC with the originating IANA zone retained as metadata, because the duration rule is applied in facility-local time while the rolling weekly window is summed in UTC.

Rest-period compliance schema entity-relationship diagram A crew_member is assigned many duty_period rows, each bounded by report_utc and release_utc and tagged with the facility_zone of the following rest. A duty_period is composed of many flight_segment rows and may be interrupted by zero or more split_rest rows carrying an accommodation_class. Each consecutive pair of duty_periods bounds exactly one derived rest_period, whose raw_hours, restorative_hours and interrupted flag are computed, never entered. An effective-dated rest_rule supplies min_rest_hours, weekly_rest_hours and window_hours and governs each rest_period. The single-gap duration rule is evaluated in facility-local time, while the 168-hour weekly-rest window is summed in UTC. assigned 1 N includes 1 N interrupts 1 0..N bounds gap (release → report) 2 1 governs crew_member crew_idPK base_iata home_zone duty_period duty_idPK crew_idFK report_utc release_utc facility_zone bounding report / release instants stored in UTC flight_segment segment_idPK duty_idFK out_utc in_utc split_rest rest_idPK duty_idFK accommodation_class credited_min rest_rule EFFECTIVE-DATED · effective_from / _to rule_idPK jurisdiction min_rest_hours weekly_rest_hours window_hours rest_period prev_duty_idPK · FK next_duty_idFK DERIVED — NEVER ENTERED raw_hours restorative_hours interrupted facility_zone Single-gap duration rule evaluated in facility-local time. Rolling 168-hour weekly-rest window summed in UTC.
Rest-compliance schema: stored duty and segment history bounds each rest window, but the rest_period the validator reasons about is derived — its raw and restorative hours are computed from consecutive duties, never entered by hand, and governed by an effective-dated rest_rule.

Field names, the boundary between what counts as duty versus rest, and the classification of positioning, standby, and reserve time all follow the shared crew duty time taxonomy. Anchoring the schema to that taxonomy is what stops the validator from mistaking a positioning segment for rest, or a callable standby for genuine time free from duty — the two most common ways a rest check silently over-credits recovery. The raw duration of each window in turn depends on precise segment boundaries produced by the Flight Time Calculation Algorithms, because an unaccounted taxi buffer or a mis-partitioned ground segment shifts the release instant that bounds the rest window.

The end-to-end flow from raw logs to a compliance verdict is summarised below: a multi-source duty stream is normalized to UTC, partitioned into airborne, ground, and rest components, and each rest window is checked against the applicable minimum, with marginal-but-legal windows escalated to a fatigue review rather than passed silently.

Rest validation flow Multi-source duty logs are normalized to a single UTC event stream, then partitioned into airborne, ground and rest components. Each rest window is tested against the applicable minimum. A window at or above the minimum is compliant; a window that clears the floor but sits close to it is escalated to a secondary fatigue review rather than passed silently; a window below the minimum produces a violation record with its shortfall, which the pairing engine then adjusts. yes marginal no Duty logs multi-source Normalize to UTC stream Partition airborne · ground · rest Rest ≥ minimum? Compliant Secondary fatigue review Violation record + shortfall Pairing engine adjusts
Rest validation flow: a normalized event stream is partitioned by duty type, then each rest window is checked against the minimum — a near-minimum window is escalated to a fatigue review rather than passed silently, and a shortfall is written back for the pairing engine to resolve.

Regulatory Mapping

Rest validation encodes specific regulatory provisions, and the schema decisions above trace directly to their text. The authoritative US wording lives in FAA Part 117, and the European counterpart under EASA CS-FTL derives from Regulation (EU) No 965/2012, Annex III, Subpart FTL. The mapping below is the ground truth the checks implement.

The distinction between when these provisions merely warn and when they hard-block a pairing is worked through against the underlying tables in FAA Part 117 rule schema design, and the European branch against EASA FTL compliance. The rest checks here consume those ceilings; they do not redefine them.

Python Implementation Walkthrough

Production implementations express the rest inputs as typed models so a malformed payload fails at the boundary rather than deep inside the temporal core. Using pydantic for the contract guarantees that duty boundaries are timezone-aware and that the facility zone is present before any duration is computed. All arithmetic runs on aware instants; the facility zone is applied only to select the correct local wall-clock window, never to do naive subtraction.

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from pydantic import BaseModel, field_validator


class DutyPeriod(BaseModel):
    duty_id: str
    report_utc: datetime
    release_utc: datetime
    facility_zone: str  # IANA zone of the rest facility after release

    @field_validator("report_utc", "release_utc")
    @classmethod
    def must_be_aware(cls, value: datetime) -> datetime:
        if value.tzinfo is None:
            raise ValueError("duty boundaries must be timezone-aware (UTC)")
        return value


class RestRule(BaseModel):
    min_rest_hours: float       # e.g. 10.0 under FAA §117.25(e)
    weekly_rest_hours: float    # e.g. 30.0 under FAA §117.25(b)
    window_hours: float         # e.g. 168.0 rolling window
    buffer_minutes: int = 0     # contractual buffer above the floor

The single-gap check is the simplest, but its correctness depends entirely on computing the duration from aware instants. Subtracting two aware datetimes yields the true elapsed time regardless of any daylight-saving transition the local clock crossed, which is exactly the behaviour the regulation intends.

def rest_gap_hours(prev: DutyPeriod, nxt: DutyPeriod) -> float:
    """True elapsed rest between one duty's release and the next report."""
    delta = nxt.report_utc - prev.release_utc
    return delta.total_seconds() / 3600.0


def check_min_rest(prev: DutyPeriod, nxt: DutyPeriod, rule: RestRule) -> dict:
    required = rule.min_rest_hours + rule.buffer_minutes / 60.0
    actual = rest_gap_hours(prev, nxt)
    shortfall = round(required - actual, 4)
    return {
        "duty_id": nxt.duty_id,
        "required_hours": required,
        "actual_hours": round(actual, 4),
        "compliant": actual >= required,
        "shortfall_hours": max(shortfall, 0.0),
    }

The EASA duration-relative-to-preceding-duty rule is a thin variation on the same function: the required minimum becomes the greater of the fixed floor and the preceding duty’s length, selected by jurisdiction. Expressing the minimum as a function of the prior duty rather than a constant keeps the home-base 12-hour and away-from-base 10-hour cases in one code path.

def easa_min_rest_hours(prev: DutyPeriod, floor_hours: float) -> float:
    duty_len = (prev.release_utc - prev.report_utc).total_seconds() / 3600.0
    return max(floor_hours, duty_len)

The 8-hour uninterrupted sleep opportunity under §117.25(e) is a second, distinct condition. Because the sleep opportunity must fall inside the rest window and be uninterrupted, the validator subtracts any recorded interruption from the window and confirms the largest remaining uninterrupted block meets the sleep floor — a rest window can satisfy the 10-hour envelope yet fail the 8-hour opportunity if it was broken by a callout.

Rolling Windows and Temporal Aggregation

The weekly-rest requirement is a rolling-window problem that is far cleaner to express in SQL than to hand-roll in Python. For every duty a crew member is assigned, the engine must confirm that at least one qualifying long rest exists within the trailing 168-hour window. A window function keyed on the crew member, ordered by time, with a RANGE frame expressed as an interval, evaluates every duty against its own trailing week in a single pass.

SELECT
    d.crew_id,
    d.duty_id,
    d.report_utc,
    MAX(r.rest_hours) OVER (
        PARTITION BY d.crew_id
        ORDER BY d.report_utc
        RANGE BETWEEN INTERVAL '168 hours' PRECEDING AND CURRENT ROW
    ) AS longest_rest_in_window
FROM duty_period AS d
JOIN rest_period AS r
    ON r.crew_id = d.crew_id
   AND r.ends_utc <= d.report_utc
GROUP BY d.crew_id, d.duty_id, d.report_utc;

Duties whose longest_rest_in_window falls below the weekly_rest_hours threshold are the weekly-rest violations. The same rolling pattern generalises to cumulative-rest reporting: swapping MAX for SUM over the frame yields total off-duty hours in the trailing window, which the fatigue layer consumes as a sleep-debt proxy. When the evaluation runs inside the pairing optimiser rather than a reporting job, the equivalent Polars expression keeps the computation in-process:

import polars as pl

weekly = (
    duties.sort("report_utc")
    .rolling(index_column="report_utc", period="168h", group_by="crew_id")
    .agg(pl.col("rest_hours").max().alias("longest_rest_in_window"))
)

The critical correctness detail is that both the SQL RANGE frame and the Polars rolling window operate on the UTC instant, never on local wall-clock time. A date-line crossing makes local ordering diverge from UTC ordering, and summing a trailing window on local time would double-count or skip a day precisely on the long-haul rotations the weekly-rest rule exists to protect.

Integration Points

Rest validation does not stand alone; it is a node in the wider pipeline and only as correct as the data feeding it. Upstream, it depends on flight data ingestion to deliver normalized, deduplicated duty and segment events with authoritative timestamps — a missing release event or a duplicated segment corrupts the window boundaries before any rule runs. The raw window durations are only trustworthy once the block-to-block reductions from the Flight Time Calculation Algorithms have partitioned airborne and ground time correctly.

Downstream, the restorative fraction of each rest window is exactly the input the Fatigue Risk Scoring Models weight sleep debt against, so a rest that is legal but scheduled entirely across daytime should pass the hard check while still lowering the fatigue score. Where a window clears the regulatory floor but sits close to it, the verdict is routed through Threshold Tuning & Alerting, which decides whether a near-minimum rest becomes an informational note, a warning, or an escalation to a scheduler. Every verdict — pass or fail, with its computed shortfall and the rule version applied — is written to the audit log so a compliance team can reconstruct why the engine believed a rest was sufficient six months after the fact.

Testing and Edge Cases

Rest checks fail in production on the boundaries, so the test suite is built around them rather than the happy path. Property-based tests assert invariants that must hold for any input: adding rest to a window never turns a compliant gap non-compliant, and the check is deterministic for identical inputs and rule versions.

Rule thresholds are version-controlled and validated against the operator’s published FTL scheme before every deployment, so a contractual buffer change or a regulatory revision is a reviewable diff rather than a silent behaviour change — the same discipline the fatigue and flight-time layers follow.

Explore This Topic in Depth

The general checks above become concrete in the step-by-step implementation guides beneath this topic:

Back to Duty Time Validation & Rule Engines.

Explore this section