Skip to content

Commit

Permalink
Merge pull request #505 from fast-aircraft-design/mission-segment-update
Browse files Browse the repository at this point in the history
Consumed fuel is now directly computed for each time step
  • Loading branch information
christophe-david committed Feb 5, 2024
2 parents a06f542 + 9f49e2a commit 6c761a7
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 29 deletions.
2 changes: 2 additions & 0 deletions src/fastoad/model_base/flight_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class FlightPoint:
isa_offset: float = 0.0 #: temperature deviation from Standard Atmosphere
ground_distance: float = 0.0 #: Covered ground distance in meters.
mass: float = None #: Mass in kg.
consumed_fuel: float = 0.0 #: Consumed fuel since mission start, in kg.
true_airspeed: float = None #: True airspeed (TAS) in m/s.
equivalent_airspeed: float = None #: Equivalent airspeed (EAS) in m/s.
mach: float = None #: Mach number.
Expand Down Expand Up @@ -112,6 +113,7 @@ class FlightPoint:
altitude="m",
ground_distance="m",
mass="kg",
consumed_fuel="kg",
true_airspeed="m/s",
equivalent_airspeed="m/s",
mach="-",
Expand Down
5 changes: 1 addition & 4 deletions src/fastoad/models/performances/mission/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,13 @@ def compute_from(self, start: FlightPoint) -> pd.DataFrame:
part_start.scalarize()

self.consumed_mass_before_input_weight = 0.0
consumed_mass = 0.0
for part in self._sequence:
# This check has to be done first because relative target parameters
# will be made absolute during compute_from()
part_has_target_mass = not (part.target.mass is None or part.target.is_relative("mass"))

flight_points = part.compute_from(part_start)

consumed_mass += flight_points.iloc[0].mass - flight_points.iloc[-1].mass

if isinstance(part, FlightSequence):
self.consumed_mass_before_input_weight += part.consumed_mass_before_input_weight

Expand All @@ -94,7 +91,7 @@ def compute_from(self, start: FlightPoint) -> pd.DataFrame:
mass_offset = flight_points.iloc[0].mass - part_start.mass
for previous_flight_points in self.part_flight_points:
previous_flight_points.mass += mass_offset
self.consumed_mass_before_input_weight = float(consumed_mass)
self.consumed_mass_before_input_weight = flight_points.iloc[-1].consumed_fuel

if len(self.part_flight_points) > 0 and len(flight_points) > 1:
# First point of the segment is omitted, as it is the last of previous segment.
Expand Down
20 changes: 10 additions & 10 deletions src/fastoad/models/performances/mission/mission.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import logging
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Optional
Expand All @@ -25,6 +26,8 @@
from .routes import RangedRoute
from .segments.registered.cruise import CruiseSegment

_LOGGER = logging.getLogger(__name__) # Logger for this module


@dataclass
class Mission(FlightSequence):
Expand Down Expand Up @@ -52,11 +55,7 @@ def consumed_fuel(self) -> Optional[float]:
if self._flight_points is None:
return None

return (
self._flight_points.mass.iloc[0]
- self._flight_points.mass.iloc[-1]
+ self.get_reserve_fuel()
)
return self._flight_points.iloc[-1].consumed_fuel + self.get_reserve_fuel()

