Skip to main content

efb/fp/
fuel_planning.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024, 2026 Joe Pearson
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use log::{debug, trace};
17
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20
21use super::{LegPerformance, Performance};
22use crate::aircraft::Aircraft;
23use crate::measurements::Duration;
24use crate::route::Route;
25use crate::{Fuel, VerticalDistance};
26
27#[repr(C)]
28#[derive(Copy, Clone, Eq, PartialEq, Debug)]
29pub enum Reserve {
30    Manual(Duration),
31}
32
33impl Reserve {
34    pub fn fuel(self, perf: &Performance, cruise: &VerticalDistance) -> Fuel {
35        match self {
36            Self::Manual(duration) => perf.ff(cruise) * duration,
37        }
38    }
39}
40
41impl Default for Reserve {
42    fn default() -> Self {
43        Self::Manual(Duration::default())
44    }
45}
46
47/// Policy for determining fuel to load.
48///
49/// Represents different approaches to fuel planning. The [`fuel planning`] is
50/// based on this policy.
51///
52/// [`fuel planning`]: `FuelPlanning`
53#[repr(C)]
54#[derive(Copy, Clone, Eq, PartialEq, Debug)]
55pub enum FuelPolicy {
56    /// Load minimum required fuel only.
57    MinimumFuel,
58    /// Fill tanks to capacity.
59    MaximumFuel,
60    /// Total fuel to load (includes required fuel).
61    ManualFuel(Fuel),
62    /// Desired fuel remaining after landing.
63    FuelAtLanding(Fuel),
64    /// Additional fuel beyond minimum requirements.
65    ExtraFuel(Fuel),
66}
67
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69#[derive(Copy, Clone, Eq, PartialEq, Debug)]
70pub struct FuelPlanning {
71    taxi: Fuel,
72    trip: Fuel,
73    alternate: Option<Fuel>,
74    reserve: Fuel,
75    total: Fuel,
76    min: Fuel,
77    extra: Option<Fuel>,
78    after_landing: Fuel,
79}
80
81impl FuelPlanning {
82    pub fn new(
83        aircraft: &Aircraft,
84        policy: &FuelPolicy,
85        taxi: Fuel,
86        route: &Route,
87        reserve: &Reserve,
88        perf: &LegPerformance,
89    ) -> Option<Self> {
90        let trip = *route.totals(Some(perf))?.fuel()?.total();
91
92        // Use the last leg's level for reserve fuel calculation
93        let last_level = route.legs().last().and_then(|l| l.level()).cloned()?;
94
95        let alternate = route
96            .alternate()
97            .and_then(|alternate| alternate.fuel(perf))
98            .map(|lf| *lf.total());
99        let reserve = reserve.fuel(perf.cruise()?, &last_level);
100
101        trace!(
102            "fuel planning: trip={:.1}, alternate={:.1?}, reserve={:.1?}",
103            trip,
104            alternate,
105            reserve
106        );
107
108        let min = {
109            let mut min = taxi + trip + reserve;
110
111            if let Some(alternate) = alternate {
112                min = min + alternate;
113            }
114
115            min
116        };
117
118        let extra = {
119            match policy {
120                FuelPolicy::MinimumFuel => None,
121                FuelPolicy::MaximumFuel => {
122                    aircraft.usable_fuel().map(|usable_fuel| usable_fuel - min)
123                }
124                FuelPolicy::ManualFuel(fuel) => Some(*fuel - min),
125                FuelPolicy::FuelAtLanding(fuel) => Some(*fuel), // TODO is this correct?
126                FuelPolicy::ExtraFuel(fuel) => Some(*fuel),
127            }
128        };
129
130        let total = {
131            match extra {
132                Some(extra) => min + extra,
133                None => min,
134            }
135        };
136
137        let after_landing = total - taxi - trip;
138
139        debug!(
140            "fuel planning: min={:.1}, total={:.1}, extra={:.1?}, after_landing={:.1}",
141            min, total, extra, after_landing
142        );
143
144        Some(Self {
145            taxi,
146            trip,
147            alternate,
148            reserve,
149            total,
150            min,
151            extra,
152            after_landing,
153        })
154    }
155
156    pub fn taxi(&self) -> &Fuel {
157        &self.taxi
158    }
159
160    pub fn trip(&self) -> &Fuel {
161        &self.trip
162    }
163
164    pub fn alternate(&self) -> Option<&Fuel> {
165        self.alternate.as_ref()
166    }
167
168    pub fn reserve(&self) -> &Fuel {
169        &self.reserve
170    }
171
172    pub fn total(&self) -> &Fuel {
173        &self.total
174    }
175
176    pub fn min(&self) -> &Fuel {
177        &self.min
178    }
179
180    pub fn extra(&self) -> Option<&Fuel> {
181        self.extra.as_ref()
182    }
183
184    pub fn on_ramp(&self) -> &Fuel {
185        &self.total
186    }
187
188    pub fn after_landing(&self) -> &Fuel {
189        &self.after_landing
190    }
191}