Skip to content

Commit

Permalink
Merge pull request #181 from fast-aircraft-design/engine-rework
Browse files Browse the repository at this point in the history
Engine rework
  • Loading branch information
christophe-david committed Jun 8, 2020
2 parents cf12879 + adc04f1 commit df3db3f
Show file tree
Hide file tree
Showing 20 changed files with 484 additions and 191 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- uses: actions/cache@v2
if: startsWith(runner.os, 'macOS')
with:
path: ~/Library/Caches/pypoetry
path: /Users/runner/Library/Caches/pypoetry
key: ${{ runner.os }}-Py${{ matrix.python-version }}-pypoetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-Py${{ matrix.python-version }}-pypoetry-
Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False

# Use docstring from class and __init__
autoclass_content = "both"

# -- Options for HTML output ---------------------------------------------------

Expand Down
3 changes: 1 addition & 2 deletions src/fastoad/cmd/resources/fastoad.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ driver = "om.ScipyOptimizeDriver(tol=1e-6, optimizer='COBYLA')"
use_xfoil = false
[model.performance]
id = "fastoad.performances.breguet"
[model.propulsion]
id = "fastoad.propulsion.rubber_engine"
propulsion_id = "fastoad.wrapper.propulsion.rubber_engine"
[model.hq.tail_sizing]
id = "fastoad.handling_qualities.tail_sizing"
[model.hq.static_margin]
Expand Down
6 changes: 6 additions & 0 deletions src/fastoad/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ class XMLReadError(FastError):
This exception indicates that an error occurred when reading an xml file.
"""


class FastUnknownFlightPhaseError(FastError):
"""
Raised when an unknown flight phase code has been encountered
"""
110 changes: 85 additions & 25 deletions src/fastoad/models/performances/breguet.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""
Simple module for performances
"""
"""Simple 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
Expand All @@ -16,6 +14,7 @@

import numpy as np
import openmdao.api as om
from fastoad import BundleLoader
from fastoad.constants import FlightPhase
from fastoad.utils.physics import Atmosphere
from scipy.constants import g
Expand All @@ -28,48 +27,63 @@

class BreguetFromMTOW(om.Group):
"""
Estimation of fuel consumption through Breguet formula with a rough estimate
of climb and descent phases.
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)
This model does not ensure consistency with OWE (Operating Empty Weight).
"""

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 ?
def setup(self):
self.add_subsystem("propulsion", _BreguetPropulsion(), promotes=["*"])
if self.options["propulsion_id"] is None:
self.add_subsystem("propulsion_link", _BreguetPropulsion(), promotes=["*"])
else:
self.add_subsystem(
"propulsion",
_BreguetEngine(propulsion_id=self.options["propulsion_id"]),
promotes=["*"],
)
self.add_subsystem("distances", _Distances(), promotes=["*"])
self.add_subsystem("cruise_mass_ratio", _CruiseMassRatio(), promotes=["*"])
self.add_subsystem("fuel_weights", _FuelWeightFromMTOW(), promotes=["*"])
self.add_subsystem("consumption", _Consumption(), promotes=["*"])


class BreguetFromOWE(om.Group):
class BreguetFromOWE(BreguetFromMTOW):
"""
Estimation of fuel consumption through Breguet formula with a rough estimate
of climb and descent phases.
Estimation of fuel consumption through Breguet formula.
It uses a rough estimate of climb and descent phases.
For the sizing mission, the Breguet formula links MTOW (Max TakeOff Weight) to
ZFW (Zero Fuel Weight).
OWE (Operating Weight Empty) being linked to ZFW and MTOW, a cycle is implemented
to have consistency between these 3 values.
Options:
- propulsion_id:
- if not provided, the propulsion model is expected to be an outside OpenMDAO
component
- if provided, the propulsion model matching the provided identifier will be
called directly in the performance process
"""

def setup(self):
self.add_subsystem("propulsion", _BreguetPropulsion(), promotes=["*"])
self.add_subsystem("distances", _Distances(), promotes=["*"])
self.add_subsystem("cruise_mass_ratio", _CruiseMassRatio(), promotes=["*"])
super().setup()
self.add_subsystem("mtow", _MTOWFromOWE(), promotes=["*"])
self.add_subsystem("fuel_weights", _FuelWeightFromMTOW(), promotes=["*"])
self.add_subsystem("consumption", _Consumption(), promotes=["*"])

self.nonlinear_solver = om.NewtonSolver()
self.nonlinear_solver.options["iprint"] = 0
self.nonlinear_solver.options["solve_subsystems"] = False
self.nonlinear_solver.linesearch = om.BoundsEnforceLS()
self.linear_solver = om.DirectSolver()


Expand All @@ -94,9 +108,53 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
outputs["data:mission:sizing:fuel:unitary"] = fuel / npax / distance


class _BreguetEngine(om.ExplicitComponent):
def __init__(self, **kwargs):
"""
Computes thrust, SFC and thrust rate by direct call to engine model.
"""
super().__init__(**kwargs)
self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"])

def initialize(self):
self.options.declare("propulsion_id", default="", types=str)

def setup(self):
self._engine_wrapper.setup(self)
self.add_input("data:mission:sizing: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_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")

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 * CLIMB_MASS_RATIO

thrust = initial_cruise_mass / ld_ratio * g / engine_count
sfc, thrust_rate, _ = self._engine_wrapper.get_engine(inputs).compute_flight_points(
inputs["data:TLAR:cruise_mach"],
inputs["data:mission:sizing:cruise:altitude"],
FlightPhase.CRUISE,
thrust=thrust,
)
outputs["data:propulsion:thrust"] = thrust
outputs["data:propulsion:SFC"] = sfc
outputs["data:propulsion:thrust_rate"] = thrust_rate


class _BreguetPropulsion(om.ExplicitComponent):
"""
Link with engine computation
Link with engine computation.
"""

def setup(self):
Expand All @@ -113,8 +171,6 @@ def setup(self):
self.add_output("data:propulsion:altitude", units="m", ref=1e4)
self.add_output("data:propulsion:mach")

self.declare_partials("data:propulsion:phase", "*", method="fd")
self.declare_partials("data:propulsion:use_thrust_rate", "*", method="fd")
self.declare_partials("data:propulsion:required_thrust_rate", "*", method="fd")
self.declare_partials("data:propulsion:required_thrust", "*", method="fd")
self.declare_partials(
Expand All @@ -140,7 +196,7 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
class _FuelWeightFromMTOW(om.ExplicitComponent):
"""
Estimation of fuel consumption through Breguet formula with a rough estimate
of climb and descent phases
of climb and descent phases.
"""

def setup(self):
Expand Down Expand Up @@ -193,7 +249,7 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
class _MTOWFromOWE(om.ImplicitComponent):
"""
Estimation of fuel consumption through Breguet formula with a rough estimate
of climb and descent phases
of climb and descent phases.
"""

def setup(self):
Expand All @@ -202,7 +258,8 @@ def setup(self):
self.add_input("data:weight:aircraft:OWE", np.nan, units="kg")
self.add_input("data:weight:aircraft:payload", np.nan, units="kg")

self.add_output("data:weight:aircraft:MTOW", units="kg", ref=1e5)
# The upper bound helps for convergence
self.add_output("data:weight:aircraft:MTOW", units="kg", ref=1e5, lower=0.0, upper=1e6)

self.declare_partials("data:weight:aircraft:MTOW", "*", method="fd")

Expand All @@ -227,7 +284,9 @@ def guess_nonlinear(


class _Distances(om.ExplicitComponent):
""" Rough estimation of distances for each flight phase"""
"""
Rough estimation of distances for each flight phase.
"""

def setup(self):
self.add_input("data:TLAR:range", np.nan, units="m")
Expand All @@ -248,7 +307,7 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):

class _CruiseMassRatio(om.ExplicitComponent):
"""
Estimation of fuel consumption through Breguet formula for a given cruise distance
Estimation of fuel consumption through Breguet formula for a given cruise distance.
"""

def setup(self):
Expand All @@ -258,7 +317,8 @@ def setup(self):
self.add_input("data:mission:sizing:cruise:altitude", np.nan, units="m")
self.add_input("data:mission:sizing:cruise:distance", np.nan, units="m")

self.add_output("data:mission:sizing:cruise:mass_ratio")
# The lower bound helps a lot for convergence
self.add_output("data:mission:sizing:cruise:mass_ratio", lower=0.5, upper=1.0)

self.declare_partials("data:mission:sizing:cruise:mass_ratio", "*", method="fd")

Expand Down
48 changes: 45 additions & 3 deletions src/fastoad/models/performances/tests/test_breguet.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import openmdao.api as om
from fastoad.constants import FlightPhase
from fastoad.models.propulsion.fuel_engine.rubber_engine import RubberEngine
from numpy.testing import assert_allclose
from scipy.constants import foot

from tests.testing_utilities import run_system
from ..breguet import BreguetFromMTOW, BreguetFromOWE
from ...propulsion.fuel_engine.rubber_engine import OMRubberEngine
from ...propulsion.fuel_engine.rubber_engine import OMRubberEngineComponent


def test_breguet_from_mtow():
Expand Down Expand Up @@ -83,16 +86,26 @@ def test_breguet_from_mtow_with_rubber_engine():
ivc.add_output("data:propulsion:rubber_engine:overall_pressure_ratio", 30)
ivc.add_output("data:propulsion:rubber_engine:turbine_inlet_temperature", 1500, units="K")

# With rubber engine OM component
group = om.Group()
group.add_subsystem("breguet", BreguetFromMTOW(), promotes=["*"])
group.add_subsystem("engine", OMRubberEngine(), promotes=["*"])
group.add_subsystem("engine", OMRubberEngineComponent(), promotes=["*"])
group.nonlinear_solver = om.NonlinearBlockGS()
problem = run_system(group, ivc)

assert_allclose(problem["data:mission:sizing:ZFW"], 65076.0, atol=1)
assert_allclose(problem["data:mission:sizing:fuel"], 8924.0, atol=1)
assert_allclose(problem["data:mission:sizing:fuel:unitary"], 0.0642, rtol=1e-3)

# With direct call to rubber engine
problem2 = run_system(
BreguetFromMTOW(propulsion_id="fastoad.wrapper.propulsion.rubber_engine"), ivc
)

assert_allclose(problem2["data:mission:sizing:ZFW"], 65076.0, atol=1)
assert_allclose(problem2["data:mission:sizing:fuel"], 8924.0, atol=1)
assert_allclose(problem2["data:mission:sizing:fuel:unitary"], 0.0642, rtol=1e-3)


def test_breguet_from_owe():
ivc = om.IndepVarComp()
Expand Down Expand Up @@ -130,14 +143,43 @@ def test_breguet_from_owe_with_rubber_engine():
ivc.add_output("data:propulsion:rubber_engine:overall_pressure_ratio", 30)
ivc.add_output("data:propulsion:rubber_engine:turbine_inlet_temperature", 1500, units="K")

# With rubber engine OM component
group = om.Group()

group.add_subsystem("engine", OMRubberEngine(), promotes=["*"])
group.add_subsystem("engine", OMRubberEngineComponent(), promotes=["*"])
group.add_subsystem("breguet", BreguetFromOWE(), promotes=["*"])
group.nonlinear_solver = om.NonlinearBlockGS()
problem = run_system(group, ivc)

engine = RubberEngine(5, 30, 1500, 100000, 0.95, 35000 * foot, -50)
assert_allclose(
engine.compute_flight_points(
0.78,
35000 * foot,
FlightPhase.CRUISE,
thrust=problem["data:propulsion:required_thrust"],
)[0],
problem["data:propulsion:SFC"],
)

assert_allclose(problem["data:weight:aircraft:MTOW"], 74000.0, atol=10)
assert_allclose(problem["data:mission:sizing:ZFW"], 65076.0, atol=1)
assert_allclose(problem["data:mission:sizing:fuel"], 8924.0, atol=1)
assert_allclose(problem["data:mission:sizing:fuel:unitary"], 0.0642, rtol=1e-3)

# With direct call to rubber engine
problem2 = run_system(
BreguetFromOWE(propulsion_id="fastoad.wrapper.propulsion.rubber_engine"), ivc
)

assert_allclose(
engine.compute_flight_points(
0.78, 35000 * foot, FlightPhase.CRUISE, thrust=problem2["data:propulsion:thrust"],
)[0],
problem2["data:propulsion:SFC"],
)

assert_allclose(problem2["data:weight:aircraft:MTOW"], 74000.0, atol=10)
assert_allclose(problem2["data:mission:sizing:ZFW"], 65076.0, atol=1)
assert_allclose(problem2["data:mission:sizing:fuel"], 8924.0, atol=1)
assert_allclose(problem2["data:mission:sizing:fuel:unitary"], 0.0642, rtol=1e-3)
2 changes: 1 addition & 1 deletion src/fastoad/models/propulsion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from .engine import IEngine, IEngineSubclass, OMIEngine
from .engine import IEngine, IOMEngineWrapper, BaseOMEngineComponent

0 comments on commit df3db3f

Please sign in to comment.