import warnings
import numpy as np
import openmdao.api as om
from aviary.utils.aviary_values import AviaryValues
from aviary.utils.named_values import get_keys
from aviary.variable_info.variable_meta_data import _MetaData
from aviary.variable_info.variables import Aircraft, Mission
from aviary.utils.test_utils.variable_test import get_names_from_hierarchy
[docs]
def preprocess_options(aviary_options: AviaryValues, **kwargs):
"""
Run all preprocessors on provided AviaryValues object
Parameters
----------
aviary_options : AviaryValues
Options to be updated
"""
try:
engine_models = kwargs['engine_models']
except KeyError:
engine_models = None
preprocess_crewpayload(aviary_options)
preprocess_propulsion(aviary_options, engine_models)
[docs]
def preprocess_crewpayload(aviary_options: AviaryValues):
"""
Calculates option values that are derived from other options, and are not direct inputs.
This function modifies the entries in the supplied collection, and for convenience also
returns the modified collection.
"""
if Aircraft.CrewPayload.NUM_PASSENGERS not in aviary_options:
passenger_count = 0
for key in (Aircraft.CrewPayload.NUM_FIRST_CLASS,
Aircraft.CrewPayload.NUM_BUSINESS_CLASS,
Aircraft.CrewPayload.NUM_TOURIST_CLASS):
if key in aviary_options:
passenger_count += aviary_options.get_val(key)
if passenger_count == 0:
passenger_count = 1
aviary_options.set_val(
Aircraft.CrewPayload.NUM_PASSENGERS, passenger_count)
else:
passenger_count = aviary_options.get_val(
Aircraft.CrewPayload.NUM_PASSENGERS)
# check in here to ensure that in this case passenger count is the sum of the first class, business class, and tourist class counts.
passenger_check = aviary_options.get_val(Aircraft.CrewPayload.NUM_FIRST_CLASS)
passenger_check += aviary_options.get_val(
Aircraft.CrewPayload.NUM_BUSINESS_CLASS)
passenger_check += aviary_options.get_val(Aircraft.CrewPayload.NUM_TOURIST_CLASS)
# only perform check if at least one passenger class is entered
if passenger_check > 0 and passenger_count != passenger_check:
raise om.AnalysisError(
f"ERROR: In preprocesssors.py: passenger_count ({passenger_count}) does not equal the sum of first class + business class + tourist class passengers (total of {passenger_check}).")
if Aircraft.CrewPayload.NUM_FLIGHT_ATTENDANTS not in aviary_options:
flight_attendants_count = 0 # assume no passengers
if 0 < passenger_count:
if passenger_count < 51:
flight_attendants_count = 1
else:
flight_attendants_count = passenger_count // 40 + 1
aviary_options.set_val(
Aircraft.CrewPayload.NUM_FLIGHT_ATTENDANTS, flight_attendants_count)
if Aircraft.CrewPayload.NUM_GALLEY_CREW not in aviary_options:
galley_crew_count = 0 # assume no passengers
if 150 < passenger_count:
galley_crew_count = passenger_count // 250 + 1
aviary_options.set_val(Aircraft.CrewPayload.NUM_GALLEY_CREW, galley_crew_count)
if Aircraft.CrewPayload.NUM_FLIGHT_CREW not in aviary_options:
flight_crew_count = 3
if passenger_count < 151:
flight_crew_count = 2
aviary_options.set_val(Aircraft.CrewPayload.NUM_FLIGHT_CREW, flight_crew_count)
if (Aircraft.CrewPayload.BAGGAGE_MASS_PER_PASSENGER not in aviary_options and
Mission.Design.RANGE in aviary_options):
design_range = aviary_options.get_val(Mission.Design.RANGE, 'nmi')
if design_range <= 900.0:
baggage_mass_per_pax = 35.0
elif design_range <= 2900.0:
baggage_mass_per_pax = 40.0
else:
baggage_mass_per_pax = 44.0
aviary_options.set_val(Aircraft.CrewPayload.BAGGAGE_MASS_PER_PASSENGER,
val=baggage_mass_per_pax, units='lbm')
return aviary_options
[docs]
def preprocess_propulsion(aviary_options: AviaryValues, engine_models: list = None):
'''
Updates AviaryValues object with values taken from provided EngineModels.
If no EngineModels are provided, either in engine_models or included in
aviary_options, an EngineDeck is created using avaliable inputs and options in
aviary_options.
Vectorizes variables in aviary_options in the correct order for vehicles with
heterogeneous engines.
Performs basic sanity checks on inputs that are universal to all EngineModels.
!!! WARNING !!!
Values in aviary_options can be overwritten with corresponding values from
engine_models!
Parameters
----------
aviary_options : AviaryValues
Options to be updated. EngineModels (provided or generated) are added, and
Aircraft:Engine:* and Aircraft:Nacelle:* variables are vectorized as numpy arrays
engine_models : <list of EngineModels> (optional)
EngineModel objects to be added to aviary_options. Replaced existing EngineModels
in aviary_options
'''
# TODO add verbosity check to warnings
##############################
# Vectorize Engine Variables #
##############################
# Only vectorize variables user has defined in some way or engine model has calculated
# Combine aviary_options and all engine options into single AviaryValues
# It is assumed that all EngineModels are up-to-date at this point and will NOT
# be changed later on (otherwise preprocess_propulsion must be run again)
num_engine_type = len(engine_models)
complete_options_list = AviaryValues(aviary_options)
for engine in engine_models:
complete_options_list.update(engine.options)
# update_list has keys of all variables that are already defined, and must
# be vectorized
update_list = list(get_keys(complete_options_list))
# Vectorize engine variables. Only update variables in update_list that are relevant
# to engines (defined by _get_engine_variables())
for var in _get_engine_variables():
if var in update_list:
dtype = _MetaData[var]['types']
default_value = _MetaData[var]['default_value']
# type is optionally specified, fall back to type of default value
if dtype is None:
if isinstance(default_value, np.ndarray):
dtype = default_value.dtype
elif default_value is None:
# With no default value, we cannot determine a dtype.
dtype = None
else:
dtype = type(default_value)
# if dtype has multiple options, use type of default value
elif isinstance(dtype, (list, tuple)):
# if default value is a list/tuple, find type inside that
if isinstance(default_value, (list, tuple)):
dtype = type(default_value[0])
elif default_value is None:
# With no default value, we cannot determine a dtype.
dtype = None
else:
dtype = type(default_value)
# if var is supposed to be a unique array per engine model, assemble flat
# vector manually to avoid ragged arrays (such as for wing engine locations)
if isinstance(default_value, (list, np.ndarray)):
vec = np.zeros(0, dtype=dtype)
elif type(default_value) is tuple:
vec = ()
else:
vec = [default_value] * num_engine_type
units = _MetaData[var]['units']
# priority order is (checked per engine):
# 1. EngineModel.options
# 2. aviary_options
# 3. default value from metadata
for i, engine in enumerate(engine_models):
# test to see if engine has this variable - if so, use it
try:
# variables in engine models are known to be "safe", will only
# contain data for that engine
engine_val = engine.get_val(var, units)
if isinstance(default_value, (list, np.ndarray)):
vec = np.append(vec, engine_val)
elif isinstance(default_value, tuple):
vec = vec + (engine_val,)
else:
vec[i] = engine_val
# if the variable is not in the engine model, pull from aviary options
except KeyError:
# check if variable is defined in aviary options (for this engine's
# index) - if so, use it
try:
aviary_val = aviary_options.get_val(var, units)
# if aviary_val is an iterable, just grab val for this engine
if isinstance(aviary_val, (list, np.ndarray, tuple)):
aviary_val = aviary_val[i]
if isinstance(default_value, (list, np.ndarray)):
vec = np.append(vec, aviary_val)
elif isinstance(default_value, tuple):
vec = vec + (aviary_val,)
else:
vec[i] = aviary_val
# if not, use default value from _MetaData
except (KeyError, IndexError):
if isinstance(default_value, (list, np.ndarray)):
vec = np.append(vec, default_value)
else:
# default value is aleady in array
continue
# TODO update each engine's options with "new" values? Allows each engine
# to have a copy of all options/inputs, beyond what it was
# originally initialized with
# update aviary options and outputs with new vectors
# if data is numerical, store in a numpy array
# keep tuples as tuples, lists get converted to numpy arrays
if type(vec[0]) in (int, float, np.int64, np.float64)\
and type(vec) is not tuple:
vec = np.array(vec, dtype=dtype)
aviary_options.set_val(var, vec, units)
###################################
# Input/Option Consistency Checks #
###################################
# Make sure number of engines based on mount location match expected total
try:
num_engines_all = aviary_options.get_val(Aircraft.Engine.NUM_ENGINES)
except KeyError:
num_engines_all = np.zeros(num_engine_type).astype(int)
try:
num_fuse_engines_all = aviary_options.get_val(
Aircraft.Engine.NUM_FUSELAGE_ENGINES)
except KeyError:
num_fuse_engines_all = np.zeros(num_engine_type).astype(int)
try:
num_wing_engines_all = aviary_options.get_val(Aircraft.Engine.NUM_WING_ENGINES)
except KeyError:
num_wing_engines_all = np.zeros(num_engine_type).astype(int)
for i, engine in enumerate(engine_models):
num_engines = num_engines_all[i]
num_fuse_engines = num_fuse_engines_all[i]
num_wing_engines = num_wing_engines_all[i]
total_engines_calc = num_fuse_engines + num_wing_engines
# if engine mount type is not specified at all, default to wing
if total_engines_calc == 0:
eng_name = engine.name
num_wing_engines_all[i] = num_engines
# TODO is a warning overkill here? It can be documented wing mounted engines
# are assumed default
warnings.warn(
f'Mount location for engines of type <{eng_name}> not specified. '
'Wing-mounted engines are assumed.')
# If wing mount type are specified but inconsistent, handle it
elif total_engines_calc > num_engines:
# more defined engine locations than number of engines - increase num engines
eng_name = engine.name
num_engines_all[i] = total_engines_calc
warnings.warn(
'Sum of aircraft:engine:num_fueslage_engines and '
'aircraft:engine:num_wing_engines do not match '
f'aircraft:engine:num_engines for EngineModel <{eng_name}>. Overwriting '
'with the sum of wing and fuselage mounted engines.')
elif total_engines_calc < num_engines:
# fewer defined locations than num_engines - assume rest are wing mounted
eng_name = engine.name
num_wing_engines_all[i] = num_engines - num_fuse_engines
warnings.warn(
'Mount location was not defined for all engines of EngineModel '
f'<{eng_name}> - unspecified engines are assumed wing-mounted.')
aviary_options.set_val(Aircraft.Engine.NUM_ENGINES, num_engines_all)
aviary_options.set_val(Aircraft.Engine.NUM_WING_ENGINES, num_wing_engines_all)
aviary_options.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, num_fuse_engines_all)
if Mission.Summary.FUEL_FLOW_SCALER not in aviary_options:
aviary_options.set_val(Mission.Summary.FUEL_FLOW_SCALER, 1.0)
num_engines = aviary_options.get_val(Aircraft.Engine.NUM_ENGINES)
total_num_engines = int(sum(num_engines_all))
total_num_fuse_engines = int(sum(num_fuse_engines_all))
total_num_wing_engines = int(sum(num_wing_engines_all))
# compute propulsion-level engine count totals here
aviary_options.set_val(
Aircraft.Propulsion.TOTAL_NUM_ENGINES, total_num_engines)
aviary_options.set_val(
Aircraft.Propulsion.TOTAL_NUM_FUSELAGE_ENGINES, total_num_fuse_engines)
aviary_options.set_val(
Aircraft.Propulsion.TOTAL_NUM_WING_ENGINES, total_num_wing_engines)
def _get_engine_variables():
'''
Yields all propulsion-related variables in Aircraft that need to be vectorized
'''
for item in get_names_from_hierarchy(Aircraft.Engine):
yield item
for item in get_names_from_hierarchy(Aircraft.Nacelle):
yield item