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
779 changes: 779 additions & 0 deletions docs/internal/pyneats_units_audit.md

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions src/pyneats/core/compute_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
DEFAULT_JOBLIB_PREFERENCE : str
Default joblib backend preference.
DEFAULT_WEATHER_OFFSET_H : int
Default weather offset in hours.
Default weather time-window length in hours: ERA5 and the DWD Zarr path
load ``[asofdate, asofdate + offset]``.
DEFAULT_HORIZONTAL_RES_DEG : float
Default horizontal resolution in degrees.
Default horizontal grid resolution in degrees (ERA5 ``grid``).
DEFAULT_SDR_ACCUMULATE_DT_S : int
Default SDR accumulation time in seconds.
Default SDR accumulation step in seconds (3600 s = 1 h). When the DWD Zarr
cache is built, the ``sdr`` values (assumed W m-2) are multiplied by it and
labelled J m**-2; the live NetCDF path does not apply it (see
docs/internal/pyneats_units_audit.md).
DEFAULT_MET_CHUNKS : Mapping[str, Any]
Default chunk sizes for meteorological data.
DEFAULT_RAD_CHUNKS : Mapping[str, Any]
Expand All @@ -40,13 +44,14 @@
DEFAULT_BATCH_SIZE: Final[int] = 16
DEFAULT_JOBLIB_PREFERENCE: Final[str] = "processes"

DEFAULT_WEATHER_OFFSET_H: Final[int] = 36
DEFAULT_WEATHER_OFFSET_H: Final[int] = 36 # h

DEFAULT_HORIZONTAL_RES_DEG: Final[float] = 0.25
DEFAULT_SDR_ACCUMULATE_DT_S: Final[int] = 3600 # 1 hour
DEFAULT_HORIZONTAL_RES_DEG: Final[float] = 0.25 # degrees
DEFAULT_SDR_ACCUMULATE_DT_S: Final[int] = 3600 # s (1 hour)


# Default chunk dimensions for xarray operations using zarr backend
# (number of elements per chunk along each dimension)