@property
def first_route(self) -> RangedRoute:
Expand All @@ -78,12 +77,13 @@ def _get_first_route_in_sequence(

def compute_from(self, start: FlightPoint) -> pd.DataFrame:
if self.target_fuel_consumption is None:
flight_points = super().compute_from(start)
flight_points.loc[flight_points.name.isnull()].name = ""
self._compute_reserve(flight_points)
return flight_points
self._flight_points = super().compute_from(start)
self._flight_points.loc[self._flight_points.name.isnull()].name = ""
self._compute_reserve(self._flight_points)
else:
self._solve_cruise_distance(start)

return self._solve_cruise_distance(start)
return self._flight_points

def get_reserve_fuel(self):
""":returns: the fuel quantity for reserve, obtained after mission computation."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,8 @@ def as_scalar(value):

def _compute_outputs(self, outputs, flight_points):
# Final ================================================================
start_of_mission = FlightPoint.create(flight_points.iloc[0])
end_of_mission = FlightPoint.create(flight_points.iloc[-1])
outputs[self.name_provider.NEEDED_BLOCK_FUEL.value] = (
start_of_mission.mass - end_of_mission.mass
)
outputs[self.name_provider.NEEDED_BLOCK_FUEL.value] = end_of_mission.consumed_fuel
reserve_var_name = self._mission_wrapper.get_reserve_variable_name()
if reserve_var_name in outputs:
outputs[self.name_provider.NEEDED_BLOCK_FUEL.value] += outputs[
Expand Down
39 changes: 35 additions & 4 deletions src/fastoad/models/performances/mission/segments/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Base classes for simulating flight segments."""
# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
# Copyright (C) 2023 ONERA & ISAE-SUPAERO
# Copyright (C) 2024 ONERA & ISAE-SUPAERO
# FAST is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
Expand Down Expand Up @@ -122,7 +122,6 @@ class AbstractFlightSegment(IFlightPart, ABC):
However, when subclassing, the method to overload is :meth:`compute_from_start_to_target`.
Generic reprocessing of start and target flight points is done in :meth:`compute_from`
before calling :meth:`compute_from_start_to_target`
"""

#: A FlightPoint instance that provides parameter values that should all be reached at the
Expand Down Expand Up @@ -191,8 +190,9 @@ def compute_from(self, start: FlightPoint) -> pd.DataFrame:
.. Important::
When subclasssing, if you need to overload :meth:`compute_from`, you should consider
overriding :meth:`_compute_from` instead. Therefore, you will take benefit of the
preprocessing of start and target flight points that is done in :meth:`compute_from`
overriding :meth:`compute_from_start_to_target` instead. Therefore, you will take
benefit of the preprocessing of start and target flight points that is done in
:meth:`compute_from`.
:param start: the initial flight point, defined for `altitude`, `mass` and speed
Expand Down Expand Up @@ -264,6 +264,37 @@ def complete_flight_point_from(flight_point: FlightPoint, source: FlightPoint):
if getattr(flight_point, field_name) is None and not source.is_relative(field_name):
setattr(flight_point, field_name, getattr(source, field_name))

@staticmethod
def consume_fuel(
flight_point: FlightPoint,
previous: FlightPoint,
fuel_consumption: float = None,
mass_ratio: float = None,
):
"""
This method should be used whenever fuel consumption has to be stored.
It ensures that "mass" and "consumed_fuel" fields will be kept consistent.
Mass can be modified using the 'fuel_consumption" argument, or the 'mass_ratio'
argument. One of them should be provided.
:param flight_point: the FlightPoint instance where "mass" and "consumed_fuel"
fields will get new values
:param previous: FlightPoint instance that will be the base for the computation
:param fuel_consumption: consumed fuel, in kg, between 'previous' and 'flight_point'.
Positive when fuel is consumed.
:param mass_ratio: the ratio flight_point.mass/previous.mass
"""
flight_point.mass = previous.mass
flight_point.consumed_fuel = previous.consumed_fuel
if fuel_consumption is not None:
flight_point.mass -= fuel_consumption
flight_point.consumed_fuel += fuel_consumption
if mass_ratio is not None:
flight_point.mass *= mass_ratio
flight_point.consumed_fuel += previous.mass - flight_point.mass

def _complete_speed_values(
self, flight_point: FlightPoint, raise_error_on_missing_speeds=True
) -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def compute_from_start_to_target(self, start: FlightPoint, target: FlightPoint)
)

end = deepcopy(start)
end.mass = start.mass * cruise_mass_ratio
self.consume_fuel(end, previous=start, mass_ratio=cruise_mass_ratio)
end.ground_distance = target.ground_distance
end.time = start.time + (end.ground_distance - start.ground_distance) / end.true_airspeed
end.name = self.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def compute_from_start_to_target(self, start: FlightPoint, target: FlightPoint)
end.name = self.name

if end.mass is None:
end.mass = start.mass * self.mass_ratio
self.consume_fuel(end, previous=start, mass_ratio=self.mass_ratio)
else:
end.consumed_fuel = start.consumed_fuel + start.mass - end.mass

self.complete_flight_point_from(end, start)
self.complete_flight_point(end)
Expand All @@ -64,6 +66,9 @@ def compute_from_start_to_target(self, start: FlightPoint, target: FlightPoint)
if self.reserve_mass_ratio > 0.0:
reserve = deepcopy(end)
reserve.mass = end.mass / (1.0 + self.reserve_mass_ratio)
self.consume_fuel(
reserve, previous=end, mass_ratio=1.0 / (1.0 + self.reserve_mass_ratio)
)
flight_points.append(reserve)

return pd.DataFrame(flight_points)
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,10 @@ def compute_next_flight_point(
next_point = FlightPoint()

next_point.isa_offset = self.isa_offset
next_point.mass = previous.mass - self.propulsion.get_consumed_mass(previous, time_step)
consumed_mass = self.propulsion.get_consumed_mass(previous, time_step)
next_point.mass = previous.mass - consumed_mass
next_point.consumed_fuel = previous.consumed_fuel + consumed_mass

next_point.time = previous.time + time_step
next_point.ground_distance = (
previous.ground_distance
Expand Down
35 changes: 31 additions & 4 deletions src/fastoad/models/performances/mission/tests/test_mission.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def test_mission(low_speed_polar, high_speed_polar, propulsion):
ground_distance=100000.0,
)

# Test mission with fixed route distances
# Test mission with fixed route distances ----------------------------------
mission_1 = Mission(name="mission1")
mission_1.extend([first_route, second_route])

Expand All @@ -132,8 +132,14 @@ def test_mission(low_speed_polar, high_speed_polar, propulsion):
26953.0,
atol=1.0,
)
assert_allclose(
flight_points.iloc[0].mass - flight_points.iloc[-1].mass,
flight_points.iloc[-1].consumed_fuel,
atol=1e-10,
rtol=1e-6,
)

# Test mission with fixed route distances and reserve
# Test mission with fixed route distances and reserve ----------------------
mission_2 = Mission(name="mission2", reserve_ratio=0.03)
mission_2.extend([first_route, second_route])

Expand All @@ -152,7 +158,14 @@ def test_mission(low_speed_polar, high_speed_polar, propulsion):
atol=1.0,
)

# Test with objective fuel, with 2 routes
assert_allclose(
flight_points.iloc[0].mass - flight_points.iloc[-1].mass,
flight_points.iloc[-1].consumed_fuel,
atol=1e-10,
rtol=1e-6,
)

# Test with objective fuel, with 2 routes ----------------------------------
mission_3 = Mission(
name="mission3",
target_fuel_consumption=20000.0,
Expand All @@ -164,7 +177,14 @@ def test_mission(low_speed_polar, high_speed_polar, propulsion):
20000.0,
mission_3.fuel_accuracy,
)
# Test with objective fuel, when mission does not start with a route
assert_allclose(
flight_points.iloc[0].mass - flight_points.iloc[-1].mass,
flight_points.iloc[-1].consumed_fuel,
atol=1e-10,
rtol=1e-6,
)

# Test with objective fuel, when mission does not start with a route -------
mission_4 = Mission(name="mission4", target_fuel_consumption=20000.0)
mission_4.extend([taxi_out, first_route, second_route])

Expand All @@ -174,3 +194,10 @@ def test_mission(low_speed_polar, high_speed_polar, propulsion):
20000.0,
mission_4.fuel_accuracy,
)

assert_allclose(
flight_points.iloc[0].mass - flight_points.iloc[-1].mass,
flight_points.iloc[-1].consumed_fuel,
atol=1e-10,
rtol=1e-6,
)

0 comments on commit 6c761a7

Please sign in to comment.