Architecture
This page explains how Acropole turns a trajectory DataFrame into a fuel-flow estimate:
the polars-internal pipeline behind a pandas-compatible API, the ONNX model, feature
normalization, and the per-typecode dispatch.
Overview
graph TD
IN["DataFrame (pandas or polars)"] --> PL["polars frame (normalize input type)"]
PL --> DISP["Dispatch by typecode"]
DISP --> A320["AircraftFuelEstimator(A320)"]
DISP --> B738["AircraftFuelEstimator(B738)"]
A320 --> FEAT["Build 12-feature matrix + min/max normalize"]
B738 --> FEAT
FEAT --> ONNX["ONNX InferenceSession"]
ONNX --> SCALE["× FUEL_FLOW_TO × ENGINE_NUM"]
SCALE --> OUT["fuel_flow, fuel_flow_kgh, fuel_cumsum"]
OUT --> RET["return same type as input"]
A polars engine behind a dual API
FuelEstimator.estimate() accepts a pandas or a polars DataFrame. Internally the
work is always done in polars: a pandas input is converted with pl.from_pandas, the
estimation runs, and the result is converted back to pandas before returning. The input
type is therefore preserved on output — pandas in, pandas out; polars in, polars out.
This keeps the hot path on a single, fast columnar engine while pandas stays an
optional dependency (the [pandas] extra). Users who already live in polars pay no
conversion cost.
Dispatch by typecode
A frame may mix several aircraft types. estimate() groups rows by typecode, and for
each group builds an AircraftFuelEstimator bound to that typecode via
for_aircraft() — which reuses the already-loaded ONNX session and the parsed
parameters, so adding a typecode costs only a parameter lookup, not a model reload.
Each group's predictions are scattered back into the original row order. Rows whose
typecode is unknown stay NaN and raise a warning.
AircraftFuelEstimator is also the public fast path: bound to one typecode, it works on
plain 1-D numpy arrays with all per-aircraft constants (engine scale, normalization
arrays) precomputed at construction.
The ONNX model
The estimator runs a single ONNX InferenceSession on the CPU. The model takes a
(N, 12) matrix of features and emits one fuel-flow value per sample. The twelve
features, in order, are:
engine_type, d_altitude, d_groundspeed, d_airspeed, surface,
max_ope_alti, max_ope_speed, altitude, groundspeed, airspeed,
vertical_rate, mass_norm
Several of these (engine_type, surface, max_ope_alti, max_ope_speed) are
per-aircraft constants read from aircraft_params.csv; the rest come from the
trajectory (and its derivatives). The raw model output is the per-engine fuel flow; it is
scaled to the whole aircraft by multiplying by FUEL_FLOW_TO × ENGINE_NUM (the take-off
reference flow times the engine count), yielding fuel_flow in kg/s. fuel_flow_kgh
is that value × 3600, and fuel_cumsum is the running integral over the time step.
Feature normalization
Before inference every feature is min/max-normalized into [0, 1] using fixed bounds —
one (min, max) pair per feature, baked into the package. The transform is the plain
(x − min) / (max − min). The bounds encode the physical ranges the model was trained on
(e.g. altitude up to 50 000 ft, vertical rate ±5 000 ft/min). The normalized matrix is
cast to float32 to match the model's input dtype.
Why ONNX
Acropole originally ran a TensorFlow model. It was migrated to ONNX for two reasons:
- No heavy ML framework — the runtime depends only on
numpy,polarsandonnxruntime. There is no TensorFlow install, no GPU toolchain, no multi-hundred-MB dependency tree. The model ships as a single portable.onnxfile inside the package. - Speed — the ONNX path is 2–4.8× faster than the original TensorFlow inference, depending on batch size, with the largest gains on small/medium batches.
The migration was validated to numerical parity of 1e-6 against the TensorFlow reference, so existing results are reproduced to floating-point tolerance.
Design decisions
| Decision | Rationale |
|---|---|
| polars engine, pandas optional | One fast columnar engine; pandas stays an extra, not a core dependency |
| Same type in / same type out | Drop-in for both pandas and polars users, no surprise conversions |
| Per-typecode dispatch with shared session | Process a mixed fleet in one call without reloading the model |
| ONNX over TensorFlow | Portable single-file model, no heavy framework, 2–4.8× faster, 1e-6 parity |
| Fixed min/max normalization | Deterministic, baked-in physical ranges matching the training distribution |
src/ layout + py.typed |
PEP 561 typed package, no import-shadowing, strict mypy |