DEFAULT_MET_CHUNKS: Final[Mapping[str, Any]] = {
"time": 1,
Expand Down
21 changes: 13 additions & 8 deletions src/pyneats/core/fleet_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Utilities for Fleet ↔ List[Flight] conversions.

This module provides utility functions to convert between lists of Flight objects and Fleet objects,
preserving and restoring fuel information and original columns.
carrying fuel information across the conversion and restoring original columns.
"""

from __future__ import annotations
Expand Down Expand Up @@ -34,8 +34,10 @@ def flights_to_fleet(flights: list[Flight]) -> Fleet:
"""
Convert a list of Flight objects to a Fleet, preserving fuel information.

Fuel properties (q_fuel, ei_h2o) are extracted from flight.fuel and stored
as columns in the Fleet dataframe. The original fuel object is set to None.
Fuel properties are copied from flight.fuel into per-waypoint columns
``q_fuel`` [J/kg, lower calorific value] and ``ei_h2o`` [kg H2O / kg fuel],
which pycontrails SAC/CoCiP read on the Fleet. The original fuel object is
set to None.
Flights with different columns are harmonized by padding missing columns
with NaN values. Original column sets are stored in attrs to enable
restoration during fleet_to_flights().
Expand Down Expand Up @@ -101,17 +103,20 @@ def fleet_to_flights(
"""
Convert a Fleet object back to a list of Flight objects, restoring fuel information.

Fuel properties (q_fuel, ei_h2o) are extracted from Fleet columns and used
to reconstruct flight.fuel objects. Columns that were added during Fleet
conversion (NaN padding) are removed to restore original column sets.
flight.fuel is rebuilt with ``NEATSFuel.from_attrs(flight.attrs)``, i.e.
from the flight attributes, not from the Fleet columns. Columns that were
added during Fleet conversion (NaN padding and the ``q_fuel``/``ei_h2o``
columns) are removed without being read, restoring original column sets
(see docs/internal/pyneats_units_audit.md).

Any flights present in the original manifest but missing from the Fleet
are returned as error records.

Parameters
----------
fleet : Fleet
Fleet object with fuel information as columns.
Fleet object produced by flights_to_fleet(). Its fuel columns are not
read; fuel is rebuilt from each flight's attrs.
step_name : str
Name of the processing step, used in error messages for dropped flights.

Expand Down Expand Up @@ -142,7 +147,7 @@ def fleet_to_flights(
for col in fleet_columns - original_columns:
flight.data.pop(col, None)

# Restore fuel object
# Rebuild the fuel object from attrs; the q_fuel/ei_h2o columns are not read
flight.fuel = NEATSFuel.from_attrs(flight.attrs)

# Create error records for dropped flights
Expand Down
58 changes: 37 additions & 21 deletions src/pyneats/core/neats_default_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,51 +24,51 @@
DEFAULT_CLIMATE_IMPACT : str
Default climate impact metric.
DEFAULT_INTERPOLATION_TIME : str
Default interpolation time step.
Default interpolation time step, as a pandas frequency string ("1min" = 60 s).
DEFAULT_BADA4_VERSION : str
Default BADA4 version.
DEFAULT_BADA3_VERSION : str
Default BADA3 version.
DEFAULT_PAYLOAD_FACTOR : float
Default payload factor.
Default payload factor: dimensionless fraction of maximum payload mass.
DEFAULT_FUEL_RESERVE_FRACTION : float
Default fuel reserve fraction.
Default fuel reserve fraction: dimensionless fraction of trip fuel mass.
DEFAULT_MAX_MASS_ESTIMATION_ITER : int
Default max mass estimation iterations.
DEFAULT_MAX_REL_MASS_DIFF : float
Default max relative mass difference.
Default max relative mass difference (dimensionless fraction).
DEFAULT_TRUE_AIR_SPEED_SMOOTHING_WINDOW : int
Default smoothing window for true air speed.
Default smoothing window for true air speed, in number of points.
DEFAULT_DELTA_TAU_COMPUTE_METHOD : Literal
Default delta tau compute method.
DEFAULT_DELTA_TAU_FILL_METHOD : Literal
Default delta tau fill method.
DEFAULT_ROCD_PHASE_THRESHOLD : float
Default rate of climb/descent phase threshold.
Default rate of climb/descent phase threshold, in ft/min.
DEFAULT_EMISSIONS_KWARGS : Mapping[str, Any]
Default emissions model keyword arguments.
DEFAULT_COCIP_KWARGS : Mapping[str, Any]
Default CoCiP model keyword arguments.
DEFAULT_CLIMACCF_KWARGS : Mapping[str, Any]
Default ClimAccf model keyword arguments.
DEFAULT_AROMATICS_CONTENT : float
Default aromatics content for fuel.
Default aromatics content for fuel (fraction scale; unused).
DEFAULT_SULPHUR_CONTENT : float
Default sulphur content for fuel.
Default sulphur content for fuel (fraction scale; unused).
DEFAULT_NAPHTHALEN_CONTENT : float
Default naphthalene content for fuel.
Default naphthalene content for fuel (fraction scale; unused).
DEFAULT_HYDROGEN_CONTENT : float
Default hydrogen content for fuel.
Default hydrogen content for fuel, in wt.% (percent by mass).
REFERENCE_Q_FUEL : float
Reference q_fuel value.
Reference q_fuel value, in J/kg (pycontrails JetA lower calorific value).
DEFAULT_Q_FUEL : float
Default q_fuel value.
Default q_fuel value, in J/kg (lower calorific value).
OPEN_AIRCLIM_LEVELS : tuple of int
Default pressure levels for Open Air Clim.
Default pressure levels for Open Air Clim, in hPa.
OPEN_AIRCLIM_GRID_RES : float
Default grid resolution for Open Air Clim.
Default grid resolution for Open Air Clim, in degrees.
OPEN_AIRCLIM_COMPUTATION_HORIZON : int
Default computation horizon for Open Air Clim.
Default computation horizon for Open Air Clim, in years.
"""

from collections.abc import Mapping
Expand Down Expand Up @@ -121,6 +121,7 @@

# Interpolation defaults
# Specifies the temporal resolution for trajectory
# pandas frequency string passed to Flight.resample_and_fill ("1min" = 60 s)
DEFAULT_INTERPOLATION_TIME: Final[str] = "1min"

# Performance model defaults
Expand All @@ -134,10 +135,13 @@

# Very conservative threshold on the fuel flow. An error in BADA will be thown if higher
# Further studies need to be performed to better handle out of envelop data points
# Unit: kg/s, whole aircraft; compared only with the BADA-computed fuel flow
# (after the q_fuel correction).
DEFAULT_FF_OUTLIER_THRESHOLD = 100

# Mass estimation parameters
# Used in iterative aircraft mass computation from fuel consumption
# Fraction of the maximum payload mass MPL (MTOW - OEW when MPL is unavailable)
DEFAULT_PAYLOAD_FACTOR: Final[float] = (
1.0 # Multiplier for expected payload (1.0 = full capacity)
)
Expand Down Expand Up @@ -176,21 +180,24 @@
)

# Fuel burn guardrail: reject if total fuel > (MTOW - OEW) * threshold
# A ratio of 1.1 allows 10% margin above the useful payload capacity
# (masses in kg). A ratio of 1.2 allows a 20% margin above MTOW - OEW.
DEFAULT_FUEL_BURN_THRESHOLD: Final[float] = 1.2

# Altitude filter for performance fallback
# Minimum Flight Level (FL) threshold: points below this FL are filtered
# when retrying after a performance step failure.
# FL15 = 2000 ft ≈ 610 m — filters out ground-level / taxi data.
# FL20 = 2000 ft = 609.6 m — filters out ground-level / taxi data.
# Converted to m (FL x 100 ft) and compared with the altitude column [m].
DEFAULT_MIN_ALTITUDE_FL: Final[float] = 20

# Maximum ratio of filtered points before giving up on altitude fallback.
# Maximum ratio of filtered points before giving up on altitude fallback
# (dimensionless fraction [0, 1] of trajectory points).
# If more than 80% of trajectory points are below min FL, the flight is
# likely a low-altitude operation — retrying is unlikely to help.
DEFAULT_MAX_ALTITUDE_FILTER_RATIO: Final[float] = 0.8

# Guardrail: reject non-CO2 species producing > MAX_NONCO2_CO2_RATIO × CO2 baseline
# (dimensionless ratio of CO2-equivalent mass to emitted CO2 mass, both in kg)
MAX_NONCO2_CO2_RATIO: Final[float] = 1000.0

# Emissions model parameters
Expand Down Expand Up @@ -262,6 +269,7 @@
"horizontal_resolution": None, # Use native data resolution
"unit_K_per_kg_fuel": False, # Output in ATR units (not K per kg fuel)
}
# Compared with the pycontrails air_pressure column [Pa]; 40000 Pa = 400 hPa
DEFAULT_ACCF_VALIDITY_PRESSURE = 40000 # Pressure level (Pa) for ACCF validity check

