Flight Time Calculation Algorithms
Flight time calculation algorithms form the computational backbone of crew scheduling and regulatory compliance systems. Unlike simple elapsed-time arithmetic, these algorithms must reconcile disparate movement sources, apply jurisdiction-specific thresholds, and produce deterministic outputs that survive an audit years later. This work sits inside the broader Duty Time Validation & Rule Engines domain, where the temporal metrics computed here are transformed into pass/fail compliance verdicts. For flight operations managers and Python automation builders, the engineering challenge is building resilient pipelines that handle timezone transitions, telemetry gaps, and live operational deviations without introducing heuristic drift that no inspector can later reproduce.
The Calculation Problem This Topic Solves
The scoped problem is narrow and unforgiving: given a stream of movement events for a single leg, derive two legally distinct durations — block time and flight time — that every downstream rule engine will treat as ground truth. Block time runs from the moment the aircraft first moves under its own power (chocks-off) to the moment it comes to rest at the next point of landing (chocks-on); this is the “flight time” defined in 14 CFR §1.1 and the basis for FAA Part 117 accumulation. Flight time in the operational sense — takeoff roll to main-gear touchdown, sometimes called “air time” — is what many EASA operators track separately for maintenance and airborne-exposure metrics. Conflating the two is the single most common source of false violations in duty engines.
Three properties make this hard to encode correctly. First, the source events are noisy: ACARS out/off/on/in (OOOI) reports, FMS position logs, and crew-entered logbook times rarely agree to the minute, and any of them can be missing on a given leg. Second, the boundaries are ambiguous — a return-to-gate, a de-icing hold, or a taxi-back after a rejected takeoff all produce movement that is not flight. Third, the output must be reproducible: the same inputs must yield the same durations on every re-run, because an audit reconstructs the number from the stored events, not from a live sensor. An algorithm that rounds, interpolates, or silently prefers one source over another without recording why cannot defend its output.
Schema and Data Structure Design
The data model separates raw movement events from the derived durations computed over them. Keeping the two apart is what lets the calculation be re-run against corrected telemetry — for example when a delayed ACARS IN report arrives — without mutating the audit trail of what was known at evaluation time.
The core entities are the leg (one scheduled origin-destination operation) owning an ordered set of movement_event rows; each event carries an event_type (OUT, OFF, ON, IN), a source (ACARS, FMS, manual), a UTC instant, and the originating zone as metadata. A derived time_calculation row records the resolved block_minutes, air_minutes, the source_precedence that produced each, and a tolerance_flag when sources disagreed beyond threshold. The versioned calc_ruleset supplies source-precedence order and tolerance windows, keyed by effective date so a policy change is a diffable row rather than a code edit.
movement_event rows are kept separate from the derived time_calculation, so the durations can be recomputed against corrected telemetry without mutating the audit trail — and a versioned calc_ruleset makes every precedence or tolerance change a diffable row rather than a code edit.Field naming, unit conventions, and the classification of positioning versus revenue segments all follow the shared crew duty time taxonomy mapping. Anchoring the schema to that vocabulary is what stops the resolver from counting a taxi-back as a landing, or a positioning deadhead as a revenue sector — the two mistakes that most often corrupt cumulative totals.
Regulatory Mapping
Every schema decision above is driven by a specific regulatory provision. For US carriers the controlling text is FAA Part 117, whose numeric schema is worked through in the FAA Part 117 rule schema design; for European operators the parallel is EASA FTL compliance. The mapping below is the ground truth the resolver and accumulator encode.
- 14 CFR §1.1 — Flight time (definition). Defines flight time as the interval “from the moment an aircraft moves under its own power for the purpose of flight until the moment it comes to rest at the next point of landing.” This is the chock-to-chock (block) interval and the quantity Part 117 accumulates — hence
block_minutesis the regulated field, andair_minutesis tracked separately for operational use. - 14 CFR §117.11 — Flight time limitation. Caps flight time within a single flight duty period (8 or 9 hours depending on report time for unaugmented operations). The per-leg
block_minutessum feeds this ceiling. - 14 CFR §117.23 — Cumulative limitations. Limits flight time to no more than 100 hours in any 672 consecutive hours and 1,000 hours in any 365 consecutive days, and flight duty period to 60 hours in any 168 consecutive hours and 190 hours in any 672 consecutive hours. These four rolling windows are why the schema stores leg-level rows, not daily totals.
- EASA ORO.FTL.210 — Cumulative duty and flight time. For European operations, caps flight time at 100 hours in any 28 days, 900 hours per calendar year, and 1,000 hours in any 12 consecutive calendar months, with separate duty caps of 60/110/190 hours over 7/14/28 days. The differing window lengths are the reason accumulation is parameterised by ruleset rather than hardcoded.
Because §1.1 anchors the regulated number to block time while operators frequently reason in air time, the algorithm must compute and store both and label which one each downstream check consumes. The exact boundary rules, edge cases, and a runnable resolver are detailed in Calculating Block Time vs Flight Time in Python.
Python Implementation Walkthrough
Production implementations express movement events as typed models so a malformed payload fails at the ingestion boundary rather than deep inside the accumulator. Using pydantic for the event contract guarantees that every timestamp is timezone-aware UTC before it reaches the resolver. Timezone handling relies on the standard-library zoneinfo and datetime modules documented in the Python datetime documentation.
from datetime import datetime, timezone
from enum import Enum
from pydantic import BaseModel, field_validator
class EventType(str, Enum):
OUT = "OUT" # chocks-off, block start
OFF = "OFF" # wheels-up, air start
ON = "ON" # wheels-down, air end
IN = "IN" # chocks-on, block end
class MovementEvent(BaseModel):
leg_id: str
event_type: EventType
ts_utc: datetime
source: str # "acars" | "fms" | "manual"
origin_zone: str # e.g. "America/New_York"
@field_validator("ts_utc")
@classmethod
def must_be_utc(cls, v: datetime) -> datetime:
if v.tzinfo is None or v.utcoffset() != timezone.utc.utcoffset(None):
raise ValueError("ts_utc must be timezone-aware UTC")
return v
The durations are a pure function of the resolved boundary instants. Keeping the resolver pure — no I/O, no clock reads — is what makes every verdict reproducible from the stored events. The resolver picks one instant per boundary using a configured source precedence, records which source won, and refuses to invent a boundary that no source provided.
from datetime import timedelta
# From calc_ruleset: lower index wins when sources disagree within tolerance.
SOURCE_PRECEDENCE = ("acars", "fms", "manual")
TOLERANCE = timedelta(minutes=3)
def _resolve(events: list[MovementEvent], want: EventType) -> MovementEvent | None:
candidates = [e for e in events if e.event_type is want]
if not candidates:
return None
return min(candidates, key=lambda e: SOURCE_PRECEDENCE.index(e.source))
def compute_durations(events: list[MovementEvent]) -> dict[str, int | bool]:
out, off = _resolve(events, EventType.OUT), _resolve(events, EventType.OFF)
on, in_ = _resolve(events, EventType.ON), _resolve(events, EventType.IN)
if out is None or in_ is None:
raise ValueError("cannot compute block time without OUT and IN")
block = int((in_.ts_utc - out.ts_utc).total_seconds() // 60)
air = (
int((on.ts_utc - off.ts_utc).total_seconds() // 60)
if off is not None and on is not None
else None
)
if block < 0 or (air is not None and air < 0):
raise ValueError("negative duration: events out of chronological order")
# Air time can never exceed block time; a breach signals a boundary error.
flag = air is not None and air > block
return {"block_minutes": block, "air_minutes": air, "tolerance_flag": flag}
Storing durations as integer minutes rather than floats is deliberate: it eliminates the floating-point drift that otherwise accumulates across a 28-day rolling window and quietly under-counts against the §117.23 caps. Every branch that could produce a suspect number raises or flags rather than returning a plausible-looking value, so bad telemetry surfaces at adjudication instead of hiding inside a legal-looking total.
Rolling Window and Temporal Aggregation
The per-leg calculation is stateless, but §117.23 and ORO.FTL.210 are not: they require rolling flight-time totals over spans measured in consecutive hours and days, not calendar buckets. Timezone drift is the dominant source of cumulative error, so every instant enters the aggregation layer stamped in ISO 8601 UTC. Where the data lives in PostgreSQL, frame-bounded window functions express the caps directly; see the PostgreSQL window functions reference for frame semantics.
-- Rolling 672-hour (28-day) flight-time total per crew member,
-- evaluated at each leg's block-in, per FAA 14 CFR §117.23(b).
SELECT
crew_id,
block_in_utc,
SUM(block_minutes) OVER (
PARTITION BY crew_id
ORDER BY block_in_utc
RANGE BETWEEN INTERVAL '672 hours' PRECEDING AND CURRENT ROW
) AS rolling_672h_minutes
FROM time_calculation tc
JOIN leg USING (leg_id)
ORDER BY crew_id, block_in_utc;
For pre-assignment validation inside the scheduler, the same logic runs in memory over Polars or Pandas frames, where a rolling group-by keyed on the block-in timestamp reproduces the SQL frame without a database round trip. Whichever engine runs it, the window boundary must be inclusive of the exact regulatory span — an off-by-one on the 672-hour edge silently drops a leg and lets an over-limit roster through.
import polars as pl
def rolling_flight_time(df: pl.DataFrame) -> pl.DataFrame:
# df columns: crew_id, block_in_utc (datetime[UTC]), block_minutes (int)
return (
df.sort("block_in_utc")
.rolling(index_column="block_in_utc", period="672h", group_by="crew_id")
.agg(pl.col("block_minutes").sum().alias("rolling_672h_minutes"))
)
Integration Points
This topic is one stage in a longer chain and depends on clean contracts with its neighbours:
- Upstream — flight data ingestion. OOOI reports, FMS logs, and manual corrections arrive through an event-driven pipeline that publishes each movement to the calculation queue. Timestamps that arrive without a zone are rejected at this boundary, not coerced to a default.
- Vocabulary — crew duty time taxonomy mapping. Supplies the canonical classification of sector, positioning, and standby so the resolver counts the right legs toward cumulative totals.
- Downstream — rest period compliance checks. Consume the resolved
block_minutesandair_minutesto partition airborne, ground, and rest components before evaluating minimum off-duty windows. - Downstream — fatigue risk scoring models. Correlate the minute-level temporal exposure this topic produces with circadian disruption and workload intensity.
- Operational — threshold tuning and alerting. Watch projected cumulative flight time against the §117.23 ceilings and fire tiered warnings before a breach is inevitable.
- Perimeter — system security and access boundaries. Every recomputation emits a signed audit record so a corrected duration is traceable to the event that triggered it.
Testing and Edge Cases
Because the resolver is time-sensitive and source-dependent, example-based tests miss the cases that matter. Property-based testing with hypothesis generates thousands of synthetic event sets and asserts invariants — for instance that resolved air_minutes never exceeds block_minutes, and that computing durations twice over the same events yields identical results. The boundary conditions that most often break flight-time implementations are specific:
- Missing OFF/ON pair. A leg with valid
OUT/INbut no airborne markers must still yield block time and a null air time, not a crash or a fabricated air value. - Return to gate. A rejected takeoff produces
OUTmovement without a subsequent landing at a new point; the resolver must treat the taxi-back as one leg’s block time, not two, and must not record anair_minutesfrom a takeoff that never completed. - Daylight-saving and date-line crossings. A leg departing before and arriving after a DST change, or crossing the International Date Line, must compute from UTC instants throughout; a naive local-time subtraction silently adds or drops an hour.
- 672-hour window edge. A leg landing exactly on the rolling-window boundary must be counted inclusively; property tests should assert the cumulative total is invariant to whether the boundary leg is expressed in local or UTC time.
Every source-precedence and tolerance change is version-controlled in the calc_ruleset so a policy revision is a reviewable diff, and the full suite runs against the latest ruleset before each deployment.
Explore This Topic in Depth
- Calculating Block Time vs Flight Time in Python — a step-by-step resolver that ingests OOOI telemetry, normalises it to UTC, computes the two regulatory durations, and embeds §1.1 and §117.11 guardrails directly in the pipeline.
Related
- Duty Time Validation & Rule Engines — the parent domain these calculations feed.
- Rest Period Compliance Checks — the downstream validator that partitions airborne, ground, and rest time.
- Fatigue Risk Scoring Models — consumes minute-level exposure for predictive scoring.
- Threshold Tuning & Alerting — watches cumulative flight time against regulatory ceilings.
- FAA Part 117 Rule Schema Design — the numeric schema behind the §117.23 accumulation windows.
Back to Duty Time Validation & Rule Engines.