Source code for fastoad.models.performances.breguet.openmdao

"""Simple Breguet-based module for performances."""
#  This file is part of FAST : A framework for rapid Overall Aircraft Design
#  Copyright (C) 2020  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
#  (at your option) any later version.
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  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 numpy as np
import openmdao.api as om
from scipy.constants import g

from fastoad import BundleLoader
from fastoad.constants import EngineSetting
from fastoad.models.propulsion.fuel_propulsion.base import FuelEngineSet
from fastoad.utils.physics import AtmosphereSI
from . import Breguet


[docs]class OMBreguet(om.Group): """ Estimation of fuel consumption through Breguet formula. It uses a rough estimate of climb and descent phases. MTOW (Max TakeOff Weight) being an input, the model computes the ZFW (Zero Fuel Weight) considering that all fuel but the reserve has been consumed during the mission. This model does not ensure consistency with OWE (Operating Empty Weight). """
[docs] def initialize(self): self.options.declare("propulsion_id", default=None, types=str, allow_none=True)
# TODO: in a more general case, this module will link the starting mass to # the ending mass. Could we make the module more generic ?
[docs] def setup(self): if self.options["propulsion_id"] is None: self.add_subsystem("propulsion_link", _BreguetPropulsion(), promotes=["*"]) self.add_subsystem("distances", _Distances(), promotes=["*"]) self.add_subsystem("cruise_mass_ratio", _CruiseMassRatio(), promotes=["*"]) self.add_subsystem("fuel_weights", _FuelWeightFromMTOW(), promotes=["*"]) else: self.add_subsystem( "propulsion", BreguetWithPropulsion(propulsion_id=self.options["propulsion_id"]), promotes=["*"], ) self.add_subsystem("consumption", _Consumption(), promotes=["*"])
[docs]class BreguetWithPropulsion(om.ExplicitComponent):
[docs] def initialize(self): self.options.declare("propulsion_id", default="", types=str)
[docs] def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:mission:sizing:main_route:cruise:altitude", np.nan, units="m") self.add_input("data:TLAR:cruise_mach", np.nan) self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:aerodynamics:aircraft:cruise:L_D_max", np.nan) self.add_input("data:geometry:propulsion:engine:count", 2) self.add_input("settings:mission:sizing:breguet:climb:mass_ratio", 0.97) self.add_input("settings:mission:sizing:breguet:descent:mass_ratio", 0.98) self.add_input("settings:mission:sizing:breguet:reserve:mass_ratio", 0.06) self.add_input("data:TLAR:range", np.nan, units="m") self.add_input("settings:mission:sizing:breguet:climb_descent_distance", 500.0e3, units="m") self.add_output("data:mission:sizing:main_route:climb:distance", units="m", ref=1e3) self.add_output("data:mission:sizing:main_route:cruise:distance", units="m", ref=1e3) self.add_output("data:mission:sizing:main_route:descent:distance", units="m", ref=1e3) self.add_output("data:mission:sizing:ZFW", units="kg", ref=1e4) self.add_output("data:mission:sizing:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:climb:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:cruise:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:descent:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:fuel_reserve", units="kg", ref=1e4) self.add_output("data:propulsion:SFC", units="kg/s/N", ref=1e-4) self.add_output("data:propulsion:thrust_rate", lower=0.0, upper=1.0) self.add_output("data:propulsion:thrust", units="N", ref=1e5) self.declare_partials("*", "*", method="fd")
[docs] def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"] ) breguet = Breguet( propulsion_model, inputs["data:aerodynamics:aircraft:cruise:L_D_max"], inputs["data:TLAR:cruise_mach"], inputs["data:mission:sizing:main_route:cruise:altitude"], inputs["settings:mission:sizing:breguet:climb:mass_ratio"], inputs["settings:mission:sizing:breguet:descent:mass_ratio"], inputs["settings:mission:sizing:breguet:reserve:mass_ratio"], inputs["settings:mission:sizing:breguet:climb_descent_distance"], ) breguet.compute( inputs["data:weight:aircraft:MTOW"], inputs["data:TLAR:range"], ) outputs["data:propulsion:SFC"] = breguet.sfc outputs["data:propulsion:thrust_rate"] = breguet.thrust_rate outputs["data:propulsion:thrust"] = breguet.thrust outputs["data:mission:sizing:ZFW"] = breguet.zfw outputs["data:mission:sizing:fuel"] = breguet.mission_fuel outputs["data:mission:sizing:main_route:fuel"] = breguet.flight_fuel outputs["data:mission:sizing:main_route:climb:fuel"] = breguet.climb_fuel outputs["data:mission:sizing:main_route:cruise:fuel"] = breguet.cruise_fuel outputs["data:mission:sizing:main_route:descent:fuel"] = breguet.descent_fuel outputs["data:mission:sizing:main_route:climb:distance"] = breguet.climb_distance outputs["data:mission:sizing:main_route:cruise:distance"] = breguet.cruise_distance outputs["data:mission:sizing:main_route:descent:distance"] = breguet.descent_distance outputs["data:mission:sizing:fuel_reserve"] = breguet.reserve_fuel
class _Consumption(om.ExplicitComponent): """ Adds a variable for consumption/passenger/km """ def setup(self): self.add_input("data:mission:sizing:fuel", np.nan, units="kg") self.add_input("data:TLAR:range", np.nan, units="km") self.add_input("data:TLAR:NPAX", np.nan) self.add_output("data:mission:sizing:fuel:unitary", units="kg/km") self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): fuel = inputs["data:mission:sizing:fuel"] npax = inputs["data:TLAR:NPAX"] distance = inputs["data:TLAR:range"] outputs["data:mission:sizing:fuel:unitary"] = fuel / npax / distance class _BreguetPropulsion(om.ExplicitComponent): """ Link with engine computation. """ def setup(self): self.add_input("data:mission:sizing:main_route:cruise:altitude", np.nan, units="m") self.add_input("data:TLAR:cruise_mach", np.nan) self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:aerodynamics:aircraft:cruise:L_D_max", np.nan) self.add_input("data:geometry:propulsion:engine:count", 2) self.add_input("settings:mission:sizing:breguet:climb:mass_ratio", 0.97) self.add_output("data:propulsion:engine_setting", EngineSetting.CRUISE) self.add_output("data:propulsion:use_thrust_rate", False) self.add_output("data:propulsion:required_thrust_rate", 0.0, lower=0.0, upper=1.0) self.add_output("data:propulsion:required_thrust", units="N", ref=1e5) self.add_output("data:propulsion:altitude", units="m", ref=1e4) self.add_output("data:propulsion:mach") self.declare_partials("data:propulsion:required_thrust_rate", "*", method="fd") self.declare_partials("data:propulsion:required_thrust", "*", method="fd") self.declare_partials( "data:propulsion:altitude", "data:mission:sizing:main_route:cruise:altitude", method="fd", ) self.declare_partials("data:propulsion:mach", "data:TLAR:cruise_mach", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): engine_count = inputs["data:geometry:propulsion:engine:count"] ld_ratio = inputs["data:aerodynamics:aircraft:cruise:L_D_max"] mtow = inputs["data:weight:aircraft:MTOW"] initial_cruise_mass = mtow * inputs["settings:mission:sizing:breguet:climb:mass_ratio"] # Variables for propulsion outputs["data:propulsion:altitude"] = inputs[ "data:mission:sizing:main_route:cruise:altitude" ] outputs["data:propulsion:mach"] = inputs["data:TLAR:cruise_mach"] outputs["data:propulsion:required_thrust"] = ( initial_cruise_mass / ld_ratio * g / engine_count ) class _FuelWeightFromMTOW(om.ExplicitComponent): """ Estimation of fuel consumption through Breguet formula with a rough estimate of climb and descent phases. """ def setup(self): self.add_input("data:TLAR:cruise_mach", np.nan) self.add_input("data:TLAR:range", np.nan, units="m") self.add_input("data:aerodynamics:aircraft:cruise:L_D_max", np.nan) self.add_input("data:propulsion:SFC", np.nan, units="kg/N/s") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:cruise:mass_ratio", np.nan) self.add_input("data:mission:sizing:main_route:cruise:altitude", np.nan, units="m") self.add_input("settings:mission:sizing:breguet:climb:mass_ratio", 0.97) self.add_input("settings:mission:sizing:breguet:descent:mass_ratio", 0.98) self.add_input("settings:mission:sizing:breguet:reserve:mass_ratio", 0.06) self.add_output("data:mission:sizing:ZFW", units="kg", ref=1e4) self.add_output("data:mission:sizing:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:climb:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:cruise:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:descent:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:fuel_reserve", units="kg", ref=1e4) self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): mtow = inputs["data:weight:aircraft:MTOW"] cruise_mass_ratio = inputs["data:mission:sizing:main_route:cruise:mass_ratio"] climb_mass_ratio = inputs["settings:mission:sizing:breguet:climb:mass_ratio"] descent_mass_ratio = inputs["settings:mission:sizing:breguet:descent:mass_ratio"] reserve_mass_ratio = inputs["settings:mission:sizing:breguet:reserve:mass_ratio"] flight_mass_ratio = cruise_mass_ratio * climb_mass_ratio * descent_mass_ratio zfw = mtow * flight_mass_ratio / (1.0 + reserve_mass_ratio) mission_fuel = mtow - zfw outputs["data:mission:sizing:ZFW"] = zfw outputs["data:mission:sizing:fuel"] = mission_fuel outputs["data:mission:sizing:main_route:fuel"] = mtow * (1.0 - flight_mass_ratio) outputs["data:mission:sizing:main_route:climb:fuel"] = mtow * (1.0 - climb_mass_ratio) outputs["data:mission:sizing:main_route:cruise:fuel"] = ( mtow * climb_mass_ratio * (1.0 - cruise_mass_ratio) ) outputs["data:mission:sizing:main_route:descent:fuel"] = ( mtow * climb_mass_ratio * cruise_mass_ratio * (1.0 - descent_mass_ratio) ) outputs["data:mission:sizing:fuel_reserve"] = zfw * reserve_mass_ratio class _Distances(om.ExplicitComponent): """ Rough estimation of distances for each flight engine_setting. """ def setup(self): self.add_input("data:TLAR:range", np.nan, units="m") self.add_input("settings:mission:sizing:breguet:climb_descent_distance", 500.0e3, units="m") self.add_output("data:mission:sizing:main_route:climb:distance", units="m", ref=1e3) self.add_output("data:mission:sizing:main_route:cruise:distance", units="m", ref=1e3) self.add_output("data:mission:sizing:main_route:descent:distance", units="m", ref=1e3) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): flight_range = inputs["data:TLAR:range"] climb_descent_distance = inputs["settings:mission:sizing:breguet:climb_descent_distance"] outputs["data:mission:sizing:main_route:cruise:distance"] = ( flight_range - climb_descent_distance ) outputs["data:mission:sizing:main_route:climb:distance"] = climb_descent_distance / 2.0 outputs["data:mission:sizing:main_route:descent:distance"] = climb_descent_distance / 2.0 class _CruiseMassRatio(om.ExplicitComponent): """ Estimation of fuel consumption through Breguet formula for a given cruise distance. """ def setup(self): self.add_input("data:aerodynamics:aircraft:cruise:L_D_max", np.nan) self.add_input("data:propulsion:SFC", np.nan, units="kg/N/s") self.add_input("data:TLAR:cruise_mach", np.nan) self.add_input("data:mission:sizing:main_route:cruise:altitude", np.nan, units="m") self.add_input("data:mission:sizing:main_route:cruise:distance", np.nan, units="m") # The lower bound helps a lot for convergence self.add_output("data:mission:sizing:main_route:cruise:mass_ratio", lower=0.5, upper=1.0) self.declare_partials("data:mission:sizing:main_route:cruise:mass_ratio", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): atmosphere = AtmosphereSI(inputs["data:mission:sizing:main_route:cruise:altitude"]) cruise_speed = atmosphere.speed_of_sound * inputs["data:TLAR:cruise_mach"] cruise_distance = inputs["data:mission:sizing:main_route:cruise:distance"] ld_ratio = inputs["data:aerodynamics:aircraft:cruise:L_D_max"] sfc = inputs["data:propulsion:SFC"] range_factor = cruise_speed * ld_ratio / g / sfc # During first iterations, SFC will be incorrect and range_factor may be too low, # resulting in null or too small cruise_mass_ratio. # Forcing cruise_mass_ratio to a minimum of 0.3 avoids problems and should not # harm (no airplane loses 70% of its weight from fuel consumption) cruise_mass_ratio = np.maximum(0.3, 1.0 / np.exp(cruise_distance / range_factor)) outputs["data:mission:sizing:main_route:cruise:mass_ratio"] = cruise_mass_ratio