Skip to content
Merged
6 changes: 4 additions & 2 deletions docs/explanation/input-prioritization.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ graph TD
A quantity counts as provided when at least one point has a value. Points left empty are not filled from BADA: they stay empty and are left out of the fuel totals. After the resampling to 60 s, an empty point also empties the resampled points between its two neighbours.

!!! note "Fuel-burn plausibility check"
A flight is rejected when its total fuel burn, with the fuel flow actually used (AO, derived from the mass, or BADA), exceeds 1.2 × (MTOW − OEW). The check is not applied when fuel flow, engine efficiency and aircraft mass are all provided. A BADA fuel flow above 100 kg/s at a point counts as a BADA failure at that point; AO fuel flow is not checked point by point.
A flight is rejected when its total fuel burn, with the fuel flow actually used (AO, derived from the mass, or BADA), exceeds 1.2 × (MTOW − OEW). A flight rejected by this check goes through the retries described in [BADA Type Mapping](#bada-type-mapping): BADA 3, then (when the flight has points below FL20) the calculation without those points. It fails only if every attempt is rejected. The check is not applied when fuel flow, engine efficiency and aircraft mass are all provided. A BADA fuel flow above 100 kg/s at a point counts as a BADA failure at that point; AO fuel flow is not checked point by point.

!!! note "Low-speed points"
Before any of the cases above, points whose true airspeed is below the BADA stall speed (at MTOW, take-off configuration), or that have no true airspeed, are removed, also when the AO provides fuel flow, engine efficiency and aircraft mass. If more than 80 % of the points would be removed, none are. Currently this applies only to aircraft computed with BADA 3; this filter is under review.
Expand Down Expand Up @@ -117,7 +117,9 @@ The correspondence between ICAO aircraft types, aircraft versions, engine identi

1. **BADA 4** type from the mapping tables (preferred — higher fidelity), looked up by ICAO type, aircraft version and engine, then with fewer of these keys
2. **BADA 3** type when the mapping gives no BADA 4 type, or when the BADA 4 calculation fails
3. If that also fails and the error allows a retry, points below FL20 are removed and the calculation is tried again (BADA 4, then BADA 3)
3. If that also fails and the flight has points below FL20 (at most 80 % of its points), those points are removed and the calculation is tried again (BADA 4, then BADA 3)

A flight rejected by the fuel-burn plausibility check goes through the same chain and fails only if every attempt is rejected.

The mapping tables were built with fleet statistics, used as frequency-based proxies where no direct correspondence exists. A type absent from the tables makes the flight fail.

Expand Down
2 changes: 1 addition & 1 deletion docs/explanation/mrv-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ The performance model is used to estimate these values when the AO does not prov
Each performance quantity has its own source, so the cases below are not exclusive (an AO fuel flow can be combined with a BADA aircraft mass); the source of one quantity can still depend on what else is provided, as rules 2 and 4 show:

1. **BADA mapping:** every flight needs its aircraft type in the BADA mapping, even when the AO provides all performance data. The BADA file gives the wingspan (contrails); the mapping gives the number of engines (emissions; read from the BADA file when no BADA calculation is made) and, when no engine is declared, the engine UID (the MRR conservative engine where the type has one, otherwise the engine of the mapping row). A type not in the mapping makes the flight fail.
2. **Fuel flow, engine efficiency and aircraft mass all provided:** no BADA thrust or fuel-flow calculation; the provided values are used directly downstream. The fuel-burn plausibility check (total fuel burn at most 1.2 × (MTOW − OEW), applied on every other path with the fuel flow actually used) is not applied on this path.
2. **Fuel flow, engine efficiency and aircraft mass all provided:** no BADA thrust or fuel-flow calculation; the provided values are used directly downstream. The fuel-burn plausibility check (total fuel burn at most 1.2 × (MTOW − OEW), applied on every other path with the fuel flow actually used; a flight it rejects is retried with BADA 3 and then, when it has points below FL20, without those points; it fails only if every attempt is rejected) is not applied on this path.
3. **Otherwise BADA computes thrust and fuel flow at each point**, with the aircraft mass taken from, in order: the AO aircraft mass, if provided; else the AO take-off weight (TOW), decreased along the trajectory with the BADA fuel flow; else the iterative estimate of rule 5.
4. **Outputs:**
- **Fuel flow:** AO fuel flow (kg/s) if provided; else, if the AO provides aircraft mass, the mass decrease since the previous point divided by the time step to the next point (set to 0 where the mass increases; 0 at the first point and empty at the last, so the last segment's mass decrease is not counted); else the BADA fuel flow.
Expand Down
68 changes: 57 additions & 11 deletions docs/internal/pyneats_units_audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ cite line numbers on `dev` before the fix they describe.
| D1 (bare single-flight object) | Open; now a clear `TypeError` on the `FleetRunner` JSON path | #71 | 2026-10-20 |
| I4 (BADA3 maximum payload) | Fixed; changes BADA3 fuel and CO2 | #73 | 2026-10-21 |
| Fuel reserve and number of mass iterations misstated in the user docs (Section 9) | Fixed (docs only) | #74 | 2026-10-21 |
| Performance input decision tree and data-sources table in the user docs (Section 9) | Fixed (docs only) | `docs/performance-input-logic` | 2026-10-21 |
| A13 (mixed fuel-flow provenance) | Documented in the user docs; the design choice stays open | `docs/performance-input-logic` | 2026-10-21 |
| I9 (`segment_duration` not recomputed after the speed filter) | Open; recorded, part of the low-speed filter work | `docs/performance-input-logic` | 2026-10-21 |
| Performance input decision tree and data-sources table in the user docs (Section 9) | Fixed (docs only) | #75 | 2026-10-21 |
| A13 (mixed fuel-flow provenance) | Documented in the user docs; the design choice stays open | #75 | 2026-10-21 |
| I9 (`segment_duration` not recomputed after the speed filter) | Open; recorded, part of the low-speed filter work | #75 | 2026-10-21 |
| I10 (a failed attempt leaked columns into the BADA3 retry) | Fixed; fuel-burn rejections go through the BADA3 retry and the altitude fallback (maintainer decision); no change on the NEATS_FINAL sample | `fix/performance-retry-isolation` | 2026-10-21 |

## 1. Outcome

Expand Down Expand Up @@ -446,8 +447,8 @@ Each entry gives:
- Units agree throughout. The issue is provenance, not scale.
- **Recommendation**: document this, or integrate mass from the provided `ff`. The decision
belongs to the method owner.
- *Documented in `input-prioritization.md` and `mrv-specification.md` on branch
`docs/performance-input-logic` (see the follow-up status table). The design choice
- *Documented in `input-prioritization.md` and `mrv-specification.md` (#75, see the
follow-up status table). The design choice
(integrate mass from the provided `ff` or not) stays open for the method owner.*

## 7. New discrepancies
Expand Down Expand Up @@ -717,12 +718,12 @@ discrepancies:
| `tests/test_performance_fallback.py:4,74,80`; `tests/test_guardrails.py:133,142,151` | FL15; 500 hPa | FL20; 550 hPa | update test docstrings |
| `docs/explanation/input-prioritization.md:60`; `docs/explanation/mrv-specification.md:203` | initial-mass fuel reserve ≈ 3 % of trip fuel | 15 % (`DEFAULT_FUEL_RESERVE_FRACTION = 0.15`, `neats_default_parameters.py:148`; `bada_model.py:1058`). Found after this audit, during the I4 review | fixed (#74) |
| `docs/explanation/input-prioritization.md:70`; `docs/explanation/mrv-specification.md:214` | the mass estimate runs steps 2–4 "twice" | when neither an `aircraft_mass` column nor a `takeoff_weight` attribute is given, steps 2–4 run at least twice and at most four times per estimate: one constant-mass BADA pass, then up to three passes from the updated TOW, stopping when the TOW changes by ≤ 1 % of the previous TOW (`max_mass_estimation_iter = 4`, `max_rel_mass_diff = 0.01`, `bada_model.py:1065-1117`); the last pass is kept. Measured on the 4,303 flights computed from the NEATS_FINAL sample: 2,769 stop after two passes, 1,415 after three, 119 after four, 3 of which stop at the cap with a change of 1.0–1.4 % (1.04, 1.17 and 1.39 %) | fixed (#74) |
| `docs/explanation/input-prioritization.md:23-44`; `docs/explanation/mrv-specification.md:187-193` | the decision tree and rules 1–5 are exclusive cases checked in order: fuel flow + η skips the performance step; fuel flow only uses BADA only for η; with aircraft mass, BADA computes the fuel flow | each quantity has its own source. The BADA thrust and fuel-flow calculation is skipped only when fuel flow, η and aircraft mass are all given (`bada_model.py:741-745`); the BADA adapter is still resolved and, for BADA3 runs, the low-speed filter still removes rows on that path (`bada_model.py:483-495`). Otherwise the mass is the AO mass, else the AO TOW, else the iterative estimate, whatever fuel flow or η were given (`bada_model.py:850-874`); with aircraft mass and no fuel flow, the fuel flow is derived from the mass decrease (`bada_model.py:762-812`); the BADA mapping is needed for every flight. Checked by running one golden flight (EAF2143) with six input combinations (fuel flow + η + mass, fuel flow + η, fuel flow, mass, TOW, none) and with an aircraft type missing from the BADA mapping. A retry bug (a BADA3 retry after a BADA4 guardrail rejection reused the BADA4 columns and could take this exit) is fixed separately | fixed (`docs/performance-input-logic`) |
| `docs/explanation/input-prioritization.md:17` | modelled take-off weight = OEW + load_factor × (MTOW − OEW) | that is only the constant first-pass mass; the take-off weight used is min(MTOW, OEW + load_factor × MPL + 1.15 × trip fuel) from the iteration (MPL = MTOW − OEW when BADA gives no positive MPL). On the 4,303 computed NEATS_FINAL sample flights the first-pass mass equals MTOW on all, the take-off weight on 243 | fixed (`docs/performance-input-logic`) |
| `docs/explanation/input-prioritization.md:83,85` | unmapped types fall back to fleet-statistics proxies; the engine UID defaults to the MRR conservative value | the fleet statistics were used to build the mapping tables; a type absent from them makes the flight fail. Without a declared engine, the engine UID is the MRR conservative engine where the type has one (`MRR_conservative_mapping.csv`), otherwise the engine of the mapping row | fixed (`docs/performance-input-logic`) |
| `docs/explanation/input-prioritization.md:96-97` | "Fuel flow only: η derived from BADA"; "Mass evolution: full BADA performance chain" | with fuel flow only, the mass and TOW are also estimated from the load factor; with mass evolution, the fuel flow is derived from the mass decrease (star ranking left unchanged) | fixed (`docs/performance-input-logic`) |
| `docs/explanation/mrv-specification.md:119,172` | missing points of optional AO variables are filled by interpolation; the performance model estimates values "not available at a point" | a provided column is used whole; empty points are not filled, and resampling widens them (D4) | fixed (`docs/performance-input-logic`) |
| `docs/explanation/mrv-specification.md:181` | without AO TAS, TAS is set to 0 below 840 hPa | the wind is set to 0 where it is missing (`weather_provider.py:382-383`), so TAS equals the smoothed ground speed | fixed (`docs/performance-input-logic`) |
| `docs/explanation/input-prioritization.md:23-44`; `docs/explanation/mrv-specification.md:187-193` | the decision tree and rules 1–5 are exclusive cases checked in order: fuel flow + η skips the performance step; fuel flow only uses BADA only for η; with aircraft mass, BADA computes the fuel flow | each quantity has its own source. The BADA thrust and fuel-flow calculation is skipped only when fuel flow, η and aircraft mass are all given (`bada_model.py:741-745`); the BADA adapter is still resolved and, for BADA3 runs, the low-speed filter still removes rows on that path (`bada_model.py:483-495`). Otherwise the mass is the AO mass, else the AO TOW, else the iterative estimate, whatever fuel flow or η were given (`bada_model.py:850-874`); with aircraft mass and no fuel flow, the fuel flow is derived from the mass decrease (`bada_model.py:762-812`); the BADA mapping is needed for every flight. Checked by running one golden flight (EAF2143) with six input combinations (fuel flow + η + mass, fuel flow + η, fuel flow, mass, TOW, none) and with an aircraft type missing from the BADA mapping. A retry bug (a BADA3 retry after a BADA4 guardrail rejection reused the BADA4 columns and could take this exit) is fixed separately (I10) | fixed (#75) |
| `docs/explanation/input-prioritization.md:17` | modelled take-off weight = OEW + load_factor × (MTOW − OEW) | that is only the constant first-pass mass; the take-off weight used is min(MTOW, OEW + load_factor × MPL + 1.15 × trip fuel) from the iteration (MPL = MTOW − OEW when BADA gives no positive MPL). On the 4,303 computed NEATS_FINAL sample flights the first-pass mass equals MTOW on all, the take-off weight on 243 | fixed (#75) |
| `docs/explanation/input-prioritization.md:83,85` | unmapped types fall back to fleet-statistics proxies; the engine UID defaults to the MRR conservative value | the fleet statistics were used to build the mapping tables; a type absent from them makes the flight fail. Without a declared engine, the engine UID is the MRR conservative engine where the type has one (`MRR_conservative_mapping.csv`), otherwise the engine of the mapping row | fixed (#75) |
| `docs/explanation/input-prioritization.md:96-97` | "Fuel flow only: η derived from BADA"; "Mass evolution: full BADA performance chain" | with fuel flow only, the mass and TOW are also estimated from the load factor; with mass evolution, the fuel flow is derived from the mass decrease (star ranking left unchanged) | fixed (#75) |
| `docs/explanation/mrv-specification.md:119,172` | missing points of optional AO variables are filled by interpolation; the performance model estimates values "not available at a point" | a provided column is used whole; empty points are not filled, and resampling widens them (D4) | fixed (#75) |
| `docs/explanation/mrv-specification.md:181` | without AO TAS, TAS is set to 0 below 840 hPa | the wind is set to 0 where it is missing (`weather_provider.py:382-383`), so TAS equals the smoothed ground speed | fixed (#75) |

## 10. Incidental observations outside the unit scope

Expand Down Expand Up @@ -784,6 +785,51 @@ for a separate decision and were not fixed.
removed (639 leading, 1,947 trailing, 587 interior), carrying 1.36 % of flight time;
CO2 / (3.159 × fuel) from 0.961 to 1.360 (median 0.995) per flight, 0.17 % below at fleet
level.
- **I10. A failed attempt leaked columns into the BADA3 retry.** Found after this audit
(2026-10-21), while checking the user docs. *Fixed on branch
`fix/performance-retry-isolation` (see the follow-up status table).*
- **Defect.** `run()` passed the same preprocessed DataFrame to the first attempt and to the
BADA3 retry (`bada_model.py:350-391`), and `_run_with_altitude_fallback` did the same for
its two attempts (`bada_model.py:429-451`). `run_by_bada_version` writes into that
DataFrame: `fuel_flow` derived from `am` before the BADA calculation, then
`aircraft_mass`, `thrust`, `fuel_flow`, `phase`, `thrust_segment`, `fuel_burn` and `fuel`
(`_finalize_columns`) before the fuel-burn guardrail (`_build_result`). The DataFrame is
shared whenever the low-speed filter removes nothing, which is always the case for BADA4
(I1). The BADA3 retry therefore took the columns of the failed BADA4 attempt as AO data.
- **Effects** (reproduced on golden flight EAF2143, BADA4 fuel or AO fuel flow × 15 to
trigger the guardrail, limit 45,600 kg):

| AO data | First failure | Before the fix | With the fix |
| --- | --- | --- | --- |
| fuel flow + η | guardrail on BADA4 | BADA3 retry took the early exit ("all provided"): accepted with 55,674 kg, no guardrail, no `bada_version` | BADA3 retry rejected on the same AO fuel, then the altitude fallback; rejected if every attempt is |
| η only | guardrail on BADA4 | early exit: accepted with the rejected BADA4 fuel (49 t), no `bada_version` | clean BADA3 retry, accepted with 3,552 kg |
| none | guardrail on BADA4 | BADA3 retry reused the BADA4 fuel flow and mass: rejected again, no altitude fallback | clean BADA3 retry, accepted with 3,552 kg; otherwise the altitude fallback |
| mass + η | BADA4 fails before its result (e.g. first point) | early exit on the fuel flow derived from the mass, BADA skipped | clean BADA3 retry, with the guardrail |
| any, type without a BADA4 code | guardrail on BADA3 | altitude fallback | altitude fallback (unchanged) |

- **Fix.** Each attempt works on its own copy (`df = df.copy()` at the start of
`run_by_bada_version`).
- **Maintainer decision (2026-10-21): a fuel-burn rejection goes through the whole fallback
chain.** Before, the guardrail error carried `retryable=False`. `run()` ignored the flag
before the BADA3 retry, but that retry reused the rejected BADA4 columns, so it never gave
a plausible result; `run()` then honoured the flag before the altitude fallback, except
for types without a BADA4 code and for BADA4 types whose BADA3 retry failed for another
reason (those errors carry the default flag). The maintainers considered making the
rejection final and decided to keep the fallbacks: the guardrail error is now retryable,
so a rejected flight is retried with BADA3, then with the altitude fallback (points below
FL20 removed), and fails only if every attempt is rejected. `run()` and
`_run_with_altitude_fallback` now also honour `retryable=False` before their BADA3
retries. On this path only two internal consistency errors carry that flag (a BADA file
without MTOW or OEW when the take-off mass is estimated, and a missing initial mass), and
neither occurs with the BADA files in use.
- **Known, not changed (existed before).** For a type without a BADA4 code, the altitude
fallback runs the same BADA3 calculation twice (its "BADA4" attempt resolves to BADA3).
When the altitude filter cannot run (no point below FL20, or more than 80 % below it),
such a flight's error record reads "already performed with BADA3" and the actual reason
(for example the fuel-burn rejection) is only in the exception cause, which the fleet
runner does not record. Candidates for the AO-input / error-reporting work.
- **Effect on results.** No flight of the NEATS_FINAL sample reaches the guardrail, so its
results do not change (fleet before/after on the 4,959 sample flights).

## 11. Patch summary and checks performed

Expand Down
Loading
Loading