# Weather field interpolation configuration
Expand Down Expand Up @@ -297,6 +305,9 @@

# Jet fuel composition and thermodynamic properties
# Used in emissions and energy calculations. Default values from consortium (DLR/TO70)
# Aromatics, sulphur and naphthalene defaults are fraction scale (0.25 = 25%); the AO
# schema does not define the scale of the corresponding fields. None of the three is
# used in calculations. Hydrogen content is in wt.% (percent by mass), not a fraction.
DEFAULT_AROMATICS_CONTENT: Final[float] = (
0.25 # 25% - Default value (not used in current calculations)
)
Expand All @@ -307,14 +318,19 @@
0.03 # 3% - Default value (not used in current calculations)
)
DEFAULT_HYDROGEN_CONTENT: Final[float] = (
13.79 # 13.79% - Default value provided by consortium (DLR/TO70) for fuel composition
13.79 # wt.% - Default value from consortium (DLR/TO70) for fuel composition
)

# Fuel energy content (specific energy)
# Fuel energy content (specific energy), lower calorific value in J/kg of fuel
# Critical for converting fuel consumption to energy and emissions estimates
# REFERENCE_Q_FUEL is the pycontrails JetA q_fuel (pycontrails/core/fuel.py); it is
# not read from BADA files. The BADA fuel flow is divided by q_fuel / REFERENCE_Q_FUEL.
# Because NEATSFuel always carries a q_fuel (DEFAULT_Q_FUEL when none is supplied),
# this correction always applies (see docs/internal/pyneats_units_audit.md).
REFERENCE_Q_FUEL: Final[float] = (
43_130_000.0 # J/kg - Baseline value from PyContrails/BADA
43_130_000.0 # J/kg - Baseline value from PyContrails JetA
)
# DEFAULT_Q_FUEL is used when neither the AO nor the airport fuel file supplies q_fuel
DEFAULT_Q_FUEL: Final[float] = (
42_800_000.0 # J/kg - Default value provided by consortium (DLR/TO70)
)
Expand Down
58 changes: 48 additions & 10 deletions src/pyneats/core/neats_fuel.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,51 @@ class NEATSFuel(SAFBlend):
Parameters
----------
hydrogen_content : float, optional
Hydrogen content in weight percent (wt.% H).
Hydrogen content in wt.% (percent by mass, e.g. 13.8 for Jet-A), the
same scale as the AO field "percentage of hydrogen mass content". Not
a fraction. Defaults to ``DEFAULT_HYDROGEN_CONTENT`` (13.79 wt.%).
h_c_ratio : float, optional
Atomic H/C ratio.
H/C ratio, dimensionless, atomic (molar): hydrogen atoms per carbon
atom (Reg. (EU) 2018/2066 Annex IIIa §1(36)). Converted to wt.% with
``r * 1.008 / (12.011 + r * 1.008) * 100``; used only when
``hydrogen_content`` is None.
q_fuel : float, optional
Lower heating value (J/kg).
PyNEATS input unit: J/kg of fuel (lower calorific value).
The AO JSON field ``calorific_value`` is in MJ/kg of fuel.
The NEATS application multiplies by 1e6 before calling PyNEATS.
Values received here must already be in J/kg; do not convert again.
Defaults to ``DEFAULT_Q_FUEL`` (42.8e6 J/kg).
pct_blend_gate : float, optional
Blend gate for SAFBlend compatibility.
sulphur_content : float, optional
Sulphur content (not used in calculations).
Sulphur content, % by mass basis (Reg. (EU) 2018/2066 Annex IIIa §5(4) default);
fraction vs percentage scale not defined by the AO schema. Stored
only, not used in calculations.
aromatics_content : float, optional
Aromatics content (not used in calculations).
Aromatics content, % by volume basis (Annex IIIa §5(4) default);
fraction vs percentage scale not defined by the AO schema. Stored
only, not used in calculations.
naphthalene : float, optional
Naphthalene content (not used in calculations).
Naphthalene content, % by volume basis (Annex IIIa §5(4) default);
fraction vs percentage scale not defined by the AO schema. Stored
only, not used in calculations.
name : str, optional
Name of the fuel.

