Source code for fastoad.models.performances.mission.openmdao.mission_run

#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
#  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
#  (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 logging
from os import PathLike
from typing import Optional

import numpy as np
from openmdao import api as om

from fastoad._utils.files import make_parent_dir
from fastoad.model_base import FlightPoint
from fastoad.model_base.propulsion import IOMPropulsionWrapper
from fastoad.module_management.service_registry import RegisterPropulsion

from .base import BaseMissionComp
from ..polar import Polar
from ..segments.registered.cruise import BreguetCruiseSegment

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


[docs]class MissionComp(om.ExplicitComponent, BaseMissionComp): """ Computes a mission as specified in mission input file. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.flight_points = None self._input_weight_variable_name = "" self._engine_wrapper = None
[docs] def initialize(self): super().initialize() self.options.declare( "out_file", default="", types=(str, PathLike), desc="if provided, a csv file will be written at provided path with " "all computed flight points.", )
[docs] def setup(self): super().setup() self._engine_wrapper = self.get_engine_wrapper() self._engine_wrapper.setup(self) self._mission_wrapper.setup(self) self._input_weight_variable_name = self._mission_wrapper.get_input_weight_variable_name( self.mission_name ) try: self.add_input(self.options["reference_area_variable"], np.nan, units="m**2") except ValueError: pass # Global mission outputs self.add_output( self.name_provider.NEEDED_BLOCK_FUEL.value, units="kg", desc=f'Needed fuel to complete mission "{self.mission_name}", including reserve fuel', ) self.add_output( self.name_provider.CONSUMED_FUEL_BEFORE_INPUT_WEIGHT.value, units="kg", desc=f'consumed fuel quantity before target mass defined for "{self.mission_name}",' f" if any (e.g. TakeOff Weight)", )
[docs] def setup_partials(self): self.declare_partials(["*"], ["*"], method="fd")
[docs] def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = self._engine_wrapper.get_model(inputs) reference_area = inputs[self.options["reference_area_variable"]] self._mission_wrapper.propulsion = propulsion_model self._mission_wrapper.reference_area = reference_area # This is the default start point, that can be overridden by using the "start" # segment in the mission definition. start_flight_point = FlightPoint( altitude=0.0, mass=inputs[self._input_weight_variable_name], true_airspeed=0.0 ) self.flight_points = self._mission_wrapper.compute(start_flight_point, inputs, outputs) self._compute_outputs(outputs, self.flight_points) self._postprocess_flight_points(self.flight_points)
def _postprocess_flight_points(self, flight_points): flight_points = flight_points.copy() # local copy for renaming columns before CSV export rename_dict = { field_name: f"{field_name}{' ['+unit+']' if unit else ''}" for field_name, unit in FlightPoint.get_units().items() } flight_points.rename(columns=rename_dict, inplace=True) if self.options["out_file"]: make_parent_dir(self.options["out_file"]) flight_points.to_csv(self.options["out_file"]) return flight_points def _compute_outputs(self, outputs, flight_points): # Final ================================================================ end_of_mission = FlightPoint.create(flight_points.iloc[-1]) 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[ self._mission_wrapper.get_reserve_variable_name() ] outputs[self.name_provider.CONSUMED_FUEL_BEFORE_INPUT_WEIGHT.value] = ( self._mission_wrapper.consumed_fuel_before_input_weight )
[docs] def get_engine_wrapper(self) -> Optional[IOMPropulsionWrapper]: """ Overloading this method allows to define the engine without relying on the propulsion option. (useful for tests) :return: the engine wrapper instance """ if self.options["propulsion_id"]: return RegisterPropulsion.get_provider(self.options["propulsion_id"]) return None
[docs]class AdvancedMissionComp(MissionComp): """ Computes a mission as specified in mission input file. Compared to :class:`MissionComp`, it allows: - to use an initializer iteration (simple Breguet) at first call. - to use the mission as the design mission for the sizing process. """
[docs] def initialize(self): super().initialize() self.options.declare("use_initializer_iteration", default=True, types=bool) self.options.declare("is_sizing", default=False, types=bool)
[docs] def setup(self): super().setup() if self.options["is_sizing"]: self.add_output("data:weight:aircraft:sizing_block_fuel", units="kg") self.add_output("data:weight:aircraft:sizing_onboard_fuel_at_input_weight", units="kg")
[docs] def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): iter_count = self.iter_count_without_approx message_prefix = f"Mission computation - iteration {iter_count:d} : " if iter_count == 0 and self.options["use_initializer_iteration"]: _LOGGER.info( "%sUsing initializer computation. OTHER ITERATIONS NEEDED.", message_prefix ) self._compute_breguet(inputs, outputs) else: _LOGGER.info("%sUsing mission definition.", message_prefix) super().compute(inputs, outputs, discrete_inputs, discrete_outputs) if self.options["is_sizing"]: outputs["data:weight:aircraft:sizing_block_fuel"] = outputs[ self.name_provider.NEEDED_BLOCK_FUEL.value ] outputs["data:weight:aircraft:sizing_onboard_fuel_at_input_weight"] = ( outputs[self.name_provider.NEEDED_BLOCK_FUEL.value] - self._mission_wrapper.consumed_fuel_before_input_weight )
def _compute_breguet(self, inputs, outputs): """ Computes mission using simple Breguet formula at altitude==100m and Mach 0.1 (but max L/D ratio is assumed anyway) Useful only for initiating the computation. :param inputs: OpenMDAO input vector :param outputs: OpenMDAO output vector """ propulsion_model = self._engine_wrapper.get_model(inputs) high_speed_polar = self._get_initial_polar(inputs) distance = np.sum(self._mission_wrapper.get_route_ranges(inputs)).item() altitude = 100.0 cruise_mach = 0.1 breguet = BreguetCruiseSegment( target=FlightPoint(ground_distance=distance), propulsion=propulsion_model, polar=high_speed_polar, use_max_lift_drag_ratio=True, ) start_point = FlightPoint( mass=inputs[self._input_weight_variable_name], altitude=altitude, mach=cruise_mach, ) flight_points = breguet.compute_from(start_point) end_point = FlightPoint.create(flight_points.iloc[-1]) outputs[self.name_provider.NEEDED_BLOCK_FUEL.value] = start_point.mass - end_point.mass @staticmethod def _get_initial_polar(inputs) -> Polar: """ At computation start, polar may be irrelevant and give a very low lift/drag ratio. In that case, this method returns a fake polar that has 10.0 as max lift drag ratio. Otherwise, the actual cruise polar is returned. """ high_speed_polar = Polar( inputs["data:aerodynamics:aircraft:cruise:CL"], inputs["data:aerodynamics:aircraft:cruise:CD"], ) use_minimum_l_d_ratio = False try: if ( high_speed_polar.optimal_cl / high_speed_polar.cd(high_speed_polar.optimal_cl) < 10.0 ): use_minimum_l_d_ratio = True except ZeroDivisionError: use_minimum_l_d_ratio = True if use_minimum_l_d_ratio: # We replace by a polar that has at least 10.0 as max L/D ratio high_speed_polar = Polar(np.array([0.0, 0.5, 1.0]), np.array([0.1, 0.05, 1.0])) return high_speed_polar