Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions docs/internal/pyneats_units_audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ cite line numbers on `dev` before the fix they describe.
| 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 |
| A10 (airport fuel file units) and the missing `q_fuel` magnitude check | Fixed: a supplied `q_fuel` outside 35–50 MJ/kg is rejected (flight fails; a wrong airport file stops the run); no change on valid inputs | #77 | 2026-10-22 |
| I11 (fleet step cache shared between runners in one process) | Fixed; no change for a single run | `fix/step-cache-per-runner` | 2026-10-22 |
| I11 (fleet step cache shared between runners in one process) | Fixed; no change for a single run | #78 | 2026-10-22 |
| D3 (fuel flow derived from `am` one point late) | Fixed; changes fuel, emissions, contrail and climate results only for flights with `am` and no `ff` | `fix/mass-derived-fuel-flow` | 2026-10-22 |

## 1. Outcome

Expand Down Expand Up @@ -523,7 +524,7 @@ is identical.*
`datetime.fromisoformat(...).astimezone(timezone.utc)`. Also **clarify the AO template**:
UTC only, whole seconds.

**D3. Fuel flow derived from `am` is misaligned in time.**
**D3. Fuel flow derived from `am` is misaligned in time.** *Fixed on branch `fix/mass-derived-fuel-flow` (see the follow-up status table).*
1. `am` → `_derive_fuel_flow_from_mass_if_needed` (`bada_model.py:705-749`). It runs when `am`
is present and `ff` is absent.
2. Expected (docstring and comment): ff[i] = −(m[i+1] − m[i]) / Δt[i]. Observed:
Expand All @@ -539,6 +540,13 @@ is identical.*
5. **Convert in PyNEATS**: use `-np.diff(mass, append=np.nan) / segment_duration`, computed
before the speed filter or with `segment_duration` recomputed after it. Regenerate golden
files that use `am` without `ff`.
6. **Fix (2026-10-22).** `ff[i] = (m[i] − m[i+1]) / Δt[i]` (`np.diff(mass, append=nan)`), so
each point carries the burn of its own segment and Σ fuel_burn = m[0] − m[−1]. Measured
on the 5 golden `aircraft_mass_col` flights (`dev` 6f35319, BADA4, uniform 60 s grid): the
old total was 0.03–0.67 % low, and single points were off by up to 107–287 % in the
descent (top of descent and descent level changes), which reaches the per-point NOx/CO/HC/nvPM indices and the CoCiP
fuel per metre. The uneven-grid case after the speed filter also needs `segment_duration`
recomputed (I9, low-speed filter work).

**D4. Partially null optional columns are not filled.**
1. `tas`, `ff`, `ee`, `am` → `neats_io.py:93-94` → `pycontrails_interpolation.py:108-112` →
Expand Down Expand Up @@ -856,8 +864,8 @@ for a separate decision and were not fixed.
results do not change (fleet before/after on the 4,959 sample flights).

- **I11. The fleet step cache was shared between runners in one process.** Found
2026-10-22, while checking the airport fuel file (A10). *Fixed on branch
`fix/step-cache-per-runner` (see the follow-up status table).*
2026-10-22, while checking the airport fuel file (A10). *Fixed in #78 (see the follow-up
status table).*
- **Defect.** `FleetRunner` builds its steps (parser, performance, emissions, ...) lazily
and caches them per process (`_STEP_CACHE` in `runners/fleet.py`), keyed by
(interface, name) only. The first runner's step was reused by any later runner in the
Expand Down
17 changes: 8 additions & 9 deletions src/pyneats/steps/performance/bada_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,12 +784,12 @@ def _derive_fuel_flow_from_mass_if_needed(self, df: pd.DataFrame) -> None:

When airlines provide aircraft_mass along the trajectory, we can infer fuel consumption
from the decrease in mass over time. The code computes:
fuel_flow[i] = -(mass[i] - mass[i-1]) / segment_duration[i]
fuel_flow[i] = (mass[i] - mass[i+1]) / segment_duration[i]

i.e. a backward mass difference [kg] divided by the forward
i.e. the mass burned over segment i [kg] divided by its duration
segment_duration[i] = t[i+1] - t[i] [s], giving kg/s for the whole
aircraft, clipped at 0. fuel_flow[0] is 0 and fuel_flow[-1] is NaN
(segment_duration[-1] is NaN) (see docs/internal/pyneats_units_audit.md).
aircraft, clipped at 0. fuel_flow[-1] is NaN (no next mass), so the
total fuel burn equals mass[0] - mass[-1] (audit D3).

The derived fuel_flow will be preserved in the output (like airline-provided fuel_flow),
while engine efficiency will still be computed using BADA thrust and fuel_flow for
Expand All @@ -809,11 +809,10 @@ def _derive_fuel_flow_from_mass_if_needed(self, df: pd.DataFrame) -> None:
mass_vals = df["aircraft_mass"].values
dt_vals = df["segment_duration"].values

# mass_diff[i] = mass[i] - mass[i-1] (backward, kg), divided by the
# forward segment_duration[i] = t[i+1] - t[i] (s); last value is NaN
# (see docs/internal/pyneats_units_audit.md).
# Use prepend to handle first waypoint (assumes no mass change at t=0)
mass_diff = np.diff(mass_vals, prepend=mass_vals[0]) # First segment: diff=0
# mass_diff[i] = mass[i+1] - mass[i] (forward, kg), over the same
# segment as segment_duration[i] = t[i+1] - t[i] (s). The last value is
# NaN: there is no next mass (audit D3).
mass_diff = np.diff(mass_vals, append=np.nan)
fuel_flow_derived = -mass_diff / dt_vals # Negative sign because mass decreases

# Ensure non-negative (handle numerical errors or unusual cases)
Expand Down
Loading
Loading