Notes
-----
Derived emission indices, all in kg per kg of fuel:

- ``ei_co2`` is the pycontrails JetA value, independent of hydrogen content.
- ``ei_h2o`` is the JetA value scaled by the resolved hydrogen content
(wt.%) divided by 13.8 (JetA hydrogen content, wt.%).
- ``ei_so2`` (and ``ei_sulphates`` derived from it) is the JetA value
whatever ``sulphur_content`` is, so SO2 and sulphate outputs do not
depend on the supplied sulphur (see docs/internal/pyneats_units_audit.md).
"""

# Class defaults are fraction scale (see neats_default_parameters); values
# supplied by the AO keep the AO scale, which the schema does not define.
aromatics_content: float = (
DEFAULT_AROMATICS_CONTENT # default value - Not used in calculations
)
Expand Down Expand Up @@ -82,13 +110,14 @@ def __init__(
h_wt_pct = float(hydrogen_content)
elif h_c_ratio is not None:
r = float(h_c_ratio)
# Convert H/C atomic ratio to hydrogen *mass percent*
# (×100 to keep PyContrails' convention)
# Convert H/C atomic (molar) ratio to hydrogen *mass percent* using
# atomic masses H = 1.008, C = 12.011 g/mol
# (×100 to keep PyContrails' wt.% convention)
h_wt_pct = (r * 1.008) / (12.011 + r * 1.008) * 100.0
else:
h_wt_pct = DEFAULT_HYDROGEN_CONTENT

# --- Resolve q_fuel
# --- Resolve q_fuel [J/kg, LHV]; already in J/kg on entry (see docstring)
qf = float(q_fuel) if q_fuel is not None else DEFAULT_Q_FUEL

# --- Storing Optional Fuel Attributes from AOs, not used at this stage
Expand All @@ -101,7 +130,8 @@ def __init__(
if naphthalene is not None:
object.__setattr__(self, "naphthalene", naphthalene)

# --- Derive dependent indices consistently
# --- Derive dependent indices consistently [kg/kg fuel]
# ei_co2 and ei_so2 are the JetA values; only ei_h2o follows h_wt_pct
ei_co2 = base.ei_co2
ei_h2o = base.ei_h2o * (h_wt_pct / base.hydrogen_content)
ei_so2 = base.ei_so2
Expand Down Expand Up @@ -172,6 +202,14 @@ def from_attrs(cls, attrs: Mapping[str, Any]) -> NEATSFuel:
Handles the case where fuel properties are stored as arrays (from column serialization)
by extracting the first element.

Values are passed through unconverted, so they must already be in the
units of the constructor (``q_fuel`` in J/kg, ``hydrogen_content`` in
wt.%). Only the key ``naphthalene`` is read; the airport-file path stores
the value under ``naphtalene`` and the AO JSON path drops it, so a parsed
naphthalene value is not picked up. Alternative keys are joined with
``or``, so an explicit 0.0 under the first key falls back to the second
key, then to the default (see docs/internal/pyneats_units_audit.md).

Parameters
----------
attrs : Mapping[str, Any]
Expand Down
Loading
Loading