From a6cb6cbcd7f3290cc8715b7c7a8b477867ded633 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 9 May 2024 14:23:38 -0400 Subject: [PATCH 01/22] first pass at fixing external engine model methods --- aviary/interface/methods_for_level2.py | 139 ++++++++------- aviary/subsystems/propulsion/engine_model.py | 7 +- .../propulsion/propulsion_builder.py | 165 +++++++++++++++++- .../propulsion/propulsion_mission.py | 16 +- .../propulsion/propulsion_premission.py | 12 +- .../test/test_custom_engine_model.py | 10 +- .../test/test_propulsion_premission.py | 11 +- aviary/subsystems/propulsion/utils.py | 49 ++++++ aviary/variable_info/variable_meta_data.py | 2 +- 9 files changed, 315 insertions(+), 96 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index a0fd62aa3..e1f048417 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -48,7 +48,7 @@ from aviary.variable_info.enums import AnalysisScheme, ProblemType, SpeedType, AlphaModes, EquationsOfMotion, LegacyCode, Verbosity from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData -from aviary.subsystems.propulsion.engine_deck import EngineDeck +from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder from aviary.subsystems.mass.mass_builder import CoreMassBuilder @@ -234,7 +234,7 @@ def __init__(self, analysis_scheme=AnalysisScheme.COLLOCATION, **kwargs): self.regular_phases = [] self.reserve_phases = [] - def load_inputs(self, aviary_inputs, phase_info=None, engine_builder=None, verbosity=Verbosity.BRIEF): + def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, verbosity=Verbosity.BRIEF): """ This method loads the aviary_values inputs and options that the user specifies. They could specify files to load and values to @@ -328,68 +328,9 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builder=None, verbo self.post_mission_info = {'include_landing': True, 'external_subsystems': []} - ## PROCESSING ## - # set up core subsystems - if mission_method in (HEIGHT_ENERGY, SOLVED_2DOF): - everything_else_origin = FLOPS - elif mission_method is TWO_DEGREES_OF_FREEDOM: - everything_else_origin = GASP - else: - raise ValueError(f'Unknown mission method {self.mission_method}') - - prop = CorePropulsionBuilder('core_propulsion') - mass = CoreMassBuilder('core_mass', code_origin=self.mass_method) - aero = CoreAerodynamicsBuilder( - 'core_aerodynamics', code_origin=everything_else_origin) - - # TODO These values are currently hardcoded, in future should come from user - both_geom = False - code_origin_to_prioritize = None - - # which geometry methods should be used, or both? - geom_code_origin = None - if (everything_else_origin is FLOPS) and (mass_method is FLOPS): - geom_code_origin = FLOPS - elif (everything_else_origin is GASP) and (mass_method is GASP): - geom_code_origin = GASP - else: - both_geom = True - - # which geometry method gets prioritized in case of conflicting outputs - if not code_origin_to_prioritize: - if everything_else_origin is GASP: - code_origin_to_prioritize = GASP - elif everything_else_origin is FLOPS: - code_origin_to_prioritize = FLOPS - - geom = CoreGeometryBuilder('core_geometry', - code_origin=geom_code_origin, - use_both_geometries=both_geom, - code_origin_to_prioritize=code_origin_to_prioritize) - - self.core_subsystems = {'propulsion': prop, - 'geometry': geom, - 'mass': mass, - 'aerodynamics': aero} - - # TODO optionally accept which subsystems to load from phase_info - subsystems = self.core_subsystems - default_mission_subsystems = [ - subsystems['aerodynamics'], subsystems['propulsion']] - self.ode_args = dict(aviary_options=aviary_inputs, - core_subsystems=default_mission_subsystems) - - if 'engine_models' in aviary_inputs: - engine_models = aviary_inputs.get_val('engine_models') - else: - engine_models = [EngineDeck(options=aviary_inputs)] - - preprocess_propulsion(aviary_inputs, engine_models) - - self._update_metadata_from_subsystems() - - if Settings.VERBOSITY not in aviary_inputs: - aviary_inputs.set_val(Settings.VERBOSITY, Verbosity.BRIEF) + if engine_builders is None: + engine_builders = build_engine_deck(aviary_inputs) + self.engine_builders = engine_builders self.aviary_inputs = aviary_inputs return aviary_inputs @@ -397,7 +338,7 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builder=None, verbo def _update_metadata_from_subsystems(self): self.meta_data = BaseMetaData.copy() - variables_to_pop = [] + # variables_to_pop = [] # loop through phase_info and external subsystems for phase_name in self.phase_info: @@ -447,7 +388,7 @@ def check_and_preprocess_inputs(self): This method checks the user-supplied input values for any potential problems and preprocesses the inputs to prepare them for use in the Aviary problem. """ - + aviary_inputs = self.aviary_inputs # Target_distance verification for all phases # Checks to make sure target_distance is positive, for idx, phase_name in enumerate(self.phase_info): @@ -497,13 +438,71 @@ def check_and_preprocess_inputs(self): for phase_name in self.phase_info: for external_subsystem in self.phase_info[phase_name]['external_subsystems']: - self.aviary_inputs = external_subsystem.preprocess_inputs( - self.aviary_inputs) + aviary_inputs = external_subsystem.preprocess_inputs( + aviary_inputs) - # TODO find preprocessors a permanent home # PREPROCESSORS # # Fill in anything missing in the options with computed defaults. - preprocess_crewpayload(self.aviary_inputs) + preprocess_propulsion(aviary_inputs, self.engine_builders) + preprocess_crewpayload(aviary_inputs) + + mission_method = aviary_inputs.get_val(Settings.EQUATIONS_OF_MOTION) + mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) + + ## Set Up Core Subsystems ## + if mission_method in (HEIGHT_ENERGY, SOLVED_2DOF): + everything_else_origin = FLOPS + elif mission_method is TWO_DEGREES_OF_FREEDOM: + everything_else_origin = GASP + else: + raise ValueError(f'Unknown mission method {self.mission_method}') + + if Settings.VERBOSITY not in aviary_inputs: + aviary_inputs.set_val(Settings.VERBOSITY, Verbosity.BRIEF) + prop = CorePropulsionBuilder( + 'core_propulsion', engine_models=self.engine_builders) + mass = CoreMassBuilder('core_mass', code_origin=self.mass_method) + aero = CoreAerodynamicsBuilder( + 'core_aerodynamics', code_origin=everything_else_origin) + + # TODO These values are currently hardcoded, in future should come from user + both_geom = False + code_origin_to_prioritize = None + + # which geometry methods should be used, or both? + geom_code_origin = None + if (everything_else_origin is FLOPS) and (mass_method is FLOPS): + geom_code_origin = FLOPS + elif (everything_else_origin is GASP) and (mass_method is GASP): + geom_code_origin = GASP + else: + both_geom = True + + # which geometry method gets prioritized in case of conflicting outputs + if not code_origin_to_prioritize: + if everything_else_origin is GASP: + code_origin_to_prioritize = GASP + elif everything_else_origin is FLOPS: + code_origin_to_prioritize = FLOPS + + geom = CoreGeometryBuilder('core_geometry', + code_origin=geom_code_origin, + use_both_geometries=both_geom, + code_origin_to_prioritize=code_origin_to_prioritize) + + self.core_subsystems = {'propulsion': prop, + 'geometry': geom, + 'mass': mass, + 'aerodynamics': aero} + + # TODO optionally accept which subsystems to load from phase_info + subsystems = self.core_subsystems + default_mission_subsystems = [ + subsystems['aerodynamics'], subsystems['propulsion']] + self.ode_args = dict(aviary_options=aviary_inputs, + core_subsystems=default_mission_subsystems) + + self._update_metadata_from_subsystems() if self.mission_method in (HEIGHT_ENERGY, SOLVED_2DOF, TWO_DEGREES_OF_FREEDOM): self.phase_separator() diff --git a/aviary/subsystems/propulsion/engine_model.py b/aviary/subsystems/propulsion/engine_model.py index 9c5abc8be..64fbe28a1 100644 --- a/aviary/subsystems/propulsion/engine_model.py +++ b/aviary/subsystems/propulsion/engine_model.py @@ -11,7 +11,7 @@ from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings +from aviary.variable_info.variables import Settings from aviary.variable_info.enums import Verbosity @@ -43,7 +43,10 @@ def __init__( self, name: str = None, options: AviaryValues = None, meta_data: dict = None, ): super().__init__(name, meta_data=meta_data) - self.options = options.deepcopy() + if options is not None: + self.options = options.deepcopy() + else: + self.options = AviaryValues() # Hybrid throttle is currently the only optional independent variable, requiring # this flag so Aviary knows how to handle EngineModels during mission diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index 950bff4d6..2693fb24b 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -11,20 +11,25 @@ import numpy as np from aviary.interface.utils.markdown_utils import write_markdown_variable_table + from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase from aviary.subsystems.propulsion.propulsion_premission import PropulsionPreMission from aviary.subsystems.propulsion.propulsion_mission import PropulsionMission +from aviary.subsystems.propulsion.engine_model import EngineModel + from aviary.variable_info.variables import Aircraft # NOTE These are currently needed to get around variable hierarchy being class-based. # Ideally, an alternate solution to loop through the hierarchy will be created and # these can be replaced. from aviary.utils.preprocessors import _get_engine_variables -from aviary.variable_info.variable_meta_data import _MetaData _default_name = 'propulsion' +# NOTE unlike the other subsystem builders, it is not reccomended to create additional +# propulsion subsystems, as propulsion is intended to be an agnostic carrier of +# all propulsion-related subsystem builders. class PropulsionBuilderBase(SubsystemBuilderBase): def __init__(self, name=None, meta_data=None): if name is None: @@ -47,13 +52,59 @@ def __init__(self, name=None, meta_data=None, **kwargs): super().__init__(name=name, meta_data=meta_data) + try: + engine_models = kwargs['engine_models'] + except KeyError: + engine_models = None + else: + if not isinstance(engine_models, (list, np.ndarray)): + engine_models = [engine_models] + + for engine in engine_models: + if not isinstance(engine, EngineModel): + raise UserWarning('Engine provided to propulsion builder is not an ' + 'EngineModel object') + + self.engine_models = engine_models + def build_pre_mission(self, aviary_inputs): - return PropulsionPreMission(aviary_options=aviary_inputs) + return PropulsionPreMission(aviary_options=aviary_inputs, + engine_models=self.engine_models) def build_mission(self, num_nodes, aviary_inputs, **kwargs): - return PropulsionMission(num_nodes=num_nodes, aviary_options=aviary_inputs) + return PropulsionMission(num_nodes=num_nodes, aviary_options=aviary_inputs, + engine_models=self.engine_models) + + # NOTE untested! + def get_states(self): + """ + Call get_states() on all engine models and return combined result. + """ + states = {} + for engine in self.engine_models: + engine_states = engine.get_states() + states.update(engine_states) + + return states + + # NOTE untested! + def get_controls(self): + """ + Call get_controls() on all engine models and return combined result. + """ + controls = {} + for engine in self.engine_models: + engine_controls = engine.get_controls() + controls.update(engine_controls) + + return controls + # TODO add parameters defined by individual engines, update to correct shape if necessary def get_parameters(self, aviary_inputs=None, phase_info=None): + """ + Set expected shape of all variables that need to be vectorized for multiple + engine types. + """ engine_count = len(aviary_inputs.get_val(Aircraft.Engine.NUM_ENGINES)) params = {} @@ -69,6 +120,114 @@ def get_parameters(self, aviary_inputs=None, phase_info=None): 'static_target': True} return params + # NOTE untested! + def get_constraints(self): + """ + Call get_constraints() on all engine models and return combined result. + """ + constraints = {} + for engine in self.engine_models: + engine_constraints = engine.get_constraints() + constraints.update(engine_constraints) + + return constraints + + # NOTE untested! + def get_linked_variables(self): + """ + Call get_linked_variables() on all engine models and return combined result. + """ + linked_vars = {} + for engine in self.engine_models: + engine_linked_vars = engine.get_linked_variables() + linked_vars.update(engine_linked_vars) + + return linked_vars + + # NOTE untested! + def get_bus_variables(self): + """ + Call get_linked_variables() on all engine models and return combined result. + """ + linked_vars = {} + for engine in self.engine_models: + engine_linked_vars = engine.get_linked_variables() + linked_vars.update(engine_linked_vars) + + return linked_vars + + # NOTE untested! + def define_order(self): + """ + Call define_order() on all engine models and return combined result. + """ + subsys_order = [] + for engine in self.engine_models: + engine_subsys_order = engine.define_order() + subsys_order.append(engine_subsys_order) + + return subsys_order + + # NOTE untested! + def get_design_vars(self): + """ + Call get_design_vars() on all engine models and return combined result. + """ + design_vars = {} + for engine in self.engine_models: + engine_design_vars = engine.get_design_vars() + design_vars.update(engine_design_vars) + + return design_vars + + # NOTE untested! + def get_initial_guesses(self): + """ + Call get_initial_guesses() on all engine models and return combined result. + """ + initial_guesses = {} + for engine in self.engine_models: + engine_initial_guesses = engine.get_initial_guesses() + initial_guesses.update(engine_initial_guesses) + + return initial_guesses + + # NOTE untested! + def get_mass_names(self): + """ + Call get_mass_names() on all engine models and return combined result. + """ + mass_names = {} + for engine in self.engine_models: + engine_mass_names = engine.get_mass_names() + mass_names.update(engine_mass_names) + + return mass_names + + # NOTE untested! + def preprocess_inputs(self): + """ + Call get_mass_names() on all engine models and return combined result. + """ + mass_names = {} + for engine in self.engine_models: + engine_mass_names = engine.get_mass_names() + mass_names.update(engine_mass_names) + + return mass_names + + # NOTE untested! + def get_outputs(self): + """ + Call get_outputs() on all engine models and return combined result. + """ + outputs = [] + for engine in self.engine_models: + engine_outputs = engine.get_outputs() + outputs.append(engine_outputs) + + return outputs + def report(self, prob, reports_folder, **kwargs): """ Generate the report for Aviary core propulsion analysis diff --git a/aviary/subsystems/propulsion/propulsion_mission.py b/aviary/subsystems/propulsion/propulsion_mission.py index 40097c28f..a3eae1b76 100644 --- a/aviary/subsystems/propulsion/propulsion_mission.py +++ b/aviary/subsystems/propulsion/propulsion_mission.py @@ -16,23 +16,25 @@ class PropulsionMission(om.Group): def initialize(self): self.options.declare( - 'num_nodes', - types=int, - lower=0 + 'num_nodes', types=int, lower=0 ) self.options.declare( 'aviary_options', types=AviaryValues, - desc='collection of Aircraft/Mission specific options') + desc='collection of Aircraft/Mission specific options' + ) + + self.options.declare( + 'engine_models', types=list, + desc='list of EngineModels on aircraft' + ) def setup(self): nn = self.options['num_nodes'] options: AviaryValues = self.options['aviary_options'] - engine_models = options.get_val('engine_models') + engine_models = self.options['engine_models'] engine_count = len(engine_models) - # TODO what if "engine" is not an EngineModel object? Type is never checked/enforced - if engine_count > 1: # We need a single component with scale_factor. Dymos can't find it when it is diff --git a/aviary/subsystems/propulsion/propulsion_premission.py b/aviary/subsystems/propulsion/propulsion_premission.py index ce3aee46c..196aaf857 100644 --- a/aviary/subsystems/propulsion/propulsion_premission.py +++ b/aviary/subsystems/propulsion/propulsion_premission.py @@ -18,17 +18,21 @@ def initialize(self): self.options.declare( 'aviary_options', types=AviaryValues, desc='collection of Aircraft/Mission specific options') + self.options.declare( + 'engine_models', types=list, + desc='list of EngineModels on aircraft' + ) def setup(self): options = self.options['aviary_options'] - engine_models = options.get_val('engine_models') + engine_models = self.options['engine_models'] engine_count = len(engine_models) # Each engine model pre_mission component only needs to accept and output single # value relevant to that variable - this group's configure step will handle # promoting/connecting just the relevant index in vectorized inputs/outputs for # each component here - # Promotions are handled in configure() + # Promotions are handled in self.configure() for engine in engine_models: subsys = engine.build_pre_mission(options) if subsys: @@ -43,8 +47,8 @@ def setup(self): ) if engine_count > 1: - # Add an empty mux comp, which will be customized to handle all required outputs - # in self.configure() + # Add an empty mux comp, which will be customized to handle all required + # outputs in self.configure() self.add_subsystem( 'pre_mission_mux', subsys=om.MuxComp(), diff --git a/aviary/subsystems/propulsion/test/test_custom_engine_model.py b/aviary/subsystems/propulsion/test/test_custom_engine_model.py index f06ae6176..5de08cc13 100644 --- a/aviary/subsystems/propulsion/test/test_custom_engine_model.py +++ b/aviary/subsystems/propulsion/test/test_custom_engine_model.py @@ -171,7 +171,7 @@ def test_custom_engine(self): # Load aircraft and options data from user # Allow for user overrides here prob.load_inputs("models/test_aircraft/aircraft_for_bench_GwFm.csv", - phase_info, engine_builder=SimpleTestEngine()) + phase_info, engine_builders=[SimpleTestEngine()]) # Preprocess inputs prob.check_and_preprocess_inputs() @@ -268,7 +268,7 @@ def test_turboprop(self): # Load aircraft and options data from user # Allow for user overrides here prob.load_inputs("models/test_aircraft/aircraft_for_bench_FwFm.csv", - phase_info, engine_builder=engine) + phase_info, engine_builders=[engine]) # Preprocess inputs prob.check_and_preprocess_inputs() @@ -293,7 +293,7 @@ def test_turboprop(self): prob.set_initial_guesses() prob.set_val( - f'traj.cruise.rhs_all.{Aircraft.Design.MAX_TIP_SPEED}', 710., units='ft/s') + f'traj.cruise.rhs_all.{Aircraft.Design.MAX_PROPELLER_TIP_SPEED}', 710., units='ft/s') prob.set_val( f'traj.cruise.rhs_all.{Dynamic.Mission.PERCENT_ROTOR_RPM_CORRECTED}', 0.915, units='unitless') prob.set_val( @@ -308,4 +308,6 @@ def test_turboprop(self): if __name__ == '__main__': - unittest.main() + # unittest.main() + test = CustomEngineTest() + test.test_custom_engine() diff --git a/aviary/subsystems/propulsion/test/test_propulsion_premission.py b/aviary/subsystems/propulsion/test/test_propulsion_premission.py index cf0abd56a..334cced04 100644 --- a/aviary/subsystems/propulsion/test/test_propulsion_premission.py +++ b/aviary/subsystems/propulsion/test/test_propulsion_premission.py @@ -7,6 +7,7 @@ from aviary.subsystems.propulsion.propulsion_premission import ( PropulsionPreMission, PropulsionSum) from aviary.utils.aviary_values import AviaryValues +from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.validation_cases.validation_tests import get_flops_inputs from aviary.variable_info.variables import Aircraft, Settings @@ -20,7 +21,8 @@ def test_case(self): aviary_values.set_val(Settings.VERBOSITY, 0) options = aviary_values - self.prob.model = PropulsionPreMission(aviary_options=options) + self.prob.model = PropulsionPreMission(aviary_options=options, + engine_models=build_engine_deck(options)) self.prob.setup(force_alloc_complex=True) self.prob.set_val(Aircraft.Engine.SCALED_SLS_THRUST, options.get_val( @@ -43,7 +45,8 @@ def test_multi_engine(self): options = aviary_values - self.prob.model = PropulsionPreMission(aviary_options=options) + self.prob.model = PropulsionPreMission(aviary_options=options, + engine_models=options.get_val('engine_models')) self.prob.setup(force_alloc_complex=True) self.prob.set_val(Aircraft.Engine.SCALED_SLS_THRUST, options.get_val( @@ -63,8 +66,6 @@ def test_multi_engine(self): def test_propulsion_sum(self): options = AviaryValues() options.set_val(Aircraft.Engine.NUM_ENGINES, np.array([1, 2, 5])) - # it doesn't matter what goes in engine models, as long as it is length 3 - options.set_val('engine_models', [1, 1, 1]) options.set_val(Settings.VERBOSITY, 0) self.prob.model = om.Group() self.prob.model.add_subsystem('propsum', @@ -92,4 +93,4 @@ def test_propulsion_sum(self): # unittest.main() test = PropulsionPreMissionTest() test.setUp() - test.test_multi_engine() + test.test_case() diff --git a/aviary/subsystems/propulsion/utils.py b/aviary/subsystems/propulsion/utils.py index 95dc205d2..01360d2a6 100644 --- a/aviary/subsystems/propulsion/utils.py +++ b/aviary/subsystems/propulsion/utils.py @@ -13,6 +13,8 @@ from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.variables import Dynamic +from aviary.variable_info.variable_meta_data import _MetaData +from aviary.variable_info.variables import Aircraft class EngineModelVariables(Enum): @@ -104,6 +106,53 @@ def convert_geopotential_altitude(altitude): return altitude +def build_engine_deck(aviary_options: AviaryValues): + ''' + Creates an EngineDeck using avaliable inputs and options in aviary_options. + + Parameter + ---------- + aviary_options : AviaryValues + Options to use in creation of EngineDecks. + + Returns + ---------- + engine_models : + List of EngineDecks created using provided aviary_options. + ''' + # import locally placed to avoid circular import + from aviary.subsystems.propulsion.engine_deck import EngineDeck + + # Build a single engine deck, currently ignoring vectorization + # of AviaryValues (use first index) + # TODO build test to verify this works as expected + engine_options = AviaryValues() + for entry in Aircraft.Engine.__dict__: + var = getattr(Aircraft.Engine, entry) + # check if this variable exist with useable metadata + try: + units = _MetaData[var]['units'] + try: + # add value from aviary_options to engine_options + aviary_val = aviary_options.get_val(var, units) + if isinstance(aviary_val, np.ndarray): + # "Convert" numpy types to standard Python types. Wrap first + # index in numpy array before calling item() to safeguard against + # non-standard types, such as objects + aviary_val = aviary_val[0].item() + elif isinstance(aviary_val, (list, tuple)): + aviary_val = aviary_val[0] + engine_options.set_val(var, aviary_val, units) + # if not, use default value from _MetaData + except KeyError: + # engine_options.set_val(var, _MetaData[var]['default_value'], units) + continue + except (KeyError, TypeError): + continue + + return [EngineDeck('engine_deck', options=engine_options)] + + class UncorrectData(om.Group): def initialize(self): self.options.declare( diff --git a/aviary/variable_info/variable_meta_data.py b/aviary/variable_info/variable_meta_data.py index 2e694e210..ad79397a9 100644 --- a/aviary/variable_info/variable_meta_data.py +++ b/aviary/variable_info/variable_meta_data.py @@ -1666,7 +1666,7 @@ "LEAPS1": None }, units='unitless', - types=(str, Path, None), + types=(str, Path), default_value=None, option=True, desc='filepath to data file containing engine performance tables' From a4a90e2b3dfd3db4c4a71a9398fff8dce40388ac Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 10 May 2024 11:14:48 -0400 Subject: [PATCH 02/22] propulsion connections in configure properly use promoted name --- aviary/subsystems/propulsion/propulsion_premission.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aviary/subsystems/propulsion/propulsion_premission.py b/aviary/subsystems/propulsion/propulsion_premission.py index 196aaf857..48e0626ee 100644 --- a/aviary/subsystems/propulsion/propulsion_premission.py +++ b/aviary/subsystems/propulsion/propulsion_premission.py @@ -124,9 +124,9 @@ def configure(self): # promote all other inputs/outputs for this component normally (handle special outputs later) self.promotes(comp.name, inputs=[ - input for input in comp_inputs if input not in input_dict[comp.name]], + comp_inputs[input]['prom_name'] for input in comp_inputs if input not in input_dict[comp.name]], outputs=[ - output for output in comp_outputs if output not in output_dict[comp.name]]) + comp_outputs[output]['prom_name'] for output in comp_outputs if output not in output_dict[comp.name]]) # add variables to the mux component and make connections to individual # component outputs From 2b29a0bcf127e791e97706190a5153cce260ac4f Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 10 May 2024 14:08:54 -0400 Subject: [PATCH 03/22] moved hardcoded propulsion parameter assumptions into enginedecks --- aviary/subsystems/propulsion/engine_deck.py | 5 +++++ aviary/subsystems/propulsion/propulsion_builder.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/aviary/subsystems/propulsion/engine_deck.py b/aviary/subsystems/propulsion/engine_deck.py index fe8cdddee..f3edf80c5 100644 --- a/aviary/subsystems/propulsion/engine_deck.py +++ b/aviary/subsystems/propulsion/engine_deck.py @@ -1010,6 +1010,11 @@ def build_mission(self, num_nodes, aviary_inputs) -> om.Group: return engine_group + def get_parameters(self): + params = {} + params[Aircraft.Engine.SCALE_FACTOR] = {'static_target': True} + return params + def report(self, problem, reports_file, **kwargs): meta_data = kwargs['meta_data'] diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index 2693fb24b..b1d0df687 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -115,9 +115,9 @@ def get_parameters(self, aviary_inputs=None, phase_info=None): # TODO engine_wing_location params[var] = {'shape': (engine_count, ), 'static_target': True} - params = {} # For now - params[Aircraft.Engine.SCALE_FACTOR] = {'shape': (engine_count, ), - 'static_target': True} + # params = {} # For now + # params[Aircraft.Engine.SCALE_FACTOR] = {'shape': (engine_count, ), + # 'static_target': True} return params # NOTE untested! From dbe687ecb7b2cf8ae240dce622818bc0c5e81050 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 10 May 2024 15:28:36 -0400 Subject: [PATCH 04/22] Fixed test_custom_engine_model.py with new engine_builders paradigm --- .../propulsion/propulsion_builder.py | 37 +++++++++++++------ .../propulsion/propulsion_premission.py | 2 +- .../test/test_custom_engine_model.py | 8 ++-- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index b1d0df687..cf7e4d1b6 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -99,7 +99,7 @@ def get_controls(self): return controls - # TODO add parameters defined by individual engines, update to correct shape if necessary + # NOTE untested! def get_parameters(self, aviary_inputs=None, phase_info=None): """ Set expected shape of all variables that need to be vectorized for multiple @@ -108,12 +108,20 @@ def get_parameters(self, aviary_inputs=None, phase_info=None): engine_count = len(aviary_inputs.get_val(Aircraft.Engine.NUM_ENGINES)) params = {} - # add all variables from Engine & Nacelle to params - # TODO this assumes that no new categories are added for custom engine models - for var in _get_engine_variables(): - if var in aviary_inputs: - # TODO engine_wing_location - params[var] = {'shape': (engine_count, ), 'static_target': True} + # collect all the parameters for engines + for engine in self.engine_models: + engine_params = engine.get_parameters() + params.update(engine_params) + + # for any parameters that need to be vectorized for multiple engines, apply + # correct shape + engine_vars = _get_engine_variables() + for var in params: + if var in engine_vars: + # TODO shape for variables that are supposed to be vectors, like wing + # engine locations + params[var]['shape'] = (engine_count,) + params[var]['static_target'] = True # params = {} # For now # params[Aircraft.Engine.SCALE_FACTOR] = {'shape': (engine_count, ), @@ -149,12 +157,19 @@ def get_bus_variables(self): """ Call get_linked_variables() on all engine models and return combined result. """ - linked_vars = {} + bus_vars = {} for engine in self.engine_models: - engine_linked_vars = engine.get_linked_variables() - linked_vars.update(engine_linked_vars) + engine_bus_vars = engine.get_bus_variables() + bus_vars.update(engine_bus_vars) - return linked_vars + # append propulsion group name to all engine-level bus variables + # engine models only need to use variable paths starting at that engine group + complete_bus_vars = {} + for var in bus_vars: + info = bus_vars[var] + complete_bus_vars[self.name + '.' + var] = info + + return complete_bus_vars # NOTE untested! def define_order(self): diff --git a/aviary/subsystems/propulsion/propulsion_premission.py b/aviary/subsystems/propulsion/propulsion_premission.py index 48e0626ee..a7d90b25c 100644 --- a/aviary/subsystems/propulsion/propulsion_premission.py +++ b/aviary/subsystems/propulsion/propulsion_premission.py @@ -121,7 +121,7 @@ def configure(self): self.promotes( comp.name, inputs=input_dict[comp.name].keys(), src_indices=om.slicer[idx]) - # promote all other inputs/outputs for this component normally (handle special outputs later) + # promote all other inputs/outputs for this component normally (handle vectorized outputs later) self.promotes(comp.name, inputs=[ comp_inputs[input]['prom_name'] for input in comp_inputs if input not in input_dict[comp.name]], diff --git a/aviary/subsystems/propulsion/test/test_custom_engine_model.py b/aviary/subsystems/propulsion/test/test_custom_engine_model.py index 5de08cc13..85c64930d 100644 --- a/aviary/subsystems/propulsion/test/test_custom_engine_model.py +++ b/aviary/subsystems/propulsion/test/test_custom_engine_model.py @@ -124,8 +124,6 @@ def get_initial_guesses(self): return initial_guesses_dict -@unittest.skip('This test is not compatile with multiengine, requires rework so ' - 'engine-level methods can be called') @use_tempdirs class CustomEngineTest(unittest.TestCase): def test_custom_engine(self): @@ -308,6 +306,6 @@ def test_turboprop(self): if __name__ == '__main__': - # unittest.main() - test = CustomEngineTest() - test.test_custom_engine() + unittest.main() + # test = CustomEngineTest() + # test.test_custom_engine() From 60effa1ba06cae30846cbc7fd99140546d6cbc00 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 10 May 2024 15:41:50 -0400 Subject: [PATCH 05/22] cleanup --- aviary/subsystems/propulsion/propulsion_builder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index cf7e4d1b6..89b9f2e20 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -87,7 +87,6 @@ def get_states(self): return states - # NOTE untested! def get_controls(self): """ Call get_controls() on all engine models and return combined result. @@ -152,7 +151,6 @@ def get_linked_variables(self): return linked_vars - # NOTE untested! def get_bus_variables(self): """ Call get_linked_variables() on all engine models and return combined result. @@ -195,7 +193,6 @@ def get_design_vars(self): return design_vars - # NOTE untested! def get_initial_guesses(self): """ Call get_initial_guesses() on all engine models and return combined result. From c51d8de591f6ff8eddc55cc120f13eaa8287f826 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Tue, 14 May 2024 18:19:15 -0400 Subject: [PATCH 06/22] tests updated with api changes for loading engines --- aviary/api.py | 2 +- .../getting_started/onboarding_level3.ipynb | 15 ++- ..._same_mission_at_different_UI_levels.ipynb | 28 ++++-- .../default_phase_info/height_energy.py | 18 +--- .../interface/default_phase_info/two_dof.py | 18 +--- aviary/interface/methods_for_level2.py | 2 - aviary/interface/test/test_phase_info.py | 6 +- aviary/mission/flight_phase_builder.py | 3 +- aviary/mission/flops_based/ode/mission_ODE.py | 2 +- .../flops_based/ode/test/test_landing_ode.py | 7 +- .../flops_based/ode/test/test_takeoff_ode.py | 6 +- .../test/test_time_integration_phases.py | 12 ++- .../gasp_based/ode/test/test_accel_ode.py | 10 +- .../gasp_based/ode/test/test_ascent_ode.py | 10 +- .../gasp_based/ode/test/test_climb_ode.py | 8 +- .../gasp_based/ode/test/test_descent_ode.py | 8 +- .../ode/test/test_groundroll_ode.py | 8 +- .../gasp_based/ode/test/test_rotation_ode.py | 9 +- .../test/test_unsteady_solved_ode.py | 9 +- .../gasp_based/phases/landing_group.py | 98 +++++++++++-------- .../mission/gasp_based/phases/taxi_group.py | 22 ++--- .../phases/test/test_landing_group.py | 14 ++- .../gasp_based/phases/test/test_taxi_group.py | 10 +- .../test/test_idle_descent_estimation.py | 14 ++- aviary/models/N3CC/N3CC_data.py | 17 ++-- .../large_single_aisle_1_FLOPS_data.py | 96 +++++++----------- .../large_single_aisle_2_FLOPS_data.py | 64 +++++------- .../large_single_aisle_2_altwt_FLOPS_data.py | 89 +++++++---------- ...ge_single_aisle_2_detailwing_FLOPS_data.py | 62 +++++------- .../multi_engine_single_aisle_data.py | 14 --- .../test/test_computed_aero_group.py | 33 +++++-- .../test/test_tabular_aero_group.py | 12 ++- .../flops_based/test/test_prep_geom.py | 28 ++++-- .../mass/flops_based/landing_gear.py | 7 +- aviary/subsystems/mass/flops_based/starter.py | 5 +- .../mass/flops_based/test/test_anti_icing.py | 2 +- .../mass/flops_based/test/test_engine.py | 2 +- .../flops_based/test/test_engine_controls.py | 2 +- .../mass/flops_based/test/test_engine_oil.py | 12 ++- .../mass/flops_based/test/test_engine_pod.py | 2 +- .../mass/flops_based/test/test_fuel_system.py | 6 +- .../mass/flops_based/test/test_fuselage.py | 3 +- .../mass/flops_based/test/test_hydraulics.py | 6 +- .../flops_based/test/test_mass_summation.py | 4 +- .../mass/flops_based/test/test_misc_engine.py | 2 +- .../mass/flops_based/test/test_nacelle.py | 2 +- .../mass/flops_based/test/test_starter.py | 3 +- .../flops_based/test/test_thrust_reverser.py | 3 +- .../flops_based/test/test_unusable_fuel.py | 2 +- .../flops_based/test/test_wing_detailed.py | 11 ++- .../mass/flops_based/test/test_wing_simple.py | 3 +- .../gasp_based/equipment_and_useful_load.py | 5 +- .../gasp_based/test/test_mass_summation.py | 5 +- aviary/subsystems/propulsion/engine_deck.py | 30 +++--- .../subsystems/propulsion/engine_scaling.py | 14 --- aviary/subsystems/propulsion/engine_sizing.py | 25 ----- .../propulsion/propulsion_builder.py | 22 ++--- .../propulsion/propulsion_mission.py | 3 +- .../propulsion/propulsion_premission.py | 9 +- .../propulsion/test/test_data_interpolator.py | 3 +- .../propulsion/test/test_engine_deck.py | 5 +- .../test/test_propulsion_mission.py | 16 +-- .../test/test_propulsion_premission.py | 28 +++--- aviary/subsystems/propulsion/utils.py | 4 +- .../test/test_flops_based_premission.py | 17 +++- aviary/subsystems/test/test_premission.py | 11 ++- aviary/utils/preprocessors.py | 85 +++++----------- aviary/utils/test_utils/default_subsystems.py | 24 +++++ .../test_FLOPS_balanced_field_length.py | 11 ++- .../test_FLOPS_based_sizing_N3CC.py | 9 +- .../test_FLOPS_detailed_landing.py | 10 +- .../test_FLOPS_detailed_takeoff.py | 10 +- .../benchmark_tests/test_bench_multiengine.py | 24 ++++- aviary/validation_cases/validation_tests.py | 8 +- 74 files changed, 626 insertions(+), 583 deletions(-) create mode 100644 aviary/utils/test_utils/default_subsystems.py diff --git a/aviary/api.py b/aviary/api.py index 2ff96be41..4aa7b951c 100644 --- a/aviary/api.py +++ b/aviary/api.py @@ -44,7 +44,7 @@ from aviary.utils.options import list_options from aviary.constants import GRAV_METRIC_GASP, GRAV_ENGLISH_GASP, GRAV_METRIC_FLOPS, GRAV_ENGLISH_FLOPS, GRAV_ENGLISH_LBM, RHO_SEA_LEVEL_ENGLISH, RHO_SEA_LEVEL_METRIC, MU_TAKEOFF, MU_LANDING, PSLS_PSF, TSLS_DEGR, RADIUS_EARTH_METRIC from aviary.subsystems.test.subsystem_tester import TestSubsystemBuilderBase, skipIfMissingDependencies -from aviary.interface.default_phase_info.height_energy import default_premission_subsystems, default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck ################### # Level 3 Imports # diff --git a/aviary/docs/getting_started/onboarding_level3.ipynb b/aviary/docs/getting_started/onboarding_level3.ipynb index 49c9136b6..9295df12e 100644 --- a/aviary/docs/getting_started/onboarding_level3.ipynb +++ b/aviary/docs/getting_started/onboarding_level3.ipynb @@ -241,13 +241,22 @@ " av.Mission.Landing.LIFT_COEFFICIENT_MAX) # no units\n", ")\n", "\n", - "av.preprocess_crewpayload(aviary_inputs)\n", + "engine = av.build_engine_deck(aviary_inputs)\n", + "av.preprocess_options(aviary_inputs, engine)\n", + "\n", + "# default subsystems\n", + "aero = av.CoreAerodynamicsBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "geom = av.CoreGeometryBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "mass = av.CoreMassBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "prop = av.CorePropulsionBuilder(engine_models=engine)\n", + "\n", + "premission_subsystems = [prop, geom, aero, mass]\n", "\n", "# Upstream static analysis for aero\n", "prob.model.add_subsystem(\n", " 'pre_mission',\n", " av.CorePreMission(aviary_options=aviary_inputs,\n", - " subsystems=av.default_premission_subsystems),\n", + " subsystems=premission_subsystems),\n", " promotes_inputs=['aircraft:*', 'mission:*'],\n", " promotes_outputs=['aircraft:*', 'mission:*'])\n", "\n", @@ -593,7 +602,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.17" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb b/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb index a16a2f165..0b6d1f3c3 100644 --- a/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb +++ b/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb @@ -207,8 +207,8 @@ "\n", "aviary_inputs, _ = av.create_vehicle('models/test_aircraft/aircraft_for_bench_FwFm.csv')\n", "\n", - "engine = av.EngineDeck(options=aviary_inputs)\n", - "av.preprocess_propulsion(aviary_inputs, [engine])\n", + "engine = av.build_engine_deck(aviary_inputs)\n", + "av.preprocess_propulsion(aviary_inputs, engine)\n", "\n", "alt_airport = 0 # ft\n", "\n", @@ -325,11 +325,21 @@ "\n", "av.preprocess_crewpayload(aviary_inputs)\n", "\n", + "####################\n", + "# Build Subsystems #\n", + "####################\n", + "aero = av.CoreAerodynamicsBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "geom = av.CoreGeometryBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "mass = av.CoreMassBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "prop = av.CorePropulsionBuilder(engine_models=engine)\n", + "\n", + "premission_subsystems = [prop, geom, aero, mass]\n", + "\n", "# Upstream static analysis for aero\n", "prob.model.add_subsystem(\n", " 'pre_mission',\n", " av.CorePreMission(aviary_options=aviary_inputs,\n", - " subsystems=av.default_premission_subsystems),\n", + " subsystems=premission_subsystems),\n", " promotes_inputs=['aircraft:*', 'mission:*'],\n", " promotes_outputs=['aircraft:*', 'mission:*'])\n", "\n", @@ -373,9 +383,9 @@ "\n", "traj = av.setup_trajectory_params(prob.model, traj, aviary_inputs)\n", "\n", - "##########################\n", - "# Constraints #\n", - "##########################\n", + "###############\n", + "# Constraints #\n", + "###############\n", "\n", "ecomp = om.ExecComp('fuel_burned = initial_mass - descent_mass_final',\n", " initial_mass={'units': 'lbm', 'shape': 1},\n", @@ -408,9 +418,9 @@ "\n", "prob.setup(force_alloc_complex=True)\n", "\n", - "###########################################\n", + "############################################\n", "# Initial Settings for States and Controls #\n", - "###########################################\n", + "############################################\n", "\n", "prob.set_val('traj.climb.t_initial', t_i_climb, units='s')\n", "prob.set_val('traj.climb.t_duration', t_duration_climb, units='s')\n", @@ -475,7 +485,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/aviary/interface/default_phase_info/height_energy.py b/aviary/interface/default_phase_info/height_energy.py index 6f15be48e..0e7a91f56 100644 --- a/aviary/interface/default_phase_info/height_energy.py +++ b/aviary/interface/default_phase_info/height_energy.py @@ -1,20 +1,4 @@ -from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder -from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder -from aviary.subsystems.mass.mass_builder import CoreMassBuilder -from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder -from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData -from aviary.variable_info.variables import Dynamic, Mission -from aviary.variable_info.enums import LegacyCode - -FLOPS = LegacyCode.FLOPS - -prop = CorePropulsionBuilder('core_propulsion', BaseMetaData) -mass = CoreMassBuilder('core_mass', BaseMetaData, FLOPS) -aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, FLOPS) -geom = CoreGeometryBuilder('core_geometry', BaseMetaData, FLOPS) - -default_premission_subsystems = [prop, geom, mass, aero] -default_mission_subsystems = [aero, prop] +from aviary.variable_info.variables import Mission phase_info = { diff --git a/aviary/interface/default_phase_info/two_dof.py b/aviary/interface/default_phase_info/two_dof.py index 3bc8fbed3..9c6bca3da 100644 --- a/aviary/interface/default_phase_info/two_dof.py +++ b/aviary/interface/default_phase_info/two_dof.py @@ -1,21 +1,5 @@ from aviary.variable_info.enums import SpeedType -from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder -from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder -from aviary.subsystems.mass.mass_builder import CoreMassBuilder -from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder -from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData, Mission -from aviary.variable_info.enums import LegacyCode - - -GASP = LegacyCode.GASP - -prop = CorePropulsionBuilder('core_propulsion', BaseMetaData) -mass = CoreMassBuilder('core_mass', BaseMetaData, GASP) -aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, GASP) -geom = CoreGeometryBuilder('core_geometry', BaseMetaData, GASP) - -default_premission_subsystems = [prop, geom, aero, mass] -default_mission_subsystems = [aero, prop] +from aviary.variable_info.variables import Mission mission_distance = 3675 diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index e1f048417..957fc1a7f 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -2353,8 +2353,6 @@ def _save_to_csv_file(self, filename): writer = csv.DictWriter(csvfile, fieldnames=fieldnames) for name, value_units in sorted(self.aviary_inputs): - if 'engine_models' in name: - continue value, units = value_units writer.writerow({'name': name, 'value': value, 'units': units}) diff --git a/aviary/interface/test/test_phase_info.py b/aviary/interface/test/test_phase_info.py index 8e8ee46b9..1698f8674 100644 --- a/aviary/interface/test/test_phase_info.py +++ b/aviary/interface/test/test_phase_info.py @@ -156,6 +156,6 @@ def test_phase_info_parameterization_height_energy(self): # To run the tests if __name__ == '__main__': - unittest.main() - # test = TestPhaseInfo() - # test.test_default_phase_height_energy() + # unittest.main() + test = TestParameterizePhaseInfo() + test.test_phase_info_parameterization_two_dof() diff --git a/aviary/mission/flight_phase_builder.py b/aviary/mission/flight_phase_builder.py index 34967f808..ed0a2eab5 100644 --- a/aviary/mission/flight_phase_builder.py +++ b/aviary/mission/flight_phase_builder.py @@ -11,6 +11,7 @@ from aviary.variable_info.variables import Dynamic from aviary.mission.flops_based.ode.mission_ODE import MissionODE from aviary.variable_info.enums import EquationsOfMotion, ThrottleAllocation +from aviary.variable_info.variables import Aircraft # TODO: support/handle the following in the base class @@ -73,7 +74,7 @@ def build_phase(self, aviary_options: AviaryValues = None, phase_type=EquationsO ''' phase: dm.Phase = super().build_phase(aviary_options) - engine_models = aviary_options.get_val('engine_models') + engine_models = aviary_options.get_val(Aircraft.Engine.NUM_ENGINES) num_eng = len(engine_models) user_options: AviaryValues = self.user_options diff --git a/aviary/mission/flops_based/ode/mission_ODE.py b/aviary/mission/flops_based/ode/mission_ODE.py index 4dcbc058d..24cd87361 100644 --- a/aviary/mission/flops_based/ode/mission_ODE.py +++ b/aviary/mission/flops_based/ode/mission_ODE.py @@ -67,7 +67,7 @@ def setup(self): aviary_options = options['aviary_options'] core_subsystems = options['core_subsystems'] subsystem_options = options['subsystem_options'] - engine_count = len(aviary_options.get_val('engine_models')) + engine_count = len(aviary_options.get_val(Aircraft.Engine.NUM_ENGINES)) if analysis_scheme is AnalysisScheme.SHOOTING: SGM_required_inputs = { diff --git a/aviary/mission/flops_based/ode/test/test_landing_ode.py b/aviary/mission/flops_based/ode/test/test_landing_ode.py index 526fbc9cf..564c347ab 100644 --- a/aviary/mission/flops_based/ode/test/test_landing_ode.py +++ b/aviary/mission/flops_based/ode/test/test_landing_ode.py @@ -2,7 +2,8 @@ import openmdao.api as om -from aviary.interface.default_phase_info.height_energy import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.mission.flops_based.ode.landing_ode import FlareODE from aviary.models.N3CC.N3CC_data import ( detailed_landing_flare, inputs, landing_subsystem_options) @@ -13,11 +14,13 @@ class FlareODETest(unittest.TestCase): def test_case(self): prob = om.Problem() - time, _ = detailed_landing_flare.get_item('time') nn = len(time) aviary_options = inputs + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + prob.model.add_subsystem( "landing_flare_ode", FlareODE( diff --git a/aviary/mission/flops_based/ode/test/test_takeoff_ode.py b/aviary/mission/flops_based/ode/test/test_takeoff_ode.py index 6ffbf6e20..a7c0c9cf7 100644 --- a/aviary/mission/flops_based/ode/test/test_takeoff_ode.py +++ b/aviary/mission/flops_based/ode/test/test_takeoff_ode.py @@ -3,7 +3,8 @@ import openmdao.api as om -from aviary.interface.default_phase_info.height_energy import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE from aviary.models.N3CC.N3CC_data import ( detailed_takeoff_climbing, detailed_takeoff_ground, takeoff_subsystem_options, inputs) @@ -70,6 +71,9 @@ def _make_prob(climbing): nn = len(time) aviary_options = inputs + default_mission_subsystems = get_default_mission_subsystems( + 'FLOPS', build_engine_deck(aviary_options)) + prob.model.add_subsystem( "takeoff_ode", TakeoffODE( diff --git a/aviary/mission/flops_based/phases/test/test_time_integration_phases.py b/aviary/mission/flops_based/phases/test/test_time_integration_phases.py index a97846ad2..f47e4d36b 100644 --- a/aviary/mission/flops_based/phases/test/test_time_integration_phases.py +++ b/aviary/mission/flops_based/phases/test/test_time_integration_phases.py @@ -11,8 +11,8 @@ from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings from aviary.variable_info.variables_in import VariablesIn -from aviary.interface.default_phase_info.height_energy import aero, prop, geom -from aviary.subsystems.propulsion.engine_deck import EngineDeck +from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.process_input_decks import create_vehicle from aviary.utils.preprocessors import preprocess_propulsion from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData @@ -35,8 +35,12 @@ def setUp(self): val=0.35, units="unitless") aviary_inputs.set_val(Settings.EQUATIONS_OF_MOTION, val=EquationsOfMotion.SOLVED_2DOF) - ode_args = dict(aviary_options=aviary_inputs, core_subsystems=[prop, geom, aero]) - preprocess_propulsion(aviary_inputs, [EngineDeck(options=aviary_inputs)]) + + engines = build_engine_deck(aviary_inputs) + # don't need mass + core_subsystems = get_default_premission_subsystems('FLOPS', engines)[:-1] + ode_args = dict(aviary_options=aviary_inputs, core_subsystems=core_subsystems) + preprocess_propulsion(aviary_inputs, engines) ode_args['num_nodes'] = 1 ode_args['subsystem_options'] = {'core_aerodynamics': {'method': 'computed'}} diff --git a/aviary/mission/gasp_based/ode/test/test_accel_ode.py b/aviary/mission/gasp_based/ode/test/test_accel_ode.py index 3645de3a4..7781dd58a 100644 --- a/aviary/mission/gasp_based/ode/test/test_accel_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_accel_ode.py @@ -8,14 +8,20 @@ from aviary.variable_info.options import get_option_defaults from aviary.utils.test_utils.IO_test_util import check_prob_outputs from aviary.variable_info.variables import Dynamic -from aviary.interface.default_phase_info.two_dof import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems class AccelerationODETestCase(unittest.TestCase): def setUp(self): self.prob = om.Problem() + + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + self.sys = self.prob.model = AccelODE(num_nodes=2, - aviary_options=get_option_defaults(), + aviary_options=aviary_options, core_subsystems=default_mission_subsystems) def test_accel(self): diff --git a/aviary/mission/gasp_based/ode/test/test_ascent_ode.py b/aviary/mission/gasp_based/ode/test/test_ascent_ode.py index 1493ff78b..b17f051f1 100644 --- a/aviary/mission/gasp_based/ode/test/test_ascent_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_ascent_ode.py @@ -4,7 +4,8 @@ from openmdao.utils.assert_utils import assert_check_partials from aviary.mission.gasp_based.ode.ascent_ode import AscentODE -from aviary.interface.default_phase_info.two_dof import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.variable_info.options import get_option_defaults from aviary.variable_info.variables import Dynamic @@ -12,8 +13,13 @@ class AscentODETestCase(unittest.TestCase): def setUp(self): self.prob = om.Problem() + + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + self.prob.model = AscentODE(num_nodes=2, - aviary_options=get_option_defaults(), + aviary_options=aviary_options, core_subsystems=default_mission_subsystems) def test_ascent_partials(self): diff --git a/aviary/mission/gasp_based/ode/test/test_climb_ode.py b/aviary/mission/gasp_based/ode/test/test_climb_ode.py index 47eb53f5f..e11a0069d 100644 --- a/aviary/mission/gasp_based/ode/test/test_climb_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_climb_ode.py @@ -9,12 +9,18 @@ from aviary.utils.test_utils.IO_test_util import check_prob_outputs from aviary.variable_info.options import get_option_defaults from aviary.variable_info.variables import Aircraft, Dynamic -from aviary.interface.default_phase_info.two_dof import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems class ClimbODETestCase(unittest.TestCase): def setUp(self): self.prob = om.Problem() + + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + self.sys = self.prob.model = ClimbODE( num_nodes=1, EAS_target=250, diff --git a/aviary/mission/gasp_based/ode/test/test_descent_ode.py b/aviary/mission/gasp_based/ode/test/test_descent_ode.py index 7d642f6e8..5e2c70db3 100644 --- a/aviary/mission/gasp_based/ode/test/test_descent_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_descent_ode.py @@ -12,12 +12,18 @@ from aviary.utils.test_utils.IO_test_util import check_prob_outputs from aviary.variable_info.enums import SpeedType from aviary.variable_info.variables import Dynamic -from aviary.interface.default_phase_info.two_dof import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems class DescentODETestCase(unittest.TestCase): def setUp(self): self.prob = om.Problem() + + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + self.sys = self.prob.model = DescentODE(num_nodes=1, mach_cruise=0.8, aviary_options=get_option_defaults(), diff --git a/aviary/mission/gasp_based/ode/test/test_groundroll_ode.py b/aviary/mission/gasp_based/ode/test/test_groundroll_ode.py index 57d36fcac..18af08fe3 100644 --- a/aviary/mission/gasp_based/ode/test/test_groundroll_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_groundroll_ode.py @@ -5,13 +5,19 @@ from aviary.mission.gasp_based.ode.groundroll_ode import GroundrollODE from aviary.variable_info.options import get_option_defaults -from aviary.interface.default_phase_info.two_dof import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.variable_info.variables import Dynamic class GroundrollODETestCase(unittest.TestCase): def setUp(self): self.prob = om.Problem() + + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + self.prob.model = GroundrollODE(num_nodes=2, aviary_options=get_option_defaults(), core_subsystems=default_mission_subsystems) diff --git a/aviary/mission/gasp_based/ode/test/test_rotation_ode.py b/aviary/mission/gasp_based/ode/test/test_rotation_ode.py index 34d39ac0b..f1c8b9d82 100644 --- a/aviary/mission/gasp_based/ode/test/test_rotation_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_rotation_ode.py @@ -1,5 +1,4 @@ import unittest -import os import openmdao.api as om from openmdao.utils.assert_utils import assert_check_partials @@ -7,12 +6,18 @@ from aviary.mission.gasp_based.ode.rotation_ode import RotationODE from aviary.variable_info.options import get_option_defaults from aviary.variable_info.variables import Aircraft, Dynamic -from aviary.interface.default_phase_info.two_dof import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems class RotationODETestCase(unittest.TestCase): def setUp(self): self.prob = om.Problem() + + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + self.prob.model = RotationODE(num_nodes=2, aviary_options=get_option_defaults(), core_subsystems=default_mission_subsystems) diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_solved_ode.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_solved_ode.py index 80e3ebc68..26ee05a11 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_solved_ode.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_solved_ode.py @@ -11,7 +11,8 @@ from aviary.variable_info.options import get_option_defaults from aviary.variable_info.enums import SpeedType from aviary.variable_info.variables import Aircraft, Dynamic, Mission -from aviary.interface.default_phase_info.two_dof import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems class TestUnsteadySolvedODE(unittest.TestCase): @@ -22,11 +23,15 @@ def _test_unsteady_solved_ode(self, ground_roll=False, input_speed_type=SpeedTyp p = om.Problem() + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + ode = UnsteadySolvedODE(num_nodes=nn, input_speed_type=input_speed_type, clean=clean, ground_roll=ground_roll, - aviary_options=get_option_defaults(), + aviary_options=aviary_options, core_subsystems=default_mission_subsystems) p.model.add_subsystem("ode", ode, promotes=["*"]) diff --git a/aviary/mission/gasp_based/phases/landing_group.py b/aviary/mission/gasp_based/phases/landing_group.py index c0a342c88..51063df58 100644 --- a/aviary/mission/gasp_based/phases/landing_group.py +++ b/aviary/mission/gasp_based/phases/landing_group.py @@ -6,14 +6,17 @@ from aviary.mission.gasp_based.phases.landing_components import ( GlideConditionComponent, LandingAltitudeComponent, LandingGroundRollComponent) -from aviary.subsystems.aerodynamics.gasp_based.gaspaero import LowSpeedAero -from aviary.subsystems.propulsion.propulsion_mission import PropulsionMission from aviary.variable_info.enums import SpeedType from aviary.variable_info.variables import Aircraft, Dynamic, Mission +from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase +from aviary.subsystems.propulsion.propulsion_builder import PropulsionBuilderBase class LandingSegment(BaseODE): def setup(self): + aviary_options = self.options['aviary_options'] + core_subsystems = self.options['core_subsystems'] + # TODO: paramport self.add_subsystem("params", ParamPort(), promotes=["*"]) @@ -51,44 +54,51 @@ def setup(self): promotes_outputs=[(Dynamic.Mission.DYNAMIC_PRESSURE, "q_app")], ) - propulsion_mission = self.add_subsystem( - name='propulsion', - subsys=PropulsionMission( - num_nodes=1, - aviary_options=self.options['aviary_options']), - promotes_inputs=["*", (Dynamic.Mission.ALTITUDE, Mission.Landing.INITIAL_ALTITUDE), - (Dynamic.Mission.MACH, Mission.Landing.INITIAL_MACH)], - promotes_outputs=[(Dynamic.Mission.THRUST_TOTAL, "thrust_idle")]) - propulsion_mission.set_input_defaults(Dynamic.Mission.THROTTLE, 0.0) - - # alpha input not needed, only used for CL_max - self.add_subsystem( - "aero_app", - LowSpeedAero(num_nodes=1, aviary_options=self.options['aviary_options']), - promotes_inputs=[ - "*", - (Dynamic.Mission.ALTITUDE, Mission.Landing.INITIAL_ALTITUDE), - ("rho", "rho_app"), - (Dynamic.Mission.SPEED_OF_SOUND, "sos_app"), - ("viscosity", "viscosity_app"), - ("airport_alt", Mission.Landing.AIRPORT_ALTITUDE), - (Dynamic.Mission.MACH, Mission.Landing.INITIAL_MACH), - (Dynamic.Mission.DYNAMIC_PRESSURE, "q_app"), - ("flap_defl", Aircraft.Wing.FLAP_DEFLECTION_LANDING), - ("t_init_flaps", "t_init_flaps_app"), - ("t_init_gear", "t_init_gear_app"), - ("CL_max_flaps", Mission.Landing.LIFT_COEFFICIENT_MAX), - ( - "dCL_flaps_model", - Mission.Landing.LIFT_COEFFICIENT_FLAP_INCREMENT, - ), - ( - "dCD_flaps_model", - Mission.Landing.DRAG_COEFFICIENT_FLAP_INCREMENT, - ), - ], - promotes_outputs=["CL_max"], - ) + # collect the propulsion group names for later use with + for subsystem in core_subsystems: + if isinstance(subsystem, AerodynamicsBuilderBase): + kwargs = {'method': 'low_speed'} + aero_builder = subsystem + aero_system = subsystem.build_mission(num_nodes=1, + aviary_inputs=aviary_options, + **kwargs) + self.add_subsystem(subsystem.name, + aero_system, + promotes_inputs=[ + "*", + (Dynamic.Mission.ALTITUDE, + Mission.Landing.INITIAL_ALTITUDE), + ("rho", "rho_app"), + (Dynamic.Mission.SPEED_OF_SOUND, "sos_app"), + ("viscosity", "viscosity_app"), + ("airport_alt", Mission.Landing.AIRPORT_ALTITUDE), + (Dynamic.Mission.MACH, Mission.Landing.INITIAL_MACH), + (Dynamic.Mission.DYNAMIC_PRESSURE, "q_app"), + ("flap_defl", Aircraft.Wing.FLAP_DEFLECTION_LANDING), + ("t_init_flaps", "t_init_flaps_app"), + ("t_init_gear", "t_init_gear_app"), + ("CL_max_flaps", Mission.Landing.LIFT_COEFFICIENT_MAX), + ( + "dCL_flaps_model", + Mission.Landing.LIFT_COEFFICIENT_FLAP_INCREMENT, + ), + ( + "dCD_flaps_model", + Mission.Landing.DRAG_COEFFICIENT_FLAP_INCREMENT, + ), + ], + promotes_outputs=["CL_max"], + ) + + if isinstance(subsystem, PropulsionBuilderBase): + propulsion_system = subsystem.build_mission( + num_nodes=1, aviary_inputs=aviary_options) + propulsion_mission = self.add_subsystem(subsystem.name, + propulsion_system, + promotes_inputs=[ + "*", (Dynamic.Mission.ALTITUDE, Mission.Landing.INITIAL_ALTITUDE), (Dynamic.Mission.MACH, Mission.Landing.INITIAL_MACH)], + promotes_outputs=[(Dynamic.Mission.THRUST_TOTAL, "thrust_idle")]) + propulsion_mission.set_input_defaults(Dynamic.Mission.THROTTLE, 0.0) self.add_subsystem( "glide", @@ -142,10 +152,14 @@ def setup(self): (Dynamic.Mission.MACH, "mach_td")], ) + kwargs = {'method': 'low_speed', + 'retract_flaps': True, + 'retract_gear': False} + self.add_subsystem( "aero_td", - LowSpeedAero( - num_nodes=1, aviary_options=self.options['aviary_options'], retract_flaps=True, retract_gear=False), + aero_builder.build_mission( + num_nodes=1, aviary_inputs=aviary_options, **kwargs), promotes_inputs=[ "*", (Dynamic.Mission.ALTITUDE, Mission.Landing.AIRPORT_ALTITUDE), diff --git a/aviary/mission/gasp_based/phases/taxi_group.py b/aviary/mission/gasp_based/phases/taxi_group.py index a5035ebd9..3094d20ac 100644 --- a/aviary/mission/gasp_based/phases/taxi_group.py +++ b/aviary/mission/gasp_based/phases/taxi_group.py @@ -5,14 +5,14 @@ from aviary.mission.gasp_based.ode.base_ode import BaseODE from aviary.mission.gasp_based.ode.params import ParamPort from aviary.mission.gasp_based.phases.taxi_component import TaxiFuelComponent -from aviary.subsystems.propulsion.propulsion_mission import \ - PropulsionMission +from aviary.subsystems.propulsion.propulsion_builder import PropulsionBuilderBase from aviary.variable_info.variables import Dynamic, Mission class TaxiSegment(BaseODE): def setup(self): options: AviaryValues = self.options['aviary_options'] + core_subsystems = self.options['core_subsystems'] self.add_subsystem("params", ParamPort(), promotes=["*"]) self.add_subsystem( "USatm", @@ -25,15 +25,15 @@ def setup(self): add_opts2vals(self, create_opts2vals( [Mission.Taxi.MACH]), options) - self.add_subsystem( - name='propulsion', - subsys=PropulsionMission( - num_nodes=1, - aviary_options=options, - ), - promotes_inputs=['*', (Dynamic.Mission.ALTITUDE, Mission.Takeoff.AIRPORT_ALTITUDE), - (Dynamic.Mission.MACH, Mission.Taxi.MACH)], - promotes_outputs=['*']) + for subsystem in core_subsystems: + if isinstance(subsystem, PropulsionBuilderBase): + system = subsystem.build_mission(num_nodes=1, aviary_inputs=options) + + self.add_subsystem(subsystem.name, + system, + promotes_inputs=['*', (Dynamic.Mission.ALTITUDE, Mission.Takeoff.AIRPORT_ALTITUDE), + (Dynamic.Mission.MACH, Mission.Taxi.MACH)], + promotes_outputs=['*']) self.add_subsystem("taxifuel", TaxiFuelComponent( aviary_options=options), promotes=["*"]) diff --git a/aviary/mission/gasp_based/phases/test/test_landing_group.py b/aviary/mission/gasp_based/phases/test/test_landing_group.py index 6ed7c3265..8f6efd371 100644 --- a/aviary/mission/gasp_based/phases/test/test_landing_group.py +++ b/aviary/mission/gasp_based/phases/test/test_landing_group.py @@ -11,13 +11,22 @@ from aviary.variable_info.options import get_option_defaults from aviary.utils.test_utils.IO_test_util import check_prob_outputs from aviary.variable_info.variables import Dynamic, Mission +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase +from aviary.subsystems.propulsion.utils import build_engine_deck class DLandTestCase(unittest.TestCase): def setUp(self): self.prob = om.Problem() - self.prob.model = LandingSegment(aviary_options=get_option_defaults()) + + options = get_option_defaults() + engine = build_engine_deck(options) + core_subsystems = get_default_mission_subsystems('GASP', engine) + + self.prob.model = LandingSegment( + aviary_options=options, core_subsystems=core_subsystems) @unittest.skipIf(version.parse(openmdao.__version__) < version.parse("3.26"), "Skipping due to OpenMDAO version being too low (<3.26)") def test_dland(self): @@ -56,3 +65,6 @@ def test_dland(self): if __name__ == "__main__": unittest.main() + # test = DLandTestCase() + # test.setUp() + # test.test_dland() diff --git a/aviary/mission/gasp_based/phases/test/test_taxi_group.py b/aviary/mission/gasp_based/phases/test/test_taxi_group.py index 6ed7c3265..c2f5daced 100644 --- a/aviary/mission/gasp_based/phases/test/test_taxi_group.py +++ b/aviary/mission/gasp_based/phases/test/test_taxi_group.py @@ -11,13 +11,21 @@ from aviary.variable_info.options import get_option_defaults from aviary.utils.test_utils.IO_test_util import check_prob_outputs from aviary.variable_info.variables import Dynamic, Mission +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems class DLandTestCase(unittest.TestCase): def setUp(self): self.prob = om.Problem() - self.prob.model = LandingSegment(aviary_options=get_option_defaults()) + + options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(options)) + + self.prob.model = LandingSegment( + aviary_options=options, core_subsystems=default_mission_subsystems) @unittest.skipIf(version.parse(openmdao.__version__) < version.parse("3.26"), "Skipping due to OpenMDAO version being too low (<3.26)") def test_dland(self): diff --git a/aviary/mission/gasp_based/test/test_idle_descent_estimation.py b/aviary/mission/gasp_based/test/test_idle_descent_estimation.py index 278d6997d..376ecc4d6 100644 --- a/aviary/mission/gasp_based/test/test_idle_descent_estimation.py +++ b/aviary/mission/gasp_based/test/test_idle_descent_estimation.py @@ -5,9 +5,10 @@ from openmdao.utils.assert_utils import assert_near_equal -from aviary.interface.default_phase_info.two_dof import default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.mission.gasp_based.idle_descent_estimation import descent_range_and_fuel -from aviary.subsystems.propulsion.engine_deck import EngineDeck +from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.variable_info.variables import Aircraft, Dynamic from aviary.variable_info.enums import Verbosity from aviary.utils.process_input_decks import create_vehicle @@ -23,10 +24,15 @@ def setUp(self): aviary_inputs.set_val('verbosity', Verbosity.QUIET) aviary_inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, val=28690, units="lbf") aviary_inputs.set_val(Dynamic.Mission.THROTTLE, val=0, units="unitless") + + engine = build_engine_deck(aviary_options=aviary_inputs)[0] + preprocess_propulsion(aviary_inputs, [engine]) + + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_inputs)) + ode_args = dict(aviary_options=aviary_inputs, core_subsystems=default_mission_subsystems) - engine = EngineDeck(options=aviary_inputs) - preprocess_propulsion(aviary_inputs, [engine]) self.ode_args = ode_args self.aviary_inputs = aviary_inputs diff --git a/aviary/models/N3CC/N3CC_data.py b/aviary/models/N3CC/N3CC_data.py index efcb56418..a1ab0f01b 100644 --- a/aviary/models/N3CC/N3CC_data.py +++ b/aviary/models/N3CC/N3CC_data.py @@ -17,14 +17,13 @@ TakeoffEngineCutback, TakeoffEngineCutbackToMicP1, TakeoffLiftoffToObstacle, TakeoffMicP1ToClimb, TakeoffMicP2ToEngineCutback, TakeoffObstacleToMicP2, TakeoffRotateToLiftoff, TakeoffTrajectory) -from aviary.subsystems.propulsion.engine_deck import EngineDeck +from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.aviary_values import AviaryValues -from aviary.utils.preprocessors import preprocess_propulsion +from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems, get_default_mission_subsystems +from aviary.utils.preprocessors import preprocess_options from aviary.utils.functions import get_path from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings from aviary.variable_info.enums import EquationsOfMotion, LegacyCode -from aviary.interface.default_phase_info.height_energy import default_mission_subsystems - N3CC = {} inputs = N3CC['inputs'] = AviaryValues() @@ -443,10 +442,12 @@ outputs.set_val(Mission.Design.LIFT_COEFFICIENT, 0.583) # Create engine model -engine = EngineDeck(name='engine', - options=engine_inputs - ) -preprocess_propulsion(inputs, [engine]) +engine = build_engine_deck(aviary_options=engine_inputs) +preprocess_options(inputs, engine_models=engine) + +# build subsystems +default_premission_subsystems = get_default_premission_subsystems('FLOPS', engine) +default_mission_subsystems = get_default_mission_subsystems('FLOPS', engine) # region - detailed takeoff takeoff_trajectory_builder = TakeoffTrajectory('detailed_takeoff') diff --git a/aviary/models/large_single_aisle_1/large_single_aisle_1_FLOPS_data.py b/aviary/models/large_single_aisle_1/large_single_aisle_1_FLOPS_data.py index 0f971994b..632dc2169 100644 --- a/aviary/models/large_single_aisle_1/large_single_aisle_1_FLOPS_data.py +++ b/aviary/models/large_single_aisle_1/large_single_aisle_1_FLOPS_data.py @@ -1,8 +1,6 @@ import numpy as np -from aviary.subsystems.propulsion.engine_deck import EngineDeck from aviary.utils.aviary_values import AviaryValues -from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.functions import get_path from aviary.variable_info.enums import EquationsOfMotion, LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Settings @@ -145,52 +143,34 @@ inputs.set_val(Aircraft.Propulsion.ENGINE_OIL_MASS_SCALER, 1.0) inputs.set_val(Aircraft.Propulsion.MISC_MASS_SCALER, 1.0) -filename = get_path( - 'models/engines/turbofan_28k.deck') - -engine_inputs = AviaryValues() -engine_inputs.set_val(Aircraft.Engine.DATA_FILE, filename) -engine_mass = 7400 -engine_mass_units = 'lbm' -engine_inputs.set_val(Aircraft.Engine.MASS, engine_mass, engine_mass_units) -engine_inputs.set_val( - Aircraft.Engine.REFERENCE_MASS, - engine_mass, - engine_mass_units) -scaled_sls_thrust = 28928.1 -scaled_sls_thrust_units = 'lbf' -engine_inputs.set_val( - Aircraft.Engine.SCALED_SLS_THRUST, scaled_sls_thrust, scaled_sls_thrust_units -) -engine_inputs.set_val( - Aircraft.Engine.REFERENCE_SLS_THRUST, scaled_sls_thrust, scaled_sls_thrust_units) -num_engines = 2 -engine_inputs.set_val(Aircraft.Engine.NUM_ENGINES, num_engines) -num_fuselage_engines = 0 -engine_inputs.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, 0) -num_wing_engines = num_engines -engine_inputs.set_val(Aircraft.Engine.NUM_WING_ENGINES, num_wing_engines) -engine_inputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS_SCALER, 0.0) -engine_inputs.set_val( - Aircraft.Engine.WING_LOCATIONS, 15.8300 / (117.83 / 2)) -engine_inputs.set_val(Aircraft.Engine.SCALE_MASS, True) -engine_inputs.set_val(Aircraft.Engine.MASS_SCALER, 1.15) -engine_inputs.set_val(Aircraft.Engine.SCALE_PERFORMANCE, True) -engine_inputs.set_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER, 1.0) -engine_inputs.set_val(Aircraft.Engine.SUPERSONIC_FUEL_FLOW_SCALER, 1.0) -engine_inputs.set_val( - Aircraft.Engine.FUEL_FLOW_SCALER_CONSTANT_TERM, 0.0) -engine_inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, 0.0) -engine_inputs.set_val(Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, 0.0, units='lbm/h') -engine_inputs.set_val(Aircraft.Engine.ADDITIONAL_MASS_FRACTION, 0.0) -engine_inputs.set_val(Aircraft.Engine.GENERATE_FLIGHT_IDLE, True) -engine_inputs.set_val(Aircraft.Engine.IGNORE_NEGATIVE_THRUST, False) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_THRUST_FRACTION, 0.0) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MAX_FRACTION, 1.0) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) -engine_inputs.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) -engine_inputs.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') - +filename = get_path('models/engines/turbofan_28k.deck') + +inputs.set_val(Aircraft.Engine.DATA_FILE, filename) +inputs.set_val(Aircraft.Engine.MASS, 7400, 'lbm') +inputs.set_val(Aircraft.Engine.REFERENCE_MASS, 7400, 'lbm') +inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, 28928.1, 'lbf') +inputs.set_val(Aircraft.Engine.REFERENCE_SLS_THRUST, 28928.1, 'lbf') +inputs.set_val(Aircraft.Engine.NUM_ENGINES, 2) +inputs.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, 0) +inputs.set_val(Aircraft.Engine.NUM_WING_ENGINES, 2) +inputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS_SCALER, 0.0) +inputs.set_val(Aircraft.Engine.WING_LOCATIONS, 15.8300 / (117.83 / 2)) +inputs.set_val(Aircraft.Engine.SCALE_MASS, True) +inputs.set_val(Aircraft.Engine.MASS_SCALER, 1.15) +inputs.set_val(Aircraft.Engine.SCALE_PERFORMANCE, True) +inputs.set_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.SUPERSONIC_FUEL_FLOW_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_CONSTANT_TERM, 0.0) +inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, 0.0) +inputs.set_val(Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, 0.0, units='lbm/h') +inputs.set_val(Aircraft.Engine.ADDITIONAL_MASS_FRACTION, 0.0) +inputs.set_val(Aircraft.Engine.GENERATE_FLIGHT_IDLE, True) +inputs.set_val(Aircraft.Engine.IGNORE_NEGATIVE_THRUST, False) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_THRUST_FRACTION, 0.0) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MAX_FRACTION, 1.0) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) +inputs.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) +inputs.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') # Vertical Tail # --------------------------- @@ -350,11 +330,10 @@ outputs.set_val(Aircraft.Paint.MASS, 306.2, 'lbm') outputs.set_val( - Aircraft.Propulsion.TOTAL_SCALED_SLS_THRUST, - scaled_sls_thrust * num_engines, scaled_sls_thrust_units) + Aircraft.Propulsion.TOTAL_SCALED_SLS_THRUST, 28928.1*2, 'lbf') outputs.set_val( - Aircraft.Propulsion.TOTAL_NUM_ENGINES, num_engines) + Aircraft.Propulsion.TOTAL_NUM_ENGINES, 2) engine_ctrls_mass = 88.44 engine_ctrls_mass_units = 'lbm' @@ -365,9 +344,9 @@ engine_ctrls_mass, engine_ctrls_mass_units) outputs.set_val(Aircraft.Propulsion.TOTAL_ENGINE_OIL_MASS, 130.23, 'lbm') -outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_WING_ENGINES, num_wing_engines) +outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_WING_ENGINES, 2) -outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_FUSELAGE_ENGINES, num_fuselage_engines) +outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_FUSELAGE_ENGINES, 0) outputs.set_val(Aircraft.Engine.MASS, 14800/2, 'lbm') outputs.set_val(Aircraft.Engine.POD_MASS, 9000, 'lbm') @@ -379,9 +358,7 @@ outputs.set_val(Aircraft.Propulsion.TOTAL_THRUST_REVERSERS_MASS, 0, 'lbm') outputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS, 0, 'lbm') -outputs.set_val( - Aircraft.Propulsion.TOTAL_ENGINE_MASS, - engine_mass * num_engines, engine_mass_units) +outputs.set_val(Aircraft.Propulsion.TOTAL_ENGINE_MASS, 7400 * 2, 'lbm') outputs.set_val(Aircraft.VerticalTail.CHARACTERISTIC_LENGTH, 12.74, 'ft') outputs.set_val(Aircraft.VerticalTail.FINENESS, 0.1195) @@ -402,10 +379,3 @@ outputs.set_val(Mission.Design.MACH, 0.800) outputs.set_val(Mission.Design.LIFT_COEFFICIENT, 0.568) - -# Create engine model -engine_inputs.set_val(Settings.VERBOSITY, 0) -engine = EngineDeck(name='engine', - options=engine_inputs - ) -preprocess_propulsion(inputs, [engine]) diff --git a/aviary/models/large_single_aisle_2/large_single_aisle_2_FLOPS_data.py b/aviary/models/large_single_aisle_2/large_single_aisle_2_FLOPS_data.py index 6bb8743c9..17aae96d6 100644 --- a/aviary/models/large_single_aisle_2/large_single_aisle_2_FLOPS_data.py +++ b/aviary/models/large_single_aisle_2/large_single_aisle_2_FLOPS_data.py @@ -1,9 +1,7 @@ import numpy as np from numpy import pi -from aviary.subsystems.propulsion.engine_deck import EngineDeck from aviary.utils.aviary_values import AviaryValues -from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.functions import get_path from aviary.variable_info.enums import EquationsOfMotion, LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Settings @@ -156,46 +154,45 @@ filename = get_path( 'models/engines/turbofan_24k_1.deck') -engine_inputs = AviaryValues() -engine_inputs.set_val(Aircraft.Engine.DATA_FILE, filename) +inputs.set_val(Aircraft.Engine.DATA_FILE, filename) engine_mass = 8071.35 engine_mass_units = 'lbm' -engine_inputs.set_val(Aircraft.Engine.MASS, engine_mass, engine_mass_units) -engine_inputs.set_val( +inputs.set_val(Aircraft.Engine.MASS, engine_mass, engine_mass_units) +inputs.set_val( Aircraft.Engine.REFERENCE_MASS, engine_mass, engine_mass_units) scaled_sls_thrust = 27301.0 scaled_sls_thrust_units = 'lbf' -engine_inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, - scaled_sls_thrust, scaled_sls_thrust_units) -engine_inputs.set_val( +inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, + scaled_sls_thrust, scaled_sls_thrust_units) +inputs.set_val( Aircraft.Engine.REFERENCE_SLS_THRUST, scaled_sls_thrust, scaled_sls_thrust_units) num_engines = 2 -engine_inputs.set_val(Aircraft.Engine.NUM_ENGINES, num_engines) +inputs.set_val(Aircraft.Engine.NUM_ENGINES, num_engines) num_fuselage_engines = 0 -engine_inputs.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, num_fuselage_engines) +inputs.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, num_fuselage_engines) num_wing_engines = 2 -engine_inputs.set_val(Aircraft.Engine.NUM_WING_ENGINES, num_wing_engines) -engine_inputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS_SCALER, 1.0) -engine_inputs.set_val(Aircraft.Engine.SCALE_MASS, True) -engine_inputs.set_val(Aircraft.Engine.MASS_SCALER, 1.15) -engine_inputs.set_val(Aircraft.Engine.SCALE_PERFORMANCE, True) -engine_inputs.set_val(Aircraft.Engine.SCALE_FACTOR, 1.0) -engine_inputs.set_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER, 1.0) -engine_inputs.set_val(Aircraft.Engine.SUPERSONIC_FUEL_FLOW_SCALER, 1.0) -engine_inputs.set_val( +inputs.set_val(Aircraft.Engine.NUM_WING_ENGINES, num_wing_engines) +inputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.SCALE_MASS, True) +inputs.set_val(Aircraft.Engine.MASS_SCALER, 1.15) +inputs.set_val(Aircraft.Engine.SCALE_PERFORMANCE, True) +inputs.set_val(Aircraft.Engine.SCALE_FACTOR, 1.0) +inputs.set_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.SUPERSONIC_FUEL_FLOW_SCALER, 1.0) +inputs.set_val( Aircraft.Engine.FUEL_FLOW_SCALER_CONSTANT_TERM, 0.0) -engine_inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, 0.0) -engine_inputs.set_val(Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, 0.0, units='lbm/h') -engine_inputs.set_val(Aircraft.Engine.ADDITIONAL_MASS_FRACTION, 0.0) -engine_inputs.set_val(Aircraft.Engine.GENERATE_FLIGHT_IDLE, True) -engine_inputs.set_val(Aircraft.Engine.IGNORE_NEGATIVE_THRUST, False) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_THRUST_FRACTION, 0.0) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MAX_FRACTION, 1.0) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) -engine_inputs.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) -engine_inputs.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') +inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, 0.0) +inputs.set_val(Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, 0.0, units='lbm/h') +inputs.set_val(Aircraft.Engine.ADDITIONAL_MASS_FRACTION, 0.0) +inputs.set_val(Aircraft.Engine.GENERATE_FLIGHT_IDLE, True) +inputs.set_val(Aircraft.Engine.IGNORE_NEGATIVE_THRUST, False) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_THRUST_FRACTION, 0.0) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MAX_FRACTION, 1.0) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) +inputs.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) +inputs.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') # Vertical Tail # --------------------------- @@ -408,10 +405,3 @@ outputs.set_val(Mission.Design.MACH, 0.799) outputs.set_val(Mission.Design.LIFT_COEFFICIENT, 0.523) - -# Create engine model -engine_inputs.set_val(Settings.VERBOSITY, 0) -engine = EngineDeck(name='engine', - options=engine_inputs - ) -preprocess_propulsion(inputs, [engine]) diff --git a/aviary/models/large_single_aisle_2/large_single_aisle_2_altwt_FLOPS_data.py b/aviary/models/large_single_aisle_2/large_single_aisle_2_altwt_FLOPS_data.py index 9b0bf88e6..e4864361a 100644 --- a/aviary/models/large_single_aisle_2/large_single_aisle_2_altwt_FLOPS_data.py +++ b/aviary/models/large_single_aisle_2/large_single_aisle_2_altwt_FLOPS_data.py @@ -1,9 +1,7 @@ import numpy as np from numpy import pi -from aviary.subsystems.propulsion.engine_deck import EngineDeck from aviary.utils.aviary_values import AviaryValues -from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.functions import get_path from aviary.variable_info.enums import EquationsOfMotion, LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Settings @@ -153,47 +151,34 @@ inputs.set_val(Aircraft.Propulsion.ENGINE_OIL_MASS_SCALER, 1.0) inputs.set_val(Aircraft.Propulsion.MISC_MASS_SCALER, 1.0) -filename = get_path( - 'models/engines/turbofan_24k_1.deck') - -engine_inputs = AviaryValues() -engine_inputs.set_val(Aircraft.Engine.DATA_FILE, filename) -engine_mass = 8071.35 -engine_mass_units = 'lbm' -engine_inputs.set_val(Aircraft.Engine.MASS, engine_mass, engine_mass_units) -engine_inputs.set_val(Aircraft.Engine.REFERENCE_MASS, - engine_mass, engine_mass_units) -scaled_sls_thrust = 27301.0 -scaled_sls_thrust_units = 'lbf' -engine_inputs.set_val( - Aircraft.Engine.SCALED_SLS_THRUST, scaled_sls_thrust, scaled_sls_thrust_units) -engine_inputs.set_val( - Aircraft.Engine.REFERENCE_SLS_THRUST, scaled_sls_thrust, scaled_sls_thrust_units) -num_engines = 2 -engine_inputs.set_val(Aircraft.Engine.NUM_ENGINES, num_engines) -num_fuselage_engines = 0 -engine_inputs.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, num_fuselage_engines) -num_wing_engines = 2 -engine_inputs.set_val(Aircraft.Engine.NUM_WING_ENGINES, num_wing_engines) -engine_inputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS_SCALER, 1.0) -engine_inputs.set_val(Aircraft.Engine.SCALE_MASS, True) -engine_inputs.set_val(Aircraft.Engine.MASS_SCALER, 1.15) -engine_inputs.set_val(Aircraft.Engine.SCALE_PERFORMANCE, True) - -engine_inputs.set_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER, 1.0) -engine_inputs.set_val(Aircraft.Engine.SUPERSONIC_FUEL_FLOW_SCALER, 1.0) -engine_inputs.set_val( - Aircraft.Engine.FUEL_FLOW_SCALER_CONSTANT_TERM, 0.0) -engine_inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, 0.0) -engine_inputs.set_val(Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, 0.0, units='lbm/h') -engine_inputs.set_val(Aircraft.Engine.ADDITIONAL_MASS_FRACTION, 0.0) -engine_inputs.set_val(Aircraft.Engine.GENERATE_FLIGHT_IDLE, True) -engine_inputs.set_val(Aircraft.Engine.IGNORE_NEGATIVE_THRUST, False) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_THRUST_FRACTION, 0.0) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MAX_FRACTION, 1.0) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) -engine_inputs.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) -engine_inputs.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') +filename = get_path('models/engines/turbofan_24k_1.deck') + +inputs.set_val(Aircraft.Engine.DATA_FILE, filename) +inputs.set_val(Aircraft.Engine.MASS, 8071.35, 'lbm') +inputs.set_val(Aircraft.Engine.REFERENCE_MASS, 8071.35, 'lbm') +inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, 27301.0, 'lbf') +inputs.set_val(Aircraft.Engine.REFERENCE_SLS_THRUST, 27301.0, 'lbf') +inputs.set_val(Aircraft.Engine.NUM_ENGINES, 2) +inputs.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, 0) +inputs.set_val(Aircraft.Engine.NUM_WING_ENGINES, 2) +inputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.SCALE_MASS, True) +inputs.set_val(Aircraft.Engine.MASS_SCALER, 1.15) +inputs.set_val(Aircraft.Engine.SCALE_PERFORMANCE, True) + +inputs.set_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.SUPERSONIC_FUEL_FLOW_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_CONSTANT_TERM, 0.0) +inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, 0.0) +inputs.set_val(Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, 0.0, units='lbm/h') +inputs.set_val(Aircraft.Engine.ADDITIONAL_MASS_FRACTION, 0.0) +inputs.set_val(Aircraft.Engine.GENERATE_FLIGHT_IDLE, True) +inputs.set_val(Aircraft.Engine.IGNORE_NEGATIVE_THRUST, False) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_THRUST_FRACTION, 0.0) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MAX_FRACTION, 1.0) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) +inputs.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) +inputs.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') # Vertical Tail @@ -355,10 +340,10 @@ outputs.set_val(Aircraft.Paint.MASS, 582.3, 'lbm') -outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_ENGINES, num_engines) +outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_ENGINES, 2) outputs.set_val( Aircraft.Propulsion.TOTAL_SCALED_SLS_THRUST, - scaled_sls_thrust * num_engines, scaled_sls_thrust_units) + 27301.0 * 2, 'lbf') outputs.set_val( Aircraft.Propulsion.TOTAL_ENGINE_CONTROLS_MASS, 0.26 * 2 * 27301.0**0.5, 'lbm') # 85.92 @@ -374,13 +359,12 @@ outputs.set_val( Aircraft.Engine.THRUST_REVERSERS_MASS, thrust_reversers_mass, thrust_reversers_mass_units) -outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_FUSELAGE_ENGINES, num_fuselage_engines) -outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_WING_ENGINES, num_wing_engines) +outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_FUSELAGE_ENGINES, 0) +outputs.set_val(Aircraft.Propulsion.TOTAL_NUM_WING_ENGINES, 2) outputs.set_val(Aircraft.Engine.MASS, 16143./2.0, 'lbm') outputs.set_val(Aircraft.Engine.ADDITIONAL_MASS, 0.0, 'lbm') outputs.set_val(Aircraft.Engine.SCALE_FACTOR, 1.0) -outputs.set_val( - Aircraft.Propulsion.TOTAL_ENGINE_MASS, engine_mass * num_engines, engine_mass_units) +outputs.set_val(Aircraft.Propulsion.TOTAL_ENGINE_MASS, 8071.35 * 2, 'lbm') outputs.set_val(Aircraft.VerticalTail.CHARACTERISTIC_LENGTH, 11.30, 'ft') outputs.set_val(Aircraft.VerticalTail.FINENESS, 0.1375) @@ -422,10 +406,3 @@ # Aircraft.Design.SYSTEMS_EQUIP_MASS_BASE, # Aircraft.Propulsion.MASS]: # inputs.set_val(key, outputs[key] - -# Create engine model -engine_inputs.set_val(Settings.VERBOSITY, 0) -engine = EngineDeck(name='engine', - options=engine_inputs - ) -preprocess_propulsion(inputs, [engine]) diff --git a/aviary/models/large_single_aisle_2/large_single_aisle_2_detailwing_FLOPS_data.py b/aviary/models/large_single_aisle_2/large_single_aisle_2_detailwing_FLOPS_data.py index bc3ffcb52..a2981ebb4 100644 --- a/aviary/models/large_single_aisle_2/large_single_aisle_2_detailwing_FLOPS_data.py +++ b/aviary/models/large_single_aisle_2/large_single_aisle_2_detailwing_FLOPS_data.py @@ -1,8 +1,6 @@ import numpy as np -from aviary.subsystems.propulsion.engine_deck import EngineDeck from aviary.utils.aviary_values import AviaryValues -from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.functions import get_path from aviary.variable_info.enums import EquationsOfMotion, LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Settings @@ -149,46 +147,45 @@ filename = get_path( 'models/engines/turbofan_24k_1.deck') -engine_inputs = AviaryValues() -engine_inputs.set_val(Aircraft.Engine.DATA_FILE, filename) +inputs.set_val(Aircraft.Engine.DATA_FILE, filename) engine_mass = 8071.35 engine_mass_units = 'lbm' -engine_inputs.set_val(Aircraft.Engine.MASS, engine_mass, engine_mass_units) -engine_inputs.set_val( +inputs.set_val(Aircraft.Engine.MASS, engine_mass, engine_mass_units) +inputs.set_val( Aircraft.Engine.REFERENCE_MASS, engine_mass, engine_mass_units) scaled_sls_thrust = 27301.0 scaled_sls_thrust_units = 'lbf' -engine_inputs.set_val( +inputs.set_val( Aircraft.Engine.SCALED_SLS_THRUST, scaled_sls_thrust, scaled_sls_thrust_units) -engine_inputs.set_val( +inputs.set_val( Aircraft.Engine.REFERENCE_SLS_THRUST, scaled_sls_thrust, scaled_sls_thrust_units) num_engines = 2 -engine_inputs.set_val(Aircraft.Engine.NUM_ENGINES, num_engines) +inputs.set_val(Aircraft.Engine.NUM_ENGINES, num_engines) num_fuselage_engines = 0 -engine_inputs.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, num_fuselage_engines) +inputs.set_val(Aircraft.Engine.NUM_FUSELAGE_ENGINES, num_fuselage_engines) num_wing_engines = num_engines -engine_inputs.set_val(Aircraft.Engine.NUM_WING_ENGINES, num_wing_engines) -engine_inputs.set_val(Aircraft.Engine.WING_LOCATIONS, 0.28131) -engine_inputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS_SCALER, 1.0) -engine_inputs.set_val(Aircraft.Engine.SCALE_MASS, True) -engine_inputs.set_val(Aircraft.Engine.MASS_SCALER, 1.15) -engine_inputs.set_val(Aircraft.Engine.SCALE_PERFORMANCE, True) -engine_inputs.set_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER, 1.0) -engine_inputs.set_val(Aircraft.Engine.SUPERSONIC_FUEL_FLOW_SCALER, 1.0) -engine_inputs.set_val( +inputs.set_val(Aircraft.Engine.NUM_WING_ENGINES, num_wing_engines) +inputs.set_val(Aircraft.Engine.WING_LOCATIONS, 0.28131) +inputs.set_val(Aircraft.Engine.THRUST_REVERSERS_MASS_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.SCALE_MASS, True) +inputs.set_val(Aircraft.Engine.MASS_SCALER, 1.15) +inputs.set_val(Aircraft.Engine.SCALE_PERFORMANCE, True) +inputs.set_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER, 1.0) +inputs.set_val(Aircraft.Engine.SUPERSONIC_FUEL_FLOW_SCALER, 1.0) +inputs.set_val( Aircraft.Engine.FUEL_FLOW_SCALER_CONSTANT_TERM, 0.0) -engine_inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, 0.0) -engine_inputs.set_val(Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, 0.0, units='lbm/h') -engine_inputs.set_val(Aircraft.Engine.ADDITIONAL_MASS_FRACTION, 0.0) -engine_inputs.set_val(Aircraft.Engine.GENERATE_FLIGHT_IDLE, True) -engine_inputs.set_val(Aircraft.Engine.IGNORE_NEGATIVE_THRUST, False) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_THRUST_FRACTION, 0.0) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MAX_FRACTION, 1.0) -engine_inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) -engine_inputs.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) -engine_inputs.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') +inputs.set_val(Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, 0.0) +inputs.set_val(Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, 0.0, units='lbm/h') +inputs.set_val(Aircraft.Engine.ADDITIONAL_MASS_FRACTION, 0.0) +inputs.set_val(Aircraft.Engine.GENERATE_FLIGHT_IDLE, True) +inputs.set_val(Aircraft.Engine.IGNORE_NEGATIVE_THRUST, False) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_THRUST_FRACTION, 0.0) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MAX_FRACTION, 1.0) +inputs.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) +inputs.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) +inputs.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') # Vertical Tail @@ -410,10 +407,3 @@ outputs.set_val(Mission.Design.MACH, 0.799) outputs.set_val(Mission.Design.LIFT_COEFFICIENT, 0.523) - -# Create engine model -engine_inputs.set_val(Settings.VERBOSITY, 0) -engine = EngineDeck(name='engine', - options=engine_inputs - ) -preprocess_propulsion(inputs, [engine]) diff --git a/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py b/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py index ec135c5f6..86a7d4cf7 100644 --- a/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py +++ b/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py @@ -1,8 +1,6 @@ import numpy as np -from aviary.subsystems.propulsion.engine_deck import EngineDeck from aviary.utils.aviary_values import AviaryValues -from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.functions import get_path from aviary.variable_info.enums import EquationsOfMotion, LegacyCode, Verbosity from aviary.variable_info.variables import Aircraft, Mission, Settings @@ -419,15 +417,3 @@ outputs.set_val(Mission.Design.MACH, 0.800) outputs.set_val(Mission.Design.LIFT_COEFFICIENT, 0.568) - -# Create engine model -engine_1_inputs.set_val(Settings.VERBOSITY, 0) -engine1 = EngineDeck(name='engine_1', - options=engine_1_inputs - ) -# Create engine model -engine_2_inputs.set_val(Settings.VERBOSITY, 0) -engine2 = EngineDeck(name='engine_2', - options=engine_2_inputs - ) -preprocess_propulsion(inputs, [engine1, engine2]) diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py index d74ca7f62..d38d8347f 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py @@ -5,12 +5,13 @@ from openmdao.utils.assert_utils import assert_near_equal from aviary.subsystems.premission import CorePreMission -from aviary.interface.default_phase_info.height_energy import aero, prop, geom from aviary.utils.aviary_values import get_items from aviary.utils.functions import set_aviary_initial_values from aviary.validation_cases.validation_tests import get_flops_inputs, get_flops_outputs from aviary.variable_info.variables import Aircraft, Dynamic, Settings from aviary.variable_info.variables_in import VariablesIn +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems class MissionDragTest(unittest.TestCase): @@ -29,6 +30,12 @@ def test_basic_large_single_aisle_1(self): flops_inputs.set_val(key, *(flops_outputs.get_item(key))) flops_inputs.set_val(Settings.VERBOSITY, 0) + # don't need mass subsystem, so we skip it + default_premission_subsystems = get_default_premission_subsystems( + 'FLOPS', build_engine_deck(flops_inputs))[:-1] + # we just want aero for mission, make a copy by itself + aero = default_premission_subsystems[-1] + # Design conditions: alt = 41000 # mach = 0.79 @@ -56,13 +63,11 @@ def test_basic_large_single_aisle_1(self): prob = om.Problem() model = prob.model - core_subsystems = [prop, geom, aero] - # Upstream static analysis for aero prob.model.add_subsystem( 'pre_mission', CorePreMission(aviary_options=flops_inputs, - subsystems=core_subsystems), + subsystems=default_premission_subsystems), promotes_inputs=['aircraft:*', 'mission:*'], promotes_outputs=['aircraft:*', 'mission:*']) @@ -162,6 +167,12 @@ def test_n3cc_drag(self): flops_inputs.set_val(key, *(flops_outputs.get_item(key))) flops_inputs.set_val(Settings.VERBOSITY, 0) + # don't need mass subsystem, so we skip it + default_premission_subsystems = get_default_premission_subsystems( + 'FLOPS', build_engine_deck(flops_inputs))[:-1] + # we just want aero for mission, make a copy by itself + aero = default_premission_subsystems[-1] + alt = 43000 Sref = 1220.0 @@ -185,13 +196,11 @@ def test_n3cc_drag(self): prob = om.Problem() model = prob.model - core_subsystems = [prop, geom, aero] - # Upstream static analysis for aero prob.model.add_subsystem( 'pre_mission', CorePreMission(aviary_options=flops_inputs, - subsystems=core_subsystems), + subsystems=default_premission_subsystems), promotes_inputs=['aircraft:*', 'mission:*'], promotes_outputs=['aircraft:*', 'mission:*']) @@ -277,6 +286,12 @@ def test_large_single_aisle_2_drag(self): flops_inputs.set_val(key, *(flops_outputs.get_item(key))) flops_inputs.set_val(Settings.VERBOSITY, 0) + # don't need mass subsystem, so we skip it + default_premission_subsystems = get_default_premission_subsystems( + 'FLOPS', build_engine_deck(flops_inputs))[:-1] + # we just want aero for mission, make a copy by itself + aero = default_premission_subsystems[-1] + alt = 41000 Sref = 1341.0 @@ -302,13 +317,11 @@ def test_large_single_aisle_2_drag(self): prob = om.Problem() model = prob.model - core_subsystems = [prop, geom, aero] - # Upstream static analysis for aero prob.model.add_subsystem( 'pre_mission', CorePreMission(aviary_options=flops_inputs, - subsystems=core_subsystems), + subsystems=default_premission_subsystems), promotes_inputs=['aircraft:*', 'mission:*'], promotes_outputs=['aircraft:*', 'mission:*']) diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py index ff631d241..d661f93da 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py @@ -8,7 +8,8 @@ from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.subsystems.premission import CorePreMission -from aviary.interface.default_phase_info.height_energy import aero, prop, geom +from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.aviary_values import AviaryValues, get_items from aviary.utils.functions import set_aviary_initial_values from aviary.utils.named_values import NamedValues @@ -164,7 +165,7 @@ class ComputedVsTabularTest(unittest.TestCase): @parameterized.expand(data_sets, name_func=print_case) def test_case(self, case_name): - flops_inputs = get_flops_inputs(case_name) + flops_inputs = get_flops_inputs(case_name, preprocess=True) flops_outputs = get_flops_outputs(case_name) flops_inputs.set_val(Aircraft.Design.LIFT_DEPENDENT_DRAG_COEFF_FACTOR, 0.9) @@ -588,13 +589,16 @@ def setup(self): gamma = options['gamma'] aviary_options: AviaryValues = options['aviary_options'] - core_subsystems = [prop, geom, aero] + engine = build_engine_deck(aviary_options) + # don't need mass, skip it + default_premission_subsystems = get_default_premission_subsystems('FLOPS', engine)[ + :-1] # Upstream static analysis for aero pre_mission: om.Group = self.add_subsystem( 'pre_mission', CorePreMission(aviary_options=aviary_options, - subsystems=core_subsystems), + subsystems=default_premission_subsystems), promotes_inputs=['aircraft:*', 'mission:*'], promotes_outputs=['aircraft:*', 'mission:*']) diff --git a/aviary/subsystems/geometry/flops_based/test/test_prep_geom.py b/aviary/subsystems/geometry/flops_based/test/test_prep_geom.py index bca5ef42f..c20ae0433 100644 --- a/aviary/subsystems/geometry/flops_based/test/test_prep_geom.py +++ b/aviary/subsystems/geometry/flops_based/test/test_prep_geom.py @@ -26,6 +26,8 @@ print_case) from aviary.variable_info.functions import override_aviary_vars from aviary.variable_info.variables import Aircraft +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.preprocessors import preprocess_options unit_data_sets = get_flops_case_names( only=['LargeSingleAisle2FLOPS', 'LargeSingleAisle2FLOPSdw', 'LargeSingleAisle2FLOPSalt', 'LargeSingleAisle1FLOPS']) @@ -61,7 +63,7 @@ def configure(self): override_aviary_vars(self, aviary_options) - options = get_flops_data(case_name, + options = get_flops_data(case_name, preprocess=True, keys=[ Aircraft.Fuselage.NUM_FUSELAGES, Aircraft.Propulsion.TOTAL_NUM_FUSELAGE_ENGINES, @@ -300,7 +302,7 @@ def test_case(self, case_name): prob.model.add_subsystem( 'tails', - _Tail(aviary_options=get_flops_inputs(case_name, + _Tail(aviary_options=get_flops_inputs(case_name, preprocess=True, keys=[Aircraft.Wing.SPAN_EFFICIENCY_REDUCTION, Aircraft.Propulsion.TOTAL_NUM_FUSELAGE_ENGINES])), promotes=['*']) @@ -396,10 +398,14 @@ def test_case(self, case_name): prob = self.prob + flops_inputs = get_flops_inputs(case_name, preprocess=True, + keys=[Aircraft.Engine.NUM_ENGINES, + Aircraft.Fuselage.NUM_FUSELAGES, + ]) + prob.model.add_subsystem( 'nacelles', - Nacelles(aviary_options=get_flops_inputs(case_name, - keys=[Aircraft.Engine.NUM_ENGINES])), + Nacelles(aviary_options=flops_inputs), promotes=['*']) prob.setup(check=False, force_alloc_complex=True) @@ -462,14 +468,16 @@ def test_case(self, case_name): prob = self.prob + flops_inputs = get_flops_inputs(case_name, preprocess=True, + keys=[Aircraft.Engine.NUM_ENGINES, + Aircraft.Fuselage.NUM_FUSELAGES, + Aircraft.VerticalTail.NUM_TAILS, + Aircraft.Wing.SPAN_EFFICIENCY_REDUCTION, + ]) + prob.model.add_subsystem( 'characteristic_lengths', - CharacteristicLengths(aviary_options=get_flops_inputs(case_name, - keys=[Aircraft.Engine.NUM_ENGINES, - Aircraft.Fuselage.NUM_FUSELAGES, - Aircraft.VerticalTail.NUM_TAILS, - Aircraft.Wing.SPAN_EFFICIENCY_REDUCTION, - "engine_models"])), + CharacteristicLengths(aviary_options=flops_inputs), promotes=['*'] ) diff --git a/aviary/subsystems/mass/flops_based/landing_gear.py b/aviary/subsystems/mass/flops_based/landing_gear.py index 56c9e4879..a69319eb9 100644 --- a/aviary/subsystems/mass/flops_based/landing_gear.py +++ b/aviary/subsystems/mass/flops_based/landing_gear.py @@ -278,15 +278,16 @@ def initialize(self): desc='collection of Aircraft/Mission specific options') def setup(self): - count = len(self.options['aviary_options'].get_val('engine_models')) + engine_count = len(self.options['aviary_options'].get_val( + Aircraft.Engine.NUM_ENGINES)) num_wing_engines = self.options['aviary_options'].get_val( Aircraft.Engine.NUM_WING_ENGINES) add_aviary_input(self, Aircraft.Fuselage.LENGTH, val=0.0) add_aviary_input(self, Aircraft.Fuselage.MAX_WIDTH, val=0.0) - add_aviary_input(self, Aircraft.Nacelle.AVG_DIAMETER, val=np.zeros(count)) + add_aviary_input(self, Aircraft.Nacelle.AVG_DIAMETER, val=np.zeros(engine_count)) add_aviary_input(self, Aircraft.Engine.WING_LOCATIONS, - val=np.zeros((count, int(num_wing_engines[0]/2)))) + val=np.zeros((engine_count, int(num_wing_engines[0]/2)))) add_aviary_input(self, Aircraft.Wing.DIHEDRAL, val=0.0) add_aviary_input(self, Aircraft.Wing.SPAN, val=0.0) diff --git a/aviary/subsystems/mass/flops_based/starter.py b/aviary/subsystems/mass/flops_based/starter.py index 97f66155a..7e53b0a37 100644 --- a/aviary/subsystems/mass/flops_based/starter.py +++ b/aviary/subsystems/mass/flops_based/starter.py @@ -22,9 +22,10 @@ def initialize(self): desc='collection of Aircraft/Mission specific options') def setup(self): - count = len(self.options['aviary_options'].get_val('engine_models')) + engine_count = len(self.options['aviary_options'].get_val( + Aircraft.Engine.NUM_ENGINES)) - add_aviary_input(self, Aircraft.Nacelle.AVG_DIAMETER, val=np.zeros(count)) + add_aviary_input(self, Aircraft.Nacelle.AVG_DIAMETER, val=np.zeros(engine_count)) add_aviary_output(self, Aircraft.Propulsion.TOTAL_STARTER_MASS, val=0.0) diff --git a/aviary/subsystems/mass/flops_based/test/test_anti_icing.py b/aviary/subsystems/mass/flops_based/test/test_anti_icing.py index 43a33a0d6..8f37f3985 100644 --- a/aviary/subsystems/mass/flops_based/test/test_anti_icing.py +++ b/aviary/subsystems/mass/flops_based/test/test_anti_icing.py @@ -28,7 +28,7 @@ def test_case(self, case_name): prob.model.add_subsystem( "anti_icing", - AntiIcingMass(aviary_options=get_flops_inputs(case_name)), + AntiIcingMass(aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'], ) diff --git a/aviary/subsystems/mass/flops_based/test/test_engine.py b/aviary/subsystems/mass/flops_based/test/test_engine.py index 6675a1176..74cf0bdf2 100644 --- a/aviary/subsystems/mass/flops_based/test/test_engine.py +++ b/aviary/subsystems/mass/flops_based/test/test_engine.py @@ -31,7 +31,7 @@ def test_case(self, case_name): prob.model.add_subsystem( "engine_mass", - EngineMass(aviary_options=get_flops_inputs(case_name)), + EngineMass(aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'], ) diff --git a/aviary/subsystems/mass/flops_based/test/test_engine_controls.py b/aviary/subsystems/mass/flops_based/test/test_engine_controls.py index 45822248c..cc863cdc1 100644 --- a/aviary/subsystems/mass/flops_based/test/test_engine_controls.py +++ b/aviary/subsystems/mass/flops_based/test/test_engine_controls.py @@ -24,7 +24,7 @@ def setUp(self): @parameterized.expand(get_flops_case_names(omit='N3CC'), name_func=print_case) def test_case(self, case_name): - flops_inputs = get_flops_inputs(case_name) + flops_inputs = get_flops_inputs(case_name, preprocess=True) prob = self.prob diff --git a/aviary/subsystems/mass/flops_based/test/test_engine_oil.py b/aviary/subsystems/mass/flops_based/test/test_engine_oil.py index 1f46699dc..c75c44c43 100644 --- a/aviary/subsystems/mass/flops_based/test/test_engine_oil.py +++ b/aviary/subsystems/mass/flops_based/test/test_engine_oil.py @@ -28,9 +28,12 @@ def test_case(self, case_name): prob = self.prob + options = get_flops_inputs(case_name) + options.set_val(Aircraft.Propulsion.TOTAL_NUM_ENGINES, 2) + prob.model.add_subsystem( 'engine_oil', - TransportEngineOilMass(aviary_options=get_flops_inputs(case_name)), + TransportEngineOilMass(aviary_options=options), promotes_outputs=['*'], promotes_inputs=['*'] ) @@ -64,9 +67,12 @@ def test_case(self, case_name): prob = self.prob + options = get_flops_inputs(case_name) + options.set_val(Aircraft.Propulsion.TOTAL_NUM_ENGINES, 2) + prob.model.add_subsystem( 'engine_oil', - AltEngineOilMass(aviary_options=get_flops_inputs(case_name)), + AltEngineOilMass(aviary_options=options), promotes_outputs=['*'], promotes_inputs=['*'] ) @@ -76,7 +82,7 @@ def test_case(self, case_name): flops_validation_test( prob, case_name, - input_keys=[Aircraft.Propulsion.ENGINE_OIL_MASS_SCALER], + input_keys=[Aircraft.Propulsion.ENGINE_OIL_MASS_SCALER,], output_keys=[Aircraft.Propulsion.TOTAL_ENGINE_OIL_MASS], version=Version.ALTERNATE) diff --git a/aviary/subsystems/mass/flops_based/test/test_engine_pod.py b/aviary/subsystems/mass/flops_based/test/test_engine_pod.py index 840dea329..7882b7178 100644 --- a/aviary/subsystems/mass/flops_based/test/test_engine_pod.py +++ b/aviary/subsystems/mass/flops_based/test/test_engine_pod.py @@ -32,7 +32,7 @@ def test_case(self, case_name): prob.model.add_subsystem( 'engine_pod', - EnginePodMass(aviary_options=get_flops_inputs(case_name)), + EnginePodMass(aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_outputs=['*'], promotes_inputs=['*'] ) diff --git a/aviary/subsystems/mass/flops_based/test/test_fuel_system.py b/aviary/subsystems/mass/flops_based/test/test_fuel_system.py index 3016a7dbc..ef492ab6d 100644 --- a/aviary/subsystems/mass/flops_based/test/test_fuel_system.py +++ b/aviary/subsystems/mass/flops_based/test/test_fuel_system.py @@ -27,7 +27,8 @@ def test_case(self, case_name): prob.model.add_subsystem( "alt_fuel_sys_test", - AltFuelSystemMass(aviary_options=get_flops_inputs(case_name)), + AltFuelSystemMass(aviary_options=get_flops_inputs( + case_name, preprocess=True)), promotes_outputs=['*'], promotes_inputs=['*'] ) @@ -59,7 +60,8 @@ def test_case(self, case_name): prob.model.add_subsystem( "transport_fuel_sys_test", - TransportFuelSystemMass(aviary_options=get_flops_inputs(case_name)), + TransportFuelSystemMass( + aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_outputs=['*'], promotes_inputs=['*'] ) diff --git a/aviary/subsystems/mass/flops_based/test/test_fuselage.py b/aviary/subsystems/mass/flops_based/test/test_fuselage.py index fb3d6652a..7b965fd46 100644 --- a/aviary/subsystems/mass/flops_based/test/test_fuselage.py +++ b/aviary/subsystems/mass/flops_based/test/test_fuselage.py @@ -27,7 +27,8 @@ def test_case(self, case_name): prob.model.add_subsystem( "fuselage", - TransportFuselageMass(aviary_options=get_flops_inputs(case_name)), + TransportFuselageMass(aviary_options=get_flops_inputs( + case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'], ) diff --git a/aviary/subsystems/mass/flops_based/test/test_hydraulics.py b/aviary/subsystems/mass/flops_based/test/test_hydraulics.py index 783bd7b03..506cf87f8 100644 --- a/aviary/subsystems/mass/flops_based/test/test_hydraulics.py +++ b/aviary/subsystems/mass/flops_based/test/test_hydraulics.py @@ -42,7 +42,8 @@ def test_case(self, case_name): prob.model.add_subsystem( 'hydraulics', - TransportHydraulicsGroupMass(aviary_options=get_flops_inputs(case_name)), + TransportHydraulicsGroupMass( + aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_outputs=['*'], promotes_inputs=['*'] ) @@ -81,7 +82,8 @@ def test_case(self, case_name): prob.model.add_subsystem( 'hydraulics', - AltHydraulicsGroupMass(aviary_options=get_flops_inputs(case_name)), + AltHydraulicsGroupMass( + aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_outputs=['*'], promotes_inputs=['*'] ) diff --git a/aviary/subsystems/mass/flops_based/test/test_mass_summation.py b/aviary/subsystems/mass/flops_based/test/test_mass_summation.py index a09142881..ef5ebe344 100644 --- a/aviary/subsystems/mass/flops_based/test/test_mass_summation.py +++ b/aviary/subsystems/mass/flops_based/test/test_mass_summation.py @@ -30,7 +30,7 @@ def test_case(self, case_name): prob.model.add_subsystem( "tot", - MassSummation(aviary_options=get_flops_inputs(case_name)), + MassSummation(aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'], ) @@ -101,7 +101,7 @@ def test_case(self, case_name): prob.model.add_subsystem( "tot", - MassSummation(aviary_options=get_flops_inputs(case_name)), + MassSummation(aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'], ) diff --git a/aviary/subsystems/mass/flops_based/test/test_misc_engine.py b/aviary/subsystems/mass/flops_based/test/test_misc_engine.py index a0beeea3d..dc90b0b99 100644 --- a/aviary/subsystems/mass/flops_based/test/test_misc_engine.py +++ b/aviary/subsystems/mass/flops_based/test/test_misc_engine.py @@ -30,7 +30,7 @@ def test_case(self, case_name): prob.model.add_subsystem( "misc_mass", - EngineMiscMass(aviary_options=get_flops_inputs(case_name)), + EngineMiscMass(aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'] ) diff --git a/aviary/subsystems/mass/flops_based/test/test_nacelle.py b/aviary/subsystems/mass/flops_based/test/test_nacelle.py index abaa948f9..4c101016f 100644 --- a/aviary/subsystems/mass/flops_based/test/test_nacelle.py +++ b/aviary/subsystems/mass/flops_based/test/test_nacelle.py @@ -30,7 +30,7 @@ def test_case(self, case_name): prob.model.add_subsystem( "nacelle", - NacelleMass(aviary_options=get_flops_inputs(case_name)), + NacelleMass(aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'], ) diff --git a/aviary/subsystems/mass/flops_based/test/test_starter.py b/aviary/subsystems/mass/flops_based/test/test_starter.py index 2a9f5ad8c..e069767ed 100644 --- a/aviary/subsystems/mass/flops_based/test/test_starter.py +++ b/aviary/subsystems/mass/flops_based/test/test_starter.py @@ -27,7 +27,8 @@ def test_case_1(self, case_name): prob.model.add_subsystem( "starter_test", - TransportStarterMass(aviary_options=get_flops_inputs(case_name)), + TransportStarterMass(aviary_options=get_flops_inputs( + case_name, preprocess=True)), promotes_outputs=['*'], promotes_inputs=['*'] ) diff --git a/aviary/subsystems/mass/flops_based/test/test_thrust_reverser.py b/aviary/subsystems/mass/flops_based/test/test_thrust_reverser.py index a55e6f62f..02e9e290b 100644 --- a/aviary/subsystems/mass/flops_based/test/test_thrust_reverser.py +++ b/aviary/subsystems/mass/flops_based/test/test_thrust_reverser.py @@ -30,7 +30,8 @@ def test_case(self, case_name): prob.model.add_subsystem( "thrust_rev", - ThrustReverserMass(aviary_options=get_flops_inputs(case_name)), + ThrustReverserMass(aviary_options=get_flops_inputs( + case_name, preprocess=True)), promotes=['*'] ) diff --git a/aviary/subsystems/mass/flops_based/test/test_unusable_fuel.py b/aviary/subsystems/mass/flops_based/test/test_unusable_fuel.py index a9bfeee81..ba947524c 100644 --- a/aviary/subsystems/mass/flops_based/test/test_unusable_fuel.py +++ b/aviary/subsystems/mass/flops_based/test/test_unusable_fuel.py @@ -37,7 +37,7 @@ def setUp(self): def test_case(self, case_name): prob = self.prob - flops_inputs = get_flops_inputs(case_name) + flops_inputs = get_flops_inputs(case_name, preprocess=True) prob.model.add_subsystem( 'unusable_fuel', diff --git a/aviary/subsystems/mass/flops_based/test/test_wing_detailed.py b/aviary/subsystems/mass/flops_based/test/test_wing_detailed.py index d3386e4e6..9ee3c9832 100644 --- a/aviary/subsystems/mass/flops_based/test/test_wing_detailed.py +++ b/aviary/subsystems/mass/flops_based/test/test_wing_detailed.py @@ -33,7 +33,8 @@ def test_case(self, case_name): self.prob.model.add_subsystem( "wing", - DetailedWingBendingFact(aviary_options=get_flops_inputs(case_name)), + DetailedWingBendingFact( + aviary_options=get_flops_inputs(case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'], ) @@ -128,8 +129,8 @@ def test_IO(self): if __name__ == "__main__": - unittest.main() - # test = DetailedWingBendingTest() - # test.setUp() + # unittest.main() + test = DetailedWingBendingTest() + test.setUp() # test.test_case(case_name='LargeSingleAisle1FLOPS') - # test.test_case_multiengine() + test.test_case_multiengine() diff --git a/aviary/subsystems/mass/flops_based/test/test_wing_simple.py b/aviary/subsystems/mass/flops_based/test/test_wing_simple.py index 6e3321ac7..da5d4148b 100644 --- a/aviary/subsystems/mass/flops_based/test/test_wing_simple.py +++ b/aviary/subsystems/mass/flops_based/test/test_wing_simple.py @@ -25,7 +25,8 @@ def test_case(self, case_name): prob.model.add_subsystem( "wing", - SimpleWingBendingFact(aviary_options=get_flops_inputs(case_name)), + SimpleWingBendingFact(aviary_options=get_flops_inputs( + case_name, preprocess=True)), promotes_inputs=['*'], promotes_outputs=['*'], ) diff --git a/aviary/subsystems/mass/gasp_based/equipment_and_useful_load.py b/aviary/subsystems/mass/gasp_based/equipment_and_useful_load.py index c402b3a73..564daaac4 100644 --- a/aviary/subsystems/mass/gasp_based/equipment_and_useful_load.py +++ b/aviary/subsystems/mass/gasp_based/equipment_and_useful_load.py @@ -25,7 +25,8 @@ def initialize(self): ) def setup(self): - count = len(self.options['aviary_options'].get_val('engine_models')) + engine_count = len(self.options['aviary_options'].get_val( + Aircraft.Engine.NUM_ENGINES)) add_aviary_input( self, Aircraft.AirConditioning.MASS_COEFFICIENT, val=1, units="unitless") @@ -61,7 +62,7 @@ def setup(self): add_aviary_input(self, Aircraft.Fuselage.PRESSURE_DIFFERENTIAL, val=7.5) add_aviary_input(self, Aircraft.Fuselage.AVG_DIAMETER, val=13.1) add_aviary_input(self, Aircraft.Engine.SCALED_SLS_THRUST, - val=np.full(count, 28690)) + val=np.full(engine_count, 28690)) add_aviary_input(self, Aircraft.Fuel.WING_FUEL_FRACTION, val=0.5) add_aviary_input(self, Aircraft.Design.EXTERNAL_SUBSYSTEMS_MASS, val=0.) diff --git a/aviary/subsystems/mass/gasp_based/test/test_mass_summation.py b/aviary/subsystems/mass/gasp_based/test/test_mass_summation.py index a47217206..87ab1210d 100644 --- a/aviary/subsystems/mass/gasp_based/test/test_mass_summation.py +++ b/aviary/subsystems/mass/gasp_based/test/test_mass_summation.py @@ -42,9 +42,8 @@ def setUp(self): ) for (key, (val, units)) in get_items(V3_bug_fixed_options): - if key != 'engine_models': - if not is_option(key): - self.prob.model.set_input_defaults(key, val=val, units=units) + if not is_option(key): + self.prob.model.set_input_defaults(key, val=val, units=units) for (key, (val, units)) in get_items(V3_bug_fixed_non_metadata): self.prob.model.set_input_defaults(key, val=val, units=units) diff --git a/aviary/subsystems/propulsion/engine_deck.py b/aviary/subsystems/propulsion/engine_deck.py index f3edf80c5..6daff15f7 100644 --- a/aviary/subsystems/propulsion/engine_deck.py +++ b/aviary/subsystems/propulsion/engine_deck.py @@ -27,7 +27,6 @@ import numpy as np import openmdao.api as om -from openmdao.core.system import System from openmdao.utils.units import convert_units @@ -40,7 +39,6 @@ from aviary.utils.aviary_values import AviaryValues, NamedValues, get_keys, get_items from aviary.variable_info.variable_meta_data import _MetaData from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings -from aviary.variable_info.enums import Verbosity from aviary.utils.csv_data_file import read_data_file from aviary.interface.utils.markdown_utils import round_it @@ -1017,22 +1015,23 @@ def get_parameters(self): def report(self, problem, reports_file, **kwargs): meta_data = kwargs['meta_data'] + engine_idx = kwargs['engine_idx'] outputs = [Aircraft.Engine.NUM_ENGINES, Aircraft.Engine.SCALED_SLS_THRUST, Aircraft.Engine.SCALE_FACTOR] # determine which index in problem-level aviary values corresponds to this engine - engine_idx = None - for idx, engine in enumerate(problem.aviary_inputs.get_val('engine_models')): - if engine.name == self.name: - engine_idx = idx + # engine_idx = None + # for idx, engine in enumerate(problem.aviary_inputs.get_val('engine_models')): + # if engine.name == self.name: + # engine_idx = idx - if engine_idx is None: - with open(reports_file, mode='a') as f: - f.write(f'\n### {self.name}') - f.write(f'\nEngine deck {self.name} not found\n') - return + # if engine_idx is None: + # with open(reports_file, mode='a') as f: + # f.write(f'\n### {self.name}') + # f.write(f'\nEngine deck {self.name} not found\n') + # return # modified version of markdown table util adjusted to handle engine decks with open(reports_file, mode='a') as f: @@ -1150,8 +1149,8 @@ def _set_reference_thrust(self): # both scale factor and target thrust provided: if thrust_provided: scaled_thrust = self.get_val(Aircraft.Engine.SCALED_SLS_THRUST, 'lbf') - if scale_performance: - if not math.isclose(scaled_thrust/ref_thrust, scale_factor): + if scale_performance: # using very rough tolerance + if not math.isclose(scaled_thrust/ref_thrust, scale_factor, abs_tol=1e-2): # user wants scaling but provided conflicting inputs, # cannot be resolved raise AttributeError( @@ -1159,6 +1158,11 @@ def _set_reference_thrust(self): 'aircraft:engine:scale_factor and ' 'aircraft:engine:scaled_sls_thrust' ) + # get thrust target & scale factor matching exactly. Scale factor is + # design variable, so don't touch it!! Instead change output thrust + else: + self.set_val(Aircraft.Engine.SCALED_SLS_THRUST, + ref_thrust*scale_factor, 'lbf') else: # engine is not scaled: just make sure scaled thrust = ref thrust self.set_val( diff --git a/aviary/subsystems/propulsion/engine_scaling.py b/aviary/subsystems/propulsion/engine_scaling.py index 1f39da284..7c3cf72dd 100644 --- a/aviary/subsystems/propulsion/engine_scaling.py +++ b/aviary/subsystems/propulsion/engine_scaling.py @@ -111,7 +111,6 @@ def compute(self, inputs, outputs): scale_factor = 1 fuel_flow_scale_factor = np.ones(nn, dtype=engine_scale_factor.dtype) - # scale_idx = np.where(scale_performance) # if len(scale_idx[0]) > 0: if scale_performance: @@ -129,19 +128,11 @@ def compute(self, inputs, outputs): supersonic_idx = np.where(mach_number >= 1.0) fuel_flow_mach_scaling[supersonic_idx] = supersonic_fuel_factor - # fuel_flow_scale_factor[:, scale_idx[0]] = engine_scale_factor[scale_idx]\ - # * fuel_flow_mach_scaling[:, scale_idx[0]]\ - # * fuel_flow_equation_scaling[scale_idx]\ - # * mission_fuel_scaler fuel_flow_scale_factor = engine_scale_factor * fuel_flow_mach_scaling\ * fuel_flow_equation_scaling * mission_fuel_scaler scale_factor = engine_scale_factor - # scale factor only applies if engine performance is scaled - default to 1 otherwise - # scale_factor = np.ones(count, dtype=engine_scale_factor.dtype) - # scale_factor[scale_idx] = engine_scale_factor[scale_idx] - outputs[Dynamic.Mission.THRUST] = unscaled_net_thrust * scale_factor outputs[Dynamic.Mission.THRUST_MAX] = unscaled_max_thrust * scale_factor # user-specified constant_fuel_flow value is currently not scaled with engine @@ -157,12 +148,8 @@ def compute(self, inputs, outputs): def setup_partials(self): nn = self.options['num_nodes'] - # options = self.options['aviary_options'] - # count = len(options.get_val('engine_models')) # number of unique engine models # matrix derivatives have known sparsity pattern - specified here - # r = np.arange(nn * count, dtype=int) - # c = np.tile(np.arange(count, dtype=int), (nn)) r = np.arange(nn) c = np.tile(0, nn) @@ -257,7 +244,6 @@ def setup_partials(self): def compute_partials(self, inputs, J): nn = self.options['num_nodes'] options: AviaryValues = self.options['aviary_options'] - # count = len(options.get_val('engine_models')) # number of unique engine models scale_performance = options.get_val(Aircraft.Engine.SCALE_PERFORMANCE) subsonic_fuel_factor = options.get_val(Aircraft.Engine.SUBSONIC_FUEL_FLOW_SCALER) diff --git a/aviary/subsystems/propulsion/engine_sizing.py b/aviary/subsystems/propulsion/engine_sizing.py index 727bb4b98..32ccff84e 100644 --- a/aviary/subsystems/propulsion/engine_sizing.py +++ b/aviary/subsystems/propulsion/engine_sizing.py @@ -36,7 +36,6 @@ def setup(self): def compute(self, inputs, outputs): options: AviaryValues = self.options['aviary_options'] - # engine_models = options.get_val('engine_models') scale_engine = options.get_val(Aircraft.Engine.SCALE_PERFORMANCE) reference_sls_thrust = options.get_val(Aircraft.Engine.REFERENCE_SLS_THRUST, @@ -44,17 +43,8 @@ def compute(self, inputs, outputs): scaled_sls_thrust = inputs[Aircraft.Engine.SCALED_SLS_THRUST] - # set a default scaling factor of 1 for each engine - # nm = len(engine_models) - - # use dtype to make complex safe - # engine_scale_factor = np.ones(nm, dtype=scaled_sls_thrust.dtype) - # Engine is only scaled if required # engine scale factor is ratio of scaled thrust target and reference thrust - # scale_idx = np.where(scale_engine) - # engine_scale_factor[scale_idx] = scaled_sls_thrust[scale_idx] / \ - # reference_sls_thrust[scale_idx] engine_scale_factor = 1 if scale_engine: engine_scale_factor = scaled_sls_thrust / reference_sls_thrust @@ -62,30 +52,15 @@ def compute(self, inputs, outputs): outputs[Aircraft.Engine.SCALE_FACTOR] = engine_scale_factor def setup_partials(self): - # count = len(self.options['aviary_options'].get_val('engine_models')) - - # shape = np.arange(count, dtype=int) - self.declare_partials(Aircraft.Engine.SCALE_FACTOR, Aircraft.Engine.SCALED_SLS_THRUST) - # rows=shape, cols=shape) def compute_partials(self, inputs, J): options: AviaryValues = self.options['aviary_options'] - # engine_models = options.get_val('engine_models') scale_engine = options.get_val(Aircraft.Engine.SCALE_PERFORMANCE) reference_sls_thrust = options.get_val( Aircraft.Engine.REFERENCE_SLS_THRUST, units='lbf') - # nm = len(engine_models) - - # scaled_sls_thrust = inputs[Aircraft.Engine.SCALED_SLS_THRUST] - # use dtype to make complex safe - # deriv_scale_factor = np.zeros(nm, dtype=scaled_sls_thrust.dtype) - - # scale_idx = np.where(scale_engine) - # deriv_scale_factor[scale_idx] = 1.0 / reference_sls_thrust[scale_idx] - deriv_scale_factor = 0 if scale_engine: deriv_scale_factor = 1.0 / reference_sls_thrust diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index 89b9f2e20..9c4f1f328 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -46,24 +46,19 @@ def mission_outputs(self, **kwargs): class CorePropulsionBuilder(PropulsionBuilderBase): # code_origin is not necessary for this subsystem, catch with kwargs and ignore - def __init__(self, name=None, meta_data=None, **kwargs): + def __init__(self, name=None, meta_data=None, engine_models=None, **kwargs): if name is None: name = 'core_propulsion' super().__init__(name=name, meta_data=meta_data) - try: - engine_models = kwargs['engine_models'] - except KeyError: - engine_models = None - else: - if not isinstance(engine_models, (list, np.ndarray)): - engine_models = [engine_models] + if not isinstance(engine_models, (list, np.ndarray)): + engine_models = [engine_models] - for engine in engine_models: - if not isinstance(engine, EngineModel): - raise UserWarning('Engine provided to propulsion builder is not an ' - 'EngineModel object') + for engine in engine_models: + if not isinstance(engine, EngineModel): + raise UserWarning('Engine provided to propulsion builder is not an ' + 'EngineModel object') self.engine_models = engine_models @@ -264,5 +259,6 @@ def report(self, prob, reports_folder, **kwargs): # each engine can append to this file kwargs['meta_data'] = self.meta_data - for engine in prob.aviary_inputs.get_val('engine_models'): + for idx, engine in enumerate(self.engine_models): + kwargs['engine_idx'] = idx engine.report(prob, filepath, **kwargs) diff --git a/aviary/subsystems/propulsion/propulsion_mission.py b/aviary/subsystems/propulsion/propulsion_mission.py index a3eae1b76..2d61b8762 100644 --- a/aviary/subsystems/propulsion/propulsion_mission.py +++ b/aviary/subsystems/propulsion/propulsion_mission.py @@ -208,7 +208,8 @@ def initialize(self): def setup(self): nn = self.options['num_nodes'] - engine_count = len(self.options['aviary_options'].get_val('engine_models')) + engine_count = len(self.options['aviary_options'].get_val( + Aircraft.Engine.NUM_ENGINES)) self.add_input(Dynamic.Mission.THRUST, val=np.zeros( (nn, engine_count)), units='lbf') diff --git a/aviary/subsystems/propulsion/propulsion_premission.py b/aviary/subsystems/propulsion/propulsion_premission.py index a7d90b25c..87ae97659 100644 --- a/aviary/subsystems/propulsion/propulsion_premission.py +++ b/aviary/subsystems/propulsion/propulsion_premission.py @@ -69,7 +69,8 @@ def configure(self): # so vectorized inputs/outputs are a problem. Slice all needed vector inputs and pass # pre_mission components only the value they need, then mux all the outputs back together - engine_count = len(self.options['aviary_options'].get_val('engine_models')) + engine_count = len(self.options['aviary_options'].get_val( + Aircraft.Engine.NUM_ENGINES)) # determine if openMDAO messages and warnings should be suppressed verbosity = self.options['aviary_options'].get_val(Settings.VERBOSITY) @@ -158,9 +159,11 @@ def initialize(self): desc='collection of Aircraft/Mission specific options') def setup(self): - count = len(self.options['aviary_options'].get_val('engine_models')) + engine_count = len(self.options['aviary_options'].get_val( + Aircraft.Engine.NUM_ENGINES)) - add_aviary_input(self, Aircraft.Engine.SCALED_SLS_THRUST, val=np.zeros(count)) + add_aviary_input(self, Aircraft.Engine.SCALED_SLS_THRUST, + val=np.zeros(engine_count)) add_aviary_output( self, Aircraft.Propulsion.TOTAL_SCALED_SLS_THRUST, val=0.0) diff --git a/aviary/subsystems/propulsion/test/test_data_interpolator.py b/aviary/subsystems/propulsion/test/test_data_interpolator.py index 914c4147d..f029b4184 100644 --- a/aviary/subsystems/propulsion/test/test_data_interpolator.py +++ b/aviary/subsystems/propulsion/test/test_data_interpolator.py @@ -13,6 +13,7 @@ from aviary.variable_info.variables import Dynamic from aviary.validation_cases.validation_data.flops_data.FLOPS_Test_Data import \ FLOPS_Test_Data +from aviary.subsystems.propulsion.utils import build_engine_deck class DataInterpolationTest(unittest.TestCase): @@ -21,7 +22,7 @@ def test_data_interpolation(self): aviary_values = FLOPS_Test_Data['LargeSingleAisle2FLOPS']['inputs'] - model = aviary_values.get_val('engine_models')[0] + model = build_engine_deck(aviary_values)[0] mach_number = model.data[keys.MACH] altitude = model.data[keys.ALTITUDE] diff --git a/aviary/subsystems/propulsion/test/test_engine_deck.py b/aviary/subsystems/propulsion/test/test_engine_deck.py index e994e8914..92bd20425 100644 --- a/aviary/subsystems/propulsion/test/test_engine_deck.py +++ b/aviary/subsystems/propulsion/test/test_engine_deck.py @@ -9,6 +9,7 @@ from aviary.utils.named_values import NamedValues from aviary.validation_cases.validation_data.flops_data.FLOPS_Test_Data import \ FLOPS_Test_Data +from aviary.subsystems.propulsion.utils import build_engine_deck class EngineDeckTest(unittest.TestCase): @@ -17,7 +18,7 @@ def test_flight_idle(self): aviary_values = FLOPS_Test_Data['LargeSingleAisle2FLOPS']['inputs'] - model = aviary_values.get_val('engine_models')[0] + model = build_engine_deck(aviary_values)[0] expected_mach_number = [] expected_altitude = [] @@ -53,7 +54,7 @@ def test_flight_idle_2(self): aviary_values = FLOPS_Test_Data['LargeSingleAisle1FLOPS']['inputs'] - model = aviary_values.get_val('engine_models')[0] + model = build_engine_deck(aviary_values)[0] # hardcoded data of processed engine model from LEAPS1 after flight idle # point generation, sorted in Aviary order diff --git a/aviary/subsystems/propulsion/test/test_propulsion_mission.py b/aviary/subsystems/propulsion/test/test_propulsion_mission.py index 495e877fb..bcd28127a 100644 --- a/aviary/subsystems/propulsion/test/test_propulsion_mission.py +++ b/aviary/subsystems/propulsion/test/test_propulsion_mission.py @@ -14,6 +14,7 @@ from aviary.utils.functions import get_path from aviary.validation_cases.validation_tests import get_flops_inputs from aviary.variable_info.variables import Aircraft, Dynamic, Mission +from aviary.subsystems.propulsion.utils import build_engine_deck class PropulsionMissionTest(unittest.TestCase): @@ -50,7 +51,8 @@ def test_case_1(self): engine = EngineDeck(options=options) preprocess_propulsion(options, [engine]) - self.prob.model = PropulsionMission(num_nodes=nn, aviary_options=options) + self.prob.model = PropulsionMission( + num_nodes=nn, aviary_options=options, engine_models=engine) IVC = om.IndepVarComp(Dynamic.Mission.MACH, np.linspace(0, 0.8, nn), @@ -99,8 +101,6 @@ def test_propulsion_sum(self): nn = 2 options = AviaryValues() options.set_val(Aircraft.Engine.NUM_ENGINES, np.array([3, 2])) - # it doesn't matter what goes in engine models, as long as it is length 2 - options.set_val('engine_models', [1, 1]) self.prob.model = om.Group() self.prob.model.add_subsystem('propsum', PropulsionSum(num_nodes=nn, @@ -151,12 +151,14 @@ def test_case_multiengine(self): options = get_flops_inputs('LargeSingleAisle2FLOPS') - engine = options.get_val('engine_models')[0] - engine2 = options.deepcopy().get_val('engine_models')[0] + engine = build_engine_deck(options)[0] + engine2 = build_engine_deck(options)[0] engine2.name = 'engine2' - preprocess_propulsion(options, [engine, engine2]) + engine_models = [engine, engine2] + preprocess_propulsion(options, engine_models=engine_models) - self.prob.model = PropulsionMission(num_nodes=20, aviary_options=options) + self.prob.model = PropulsionMission( + num_nodes=20, aviary_options=options, engine_models=engine_models) self.prob.model.add_subsystem(Dynamic.Mission.MACH, om.IndepVarComp(Dynamic.Mission.MACH, diff --git a/aviary/subsystems/propulsion/test/test_propulsion_premission.py b/aviary/subsystems/propulsion/test/test_propulsion_premission.py index 334cced04..a56a17d3a 100644 --- a/aviary/subsystems/propulsion/test/test_propulsion_premission.py +++ b/aviary/subsystems/propulsion/test/test_propulsion_premission.py @@ -9,7 +9,9 @@ from aviary.utils.aviary_values import AviaryValues from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.validation_cases.validation_tests import get_flops_inputs +from aviary.models.multi_engine_single_aisle.multi_engine_single_aisle_data import engine_1_inputs, engine_2_inputs from aviary.variable_info.variables import Aircraft, Settings +from aviary.utils.preprocessors import preprocess_options class PropulsionPreMissionTest(unittest.TestCase): @@ -17,9 +19,9 @@ def setUp(self): self.prob = om.Problem() def test_case(self): - aviary_values = get_flops_inputs('LargeSingleAisle2FLOPS') - aviary_values.set_val(Settings.VERBOSITY, 0) - options = aviary_values + options = get_flops_inputs('LargeSingleAisle2FLOPS') + options.set_val(Settings.VERBOSITY, 0) + options.set_val(Aircraft.Engine.NUM_ENGINES, np.array([2])) self.prob.model = PropulsionPreMission(aviary_options=options, engine_models=build_engine_deck(options)) @@ -40,13 +42,17 @@ def test_case(self): assert_check_partials(partial_data, atol=1e-10, rtol=1e-10) def test_multi_engine(self): - aviary_values = get_flops_inputs('MultiEngineSingleAisle') - aviary_values.set_val(Settings.VERBOSITY, 0) + options = get_flops_inputs('MultiEngineSingleAisle') + options.set_val(Settings.VERBOSITY, 0) + + engine1 = build_engine_deck(engine_1_inputs)[0] + engine2 = build_engine_deck(engine_2_inputs)[0] + engine_models = [engine1, engine2] - options = aviary_values + preprocess_options(options, engine_models=engine_models) self.prob.model = PropulsionPreMission(aviary_options=options, - engine_models=options.get_val('engine_models')) + engine_models=engine_models) self.prob.setup(force_alloc_complex=True) self.prob.set_val(Aircraft.Engine.SCALED_SLS_THRUST, options.get_val( @@ -90,7 +96,7 @@ def test_propulsion_sum(self): if __name__ == "__main__": - # unittest.main() - test = PropulsionPreMissionTest() - test.setUp() - test.test_case() + unittest.main() + # test = PropulsionPreMissionTest() + # test.setUp() + # test.test_case() diff --git a/aviary/subsystems/propulsion/utils.py b/aviary/subsystems/propulsion/utils.py index 01360d2a6..2caa6c9a2 100644 --- a/aviary/subsystems/propulsion/utils.py +++ b/aviary/subsystems/propulsion/utils.py @@ -5,6 +5,7 @@ Matches each EngineModelVariables entry with default units (str) """ from enum import Enum, auto +from pathlib import Path import numpy as np import openmdao.api as om @@ -150,7 +151,8 @@ def build_engine_deck(aviary_options: AviaryValues): except (KeyError, TypeError): continue - return [EngineDeck('engine_deck', options=engine_options)] + # name engine deck after filename + return [EngineDeck(Path(engine_options.get_val(Aircraft.Engine.DATA_FILE)).stem, options=engine_options)] class UncorrectData(om.Group): diff --git a/aviary/subsystems/test/test_flops_based_premission.py b/aviary/subsystems/test/test_flops_based_premission.py index c1887ba3c..d2544dac4 100644 --- a/aviary/subsystems/test/test_flops_based_premission.py +++ b/aviary/subsystems/test/test_flops_based_premission.py @@ -13,8 +13,9 @@ from aviary.variable_info.variables import Aircraft, Mission, Settings from aviary.variable_info.variables_in import VariablesIn from aviary.utils.functions import set_aviary_initial_values -from aviary.interface.default_phase_info.height_energy import default_premission_subsystems -from aviary.utils.preprocessors import preprocess_crewpayload +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems +from aviary.utils.preprocessors import preprocess_options class PreMissionGroupTest(unittest.TestCase): @@ -31,7 +32,10 @@ def test_case(self, case_name): flops_outputs.get_val(Aircraft.Propulsion.TOTAL_NUM_WING_ENGINES)) flops_inputs.set_val(Settings.VERBOSITY, 0.0) - preprocess_crewpayload(flops_inputs) + engine = build_engine_deck(flops_inputs) + preprocess_options(flops_inputs, engine_models=engine) + default_premission_subsystems = get_default_premission_subsystems( + 'FLOPS', engine) prob = self.prob @@ -104,6 +108,11 @@ def test_diff_configuration_mass(self): flops_outputs: AviaryValues = LargeSingleAisle2FLOPS['outputs'] flops_inputs.set_val(Settings.VERBOSITY, 0.0) + engine = build_engine_deck(flops_inputs) + preprocess_options(flops_inputs, engine_models=engine) + default_premission_subsystems = get_default_premission_subsystems( + 'FLOPS', engine) + prob.model.add_subsystem( "pre_mission", CorePreMission(aviary_options=flops_inputs, @@ -131,8 +140,6 @@ def test_diff_configuration_mass(self): # This is an option, not a variable continue - prob.run_model() - flops_validation_test( prob, "LargeSingleAisle2FLOPS", diff --git a/aviary/subsystems/test/test_premission.py b/aviary/subsystems/test/test_premission.py index d5d35dc46..689da6e31 100644 --- a/aviary/subsystems/test/test_premission.py +++ b/aviary/subsystems/test/test_premission.py @@ -18,6 +18,7 @@ from aviary.utils.functions import set_aviary_initial_values from aviary.variable_info.variables_in import VariablesIn from aviary.variable_info.enums import LegacyCode +from aviary.subsystems.propulsion.utils import build_engine_deck FLOPS = LegacyCode.FLOPS @@ -67,7 +68,9 @@ def setUp(self): input_options.delete(Aircraft.Fuel.TOTAL_CAPACITY) input_options.delete(Aircraft.Nacelle.AVG_LENGTH) - prop = CorePropulsionBuilder('core_propulsion', BaseMetaData) + engine = build_engine_deck(input_options) + + prop = CorePropulsionBuilder('core_propulsion', BaseMetaData, engine) mass = CoreMassBuilder('core_mass', BaseMetaData, GASP) aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, FLOPS) geom = CoreGeometryBuilder('core_geometry', @@ -272,11 +275,12 @@ def test_manual_override(self): aviary_inputs = setup_options(GASP_input, FLOPS_input) aviary_inputs.delete(Aircraft.Fuselage.WETTED_AREA) + engine = build_engine_deck(aviary_inputs) prob = om.Problem() model = prob.model - prop = CorePropulsionBuilder('core_propulsion', BaseMetaData) + prop = CorePropulsionBuilder('core_propulsion', BaseMetaData, engine) mass = CoreMassBuilder('core_mass', BaseMetaData, GASP) aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, FLOPS) geom = CoreGeometryBuilder('core_geometry', @@ -352,3 +356,6 @@ def test_manual_override(self): if __name__ == "__main__": unittest.main() + # test = PreMissionTestCase() + # test.setUp() + # test.test_GASP_mass_FLOPS_everything_else() diff --git a/aviary/utils/preprocessors.py b/aviary/utils/preprocessors.py index 4b31bdb00..ebf7e636e 100644 --- a/aviary/utils/preprocessors.py +++ b/aviary/utils/preprocessors.py @@ -1,15 +1,15 @@ +import warnings + import numpy as np -import math import openmdao.api as om -from aviary.subsystems.propulsion.engine_deck import EngineDeck 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 -def preprocess_options(aviary_options: AviaryValues): +def preprocess_options(aviary_options: AviaryValues, **kwargs): """ Run all preprocessors on provided AviaryValues object @@ -18,14 +18,13 @@ def preprocess_options(aviary_options: AviaryValues): aviary_options : AviaryValues Options to be updated """ - preprocess_crewpayload(aviary_options) try: - engine_models = aviary_options.get_val('engine_models') + engine_models = kwargs['engine_models'] except KeyError: - preprocess_propulsion(aviary_options) - else: - # don't catch stray exceptions in preprocess_propulsion - preprocess_propulsion(aviary_options, engine_models) + engine_models = None + + preprocess_crewpayload(aviary_options) + preprocess_propulsion(aviary_options, engine_models) def preprocess_crewpayload(aviary_options: AviaryValues): @@ -133,54 +132,7 @@ def preprocess_propulsion(aviary_options: AviaryValues, engine_models: list = No EngineModel objects to be added to aviary_options. Replaced existing EngineModels in aviary_options ''' - ######################## - # Create Engine Models # - ######################## - # Check if EngineModels are provided, either as an argument or in aviary_options - # Build new engines if no engine models provided by the user in any capacity, such as - # from an input deck - build_engines = False - if not engine_models: - try: - engine_models = aviary_options.get_val('engine_models') - except KeyError: - build_engines = True - else: - # Add engine models to aviary options for component access - # TODO this overwrites any engine models already in aviary_options without - # warning - should they get combined into the engine_models list instead? - aviary_options.set_val('engine_models', engine_models) - - # If engine models are not provided, build a single engine deck, ignore vectorization - # of AviaryValues (use first index) - # TODO build test to verify this works as expected - if build_engines: - engine_options = AviaryValues() - for entry in Aircraft.Engine.__dict__: - var = getattr(Aircraft.Engine, entry) - # check if this variable exist with useable metadata - try: - units = _MetaData[var]['units'] - try: - # add value from aviary_options to engine_options - default_val = aviary_options.get_val(var, units) - if type(default_val) in (list, np.ndarray): - default_val = default_val[0] - engine_options.set_val(default_val) - # if not, use default value from _MetaData - except KeyError: - engine_options.set_val(_MetaData[var]['default_value'], units) - except KeyError: - continue - - engine_deck = EngineDeck(engine_options) - engine_models = [engine_deck] - - count = len(engine_models) - # keys of originally provided aviary_options - # currently not used but might be useful in the future - # aviary_mapping = get_keys(aviary_options.deepcopy()) - + # TODO add verbosity check to warnings ############################## # Vectorize Engine Variables # ############################## @@ -188,6 +140,8 @@ def preprocess_propulsion(aviary_options: AviaryValues, engine_models: list = No # 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) + count = len(engine_models) + complete_options_list = AviaryValues(aviary_options) for engine in engine_models: complete_options_list.update(engine.options) @@ -301,20 +255,27 @@ def preprocess_propulsion(aviary_options: AviaryValues, engine_models: list = No num_wing_engines_all[i] = num_engines # TODO is a warning overkill here? It can be documented wing mounted engines # are assumed default - UserWarning( + 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, default num_engines to sum - # of specified mounted engines - elif total_engines_calc != num_engines: + # 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 - UserWarning( + 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) diff --git a/aviary/utils/test_utils/default_subsystems.py b/aviary/utils/test_utils/default_subsystems.py new file mode 100644 index 000000000..ed8549f62 --- /dev/null +++ b/aviary/utils/test_utils/default_subsystems.py @@ -0,0 +1,24 @@ +from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder +from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder +from aviary.subsystems.mass.mass_builder import CoreMassBuilder +from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder +from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData +from aviary.variable_info.enums import LegacyCode + + +def get_default_premission_subsystems(legacy_code=None, engines=None): + legacy_code = LegacyCode(legacy_code) + prop = CorePropulsionBuilder('core_propulsion', BaseMetaData, engine_models=engines) + mass = CoreMassBuilder('core_mass', BaseMetaData, legacy_code) + aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, legacy_code) + geom = CoreGeometryBuilder('core_geometry', BaseMetaData, legacy_code) + + return [prop, geom, aero, mass] + + +def get_default_mission_subsystems(legacy_code=None, engines=None): + legacy_code = LegacyCode(legacy_code) + prop = CorePropulsionBuilder('core_propulsion', BaseMetaData, engine_models=engines) + aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, legacy_code) + + return [aero, prop] diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py index f30f6d812..c5a410198 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py @@ -11,7 +11,7 @@ from aviary.interface.methods_for_level2 import AviaryProblem from aviary.utils.functions import set_aviary_initial_values -from aviary.utils.preprocessors import preprocess_crewpayload +from aviary.utils.preprocessors import preprocess_options from aviary.models.N3CC.N3CC_data import \ balanced_liftoff_user_options as _takeoff_liftoff_user_options from aviary.models.N3CC.N3CC_data import \ @@ -20,7 +20,8 @@ inputs as _inputs from aviary.variable_info.variables import Dynamic from aviary.variable_info.variables_in import VariablesIn -from aviary.interface.default_phase_info.height_energy import default_premission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.subsystems.premission import CorePreMission @@ -57,7 +58,9 @@ def bench_test_SNOPT(self): def _do_run(self, driver: Driver, optimizer, *args): aviary_options = _inputs.deepcopy() - preprocess_crewpayload(aviary_options) + + engine = build_engine_deck(aviary_options) + preprocess_options(aviary_options, engine_models=engine) takeoff_trajectory_builder = copy.deepcopy(_takeoff_trajectory_builder) takeoff_liftoff_user_options = _takeoff_liftoff_user_options.deepcopy() @@ -72,6 +75,8 @@ def _do_run(self, driver: Driver, optimizer, *args): driver.recording_options['record_derivatives'] = False + default_premission_subsystems = get_default_mission_subsystems('FLOPS', engine) + # Upstream static analysis for aero takeoff.model.add_subsystem( 'core_subsystems', diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_based_sizing_N3CC.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_based_sizing_N3CC.py index 79d317190..fa9884b47 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_based_sizing_N3CC.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_based_sizing_N3CC.py @@ -27,7 +27,8 @@ from aviary.variable_info.variables import Aircraft, Dynamic, Mission from aviary.subsystems.premission import CorePreMission -from aviary.interface.default_phase_info.height_energy import default_premission_subsystems, default_mission_subsystems +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.utils.preprocessors import preprocess_crewpayload try: @@ -165,6 +166,10 @@ def run_trajectory(sim=True): num_segments=num_segments_descent, order=3, compressed=True, segment_ends=descent_seg_ends) + # default subsystems + engine = build_engine_deck(aviary_inputs) + default_mission_subsystems = get_default_mission_subsystems('FLOPS', engine) + climb_options = EnergyPhase( 'test_climb', user_options=AviaryValues({ @@ -223,7 +228,7 @@ def run_trajectory(sim=True): prob.model.add_subsystem( 'pre_mission', CorePreMission(aviary_options=aviary_inputs, - subsystems=default_premission_subsystems), + subsystems=default_mission_subsystems), promotes_inputs=['aircraft:*', 'mission:*'], promotes_outputs=['aircraft:*', 'mission:*']) diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py index bc4b16846..4e8db9bbe 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py @@ -18,8 +18,9 @@ landing_fullstop_user_options as _landing_fullstop_user_options) from aviary.variable_info.variables import Dynamic -from aviary.interface.default_phase_info.height_energy import default_premission_subsystems -from aviary.utils.preprocessors import preprocess_crewpayload +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.utils.preprocessors import preprocess_options from aviary.variable_info.variables_in import VariablesIn @@ -67,7 +68,10 @@ def _do_run(self, driver: Driver, optimizer, *args): driver.recording_options['record_derivatives'] = False - preprocess_crewpayload(aviary_options) + engine = build_engine_deck(aviary_options) + preprocess_options(aviary_options, engine_models=engine) + + default_premission_subsystems = get_default_mission_subsystems('FLOPS', engine) # Upstream static analysis for aero landing.model.add_subsystem( diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py index e0752de37..2b75a5d49 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py @@ -18,8 +18,9 @@ takeoff_liftoff_user_options as _takeoff_liftoff_user_options) from aviary.variable_info.variables import Aircraft, Dynamic -from aviary.interface.default_phase_info.height_energy import default_premission_subsystems -from aviary.utils.preprocessors import preprocess_crewpayload +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.utils.preprocessors import preprocess_options from aviary.variable_info.variables_in import VariablesIn @@ -73,7 +74,10 @@ def _do_run(self, driver: Driver, optimizer, *args): driver.recording_options['record_derivatives'] = False - preprocess_crewpayload(aviary_options) + engine = build_engine_deck(aviary_options) + preprocess_options(aviary_options, engine_models=engine) + + default_premission_subsystems = get_default_mission_subsystems('FLOPS', engine) # Upstream static analysis for aero takeoff.model.add_subsystem( diff --git a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py index 305e81eac..f6a30759c 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py @@ -7,8 +7,9 @@ from aviary.interface.default_phase_info.height_energy import phase_info from aviary.interface.methods_for_level2 import AviaryProblem -from aviary.models.multi_engine_single_aisle.multi_engine_single_aisle_data import inputs +from aviary.models.multi_engine_single_aisle.multi_engine_single_aisle_data import inputs, engine_1_inputs, engine_2_inputs from aviary.variable_info.enums import ThrottleAllocation +from aviary.subsystems.propulsion.utils import build_engine_deck # Build problem @@ -48,9 +49,14 @@ def test_multiengine_fixed(self): test_phase_info['cruise']['user_options']['throttle_allocation'] = method test_phase_info['descent']['user_options']['throttle_allocation'] = method + engine1 = build_engine_deck(engine_1_inputs)[0] + engine1.name = 'engine_1' + engine2 = build_engine_deck(engine_2_inputs)[0] + engine2.name = 'engine_2' + prob = AviaryProblem() - prob.load_inputs(inputs, test_phase_info) + prob.load_inputs(inputs, test_phase_info, engine_builders=[engine1, engine2]) prob.check_and_preprocess_inputs() prob.add_pre_mission_systems() @@ -86,9 +92,12 @@ def test_multiengine_static(self): test_phase_info['cruise']['user_options']['throttle_allocation'] = method test_phase_info['descent']['user_options']['throttle_allocation'] = method + engine1 = build_engine_deck(engine_1_inputs)[0] + engine2 = build_engine_deck(engine_2_inputs)[0] + prob = AviaryProblem() - prob.load_inputs(inputs, test_phase_info) + prob.load_inputs(inputs, test_phase_info, engine_builders=[engine1, engine2]) prob.check_and_preprocess_inputs() prob.add_pre_mission_systems() @@ -126,7 +135,10 @@ def test_multiengine_dynamic(self): prob = AviaryProblem() - prob.load_inputs(inputs, test_phase_info) + engine1 = build_engine_deck(engine_1_inputs)[0] + engine2 = build_engine_deck(engine_2_inputs)[0] + + prob.load_inputs(inputs, test_phase_info, engine_builders=[engine1, engine2]) prob.check_and_preprocess_inputs() prob.add_pre_mission_systems() @@ -157,4 +169,6 @@ def test_multiengine_dynamic(self): if __name__ == '__main__': - unittest.main() + # unittest.main() + test = MultiengineTestcase() + test.test_multiengine_dynamic() diff --git a/aviary/validation_cases/validation_tests.py b/aviary/validation_cases/validation_tests.py index dbcb56d40..d7f7c6ec1 100644 --- a/aviary/validation_cases/validation_tests.py +++ b/aviary/validation_cases/validation_tests.py @@ -8,6 +8,7 @@ from aviary.utils.aviary_values import AviaryValues from aviary.utils.options import list_options as list_options_func +from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.preprocessors import preprocess_options from aviary.validation_cases.validation_data.flops_data.FLOPS_Test_Data import \ FLOPS_Test_Data, FLOPS_Lacking_Test_Data @@ -257,7 +258,7 @@ def flops_validation_test(prob: om.Problem, list_outputs=list_outputs) -def get_flops_data(case_name: str, keys: str = None) -> AviaryValues: +def get_flops_data(case_name: str, keys: str = None, preprocess: bool = False) -> AviaryValues: """ Returns an AviaryValues object containing input and output data for the named FLOPS validation case. @@ -271,7 +272,7 @@ def get_flops_data(case_name: str, keys: str = None) -> AviaryValues: List of variables whose values will be transferred from the validation data. The default is all variables. """ - flops_data_copy: AviaryValues = get_flops_inputs(case_name) + flops_data_copy: AviaryValues = get_flops_inputs(case_name, preprocess=preprocess) flops_data_copy.update(get_flops_outputs(case_name)) if keys is None: return flops_data_copy @@ -303,7 +304,8 @@ def get_flops_inputs(case_name: str, keys: str = None, preprocess: bool = False) flops_inputs_copy: AviaryValues = flops_data['inputs'].deepcopy() if preprocess: - preprocess_options(flops_inputs_copy) + preprocess_options(flops_inputs_copy, + engine_models=build_engine_deck(flops_inputs_copy)) if keys is None: return flops_inputs_copy keys_list = _assure_is_list(keys) From 55695d669e834eca8095123e44a1da60216ea1db Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Tue, 14 May 2024 18:55:18 -0400 Subject: [PATCH 07/22] more fixed tests --- .../flops_based/ode/test/test_landing_ode.py | 2 +- .../flops_based/test/test_computed_aero_group.py | 16 +++++++++++++--- .../subsystems/propulsion/propulsion_mission.py | 1 + .../propulsion/test/test_propulsion_mission.py | 10 +++++----- .../benchmark_tests/test_bench_multiengine.py | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/aviary/mission/flops_based/ode/test/test_landing_ode.py b/aviary/mission/flops_based/ode/test/test_landing_ode.py index 564c347ab..1b1845342 100644 --- a/aviary/mission/flops_based/ode/test/test_landing_ode.py +++ b/aviary/mission/flops_based/ode/test/test_landing_ode.py @@ -19,7 +19,7 @@ def test_case(self): aviary_options = inputs default_mission_subsystems = get_default_mission_subsystems( - 'GASP', build_engine_deck(aviary_options)) + 'FLOPS', build_engine_deck(aviary_options)) prob.model.add_subsystem( "landing_flare_ode", diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py index d38d8347f..eabab2fe2 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py @@ -12,6 +12,7 @@ from aviary.variable_info.variables_in import VariablesIn from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems +from aviary.utils.preprocessors import preprocess_options class MissionDragTest(unittest.TestCase): @@ -30,9 +31,12 @@ def test_basic_large_single_aisle_1(self): flops_inputs.set_val(key, *(flops_outputs.get_item(key))) flops_inputs.set_val(Settings.VERBOSITY, 0) + engine = build_engine_deck(flops_inputs) + preprocess_options(flops_inputs, engine) + # don't need mass subsystem, so we skip it default_premission_subsystems = get_default_premission_subsystems( - 'FLOPS', build_engine_deck(flops_inputs))[:-1] + 'FLOPS', engine)[:-1] # we just want aero for mission, make a copy by itself aero = default_premission_subsystems[-1] @@ -167,9 +171,12 @@ def test_n3cc_drag(self): flops_inputs.set_val(key, *(flops_outputs.get_item(key))) flops_inputs.set_val(Settings.VERBOSITY, 0) + engine = build_engine_deck(flops_inputs) + preprocess_options(flops_inputs, engine) + # don't need mass subsystem, so we skip it default_premission_subsystems = get_default_premission_subsystems( - 'FLOPS', build_engine_deck(flops_inputs))[:-1] + 'FLOPS', engine)[:-1] # we just want aero for mission, make a copy by itself aero = default_premission_subsystems[-1] @@ -286,9 +293,12 @@ def test_large_single_aisle_2_drag(self): flops_inputs.set_val(key, *(flops_outputs.get_item(key))) flops_inputs.set_val(Settings.VERBOSITY, 0) + engine = build_engine_deck(flops_inputs) + preprocess_options(flops_inputs, engine) + # don't need mass subsystem, so we skip it default_premission_subsystems = get_default_premission_subsystems( - 'FLOPS', build_engine_deck(flops_inputs))[:-1] + 'FLOPS', engine)[:-1] # we just want aero for mission, make a copy by itself aero = default_premission_subsystems[-1] diff --git a/aviary/subsystems/propulsion/propulsion_mission.py b/aviary/subsystems/propulsion/propulsion_mission.py index 2d61b8762..97c0325d7 100644 --- a/aviary/subsystems/propulsion/propulsion_mission.py +++ b/aviary/subsystems/propulsion/propulsion_mission.py @@ -39,6 +39,7 @@ def setup(self): # We need a single component with scale_factor. Dymos can't find it when it is # already sliced across several component. + # TODO this only works for engine decks. Need to fix problem in generic way comp = om.ExecComp( "y=x", y={'val': np.ones(engine_count), 'units': 'unitless'}, diff --git a/aviary/subsystems/propulsion/test/test_propulsion_mission.py b/aviary/subsystems/propulsion/test/test_propulsion_mission.py index bcd28127a..1ad3d82f2 100644 --- a/aviary/subsystems/propulsion/test/test_propulsion_mission.py +++ b/aviary/subsystems/propulsion/test/test_propulsion_mission.py @@ -52,7 +52,7 @@ def test_case_1(self): preprocess_propulsion(options, [engine]) self.prob.model = PropulsionMission( - num_nodes=nn, aviary_options=options, engine_models=engine) + num_nodes=nn, aviary_options=options, engine_models=[engine]) IVC = om.IndepVarComp(Dynamic.Mission.MACH, np.linspace(0, 0.8, nn), @@ -211,7 +211,7 @@ def test_case_multiengine(self): if __name__ == "__main__": - # unittest.main() - test = PropulsionMissionTest() - test.setUp() - test.test_case_multiengine() + unittest.main() + # test = PropulsionMissionTest() + # test.setUp() + # test.test_case_multiengine() diff --git a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py index f6a30759c..167be048b 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py @@ -161,7 +161,7 @@ def test_multiengine_dynamic(self): alloc_descent = prob.get_val('traj.descent.controls:throttle_allocations') # Cruise is pretty constant, check exact value. - assert_near_equal(alloc_cruise[0], 0.565, tolerance=1e-2) + assert_near_equal(alloc_cruise[0], 0.646, tolerance=1e-2) # Check general trend: favors engine 1. self.assertGreater(alloc_climb[2], 0.55) From b1bcf4cf1fad7a03ccfe250490ef84323ceb1fdf Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Wed, 15 May 2024 10:25:37 -0400 Subject: [PATCH 08/22] another round of fixed tests --- .../test/test_unsteady_alpha_thrust_iter_group.py | 6 +++++- .../flops_based/test/test_computed_aero_group.py | 6 +++--- .../benchmark_tests/test_bench_multiengine.py | 6 +++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py index 636d547fa..efc96dbce 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py @@ -5,7 +5,6 @@ from openmdao.utils.assert_utils import assert_near_equal from aviary.constants import GRAV_ENGLISH_LBM -from aviary.interface.default_phase_info.two_dof import aero from aviary.mission.gasp_based.ode.params import ParamPort from aviary.mission.gasp_based.ode.unsteady_solved.unsteady_control_iter_group import \ UnsteadyControlIterGroup @@ -14,6 +13,8 @@ from aviary.variable_info.enums import SpeedType from aviary.variable_info.variables import Aircraft, Dynamic, Mission from aviary.utils.aviary_values import AviaryValues +from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder +from aviary.variable_info.enums import LegacyCode class TestUnsteadyAlphaThrustIterGroup(unittest.TestCase): @@ -23,6 +24,9 @@ def _test_unsteady_alpha_thrust_iter_group(self, ground_roll=False): aviary_options = AviaryValues() aviary_options.set_val(Aircraft.Engine.NUM_ENGINES, np.array([2])) + # just need aero subsystem + aero = CoreAerodynamicsBuilder(code_origin=LegacyCode.GASP) + p = om.Problem() # TODO: paramport diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py index eabab2fe2..0ca6a40b3 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py @@ -32,7 +32,7 @@ def test_basic_large_single_aisle_1(self): flops_inputs.set_val(Settings.VERBOSITY, 0) engine = build_engine_deck(flops_inputs) - preprocess_options(flops_inputs, engine) + preprocess_options(flops_inputs, engine_models=engine) # don't need mass subsystem, so we skip it default_premission_subsystems = get_default_premission_subsystems( @@ -172,7 +172,7 @@ def test_n3cc_drag(self): flops_inputs.set_val(Settings.VERBOSITY, 0) engine = build_engine_deck(flops_inputs) - preprocess_options(flops_inputs, engine) + preprocess_options(flops_inputs, engine_models=engine) # don't need mass subsystem, so we skip it default_premission_subsystems = get_default_premission_subsystems( @@ -294,7 +294,7 @@ def test_large_single_aisle_2_drag(self): flops_inputs.set_val(Settings.VERBOSITY, 0) engine = build_engine_deck(flops_inputs) - preprocess_options(flops_inputs, engine) + preprocess_options(flops_inputs, engine_models=engine) # don't need mass subsystem, so we skip it default_premission_subsystems = get_default_premission_subsystems( diff --git a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py index 167be048b..50e6361df 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py @@ -169,6 +169,6 @@ def test_multiengine_dynamic(self): if __name__ == '__main__': - # unittest.main() - test = MultiengineTestcase() - test.test_multiengine_dynamic() + unittest.main() + # test = MultiengineTestcase() + # test.test_multiengine_dynamic() From b55852ffb2e0ece7297196154e4008128b18a031 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Wed, 15 May 2024 12:19:34 -0400 Subject: [PATCH 09/22] updated failing docs --- .../getting_started/onboarding_level3.ipynb | 61 ++++++++++--------- ..._same_mission_at_different_UI_levels.ipynb | 41 +++++++------ .../test_FLOPS_balanced_field_length.py | 4 +- 3 files changed, 54 insertions(+), 52 deletions(-) diff --git a/aviary/docs/getting_started/onboarding_level3.ipynb b/aviary/docs/getting_started/onboarding_level3.ipynb index 9295df12e..1c1d226a3 100644 --- a/aviary/docs/getting_started/onboarding_level3.ipynb +++ b/aviary/docs/getting_started/onboarding_level3.ipynb @@ -85,9 +85,9 @@ "driver.opt_settings[\"tol\"] = 1e-3\n", "driver.opt_settings['print_level'] = 4\n", "\n", - "##########################################\n", - "# Aircraft Input Variables and Options #\n", - "##########################################\n", + "########################################\n", + "# Aircraft Input Variables and Options #\n", + "########################################\n", "\n", "aviary_inputs = get_flops_inputs('N3CC')\n", "\n", @@ -154,9 +154,21 @@ "t_f_descent = 461.62*_units.minute\n", "t_duration_descent = t_f_descent - t_i_descent\n", "\n", - "##########################\n", - "# Design Variables #\n", - "##########################\n", + "engine = av.build_engine_deck(aviary_inputs)\n", + "av.preprocess_options(aviary_inputs, engine_models=engine)\n", + "\n", + "# define subsystems\n", + "aero = av.CoreAerodynamicsBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "geom = av.CoreGeometryBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "mass = av.CoreMassBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "prop = av.CorePropulsionBuilder(engine_models=engine)\n", + "\n", + "premission_subsystems = [prop, geom, aero, mass]\n", + "mission_subsystems = [aero, prop]\n", + "\n", + "####################\n", + "# Design Variables #\n", + "####################\n", "\n", "# Nudge it a bit off the correct answer to verify that the optimize takes us there.\n", "aviary_inputs.set_val(av.Mission.Design.GROSS_MASS, 135000.0, units='lbm')\n", @@ -170,9 +182,9 @@ " num_engines=aviary_inputs.get_val(av.Aircraft.Engine.NUM_ENGINES)\n", ")\n", "\n", - "##################\n", - "# Define Phases #\n", - "##################\n", + "#################\n", + "# Define Phases #\n", + "#################\n", "num_segments_climb = 6\n", "num_segments_cruise = 1\n", "num_segments_descent = 5\n", @@ -200,7 +212,7 @@ " 'input_initial': (True, 'unitless'),\n", " 'use_polynomial_control': (False, 'unitless'),\n", " }),\n", - " core_subsystems=av.default_mission_subsystems,\n", + " core_subsystems=mission_subsystems,\n", " subsystem_options={'core_aerodynamics': {'method': 'computed'}},\n", " transcription=transcription_climb,\n", ")\n", @@ -215,7 +227,7 @@ " 'required_available_climb_rate': (300, 'ft/min'),\n", " 'fix_initial': (False, 'unitless'),\n", " }),\n", - " core_subsystems=av.default_mission_subsystems,\n", + " core_subsystems=mission_subsystems,\n", " subsystem_options={'core_aerodynamics': {'method': 'computed'}},\n", " transcription=transcription_cruise,\n", ")\n", @@ -230,7 +242,7 @@ " 'fix_initial': (False, 'unitless'),\n", " 'use_polynomial_control': (False, 'unitless'),\n", " }),\n", - " core_subsystems=av.default_mission_subsystems,\n", + " core_subsystems=mission_subsystems,\n", " subsystem_options={'core_aerodynamics': {'method': 'computed'}},\n", " transcription=transcription_descent,\n", ")\n", @@ -241,22 +253,11 @@ " av.Mission.Landing.LIFT_COEFFICIENT_MAX) # no units\n", ")\n", "\n", - "engine = av.build_engine_deck(aviary_inputs)\n", - "av.preprocess_options(aviary_inputs, engine)\n", - "\n", - "# default subsystems\n", - "aero = av.CoreAerodynamicsBuilder(code_origin=av.LegacyCode('FLOPS'))\n", - "geom = av.CoreGeometryBuilder(code_origin=av.LegacyCode('FLOPS'))\n", - "mass = av.CoreMassBuilder(code_origin=av.LegacyCode('FLOPS'))\n", - "prop = av.CorePropulsionBuilder(engine_models=engine)\n", - "\n", - "premission_subsystems = [prop, geom, aero, mass]\n", - "\n", "# Upstream static analysis for aero\n", "prob.model.add_subsystem(\n", " 'pre_mission',\n", " av.CorePreMission(aviary_options=aviary_inputs,\n", - " subsystems=premission_subsystems),\n", + " subsystems=premission_subsystems),\n", " promotes_inputs=['aircraft:*', 'mission:*'],\n", " promotes_outputs=['aircraft:*', 'mission:*'])\n", "\n", @@ -307,9 +308,9 @@ " 'landing', landing, promotes_inputs=['aircraft:*', 'mission:*'],\n", " promotes_outputs=['mission:*'])\n", "\n", - "##########################################\n", - "# link phases #\n", - "##########################################\n", + "###############\n", + "# link phases #\n", + "###############\n", "\n", "traj.link_phases([\"climb\", \"cruise\", \"descent\"], [\"time\", av.Dynamic.Mission.MASS, av.Dynamic.Mission.DISTANCE], connected=strong_couple)\n", "\n", @@ -329,9 +330,9 @@ "prob.model.connect('traj.descent.control_values:altitude', av.Mission.Landing.INITIAL_ALTITUDE,\n", " src_indices=[-1])\n", "\n", - "##########################\n", - "# Constraints #\n", - "##########################\n", + "###############\n", + "# Constraints #\n", + "###############\n", "\n", "ecomp = om.ExecComp('fuel_burned = initial_mass - descent_mass_final',\n", " initial_mass={'units': 'lbm', 'shape': 1},\n", diff --git a/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb b/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb index 0b6d1f3c3..c997f2364 100644 --- a/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb +++ b/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb @@ -199,16 +199,16 @@ "driver.options[\"optimizer\"] = \"SLSQP\"\n", "driver.options[\"maxiter\"] = 1\n", "\n", - "##########################################\n", - "# Aircraft Input Variables and Options #\n", - "##########################################\n", + "########################################\n", + "# Aircraft Input Variables and Options #\n", + "########################################\n", "\n", "csv_path = \"models/test_aircraft/aircraft_for_bench_FwFm.csv\"\n", "\n", "aviary_inputs, _ = av.create_vehicle('models/test_aircraft/aircraft_for_bench_FwFm.csv')\n", "\n", "engine = av.build_engine_deck(aviary_inputs)\n", - "av.preprocess_propulsion(aviary_inputs, engine)\n", + "av.preprocess_options(aviary_inputs, engine_models=engine)\n", "\n", "alt_airport = 0 # ft\n", "\n", @@ -249,9 +249,20 @@ "t_f_descent = 299. * _units.minute\n", "t_duration_descent = t_f_descent - t_i_descent\n", "\n", - "##########################\n", - "# Design Variables #\n", - "##########################\n", + "####################\n", + "# Build Subsystems #\n", + "####################\n", + "aero = av.CoreAerodynamicsBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "geom = av.CoreGeometryBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "mass = av.CoreMassBuilder(code_origin=av.LegacyCode('FLOPS'))\n", + "prop = av.CorePropulsionBuilder(engine_models=engine)\n", + "\n", + "premission_subsystems = [prop, geom, aero, mass]\n", + "mission_subsystems = [aero, prop]\n", + "\n", + "####################\n", + "# Design Variables #\n", + "####################\n", "\n", "# Nudge it a bit off the correct answer to verify that the optimize takes us there.\n", "aviary_inputs.set_val(av.Mission.Design.GROSS_MASS, 135000.0, units='lbm')\n", @@ -259,9 +270,9 @@ "prob.model.add_design_var(av.Mission.Design.GROSS_MASS, units='lbm',\n", " lower=100000.0, upper=200000.0, ref=135000)\n", "\n", - "##################\n", - "# Define Phases #\n", - "##################\n", + "#################\n", + "# Define Phases #\n", + "#################\n", "num_segments_climb = 6\n", "num_segments_cruise = 1\n", "num_segments_descent = 5\n", @@ -325,16 +336,6 @@ "\n", "av.preprocess_crewpayload(aviary_inputs)\n", "\n", - "####################\n", - "# Build Subsystems #\n", - "####################\n", - "aero = av.CoreAerodynamicsBuilder(code_origin=av.LegacyCode('FLOPS'))\n", - "geom = av.CoreGeometryBuilder(code_origin=av.LegacyCode('FLOPS'))\n", - "mass = av.CoreMassBuilder(code_origin=av.LegacyCode('FLOPS'))\n", - "prop = av.CorePropulsionBuilder(engine_models=engine)\n", - "\n", - "premission_subsystems = [prop, geom, aero, mass]\n", - "\n", "# Upstream static analysis for aero\n", "prob.model.add_subsystem(\n", " 'pre_mission',\n", diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py index c5a410198..ed62ffd76 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py @@ -75,14 +75,14 @@ def _do_run(self, driver: Driver, optimizer, *args): driver.recording_options['record_derivatives'] = False - default_premission_subsystems = get_default_mission_subsystems('FLOPS', engine) + default_mission_subsystems = get_default_mission_subsystems('FLOPS', engine) # Upstream static analysis for aero takeoff.model.add_subsystem( 'core_subsystems', CorePreMission( aviary_options=aviary_options, - subsystems=default_premission_subsystems, + subsystems=default_mission_subsystems, ), promotes_inputs=['aircraft:*', 'mission:*'], promotes_outputs=['aircraft:*', 'mission:*']) From 7b275adbb47bf67f9bc1481e79a1cd6260ef2def Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 May 2024 13:08:58 -0400 Subject: [PATCH 10/22] Some small fixes while checking pycycle. --- aviary/interface/methods_for_level2.py | 3 ++- aviary/subsystems/propulsion/propulsion_builder.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 957fc1a7f..a53c8992a 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -822,7 +822,8 @@ def _get_phase(self, phase_name, phase_idx): control_dicts = subsystem.get_controls( phase_name=phase_name) else: - control_dicts = subsystem.get_controls() + control_dicts = subsystem.get_controls( + phase_name=phase_name) for control_name, control_dict in control_dicts.items(): phase.add_control(control_name, **control_dict) diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index 9c4f1f328..76205476c 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -82,13 +82,13 @@ def get_states(self): return states - def get_controls(self): + def get_controls(self, phase_name=None): """ Call get_controls() on all engine models and return combined result. """ controls = {} for engine in self.engine_models: - engine_controls = engine.get_controls() + engine_controls = engine.get_controls(phase_name=phase_name) controls.update(engine_controls) return controls From 5b00447e52bc63573cde5ddf279b603de25edd0f Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Wed, 15 May 2024 13:58:17 -0400 Subject: [PATCH 11/22] fix for get_controls() definition --- aviary/subsystems/propulsion/test/test_custom_engine_model.py | 2 +- aviary/subsystems/test/test_dummy_subsystem.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aviary/subsystems/propulsion/test/test_custom_engine_model.py b/aviary/subsystems/propulsion/test/test_custom_engine_model.py index 85c64930d..d682914bc 100644 --- a/aviary/subsystems/propulsion/test/test_custom_engine_model.py +++ b/aviary/subsystems/propulsion/test/test_custom_engine_model.py @@ -98,7 +98,7 @@ def build_pre_mission(self, aviary_inputs=AviaryValues()): def build_mission(self, num_nodes, aviary_inputs): return SimpleEngine(num_nodes=num_nodes) - def get_controls(self): + def get_controls(self, **kwargs): controls_dict = { "different_throttle": {'units': 'unitless', 'lower': 0., 'upper': 0.1}, } diff --git a/aviary/subsystems/test/test_dummy_subsystem.py b/aviary/subsystems/test/test_dummy_subsystem.py index 73ee69868..4c68f694b 100644 --- a/aviary/subsystems/test/test_dummy_subsystem.py +++ b/aviary/subsystems/test/test_dummy_subsystem.py @@ -289,7 +289,7 @@ def get_states(self): } } - def get_controls(self): + def get_controls(self, **kwargs): return {} def get_parameters(self, aviary_inputs=None, phase_info=None): From e680e6a263c60b0614f56536c6d00acb44c950c4 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 3 Jun 2024 10:04:09 -0400 Subject: [PATCH 12/22] merge fix --- .../test/test_unsteady_alpha_thrust_iter_group.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py index 13ced076b..f6f8b8cd4 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py @@ -12,7 +12,7 @@ UnsteadySolvedFlightConditions from aviary.variable_info.enums import SpeedType from aviary.variable_info.options import get_option_defaults -from aviary.variable_info.variables import Aircraft, Dynamic, Mission +from aviary.variable_info.variables import Aircraft, Dynamic from aviary.utils.aviary_values import AviaryValues from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.variable_info.enums import LegacyCode @@ -22,8 +22,6 @@ class TestUnsteadyAlphaThrustIterGroup(unittest.TestCase): def _test_unsteady_alpha_thrust_iter_group(self, ground_roll=False): nn = 5 - aviary_options = AviaryValues() - aviary_options.set_val(Aircraft.Engine.NUM_ENGINES, np.array([2])) # just need aero subsystem aero = CoreAerodynamicsBuilder(code_origin=LegacyCode.GASP) @@ -44,7 +42,6 @@ def _test_unsteady_alpha_thrust_iter_group(self, ground_roll=False): promotes_outputs=["*"]) g = UnsteadyControlIterGroup(num_nodes=nn, - aviary_options=aviary_options, ground_roll=ground_roll, clean=True, aviary_options=get_option_defaults(), From ea01f5454944a3018250bc4be53af92e15145f37 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Tue, 4 Jun 2024 18:49:47 -0400 Subject: [PATCH 13/22] merge fix --- .../subsystems/mass/gasp_based/equipment_and_useful_load.py | 3 ++- aviary/utils/preprocessors.py | 2 +- .../benchmark_tests/test_bench_multiengine.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/aviary/subsystems/mass/gasp_based/equipment_and_useful_load.py b/aviary/subsystems/mass/gasp_based/equipment_and_useful_load.py index 9868497e6..642bafc27 100644 --- a/aviary/subsystems/mass/gasp_based/equipment_and_useful_load.py +++ b/aviary/subsystems/mass/gasp_based/equipment_and_useful_load.py @@ -25,7 +25,8 @@ def initialize(self): ) def setup(self): - num_engine_type = len(self.options['aviary_options'].get_val('engine_models')) + num_engine_type = len(self.options['aviary_options'].get_val( + Aircraft.Engine.NUM_ENGINES)) add_aviary_input( self, Aircraft.AirConditioning.MASS_COEFFICIENT, val=1, units="unitless") diff --git a/aviary/utils/preprocessors.py b/aviary/utils/preprocessors.py index 51b4ea0b8..cb991bb20 100644 --- a/aviary/utils/preprocessors.py +++ b/aviary/utils/preprocessors.py @@ -140,7 +140,7 @@ def preprocess_propulsion(aviary_options: AviaryValues, engine_models: list = No # 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) - count = len(engine_models) + num_engine_type = len(engine_models) complete_options_list = AviaryValues(aviary_options) for engine in engine_models: diff --git a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py index 50e6361df..adaf29ac1 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py @@ -120,7 +120,7 @@ def test_multiengine_static(self): alloc_descent = prob.get_val('traj.descent.parameter_vals:throttle_allocations') assert_near_equal(alloc_climb[0], 0.5, tolerance=1e-2) - assert_near_equal(alloc_cruise[0], 0.56, tolerance=1e-2) + assert_near_equal(alloc_cruise[0], 0.64, tolerance=1e-2) assert_near_equal(alloc_descent[0], 0.999, tolerance=1e-2) @require_pyoptsparse(optimizer="SNOPT") @@ -171,4 +171,4 @@ def test_multiengine_dynamic(self): if __name__ == '__main__': unittest.main() # test = MultiengineTestcase() - # test.test_multiengine_dynamic() + # test.test_multiengine_static() From f9d8c51b9710d035d4cce6ca8248e14ad00de02a Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 5 Jun 2024 12:46:23 -0400 Subject: [PATCH 14/22] updated level 3 examples to adhere to new default subsystem syntax --- .../onboarding_ext_subsystem.ipynb | 2 +- .../getting_started/onboarding_level2.ipynb | 4 ++-- ...S_based_detailed_takeoff_and_landing.ipynb | 19 ++++++++++++------- ..._same_mission_at_different_UI_levels.ipynb | 10 +++++++--- aviary/docs/user_guide/external_aero.ipynb | 8 -------- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/aviary/docs/getting_started/onboarding_ext_subsystem.ipynb b/aviary/docs/getting_started/onboarding_ext_subsystem.ipynb index aa9bd8752..5841ee92d 100644 --- a/aviary/docs/getting_started/onboarding_ext_subsystem.ipynb +++ b/aviary/docs/getting_started/onboarding_ext_subsystem.ipynb @@ -758,7 +758,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.18" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/aviary/docs/getting_started/onboarding_level2.ipynb b/aviary/docs/getting_started/onboarding_level2.ipynb index a7dff0021..9db7d43eb 100644 --- a/aviary/docs/getting_started/onboarding_level2.ipynb +++ b/aviary/docs/getting_started/onboarding_level2.ipynb @@ -750,7 +750,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -764,7 +764,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/aviary/docs/user_guide/FLOPS_based_detailed_takeoff_and_landing.ipynb b/aviary/docs/user_guide/FLOPS_based_detailed_takeoff_and_landing.ipynb index e652c3d48..7bc2eaa8a 100644 --- a/aviary/docs/user_guide/FLOPS_based_detailed_takeoff_and_landing.ipynb +++ b/aviary/docs/user_guide/FLOPS_based_detailed_takeoff_and_landing.ipynb @@ -46,6 +46,8 @@ "\n", "from aviary.models.N3CC.N3CC_data import inputs\n", "\n", + "aviary_options = inputs.deepcopy()\n", + "\n", "# This builder can be used for both takeoff and landing phases\n", "aero_builder = av.CoreAerodynamicsBuilder(\n", " name='low_speed_aero',\n", @@ -79,7 +81,8 @@ "\n", "# We also need propulsion analysis for takeoff and landing. No additional configuration\n", "# is needed for this builder\n", - "prop_builder = av.CorePropulsionBuilder()" + "engine = av.build_engine_deck(aviary_options)\n", + "prop_builder = av.CorePropulsionBuilder(engine_models=engine)" ] }, { @@ -254,10 +257,9 @@ " takeoff_mic_p2_builder, takeoff_mic_p2_to_engine_cutback_builder,\n", " takeoff_engine_cutback_builder, takeoff_engine_cutback_to_mic_p1_builder,\n", " takeoff_mic_p1_to_climb_builder, takeoff_liftoff_user_options)\n", + "from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems\n", "\n", "\n", - "aviary_options = inputs.deepcopy()\n", - "\n", "takeoff_trajectory_builder = av.DetailedTakeoffTrajectoryBuilder('detailed_takeoff')\n", "\n", "takeoff_trajectory_builder.set_brake_release_to_decision_speed(\n", @@ -283,12 +285,15 @@ "\n", "takeoff = om.Problem()\n", "\n", + "# default subsystems\n", + "default_premission_subsystems = get_default_premission_subsystems('FLOPS', engine)\n", + " \n", "# Upstream pre-mission analysis for aero\n", "takeoff.model.add_subsystem(\n", " 'core_subsystems',\n", " av.CorePreMission(\n", " aviary_options=aviary_options,\n", - " subsystems=av.default_premission_subsystems,\n", + " subsystems=default_premission_subsystems,\n", " ),\n", " promotes_inputs=['*'],\n", " promotes_outputs=['*'])\n", @@ -541,7 +546,7 @@ " 'core_subsystems',\n", " av.CorePreMission(\n", " aviary_options=aviary_options,\n", - " subsystems=av.default_premission_subsystems,\n", + " subsystems=default_premission_subsystems,\n", " ),\n", " promotes_inputs=['*'],\n", " promotes_outputs=['*'])\n", @@ -581,7 +586,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -595,7 +600,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb b/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb index 8c8e976c3..7d542227f 100644 --- a/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb +++ b/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb @@ -193,6 +193,7 @@ "import dymos as dm\n", "\n", "import aviary.api as av\n", + "from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems\n", "\n", "\n", "prob = om.Problem(model=om.Group())\n", @@ -271,6 +272,9 @@ "prob.model.add_design_var(av.Mission.Design.GROSS_MASS, units='lbm',\n", " lower=100000.0, upper=200000.0, ref=135000)\n", "\n", + "# default subsystems\n", + "default_mission_subsystems = get_default_mission_subsystems('FLOPS', engine)\n", + "\n", "#################\n", "# Define Phases #\n", "#################\n", @@ -300,7 +304,7 @@ " 'fix_initial': (True, 'unitless'),\n", " 'use_polynomial_control': (False, 'unitless'),\n", " }),\n", - " core_subsystems=av.default_mission_subsystems,\n", + " core_subsystems=default_mission_subsystems,\n", " subsystem_options={'core_aerodynamics': {'method': 'computed'}},\n", " transcription=transcription_climb,\n", ")\n", @@ -315,7 +319,7 @@ " 'required_available_climb_rate': (300, 'ft/min'),\n", " 'fix_initial': (False, 'unitless'),\n", " }),\n", - " core_subsystems=av.default_mission_subsystems,\n", + " core_subsystems=default_mission_subsystems,\n", " subsystem_options={'core_aerodynamics': {'method': 'computed'}},\n", " transcription=transcription_cruise,\n", ")\n", @@ -330,7 +334,7 @@ " 'fix_initial': (False, 'unitless'),\n", " 'use_polynomial_control': (False, 'unitless'),\n", " }),\n", - " core_subsystems=av.default_mission_subsystems,\n", + " core_subsystems=default_mission_subsystems,\n", " subsystem_options={'core_aerodynamics': {'method': 'computed'}},\n", " transcription=transcription_descent,\n", ")\n", diff --git a/aviary/docs/user_guide/external_aero.ipynb b/aviary/docs/user_guide/external_aero.ipynb index 4c6088f65..8ef2b5435 100644 --- a/aviary/docs/user_guide/external_aero.ipynb +++ b/aviary/docs/user_guide/external_aero.ipynb @@ -252,14 +252,6 @@ "prob.add_objective(objective_type=\"mass\", ref=-1e5)\n", "prob.setup()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a9ed7c42", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 62c2395da2d94c8e2ec9522d7f7592461aef4adf Mon Sep 17 00:00:00 2001 From: Jason Kirk <110835404+jkirk5@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:08:42 -0400 Subject: [PATCH 15/22] Update aviary/interface/methods_for_level2.py Co-authored-by: crecine <51181861+crecine@users.noreply.github.com> --- aviary/interface/methods_for_level2.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 22fb73360..15cfac7a0 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -490,17 +490,15 @@ def check_and_preprocess_inputs(self): use_both_geometries=both_geom, code_origin_to_prioritize=code_origin_to_prioritize) - self.core_subsystems = {'propulsion': prop, + subsystems = self.core_subsystems = {'propulsion': prop, 'geometry': geom, 'mass': mass, 'aerodynamics': aero} # TODO optionally accept which subsystems to load from phase_info - subsystems = self.core_subsystems - default_mission_subsystems = [ - subsystems['aerodynamics'], subsystems['propulsion']] - self.ode_args = dict(aviary_options=aviary_inputs, - core_subsystems=default_mission_subsystems) + default_mission_subsystems = [subsystems['aerodynamics'], subsystems['propulsion']] + self.ode_args = {'aviary_options': aviary_inputs, + 'mission_subsystems': default_mission_subsystems} self._update_metadata_from_subsystems() From 67e0a094f3157351d80731eaa7a4853846d7b0c3 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 10 Jun 2024 18:06:13 -0400 Subject: [PATCH 16/22] PR feedback --- aviary/interface/methods_for_level2.py | 2 -- aviary/interface/test/test_phase_info.py | 6 +++--- aviary/mission/flops_based/ode/mission_ODE.py | 4 ++-- .../test/test_unsteady_alpha_thrust_iter_group.py | 1 - aviary/subsystems/propulsion/engine_deck.py | 15 +-------------- aviary/subsystems/propulsion/engine_model.py | 2 +- .../subsystems/propulsion/propulsion_builder.py | 3 --- .../subsystems/propulsion/propulsion_mission.py | 3 ++- aviary/subsystems/propulsion/turboprop_model.py | 5 ++++- aviary/subsystems/propulsion/utils.py | 6 +++--- aviary/utils/aviary_values.py | 4 +++- .../benchmark_tests/test_bench_multiengine.py | 6 +++--- 12 files changed, 22 insertions(+), 35 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 22fb73360..b837d3300 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -338,8 +338,6 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, verb def _update_metadata_from_subsystems(self): self.meta_data = BaseMetaData.copy() - # variables_to_pop = [] - # loop through phase_info and external subsystems for phase_name in self.phase_info: external_subsystems = self._get_all_subsystems( diff --git a/aviary/interface/test/test_phase_info.py b/aviary/interface/test/test_phase_info.py index 1698f8674..dd3b8ac82 100644 --- a/aviary/interface/test/test_phase_info.py +++ b/aviary/interface/test/test_phase_info.py @@ -156,6 +156,6 @@ def test_phase_info_parameterization_height_energy(self): # To run the tests if __name__ == '__main__': - # unittest.main() - test = TestParameterizePhaseInfo() - test.test_phase_info_parameterization_two_dof() + unittest.main() + # test = TestParameterizePhaseInfo() + # test.test_phase_info_parameterization_two_dof() diff --git a/aviary/mission/flops_based/ode/mission_ODE.py b/aviary/mission/flops_based/ode/mission_ODE.py index 24cd87361..0658d18c9 100644 --- a/aviary/mission/flops_based/ode/mission_ODE.py +++ b/aviary/mission/flops_based/ode/mission_ODE.py @@ -67,7 +67,7 @@ def setup(self): aviary_options = options['aviary_options'] core_subsystems = options['core_subsystems'] subsystem_options = options['subsystem_options'] - engine_count = len(aviary_options.get_val(Aircraft.Engine.NUM_ENGINES)) + num_engine_type = len(aviary_options.get_val(Aircraft.Engine.NUM_ENGINES)) if analysis_scheme is AnalysisScheme.SHOOTING: SGM_required_inputs = { @@ -178,7 +178,7 @@ def setup(self): # THROTTLE Section # TODO: Split this out into a function that can be used by the other ODEs. - if engine_count > 1: + if num_engine_type > 1: # Multi Engine diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py index 1f954e95e..7b380cebf 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py @@ -14,7 +14,6 @@ from aviary.variable_info.options import get_option_defaults from variable_info.options import get_option_defaults from aviary.variable_info.variables import Aircraft, Dynamic -from aviary.utils.aviary_values import AviaryValues from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.variable_info.enums import LegacyCode diff --git a/aviary/subsystems/propulsion/engine_deck.py b/aviary/subsystems/propulsion/engine_deck.py index 6daff15f7..9937662d4 100644 --- a/aviary/subsystems/propulsion/engine_deck.py +++ b/aviary/subsystems/propulsion/engine_deck.py @@ -1009,8 +1009,7 @@ def build_mission(self, num_nodes, aviary_inputs) -> om.Group: return engine_group def get_parameters(self): - params = {} - params[Aircraft.Engine.SCALE_FACTOR] = {'static_target': True} + params = {Aircraft.Engine.SCALE_FACTOR: {'static_target': True}} return params def report(self, problem, reports_file, **kwargs): @@ -1021,18 +1020,6 @@ def report(self, problem, reports_file, **kwargs): Aircraft.Engine.SCALED_SLS_THRUST, Aircraft.Engine.SCALE_FACTOR] - # determine which index in problem-level aviary values corresponds to this engine - # engine_idx = None - # for idx, engine in enumerate(problem.aviary_inputs.get_val('engine_models')): - # if engine.name == self.name: - # engine_idx = idx - - # if engine_idx is None: - # with open(reports_file, mode='a') as f: - # f.write(f'\n### {self.name}') - # f.write(f'\nEngine deck {self.name} not found\n') - # return - # modified version of markdown table util adjusted to handle engine decks with open(reports_file, mode='a') as f: f.write(f'\n### {self.name}') diff --git a/aviary/subsystems/propulsion/engine_model.py b/aviary/subsystems/propulsion/engine_model.py index a38b19971..f0dc330b5 100644 --- a/aviary/subsystems/propulsion/engine_model.py +++ b/aviary/subsystems/propulsion/engine_model.py @@ -174,7 +174,7 @@ def _preprocess_inputs(self): # "Convert" numpy types to standard Python types. Wrap first # index in numpy array before calling item() to safeguard against # non-standard types, such as objects - val = val[0].item() + val = np.array(val[0]).item() else: val = val[0] # update options with single value (instead of vector) diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index 43f702edf..e6647726e 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -117,9 +117,6 @@ def get_parameters(self, aviary_inputs=None, phase_info=None): params[var]['shape'] = (num_engine_type,) params[var]['static_target'] = True - # params = {} # For now - # params[Aircraft.Engine.SCALE_FACTOR] = {'shape': (engine_count, ), - # 'static_target': True} return params # NOTE untested! diff --git a/aviary/subsystems/propulsion/propulsion_mission.py b/aviary/subsystems/propulsion/propulsion_mission.py index 3cad11858..610403bb7 100644 --- a/aviary/subsystems/propulsion/propulsion_mission.py +++ b/aviary/subsystems/propulsion/propulsion_mission.py @@ -43,7 +43,8 @@ def setup(self): comp = om.ExecComp( "y=x", y={'val': np.ones(num_engine_type), 'units': 'unitless'}, - x={'val': np.ones(num_engine_type), 'units': 'unitless'} + x={'val': np.ones(num_engine_type), 'units': 'unitless'}, + has_diag_partials=True ) self.add_subsystem( "scale_passthrough", diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index ede61c0a0..c52fc1b11 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -158,7 +158,10 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs): num_nodes), 'units': 'lbf'}, turboshaft_thrust={'val': np.zeros( num_nodes), 'units': 'lbf'}, - propeller_thrust={'val': np.zeros(num_nodes), 'units': 'lbf'}) + propeller_thrust={'val': np.zeros( + num_nodes), 'units': 'lbf'}, + has_diag_partials=True + ) turboprop_group.add_subsystem('thrust_adder', subsys=thrust_adder, diff --git a/aviary/subsystems/propulsion/utils.py b/aviary/subsystems/propulsion/utils.py index 2caa6c9a2..76632d6a1 100644 --- a/aviary/subsystems/propulsion/utils.py +++ b/aviary/subsystems/propulsion/utils.py @@ -107,6 +107,7 @@ def convert_geopotential_altitude(altitude): return altitude +# TODO build test for this function def build_engine_deck(aviary_options: AviaryValues): ''' Creates an EngineDeck using avaliable inputs and options in aviary_options. @@ -126,7 +127,6 @@ def build_engine_deck(aviary_options: AviaryValues): # Build a single engine deck, currently ignoring vectorization # of AviaryValues (use first index) - # TODO build test to verify this works as expected engine_options = AviaryValues() for entry in Aircraft.Engine.__dict__: var = getattr(Aircraft.Engine, entry) @@ -140,11 +140,11 @@ def build_engine_deck(aviary_options: AviaryValues): # "Convert" numpy types to standard Python types. Wrap first # index in numpy array before calling item() to safeguard against # non-standard types, such as objects - aviary_val = aviary_val[0].item() + aviary_val = np.array(aviary_val[0]).item() elif isinstance(aviary_val, (list, tuple)): aviary_val = aviary_val[0] engine_options.set_val(var, aviary_val, units) - # if not, use default value from _MetaData + # if not, use default value from _MetaData? except KeyError: # engine_options.set_val(var, _MetaData[var]['default_value'], units) continue diff --git a/aviary/utils/aviary_values.py b/aviary/utils/aviary_values.py index 6bca9058e..23736a4ca 100644 --- a/aviary/utils/aviary_values.py +++ b/aviary/utils/aviary_values.py @@ -102,7 +102,9 @@ def _check_type(self, key, val, meta_data=_MetaData): if val.dtype == type(None): val = [val[0]] else: - val = [val[0].item()] + # item() gets us native Python equivalent object (i.e. int vs. numpy.int64) + # wrap first index in np array to ensures works on any dtype + val = [np.array(val[0]).item()] for item in val: has_bool = False # needs some fancy shenanigans because bools will register as ints if (isinstance(expected_types, type)): diff --git a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py index adaf29ac1..19be06cb0 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py @@ -169,6 +169,6 @@ def test_multiengine_dynamic(self): if __name__ == '__main__': - unittest.main() - # test = MultiengineTestcase() - # test.test_multiengine_static() + # unittest.main() + test = MultiengineTestcase() + test.test_multiengine_static() From b1ef0b5871a1821af0a3415dbea08ff2db0896ab Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 17 Jun 2024 11:59:32 -0400 Subject: [PATCH 17/22] minor cleanup --- .../test/test_unsteady_alpha_thrust_iter_group.py | 1 - aviary/utils/test_utils/default_subsystems.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py index 7b380cebf..f9170c5ef 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py @@ -12,7 +12,6 @@ UnsteadySolvedFlightConditions from aviary.variable_info.enums import SpeedType from aviary.variable_info.options import get_option_defaults -from variable_info.options import get_option_defaults from aviary.variable_info.variables import Aircraft, Dynamic from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.variable_info.enums import LegacyCode diff --git a/aviary/utils/test_utils/default_subsystems.py b/aviary/utils/test_utils/default_subsystems.py index ed8549f62..d7e05f60f 100644 --- a/aviary/utils/test_utils/default_subsystems.py +++ b/aviary/utils/test_utils/default_subsystems.py @@ -6,7 +6,7 @@ from aviary.variable_info.enums import LegacyCode -def get_default_premission_subsystems(legacy_code=None, engines=None): +def get_default_premission_subsystems(legacy_code, engines=None): legacy_code = LegacyCode(legacy_code) prop = CorePropulsionBuilder('core_propulsion', BaseMetaData, engine_models=engines) mass = CoreMassBuilder('core_mass', BaseMetaData, legacy_code) @@ -16,7 +16,7 @@ def get_default_premission_subsystems(legacy_code=None, engines=None): return [prop, geom, aero, mass] -def get_default_mission_subsystems(legacy_code=None, engines=None): +def get_default_mission_subsystems(legacy_code, engines=None): legacy_code = LegacyCode(legacy_code) prop = CorePropulsionBuilder('core_propulsion', BaseMetaData, engine_models=engines) aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, legacy_code) From 5c66633895610d39637fb0c8604837ea95afee8f Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 17 Jun 2024 17:56:39 -0400 Subject: [PATCH 18/22] merge changes --- .github/workflows/test_workflow.yml | 2 +- aviary/api.py | 3 +- aviary/docs/examples/OAS_subsystem.ipynb | 6 +- aviary/docs/examples/images/OAS_xdsm.PNG | Bin 0 -> 72585 bytes .../getting_started/input_csv_phase_info.md | 12 +- .../outputs_and_how_to_read_them.md | 10 +- .../docs/user_guide/variable_hierarchy.ipynb | 2 +- aviary/examples/level2_shooting_traj.py | 122 ++- .../run_2dof_reserve_mission_fixedrange.py | 3 +- .../run_2dof_reserve_mission_multiphase.py | 9 + aviary/examples/test/test_all_examples.py | 8 +- .../test/test_level2_shooting_traj.py | 20 +- .../default_phase_info/height_energy_fiti.py | 80 ++ .../default_phase_info/two_dof_fiti.py | 375 ++++---- .../two_dof_fiti_deprecated.py | 196 ++++ aviary/interface/methods_for_level1.py | 2 +- aviary/interface/methods_for_level2.py | 134 ++- .../test/test_time_integration_phases.py | 18 +- .../phases/time_integration_phases.py | 4 +- .../gasp_based/idle_descent_estimation.py | 172 +++- aviary/mission/gasp_based/ode/accel_eom.py | 16 - aviary/mission/gasp_based/ode/accel_ode.py | 29 +- aviary/mission/gasp_based/ode/ascent_eom.py | 12 - aviary/mission/gasp_based/ode/ascent_ode.py | 18 +- aviary/mission/gasp_based/ode/base_ode.py | 17 +- aviary/mission/gasp_based/ode/climb_eom.py | 10 - aviary/mission/gasp_based/ode/climb_ode.py | 36 +- .../ode/constraints/flight_constraints.py | 11 - aviary/mission/gasp_based/ode/descent_eom.py | 10 - aviary/mission/gasp_based/ode/descent_ode.py | 46 +- .../mission/gasp_based/ode/flight_path_eom.py | 10 - .../mission/gasp_based/ode/flight_path_ode.py | 55 +- .../mission/gasp_based/ode/groundroll_eom.py | 4 - .../mission/gasp_based/ode/groundroll_ode.py | 10 +- aviary/mission/gasp_based/ode/rotation_eom.py | 4 - aviary/mission/gasp_based/ode/rotation_ode.py | 13 +- .../ode/time_integration_base_classes.py | 28 +- .../phases/time_integration_phases.py | 29 +- .../phases/time_integration_traj.py | 18 +- .../test/test_idle_descent_estimation.py | 44 +- aviary/mission/phase_builder_base.py | 1 - aviary/models/N3CC/N3CC_data.py | 2 +- ...N3CC_generic_low_speed_polars_FLOPSinp.csv | 2 +- .../large_single_aisle_1_FLOPS_data.py | 2 +- .../large_single_aisle_2_FLOPS_data.py | 4 +- .../large_single_aisle_2_altwt_FLOPS_data.py | 4 +- ...ge_single_aisle_2_detailwing_FLOPS_data.py | 2 +- .../multi_engine_single_aisle_data.py | 2 +- .../test_aircraft/aircraft_for_bench_FwFm.csv | 2 +- .../test_aircraft/aircraft_for_bench_FwGm.csv | 2 +- .../test_aircraft/aircraft_for_bench_GwFm.csv | 2 +- .../aircraft_for_bench_solved2dof.csv | 2 +- aviary/subsystems/mass/gasp_based/fixed.py | 1 - aviary/utils/fortran_to_aviary.py | 13 +- aviary/utils/functions.py | 3 +- aviary/utils/process_input_decks.py | 89 +- aviary/utils/test/test_csv_data_file.py | 24 +- .../benchmark_tests/test_bench_FwGm.py | 4 +- .../benchmark_tests/test_bench_GwGm.py | 11 +- .../benchmark_tests/test_bench_multiengine.py | 4 +- aviary/variable_info/enums.py | 14 +- aviary/variable_info/variable_meta_data.py | 2 +- aviary/visualization/aircraft_3d_model.py | 892 ++++++++++++++++++ .../assets/aircraft_3d_file_template.html | 29 + .../visualization/assets/aviary_airlines.png | Bin 0 -> 64403 bytes aviary/visualization/dashboard.py | 95 +- setup.py | 2 +- 67 files changed, 2140 insertions(+), 668 deletions(-) create mode 100755 aviary/docs/examples/images/OAS_xdsm.PNG create mode 100644 aviary/interface/default_phase_info/height_energy_fiti.py create mode 100644 aviary/interface/default_phase_info/two_dof_fiti_deprecated.py create mode 100644 aviary/visualization/aircraft_3d_model.py create mode 100644 aviary/visualization/assets/aircraft_3d_file_template.html create mode 100644 aviary/visualization/assets/aviary_airlines.png diff --git a/.github/workflows/test_workflow.yml b/.github/workflows/test_workflow.yml index 9d867971f..966df359b 100644 --- a/.github/workflows/test_workflow.yml +++ b/.github/workflows/test_workflow.yml @@ -40,7 +40,7 @@ jobs: SCIPY: '1.6' PYOPTSPARSE: 'v2.9.1' SNOPT: '7.7' - OPENMDAO: '3.31.0' + OPENMDAO: '3.33.0' DYMOS: '1.8.0' # latest versions of openmdao/dymos diff --git a/aviary/api.py b/aviary/api.py index 4aa7b951c..f43eb032b 100644 --- a/aviary/api.py +++ b/aviary/api.py @@ -32,7 +32,8 @@ from aviary.utils.data_interpolator_builder import build_data_interpolator from aviary.variable_info.enums import AlphaModes, AnalysisScheme, ProblemType, SpeedType, GASPEngineType, FlapType, EquationsOfMotion, LegacyCode, Verbosity from aviary.interface.default_phase_info.two_dof import phase_info as default_2DOF_phase_info -from aviary.interface.default_phase_info.two_dof_fiti import create_2dof_based_ascent_phases, create_2dof_based_descent_phases +from aviary.interface.default_phase_info.two_dof_fiti import phase_info as default_2DOF_fiti_phase_info +from aviary.interface.default_phase_info.two_dof_fiti_deprecated import create_2dof_based_ascent_phases, create_2dof_based_descent_phases from aviary.interface.default_phase_info.height_energy import phase_info as default_height_energy_phase_info from aviary.interface.methods_for_level1 import run_level_1 from aviary.interface.methods_for_level1 import run_aviary diff --git a/aviary/docs/examples/OAS_subsystem.ipynb b/aviary/docs/examples/OAS_subsystem.ipynb index 9f53882e2..80ae95d9b 100644 --- a/aviary/docs/examples/OAS_subsystem.ipynb +++ b/aviary/docs/examples/OAS_subsystem.ipynb @@ -96,7 +96,9 @@ "## Analysis Model Details\n", "\n", "This analysis is based on the Aviary benchmark [aircraft_for_bench_FwFm](https://github.com/OpenMDAO/Aviary/blob/main/aviary/models/test_aircraft/aircraft_for_bench_FwFm.csv) input data representing a typical large single aisle class transport aircraft.\n", - "The analysis code [OAS_wing_weight_analysis](https://github.com/OpenMDAO/Aviary/blob/main/aviary/examples/external_subsystems/OAS_weight/OAS_wing_weight_analysis.py) contains the `OAStructures` class which performs a structural analysis of the wing.\n", + "The analysis code [OAS_wing_weight_analysis](https://github.com/OpenMDAO/Aviary/blob/main/aviary/examples/external_subsystems/OAS_weight/OAS_wing_weight_analysis.py) contains the `OAStructures` class which performs a structural analysis of the wing. The image below shows a simplified XDSM diagram of the pre-mission data flow in this example.\n", + "\n", + "![OAS XDSM](images/OAS_xdsm.PNG)\n", "\n", "We'll now discuss this code in more detail.\n", "\n", @@ -197,7 +199,7 @@ "## Example Run Script\n", "\n", "Here is the full run script used to run the simple mission with the OpenAeroStruct subsystem active.\n", - "This run script is also available in the [run_simple_OAS_mission file.](https://github.com/OpenMDAO/Aviary/blob/main/aviary/examples/external_subsystems/OAS/run_simple_OAS_mission.py)\n", + "This run script is also available in the [run_simple_OAS_mission file.](https://github.com/OpenMDAO/Aviary/blob/main/aviary/examples/external_subsystems/OAS_weight/run_simple_OAS_mission.py)\n", "\n", "\n", "```{note}\n", diff --git a/aviary/docs/examples/images/OAS_xdsm.PNG b/aviary/docs/examples/images/OAS_xdsm.PNG new file mode 100755 index 0000000000000000000000000000000000000000..b0a0010529d977965411c724be14d1ef512cf091 GIT binary patch literal 72585 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
    -pBXEKR`2ILTyqd2od@8&ig`CaGv9J6-Bm^e? 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 + Object containing values and units for all aviary inputs and options + + Returns + ------- + dict + Modified phase_info that has been changed to match the new mission + parameters + """ + + range_cruise = aviary_inputs.get_item(Mission.Design.RANGE) + alt_cruise = aviary_inputs.get_item(Mission.Design.CRUISE_ALTITUDE) + gross_mass = aviary_inputs.get_item(Mission.Design.GROSS_MASS) + mach_cruise = aviary_inputs.get_item(Mission.Design.MACH) + + phase_info['climb']['user_options']['alt_trigger'] = alt_cruise + phase_info['climb']['user_options']['mach'] = mach_cruise + + phase_info['cruise']['user_options']['mach'] = mach_cruise + + phase_info['descent']['user_options']['mach'] = mach_cruise + + return phase_info, post_mission_info diff --git a/aviary/interface/default_phase_info/two_dof_fiti.py b/aviary/interface/default_phase_info/two_dof_fiti.py index 29086e56d..ce06c54db 100644 --- a/aviary/interface/default_phase_info/two_dof_fiti.py +++ b/aviary/interface/default_phase_info/two_dof_fiti.py @@ -1,201 +1,206 @@ -import numpy as np - -from aviary.variable_info.enums import SpeedType, Verbosity +from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.enums import SpeedType, Verbosity, AlphaModes from aviary.mission.gasp_based.phases.time_integration_phases import SGMGroundroll, \ SGMRotation, SGMAscentCombined, SGMAccel, SGMClimb, SGMCruise, SGMDescent -from aviary.variable_info.variables import Aircraft, Mission, Dynamic +from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.variable_info.variable_meta_data import _MetaData as Mission # defaults for 2DOF based forward in time integeration phases - - -def create_2dof_based_ascent_phases( - ode_args, - cruise_alt=35e3, - cruise_mach=.8, -): - groundroll_kwargs = dict( - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, - ), - ) - groundroll_vals = { - # special case - 'attr:VR_value': {'val': 'SGMGroundroll_velocity_trigger', 'units': 'kn'}, - } - - rotation_kwargs = dict( - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, - ), - ) - rotation_vals = {} - - ascent_kwargs = dict( - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, - ), - ) - ascent_vals = { - 't_init_gear': {'val': 10000, 'units': 's'}, - 't_init_flaps': {'val': 10000, 'units': 's'}, - 'rotation.start_rotation': {'val': 10000, 'units': 's'}, # special case - # special case - 'attr:fuselage_angle_max': {'val': Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE, 'units': 'deg'}, - } - - accel_kwargs = dict( - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, - ), - ) - accel_vals = {} - - # need to set trigger units - climb1_kwargs = dict( - input_speed_type=SpeedType.EAS, - input_speed_units='kn', - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, - ), - ) - climb1_vals = { - 'alt_trigger': {'val': 10000, 'units': 'ft'}, - 'EAS': {'val': 250, 'units': climb1_kwargs['input_speed_units']}, - 'speed_trigger': {'val': cruise_mach, 'units': None}, - } - - climb2_kwargs = dict( - input_speed_type=SpeedType.EAS, - input_speed_units='kn', - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, - ), - ) - climb2_vals = { - 'alt_trigger': {'val': cruise_alt, 'units': 'ft'}, - 'EAS': {'val': 270, 'units': climb2_kwargs['input_speed_units']}, - 'speed_trigger': {'val': cruise_mach, 'units': None}, - } - - climb3_kwargs = dict( - input_speed_type=SpeedType.MACH, - input_speed_units='unitless', - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, - ), - ) - climb3_vals = { - 'alt_trigger': {'val': cruise_alt, 'units': 'ft'}, - 'mach': {'val': cruise_mach, 'units': climb3_kwargs['input_speed_units']}, - 'speed_trigger': {'val': 0, 'units': None}, - } - - phases = { - 'groundroll': { - 'ode': SGMGroundroll(**groundroll_kwargs), - 'vals_to_set': groundroll_vals, - }, - 'rotation': { - 'ode': SGMRotation(**rotation_kwargs), - 'vals_to_set': rotation_vals, +cruise_alt = 35e3, +cruise_mach = .8, + +takeoff_phases = { + 'groundroll': { + 'builder': SGMGroundroll, + 'user_options': { + # special case + 'attr:VR_value': ('SGMGroundroll_velocity_trigger', 'kn'), }, - 'ascent': { - 'ode': SGMAscentCombined(**ascent_kwargs), - 'vals_to_set': ascent_vals, + }, + 'rotation': { + 'builder': SGMRotation, + 'user_options': { }, - 'accel': { - 'ode': SGMAccel(**accel_kwargs), - 'vals_to_set': accel_vals, + }, + 'ascent': { + 'builder': SGMAscentCombined, + 'user_options': { + 't_init_gear': (10000, 's'), + 't_init_flaps': (10000, 's'), + # special case + 'rotation.start_rotation': (10000, 's'), + # special case + 'attr:fuselage_angle_max': (Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE, 'deg'), }, - 'climb1': { - 'ode': SGMClimb(**climb1_kwargs), - 'vals_to_set': climb1_vals, + }, + 'accel': { + 'builder': SGMAccel, + 'user_options': { }, - 'climb2': { - 'ode': SGMClimb(**climb2_kwargs), - 'vals_to_set': climb2_vals, + }, +} +climb_phases = { + 'climb1': { + 'kwargs': dict( + input_speed_type=SpeedType.EAS, + input_speed_units='kn', + speed_trigger_units='unitless', + ), + 'builder': SGMClimb, + 'user_options': { + 'alt_trigger': (10000, 'ft'), + 'EAS': (250, 'kn'), + 'speed_trigger': (cruise_mach, 'unitless'), }, - 'climb3': { - 'ode': SGMClimb(**climb3_kwargs), - 'vals_to_set': climb3_vals, + }, + 'climb2': { + 'kwargs': dict( + input_speed_type=SpeedType.EAS, + input_speed_units='kn', + speed_trigger_units='unitless', + ), + 'builder': SGMClimb, + 'user_options': { + 'alt_trigger': (cruise_alt, 'ft'), + 'EAS': (270, 'kn'), + 'speed_trigger': (cruise_mach, 'unitless'), }, - - } - - return phases - - -def create_2dof_based_descent_phases( - ode_args, - cruise_mach=.8, -): - - descent1_kwargs = dict( - input_speed_type=SpeedType.MACH, - input_speed_units="unitless", - speed_trigger_units='kn', - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, + }, + 'climb3': { + 'kwargs': dict( + input_speed_type=SpeedType.MACH, + input_speed_units='unitless', + speed_trigger_units='kn', ), - ) - descent1_vals = { - 'alt_trigger': {'val': 10000, 'units': 'ft'}, - 'mach': {'val': cruise_mach, 'units': descent1_kwargs['input_speed_units']}, - 'speed_trigger': {'val': 350, 'units': descent1_kwargs['speed_trigger_units']}, - } - - descent2_kwargs = dict( - input_speed_type=SpeedType.EAS, - input_speed_units="kn", - speed_trigger_units='kn', - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, + 'builder': SGMClimb, + 'user_options': { + 'alt_trigger': (cruise_alt, 'ft'), + 'mach': (cruise_mach, 'unitless'), + 'speed_trigger': (0, 'kn'), + }, + }, +} +ascent_phases = { + **takeoff_phases, + **climb_phases +} +cruise_phase = { + 'cruise': { + 'kwargs': dict( + input_speed_type=SpeedType.MACH, + input_speed_units="unitless", + alpha_mode=AlphaModes.REQUIRED_LIFT, ), - ) - descent2_vals = { - 'alt_trigger': {'val': 10000, 'units': 'ft'}, - 'EAS': {'val': 350, 'units': descent2_kwargs['input_speed_units']}, - 'speed_trigger': {'val': 0, 'units': descent2_kwargs['speed_trigger_units']}, - } - - descent3_kwargs = dict( - input_speed_type=SpeedType.EAS, - input_speed_units="kn", - speed_trigger_units='kn', - ode_args=ode_args, - simupy_args=dict( - verbosity=Verbosity.QUIET, + 'builder': SGMCruise, + 'user_options': { + 'mach': (cruise_mach, 'unitless'), + 'attr:mass_trigger': ('SGMCruise_mass_trigger', 'lbm') + }, + }, +} +descent_phases = { + 'desc1': { + 'kwargs': dict( + input_speed_type=SpeedType.MACH, + input_speed_units='unitless', + speed_trigger_units='kn', ), - ) - descent3_vals = { - 'alt_trigger': {'val': 1000, 'units': 'ft'}, - 'EAS': {'val': 250, 'units': descent3_kwargs['input_speed_units']}, - 'speed_trigger': {'val': 0, 'units': descent3_kwargs['speed_trigger_units']}, - } - - phases = { - 'descent1': { - 'ode': SGMDescent(**descent1_kwargs), - 'vals_to_set': descent1_vals, + 'builder': SGMDescent, + 'user_options': { + 'alt_trigger': (10000, 'ft'), + 'mach': (cruise_mach, 'unitless'), + 'speed_trigger': (350, 'kn'), + Dynamic.Mission.THROTTLE: (0, 'unitless'), }, - 'descent2': { - 'ode': SGMDescent(**descent2_kwargs), - 'vals_to_set': descent2_vals, + 'descent_phase': True, + }, + 'desc2': { + 'kwargs': dict( + input_speed_type=SpeedType.EAS, + input_speed_units='kn', + speed_trigger_units='kn', + ), + 'builder': SGMDescent, + 'user_options': { + 'alt_trigger': (10000, 'ft'), + 'EAS': (350, 'kn'), + 'speed_trigger': (0, 'kn'), + Dynamic.Mission.THROTTLE: (0, 'unitless'), }, - 'descent3': { - 'ode': SGMDescent(**descent3_kwargs), - 'vals_to_set': descent3_vals, + 'descent_phase': True, + }, + 'desc3': { + 'kwargs': dict( + input_speed_type=SpeedType.EAS, + input_speed_units='kn', + speed_trigger_units='kn', + ), + 'builder': SGMDescent, + 'user_options': { + 'alt_trigger': (1000, 'ft'), + 'EAS': (250, 'kn'), + 'speed_trigger': (0, 'kn'), + Dynamic.Mission.THROTTLE: (0, 'unitless'), }, - - } - - return phases + 'descent_phase': True, + }, +} + +phase_info = { + **ascent_phases, + **cruise_phase, + **descent_phases, +} + + +def phase_info_parameterization(phase_info, post_mission_info, aviary_inputs: AviaryValues): + """ + Modify the values in the phase_info dictionary to accomodate different values + for the following mission design inputs: cruise altitude, cruise mach number, + cruise range, design gross mass. + + Parameters + ---------- + phase_info : dict + Dictionary of phase settings for a mission profile + aviary_inputs : + Object containing values and units for all aviary inputs and options + + Returns + ------- + dict + Modified phase_info that has been changed to match the new mission + parameters + """ + + range_cruise = aviary_inputs.get_item(Mission.Design.RANGE) + alt_cruise = aviary_inputs.get_item(Mission.Design.CRUISE_ALTITUDE) + gross_mass = aviary_inputs.get_item(Mission.Design.GROSS_MASS) + mach_cruise = aviary_inputs.get_item(Mission.Design.MACH) + + phase_info['climb1']['user_options']['speed_trigger'] = mach_cruise + + phase_info['climb2']['user_options']['alt_trigger'] = alt_cruise + phase_info['climb2']['user_options']['speed_trigger'] = mach_cruise + + phase_info['climb3']['user_options']['alt_trigger'] = alt_cruise + phase_info['climb3']['user_options']['mach'] = mach_cruise + + phase_info['cruise']['user_options']['mach'] = mach_cruise + + phase_info['desc1']['user_options']['mach'] = mach_cruise + + return phase_info, post_mission_info + + +def add_default_sgm_args(phase_info: dict, ode_args: dict, verbosity=None): + for phase_name, info in phase_info.items(): + kwargs = info.get('kwargs', {}) + if 'ode_args' not in kwargs: + kwargs['ode_args'] = ode_args + if 'simupy_args' not in kwargs: + if verbosity is None: + verbosity, _ = ode_args['aviary_options'].get_item( + Settings.VERBOSITY, default=(Verbosity.QUIET, 'unitless')) + kwargs['simupy_args'] = {'verbosity': verbosity} + info['kwargs'] = kwargs diff --git a/aviary/interface/default_phase_info/two_dof_fiti_deprecated.py b/aviary/interface/default_phase_info/two_dof_fiti_deprecated.py new file mode 100644 index 000000000..7685dabde --- /dev/null +++ b/aviary/interface/default_phase_info/two_dof_fiti_deprecated.py @@ -0,0 +1,196 @@ +import warnings + +from aviary.variable_info.enums import SpeedType, Verbosity +from aviary.mission.gasp_based.phases.time_integration_phases import SGMGroundroll, \ + SGMRotation, SGMAscentCombined, SGMAccel, SGMClimb, SGMCruise, SGMDescent +from aviary.variable_info.variables import Aircraft + +# defaults for 2DOF based forward in time integeration phases + + +def create_2dof_based_ascent_phases( + ode_args, + cruise_alt=35e3, + cruise_mach=.8, + simupy_args=dict( + verbosity=Verbosity.QUIET, + ), +): + + warnings.warn("`descent_range_and_fuel` has been replaced with `add_descent_estimation_as_submodel`" + "\nThe new methodology uses a phase_info that more closely matches that of collocation", + DeprecationWarning) + + groundroll_kwargs = dict( + ode_args=ode_args, + simupy_args=simupy_args, + ) + groundroll_vals = { + # special case + 'attr:VR_value': {'val': 'SGMGroundroll_velocity_trigger', 'units': 'kn'}, + } + + rotation_kwargs = dict( + ode_args=ode_args, + simupy_args=simupy_args, + ) + rotation_vals = {} + + ascent_kwargs = dict( + ode_args=ode_args, + simupy_args=simupy_args, + ) + ascent_vals = { + 't_init_gear': {'val': 10000, 'units': 's'}, + 't_init_flaps': {'val': 10000, 'units': 's'}, + 'rotation.start_rotation': {'val': 10000, 'units': 's'}, # special case + # special case + 'attr:fuselage_angle_max': {'val': Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE, 'units': 'deg'}, + } + + accel_kwargs = dict( + ode_args=ode_args, + simupy_args=simupy_args, + ) + accel_vals = {} + + # need to set trigger units + climb1_kwargs = dict( + input_speed_type=SpeedType.EAS, + input_speed_units='kn', + ode_args=ode_args, + simupy_args=simupy_args, + ) + climb1_vals = { + 'alt_trigger': {'val': 10000, 'units': 'ft'}, + 'EAS': {'val': 250, 'units': climb1_kwargs['input_speed_units']}, + 'speed_trigger': {'val': cruise_mach, 'units': None}, + } + + climb2_kwargs = dict( + input_speed_type=SpeedType.EAS, + input_speed_units='kn', + ode_args=ode_args, + simupy_args=simupy_args, + ) + climb2_vals = { + 'alt_trigger': {'val': cruise_alt, 'units': 'ft'}, + 'EAS': {'val': 270, 'units': climb2_kwargs['input_speed_units']}, + 'speed_trigger': {'val': cruise_mach, 'units': None}, + } + + climb3_kwargs = dict( + input_speed_type=SpeedType.MACH, + input_speed_units='unitless', + ode_args=ode_args, + simupy_args=simupy_args, + ) + climb3_vals = { + 'alt_trigger': {'val': cruise_alt, 'units': 'ft'}, + 'mach': {'val': cruise_mach, 'units': climb3_kwargs['input_speed_units']}, + 'speed_trigger': {'val': 0, 'units': None}, + } + + phases = { + 'groundroll': { + 'ode': SGMGroundroll(**groundroll_kwargs), + 'vals_to_set': groundroll_vals, + }, + 'rotation': { + 'ode': SGMRotation(**rotation_kwargs), + 'vals_to_set': rotation_vals, + }, + 'ascent': { + 'ode': SGMAscentCombined(**ascent_kwargs), + 'vals_to_set': ascent_vals, + }, + 'accel': { + 'ode': SGMAccel(**accel_kwargs), + 'vals_to_set': accel_vals, + }, + 'climb1': { + 'ode': SGMClimb(**climb1_kwargs), + 'vals_to_set': climb1_vals, + }, + 'climb2': { + 'ode': SGMClimb(**climb2_kwargs), + 'vals_to_set': climb2_vals, + }, + 'climb3': { + 'ode': SGMClimb(**climb3_kwargs), + 'vals_to_set': climb3_vals, + }, + + } + + return phases + + +def create_2dof_based_descent_phases( + ode_args, + cruise_mach=.8, + simupy_args=dict( + verbosity=Verbosity.QUIET, + ), +): + + warnings.warn("`descent_range_and_fuel` has been replaced with `add_descent_estimation_as_submodel`" + "\nThe new methodology uses a phase_info that more closely matches that of collocation", + DeprecationWarning) + + descent1_kwargs = dict( + input_speed_type=SpeedType.MACH, + input_speed_units="unitless", + speed_trigger_units='kn', + ode_args=ode_args, + simupy_args=simupy_args, + ) + descent1_vals = { + 'alt_trigger': {'val': 10000, 'units': 'ft'}, + 'mach': {'val': cruise_mach, 'units': descent1_kwargs['input_speed_units']}, + 'speed_trigger': {'val': 350, 'units': descent1_kwargs['speed_trigger_units']}, + } + + descent2_kwargs = dict( + input_speed_type=SpeedType.EAS, + input_speed_units="kn", + speed_trigger_units='kn', + ode_args=ode_args, + simupy_args=simupy_args, + ) + descent2_vals = { + 'alt_trigger': {'val': 10000, 'units': 'ft'}, + 'EAS': {'val': 350, 'units': descent2_kwargs['input_speed_units']}, + 'speed_trigger': {'val': 0, 'units': descent2_kwargs['speed_trigger_units']}, + } + + descent3_kwargs = dict( + input_speed_type=SpeedType.EAS, + input_speed_units="kn", + speed_trigger_units='kn', + ode_args=ode_args, + simupy_args=simupy_args, + ) + descent3_vals = { + 'alt_trigger': {'val': 1000, 'units': 'ft'}, + 'EAS': {'val': 250, 'units': descent3_kwargs['input_speed_units']}, + 'speed_trigger': {'val': 0, 'units': descent3_kwargs['speed_trigger_units']}, + } + + phases = { + 'descent1': { + 'ode': SGMDescent(**descent1_kwargs), + 'vals_to_set': descent1_vals, + }, + 'descent2': { + 'ode': SGMDescent(**descent2_kwargs), + 'vals_to_set': descent2_vals, + }, + 'descent3': { + 'ode': SGMDescent(**descent3_kwargs), + 'vals_to_set': descent3_vals, + }, + + } + + return phases diff --git a/aviary/interface/methods_for_level1.py b/aviary/interface/methods_for_level1.py index ee1dc518f..a13bcf584 100644 --- a/aviary/interface/methods_for_level1.py +++ b/aviary/interface/methods_for_level1.py @@ -183,7 +183,7 @@ def _setup_level1_parser(parser): def _exec_level1(args, user_args): - if args.shooting: # For future use + if args.shooting: analysis_scheme = AnalysisScheme.SHOOTING else: analysis_scheme = AnalysisScheme.COLLOCATION diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index bee57bd99..a29b8838c 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -56,8 +56,8 @@ from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.merge_variable_metadata import merge_meta_data -from aviary.interface.default_phase_info.two_dof_fiti import create_2dof_based_ascent_phases, create_2dof_based_descent_phases -from aviary.mission.gasp_based.idle_descent_estimation import descent_range_and_fuel +from aviary.interface.default_phase_info.two_dof_fiti import add_default_sgm_args +from aviary.mission.gasp_based.idle_descent_estimation import add_descent_estimation_as_submodel from aviary.mission.phase_builder_base import PhaseBuilderBase @@ -234,7 +234,7 @@ def __init__(self, analysis_scheme=AnalysisScheme.COLLOCATION, **kwargs): self.regular_phases = [] self.reserve_phases = [] - def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, verbosity=Verbosity.BRIEF, meta_data=BaseMetaData): + def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, meta_data=BaseMetaData, verbosity=None): """ This method loads the aviary_values inputs and options that the user specifies. They could specify files to load and values to @@ -249,7 +249,7 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, verb # Create AviaryValues object from file (or process existing AviaryValues object # with default values from metadata) and generate initial guesses aviary_inputs, initial_guesses = create_vehicle( - aviary_inputs, verbosity=verbosity, meta_data=meta_data) + aviary_inputs, meta_data=meta_data, verbosity=verbosity) # pull which methods will be used for subsystems and mission self.mission_method = mission_method = aviary_inputs.get_val( @@ -298,7 +298,14 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, verb else: if self.mission_method is TWO_DEGREES_OF_FREEDOM: - from aviary.interface.default_phase_info.two_dof import phase_info + if self.analysis_scheme is AnalysisScheme.COLLOCATION: + from aviary.interface.default_phase_info.two_dof import phase_info + elif self.analysis_scheme is AnalysisScheme.SHOOTING: + from aviary.interface.default_phase_info.two_dof_fiti import phase_info, \ + phase_info_parameterization + phase_info, _ = phase_info_parameterization( + phase_info, None, self.aviary_inputs) + elif self.mission_method is HEIGHT_ENERGY: from aviary.interface.default_phase_info.height_energy import phase_info @@ -351,6 +358,7 @@ def phase_separator(self): This method checks for reserve=True & False Returns an error if a non-reserve phase is specified after a reserve phase. return two dictionaries of phases: regular_phases and reserve_phases + For shooting trajectories, this will also check if a phase is part of the descent """ # Check to ensure no non-reserve phases are specified after reserve phases @@ -381,6 +389,13 @@ def phase_separator(self): f'Regular Phases : {self.regular_phases} | ' f'Reserve Phases : {self.reserve_phases} ') + if self.analysis_scheme is AnalysisScheme.SHOOTING: + self.descent_phases = {} + for name, info in self.phase_info.items(): + descent = info.get('descent_phase', False) + if descent: + self.descent_phases[name] = info + def check_and_preprocess_inputs(self): """ This method checks the user-supplied input values for any potential problems @@ -432,7 +447,8 @@ def check_and_preprocess_inputs(self): self.phase_info[phase_name]["user_options"].update({ "fix_duration": True}) - check_phase_info(self.phase_info, self.mission_method) + if self.analysis_scheme is AnalysisScheme.COLLOCATION: + check_phase_info(self.phase_info, self.mission_method) for phase_name in self.phase_info: for external_subsystem in self.phase_info[phase_name]['external_subsystems']: @@ -455,8 +471,6 @@ def check_and_preprocess_inputs(self): else: raise ValueError(f'Unknown mission method {self.mission_method}') - if Settings.VERBOSITY not in aviary_inputs: - aviary_inputs.set_val(Settings.VERBOSITY, Verbosity.BRIEF) prop = CorePropulsionBuilder( 'core_propulsion', engine_models=self.engine_builders) mass = CoreMassBuilder('core_mass', code_origin=self.mass_method) @@ -489,14 +503,15 @@ def check_and_preprocess_inputs(self): code_origin_to_prioritize=code_origin_to_prioritize) subsystems = self.core_subsystems = {'propulsion': prop, - 'geometry': geom, - 'mass': mass, - 'aerodynamics': aero} + 'geometry': geom, + 'mass': mass, + 'aerodynamics': aero} # TODO optionally accept which subsystems to load from phase_info - default_mission_subsystems = [subsystems['aerodynamics'], subsystems['propulsion']] + default_mission_subsystems = [ + subsystems['aerodynamics'], subsystems['propulsion']] self.ode_args = {'aviary_options': aviary_inputs, - 'mission_subsystems': default_mission_subsystems} + 'core_subsystems': default_mission_subsystems} self._update_metadata_from_subsystems() @@ -584,7 +599,16 @@ def _add_two_dof_takeoff_systems(self): add_opts2vals(self.model, OptionsToValues, self.aviary_inputs) if self.analysis_scheme is AnalysisScheme.SHOOTING: - self._add_fuel_reserve_component(post_mission=False) + self._add_fuel_reserve_component( + post_mission=False, reserves_name='reserve_fuel_estimate') + add_default_sgm_args(self.descent_phases, self.ode_args) + add_descent_estimation_as_submodel( + self, + phases=self.descent_phases, + cruise_mach=self.cruise_mach, + cruise_alt=self.cruise_alt, + reserve_fuel='reserve_fuel_estimate', + ) # Add thrust-to-weight ratio subsystem self.model.add_subsystem( @@ -932,7 +956,7 @@ def add_phases(self, phase_info_parameterization=None): Parameters ---------- phase_info_parameterization (function, optional): A function that takes in the phase_info dictionary - and aviary_inputs and returns modified aviary_inputs. Defaults to None. + and aviary_inputs and returns modified phase_info. Defaults to None. Returns ------- @@ -945,59 +969,16 @@ def add_phases(self, phase_info_parameterization=None): phase_info = self.phase_info - phases = list(phase_info.keys()) - if self.analysis_scheme is AnalysisScheme.COLLOCATION: + phases = list(phase_info.keys()) traj = self.model.add_subsystem('traj', dm.Trajectory()) elif self.analysis_scheme is AnalysisScheme.SHOOTING: - initial_mass = self.aviary_inputs.get_val(Mission.Summary.GROSS_MASS, 'lbm') - - ascent_phases = create_2dof_based_ascent_phases( - self.ode_args, - cruise_alt=self.cruise_alt, - cruise_mach=self.cruise_mach) - - descent_phases = create_2dof_based_descent_phases( - self.ode_args, - cruise_mach=self.cruise_mach) - - descent_estimation = descent_range_and_fuel( - phases=descent_phases, - initial_mass=initial_mass, - cruise_alt=self.cruise_alt, - cruise_mach=self.cruise_mach) - - estimated_descent_range = descent_estimation['refined_guess']['distance_flown'] - end_of_cruise_range = self.target_range - estimated_descent_range - - # based on reserve_fuel - estimated_descent_fuel = descent_estimation['refined_guess']['fuel_burned'] + vb = self.aviary_inputs.get_val(Settings.VERBOSITY) + add_default_sgm_args(self.phase_info, self.ode_args, vb) - cruise_kwargs = dict( - input_speed_type=SpeedType.MACH, - input_speed_units="unitless", - ode_args=self.ode_args, - alpha_mode=AlphaModes.REQUIRED_LIFT, - simupy_args=dict( - verbosity=Verbosity.DEBUG, - ), - ) - cruise_vals = { - 'mach': {'val': self.cruise_mach, 'units': cruise_kwargs['input_speed_units']}, - 'descent_fuel': {'val': estimated_descent_fuel, 'units': 'lbm'}, - } - - phases = { - **ascent_phases, - 'cruise': { - 'ode': SGMCruise(**cruise_kwargs), - 'vals_to_set': cruise_vals, - }, - **descent_phases, - } full_traj = FlexibleTraj( - Phases=phases, + Phases=self.phase_info, traj_final_state_output=[ Dynamic.Mission.MASS, Dynamic.Mission.DISTANCE, @@ -1011,16 +992,17 @@ def add_phases(self, phase_info_parameterization=None): # specify ODE, output_name, with units that SimuPyProblem expects # assume event function is of form ODE.output_name - value # third key is event_idx associated with input - (phases['groundroll']['ode'], Dynamic.Mission.VELOCITY, 0,), - (phases['climb3']['ode'], Dynamic.Mission.ALTITUDE, 0,), - (phases['cruise']['ode'], Dynamic.Mission.MASS, 0,), + ('groundroll', Dynamic.Mission.VELOCITY, 0,), + ('climb3', Dynamic.Mission.ALTITUDE, 0,), + ('cruise', Dynamic.Mission.MASS, 0,), ], traj_intermediate_state_output=[ ('cruise', Dynamic.Mission.DISTANCE), ('cruise', Dynamic.Mission.MASS), ] ) - traj = self.model.add_subsystem('traj', full_traj) + traj = self.model.add_subsystem('traj', full_traj, promotes_inputs=[ + ('altitude_initial', Mission.Design.CRUISE_ALTITUDE)]) self.model.add_subsystem( 'actual_descent_fuel', @@ -1030,6 +1012,7 @@ def add_phases(self, phase_info_parameterization=None): traj_mass_final={'units': 'lbm'}, )) + self.model.connect('start_of_descent_mass', 'traj.SGMCruise_mass_trigger') self.model.connect( 'traj.mass_final', 'actual_descent_fuel.traj_mass_final', @@ -1191,8 +1174,7 @@ def add_post_mission_systems(self, include_landing=True): self.model.connect(f"traj.{self.reserve_phases[-1]}.timeseries.mass", "reserve_fuel_burned.mass_final", src_indices=[-1]) - if self.analysis_scheme is not AnalysisScheme.SHOOTING: - self._add_fuel_reserve_component() + self._add_fuel_reserve_component() # TODO: need to add some sort of check that this value is less than the fuel capacity # TODO: the overall_fuel variable is the burned fuel plus the reserve, but should @@ -1241,7 +1223,7 @@ def add_post_mission_systems(self, include_landing=True): # If a target distance (or time) has been specified for this phase # distance (or time) is measured from the start of this phase to the end of this phase - for idx, phase_name in enumerate(self.phase_info): + for phase_name in self.phase_info: if 'target_distance' in self.phase_info[phase_name]["user_options"]: target_distance = wrapped_convert_units( self.phase_info[phase_name]["user_options"]["target_distance"], 'nmi') @@ -1507,8 +1489,8 @@ def link_phases(self): self.model, trajs=["traj"], phases=[ - self.regular_phases, - self.reserve_phases + [*self.regular_phases, + *self.reserve_phases] ], ) @@ -1559,6 +1541,7 @@ def link_phases(self): self.model.promotes("taxi", inputs=param_list) self.model.promotes("landing", inputs=param_list) if self.analysis_scheme is AnalysisScheme.SHOOTING: + param_list.append(Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE) self.model.promotes("traj", inputs=param_list) # self.model.list_inputs() # self.model.promotes("traj", inputs=['ascent.ODE_group.eoms.'+Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE]) @@ -1667,8 +1650,11 @@ def add_driver(self, optimizer=None, use_coloring=None, max_iter=50, verbosity=V driver.options['debug_print'] = verbosity elif verbosity.value > Verbosity.DEBUG.value: driver.options['debug_print'] = ['desvars', 'ln_cons', 'nl_cons', 'objs'] - if verbosity is not Verbosity.DEBUG and optimizer in ("SNOPT", "IPOPT"): - driver.options['print_results'] = False + if optimizer in ("SNOPT", "IPOPT"): + if verbosity is Verbosity.QUIET: + driver.options['print_results'] = False + elif verbosity is not Verbosity.DEBUG: + driver.options['print_results'] = 'minimal' def add_design_variables(self): """ @@ -1711,7 +1697,7 @@ def add_design_variables(self): optimize_mass = self.pre_mission_info.get('optimize_mass') if optimize_mass: self.model.add_design_var(Mission.Design.GROSS_MASS, units='lbm', - lower=100.e2, upper=200.e3, ref=135.e3) + lower=100.e2, upper=900.e3, ref=135.e3) elif self.mission_method is TWO_DEGREES_OF_FREEDOM: if self.analysis_scheme is AnalysisScheme.COLLOCATION: diff --git a/aviary/mission/flops_based/phases/test/test_time_integration_phases.py b/aviary/mission/flops_based/phases/test/test_time_integration_phases.py index f47e4d36b..27d42e9b5 100644 --- a/aviary/mission/flops_based/phases/test/test_time_integration_phases.py +++ b/aviary/mission/flops_based/phases/test/test_time_integration_phases.py @@ -11,6 +11,7 @@ from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings from aviary.variable_info.variables_in import VariablesIn +from aviary.interface.default_phase_info.height_energy_fiti import add_default_sgm_args from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.process_input_decks import create_vehicle @@ -61,6 +62,8 @@ def setup_prob(self, phases) -> om.Problem: aviary_options = self.ode_args['aviary_options'] subsystems = self.ode_args['core_subsystems'] + add_default_sgm_args(phases, self.ode_args) + traj = FlexibleTraj( Phases=phases, promote_all_auto_ivc=True, @@ -169,16 +172,13 @@ def test_cruise(self): "traj.mach": {'val': .8, 'units': "unitless"}, } - SGMCruise = SGMHeightEnergy( - self.ode_args, - phase_name='cruise', - simupy_args=dict(verbosity=Verbosity.QUIET,) - ) - SGMCruise.triggers[0].value = 160000 - phases = {'HE': { - 'ode': SGMCruise, - 'vals_to_set': {} + 'kwargs': { + 'mass_trigger': (160000, 'lbm'), + }, + 'builder': SGMHeightEnergy, + "user_options": { + }, }} final_states = self.run_simulation(phases, initial_values_cruise) diff --git a/aviary/mission/flops_based/phases/time_integration_phases.py b/aviary/mission/flops_based/phases/time_integration_phases.py index 7590d469c..4f01175df 100644 --- a/aviary/mission/flops_based/phases/time_integration_phases.py +++ b/aviary/mission/flops_based/phases/time_integration_phases.py @@ -12,6 +12,7 @@ def __init__( ode_args, phase_name='mission', simupy_args={}, + mass_trigger=(150000, 'lbm') ): super().__init__(MissionODE( analysis_scheme=AnalysisScheme.SHOOTING, @@ -29,7 +30,8 @@ def __init__( **simupy_args) self.phase_name = phase_name - self.add_trigger(Dynamic.Mission.MASS, 150000, units='lbm') + self.mass_trigger = mass_trigger + self.add_trigger(Dynamic.Mission.MASS, 'mass_trigger') class SGMDetailedTakeoff(SimuPyProblem): diff --git a/aviary/mission/gasp_based/idle_descent_estimation.py b/aviary/mission/gasp_based/idle_descent_estimation.py index 50fb3759d..67e282e67 100644 --- a/aviary/mission/gasp_based/idle_descent_estimation.py +++ b/aviary/mission/gasp_based/idle_descent_estimation.py @@ -2,9 +2,13 @@ import openmdao.api as om -from aviary.interface.default_phase_info.two_dof_fiti import create_2dof_based_descent_phases +from aviary.interface.default_phase_info.two_dof_fiti_deprecated import create_2dof_based_descent_phases from aviary.mission.gasp_based.phases.time_integration_traj import FlexibleTraj from aviary.variable_info.variables import Aircraft, Mission, Dynamic +from aviary.variable_info.enums import Verbosity +from aviary.utils.functions import set_aviary_initial_values, promote_aircraft_and_mission_vars +from aviary.variable_info.variables_in import VariablesIn +from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData def descent_range_and_fuel( @@ -17,6 +21,9 @@ def descent_range_and_fuel( payload_weight=30800, reserve_fuel=4998, ): + warnings.warn("`descent_range_and_fuel` has been replaced with `add_descent_estimation_as_submodel`," + " due to it's better integration with the other subsystems." + "\ndescent_range_and_fuel will be removed in a future release.", DeprecationWarning) prob = om.Problem() prob.driver = om.pyOptSparseDriver() @@ -106,3 +113,166 @@ def descent_range_and_fuel( } return results + + +def add_descent_estimation_as_submodel( + main_prob: om.Problem, + subsys_name='idle_descent_estimation', + phases=None, + ode_args=None, + initial_mass=None, + cruise_alt=None, + cruise_mach=None, + reserve_fuel=None, + verbosity=Verbosity.QUIET, +): + + if phases is None: + from aviary.interface.default_phase_info.two_dof_fiti import \ + descent_phases as phases, add_default_sgm_args + add_default_sgm_args(phases, ode_args) + + traj = FlexibleTraj( + Phases=phases, + traj_initial_state_input=[ + Dynamic.Mission.MASS, + Dynamic.Mission.DISTANCE, + Dynamic.Mission.ALTITUDE, + ], + traj_final_state_output=[ + Dynamic.Mission.MASS, + Dynamic.Mission.DISTANCE, + Dynamic.Mission.ALTITUDE, + ], + promote_all_auto_ivc=True, + ) + + model = om.Group() + + if isinstance(initial_mass, str): + model.add_subsystem( + 'top_of_descent_mass', + om.ExecComp( + 'mass_initial = top_of_descent_mass', + mass_initial={'units': 'lbm'}, + top_of_descent_mass={'units': 'lbm'}, + ), + promotes_inputs=['top_of_descent_mass'], + promotes_outputs=['mass_initial']) + else: + model.add_subsystem( + 'top_of_descent_mass', + om.ExecComp( + 'mass_initial = operating_mass + payload_mass + reserve_fuel + descent_fuel_estimate', + mass_initial={'units': 'lbm'}, + operating_mass={'units': 'lbm'}, + payload_mass={'units': 'lbm'}, + reserve_fuel={'units': 'lbm', 'val': 0}, + descent_fuel_estimate={'units': 'lbm', 'val': 0}, + ), + promotes_inputs=[ + ('operating_mass', Aircraft.Design.OPERATING_MASS), + ('payload_mass', Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS), + 'reserve_fuel', + # ('reserve_fuel', Mission.Design.RESERVE_FUEL), + ('descent_fuel_estimate', 'descent_fuel'), + ], + promotes_outputs=['mass_initial'] + ) + + model.add_subsystem( + 'descent_traj', traj, + promotes_inputs=['altitude_initial', 'mass_initial', 'aircraft:*'], + promotes_outputs=['mass_final', 'distance_final'], + ) + + model.add_subsystem( + 'actual_fuel_burn', + om.ExecComp( + 'actual_fuel_burn = mass_initial - mass_final', + actual_fuel_burn={'units': 'lbm'}, + mass_initial={'units': 'lbm'}, + mass_final={'units': 'lbm'}, + ), + promotes_inputs=[ + 'mass_initial', + 'mass_final', + ], + promotes_outputs=[('actual_fuel_burn', 'descent_fuel')]) + + if verbosity.value >= 1: + from aviary.utils.functions import create_printcomp + dummy_comp = create_printcomp( + all_inputs=[ + Aircraft.Design.OPERATING_MASS, + Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS, + 'descent_fuel', + 'reserve_fuel', + 'mass_initial', + 'distance_final', + ], + input_units={ + 'descent_fuel': 'lbm', + 'reserve_fuel': 'lbm', + 'mass_initial': 'lbm', + 'distance_final': 'nmi', + }) + model.add_subsystem( + "dummy_comp", + dummy_comp(), + promotes_inputs=["*"], + ) + model.set_input_defaults('reserve_fuel', 0, 'lbm') + model.set_input_defaults('mass_initial', 0, 'lbm') + + model.add_objective("descent_fuel", ref=1e4) + + model.linear_solver = om.DirectSolver(assemble_jac=True) + model.nonlinear_solver = om.NonlinearBlockGS(iprint=3, rtol=1e-2, maxiter=5) + + input_aliases = [] + if isinstance(initial_mass, str): + input_aliases.append(('top_of_descent_mass', initial_mass)) + elif isinstance(initial_mass, (int, float)): + model.set_input_defaults('mass_initial', initial_mass) + + if isinstance(cruise_alt, str): + input_aliases.append(('altitude_initial', cruise_alt)) + elif isinstance(cruise_alt, (int, float)): + model.set_input_defaults('altitude_initial', cruise_alt) + + if isinstance(reserve_fuel, str): + input_aliases.append(('reserve_fuel', reserve_fuel)) + elif isinstance(reserve_fuel, (int, float)): + model.set_input_defaults('reserve_fuel', reserve_fuel) + + model.set_input_defaults(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS, 0) + model.set_input_defaults( + Aircraft.Design.OPERATING_MASS, val=0, units='lbm') + model.set_input_defaults('descent_traj.'+Dynamic.Mission.THROTTLE, 0) + + promote_aircraft_and_mission_vars(model) + + subprob = om.Problem(model=model) + subcomp = om.SubmodelComp( + problem=subprob, + inputs=[ + 'aircraft:*', + ], + outputs=['distance_final', 'descent_fuel', 'mass_initial'], + do_coloring=False + ) + + main_prob.model.add_subsystem( + subsys_name, + subcomp, + promotes_inputs=[ + 'aircraft:*', + ] + input_aliases, + promotes_outputs=[ + ('distance_final', 'descent_range'), + 'descent_fuel', + ('mass_initial', 'start_of_descent_mass'), + ], + + ) diff --git a/aviary/mission/gasp_based/ode/accel_eom.py b/aviary/mission/gasp_based/ode/accel_eom.py index 36eb09cea..04f0d3ac9 100644 --- a/aviary/mission/gasp_based/ode/accel_eom.py +++ b/aviary/mission/gasp_based/ode/accel_eom.py @@ -2,7 +2,6 @@ import openmdao.api as om from aviary.constants import GRAV_ENGLISH_GASP, GRAV_ENGLISH_LBM -from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.variables import Dynamic @@ -16,11 +15,8 @@ class AccelerationRates(om.ExplicitComponent): def initialize(self): self.options.declare("num_nodes", types=int) - self.options.declare("analysis_scheme", types=AnalysisScheme, default=AnalysisScheme.COLLOCATION, - desc="The analysis method that will be used to close the trajectory; for example collocation or time integration") def setup(self): - analysis_scheme = self.options["analysis_scheme"] nn = self.options["num_nodes"] arange = np.arange(nn) @@ -68,16 +64,7 @@ def setup(self): self.declare_partials(Dynamic.Mission.DISTANCE_RATE, [ Dynamic.Mission.VELOCITY], rows=arange, cols=arange, val=1.) - if analysis_scheme is AnalysisScheme.SHOOTING: - self.add_input("t_curr", val=np.ones(nn), desc="time", units="s") - self.add_output(Dynamic.Mission.ALTITUDE_RATE, val=np.ones(nn), - desc="altitude rate", units="ft/s") - self.add_input( - Dynamic.Mission.DISTANCE, val=np.ones(nn), desc="distance traveled", units="ft") - def compute(self, inputs, outputs): - analysis_scheme = self.options["analysis_scheme"] - weight = inputs[Dynamic.Mission.MASS] * GRAV_ENGLISH_LBM drag = inputs[Dynamic.Mission.DRAG] thrust = inputs[Dynamic.Mission.THRUST_TOTAL] @@ -87,9 +74,6 @@ def compute(self, inputs, outputs): GRAV_ENGLISH_GASP / weight) * (thrust - drag) outputs[Dynamic.Mission.DISTANCE_RATE] = TAS - if analysis_scheme is AnalysisScheme.SHOOTING: - outputs[Dynamic.Mission.ALTITUDE_RATE] = 0 - def compute_partials(self, inputs, J): weight = inputs[Dynamic.Mission.MASS] * GRAV_ENGLISH_LBM drag = inputs[Dynamic.Mission.DRAG] diff --git a/aviary/mission/gasp_based/ode/accel_ode.py b/aviary/mission/gasp_based/ode/accel_ode.py index 4b11441e8..11fff2049 100644 --- a/aviary/mission/gasp_based/ode/accel_ode.py +++ b/aviary/mission/gasp_based/ode/accel_ode.py @@ -5,10 +5,9 @@ from aviary.mission.gasp_based.ode.base_ode import BaseODE from aviary.mission.gasp_based.ode.params import ParamPort from aviary.subsystems.mass.mass_to_weight import MassToWeight -from aviary.variable_info.enums import AnalysisScheme, SpeedType +from aviary.variable_info.enums import AnalysisScheme, AnalysisScheme, SpeedType from aviary.variable_info.variables import Aircraft, Dynamic, Mission -from aviary.mission.ode.specific_energy_rate import SpecificEnergyRate -from aviary.mission.ode.altitude_rate import AltitudeRate +from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs, add_SGM_required_outputs class AccelODE(BaseODE): @@ -25,6 +24,15 @@ def setup(self): aviary_options = self.options['aviary_options'] core_subsystems = self.options['core_subsystems'] + if analysis_scheme is AnalysisScheme.SHOOTING: + add_SGM_required_inputs(self, { + 't_curr': {'units': 's'}, + Dynamic.Mission.DISTANCE: {'units': 'ft'}, + }) + add_SGM_required_outputs(self, { + Dynamic.Mission.ALTITUDE_RATE: {'units': 'ft/s'}, + }) + # TODO: paramport self.add_subsystem("params", ParamPort(), promotes=["*"]) @@ -65,26 +73,17 @@ def setup(self): promotes_inputs=subsystem.mission_inputs(**kwargs), promotes_outputs=subsystem.mission_outputs(**kwargs)) - sgm_inputs = [ - 't_curr', Dynamic.Mission.DISTANCE] if analysis_scheme is AnalysisScheme.SHOOTING else [] - sgm_outputs = [ - Dynamic.Mission.ALTITUDE_RATE] if analysis_scheme is AnalysisScheme.SHOOTING else [] - self.add_subsystem( "accel_eom", - AccelerationRates( - num_nodes=nn, - analysis_scheme=analysis_scheme), + AccelerationRates(num_nodes=nn), promotes_inputs=[ Dynamic.Mission.MASS, Dynamic.Mission.VELOCITY, Dynamic.Mission.DRAG, - Dynamic.Mission.THRUST_TOTAL, ] - + sgm_inputs, + Dynamic.Mission.THRUST_TOTAL, ], promotes_outputs=[ Dynamic.Mission.VELOCITY_RATE, - Dynamic.Mission.DISTANCE_RATE, ] - + sgm_outputs, + Dynamic.Mission.DISTANCE_RATE, ], ) self.add_excess_rate_comps(nn) diff --git a/aviary/mission/gasp_based/ode/ascent_eom.py b/aviary/mission/gasp_based/ode/ascent_eom.py index 0cda876c5..d124d1fbf 100644 --- a/aviary/mission/gasp_based/ode/ascent_eom.py +++ b/aviary/mission/gasp_based/ode/ascent_eom.py @@ -2,7 +2,6 @@ import openmdao.api as om from aviary.constants import GRAV_ENGLISH_GASP, GRAV_ENGLISH_LBM, MU_TAKEOFF -from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.functions import add_aviary_input from aviary.variable_info.variables import Aircraft, Dynamic @@ -10,12 +9,9 @@ class AscentEOM(om.ExplicitComponent): def initialize(self): self.options.declare("num_nodes", types=int) - self.options.declare("analysis_scheme", types=AnalysisScheme, default=AnalysisScheme.COLLOCATION, - desc="The analysis method that will be used to close the trajectory; for example collocation or time integration") def setup(self): nn = self.options["num_nodes"] - analysis_scheme = self.options["analysis_scheme"] self.add_input(Dynamic.Mission.MASS, val=np.ones(nn), desc="aircraft mass", units="lbm") @@ -64,12 +60,6 @@ def setup(self): "alpha_rate", val=np.ones(nn), desc="angle of attack rate", units="deg/s" ) - if analysis_scheme is AnalysisScheme.SHOOTING: - self.add_input(Dynamic.Mission.DISTANCE, val=np.ones( - nn), desc="distance traveled", units="ft") - self.add_input(Dynamic.Mission.ALTITUDE, - val=np.ones(nn), desc="alt", units="ft") - def setup_partials(self): arange = np.arange(self.options["num_nodes"]) @@ -121,8 +111,6 @@ def setup_partials(self): self.declare_partials("fuselage_pitch", Aircraft.Wing.INCIDENCE, val=-1) def compute(self, inputs, outputs): - analysis_scheme = self.options["analysis_scheme"] - weight = inputs[Dynamic.Mission.MASS] * GRAV_ENGLISH_LBM thrust = inputs[Dynamic.Mission.THRUST_TOTAL] incremented_lift = inputs[Dynamic.Mission.LIFT] diff --git a/aviary/mission/gasp_based/ode/ascent_ode.py b/aviary/mission/gasp_based/ode/ascent_ode.py index 14cfab863..4b8751a86 100644 --- a/aviary/mission/gasp_based/ode/ascent_ode.py +++ b/aviary/mission/gasp_based/ode/ascent_ode.py @@ -9,6 +9,7 @@ from aviary.mission.gasp_based.ode.params import ParamPort from aviary.variable_info.enums import AlphaModes from aviary.variable_info.variables import Aircraft, Dynamic, Mission +from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs class AscentODE(BaseODE): @@ -33,6 +34,11 @@ def setup(self): # TODO: paramport ascent_params = ParamPort() if analysis_scheme is AnalysisScheme.SHOOTING: + add_SGM_required_inputs(self, { + Dynamic.Mission.ALTITUDE: {'units': 'ft'}, + Dynamic.Mission.DISTANCE: {'units': 'ft'}, + }) + ascent_params.add_params({ Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE: dict(units='deg', val=0), }) @@ -74,15 +80,9 @@ def setup(self): else: self.AddAlphaControl(alpha_mode=alpha_mode, num_nodes=nn) - if analysis_scheme is AnalysisScheme.SHOOTING: - shooting_inputs = [Dynamic.Mission.DISTANCE] - else: - shooting_inputs = [] - self.add_subsystem( "ascent_eom", - AscentEOM(num_nodes=nn, - analysis_scheme=analysis_scheme), + AscentEOM(num_nodes=nn), promotes_inputs=[ Dynamic.Mission.MASS, Dynamic.Mission.THRUST_TOTAL, @@ -91,9 +91,7 @@ def setup(self): Dynamic.Mission.VELOCITY, Dynamic.Mission.FLIGHT_PATH_ANGLE, "alpha", - ] - + shooting_inputs - + ["aircraft:*"], + ] + ["aircraft:*"], promotes_outputs=[ Dynamic.Mission.VELOCITY_RATE, Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, diff --git a/aviary/mission/gasp_based/ode/base_ode.py b/aviary/mission/gasp_based/ode/base_ode.py index c2a5799f7..2f5c6ac87 100644 --- a/aviary/mission/gasp_based/ode/base_ode.py +++ b/aviary/mission/gasp_based/ode/base_ode.py @@ -17,7 +17,7 @@ def initialize(self): self.options.declare( "analysis_scheme", default=AnalysisScheme.COLLOCATION, - types=enum.Enum, + types=AnalysisScheme, desc="The analysis method that will be used to close the trajectory; for example collocation or time integration", ) @@ -45,6 +45,7 @@ def AddAlphaControl( target_load_factor=1.1, target_tas_rate=0, # target_alt_rate=0, + # target_flight_path_angle=0, atol=1e-7, rtol=1e-7, add_default_solver=True, @@ -123,6 +124,20 @@ def AddAlphaControl( alpha_comp_inputs = ["required_lift", Dynamic.Mission.LIFT] # Future controller modes + # elif alpha_mode is AlphaModes.FLIGHT_PATH_ANGLE: + # alpha_comp = om.BalanceComp( + # name="alpha", + # val=np.full(nn, 1), + # units="deg", + # lhs_name=Dynamic.Mission.FLIGHT_PATH_ANGLE, + # rhs_name='target_flight_path_angle', + # rhs_val=target_flight_path_angle, + # eq_units="deg", + # upper=12.0, + # lower=-2, + # ) + # alpha_comp_inputs = [Dynamic.Mission.FLIGHT_PATH_ANGLE] + # elif alpha_mode is AlphaModes.ALTITUDE_RATE: # alpha_comp = om.BalanceComp( # name="alpha", diff --git a/aviary/mission/gasp_based/ode/climb_eom.py b/aviary/mission/gasp_based/ode/climb_eom.py index f2dcac75c..e3962e979 100644 --- a/aviary/mission/gasp_based/ode/climb_eom.py +++ b/aviary/mission/gasp_based/ode/climb_eom.py @@ -2,7 +2,6 @@ import openmdao.api as om from aviary.constants import GRAV_ENGLISH_LBM -from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.variables import Dynamic @@ -17,11 +16,8 @@ class ClimbRates(om.ExplicitComponent): def initialize(self): self.options.declare("num_nodes", types=int) - self.options.declare("analysis_scheme", types=AnalysisScheme, default=AnalysisScheme.COLLOCATION, - desc="The analysis method that will be used to close the trajectory; for example collocation or time integration") def setup(self): - analysis_scheme = self.options["analysis_scheme"] nn = self.options["num_nodes"] arange = np.arange(nn) @@ -98,12 +94,6 @@ def setup(self): rows=arange, cols=arange) - if analysis_scheme is AnalysisScheme.SHOOTING: - self.add_input("t_curr", val=np.ones(nn), desc="time", units="s") - self.add_input( - Dynamic.Mission.DISTANCE, val=np.ones(nn), desc="distance traveled", units="ft" - ) - def compute(self, inputs, outputs): TAS = inputs[Dynamic.Mission.VELOCITY] diff --git a/aviary/mission/gasp_based/ode/climb_ode.py b/aviary/mission/gasp_based/ode/climb_ode.py index c0b253c46..1baa861ad 100644 --- a/aviary/mission/gasp_based/ode/climb_ode.py +++ b/aviary/mission/gasp_based/ode/climb_ode.py @@ -5,14 +5,14 @@ from aviary.mission.gasp_based.flight_conditions import FlightConditions from aviary.mission.gasp_based.ode.base_ode import BaseODE from aviary.mission.gasp_based.ode.climb_eom import ClimbRates -from aviary.mission.gasp_based.ode.constraints.flight_constraints import \ - FlightConstraints +from aviary.mission.gasp_based.ode.constraints.flight_constraints import FlightConstraints from aviary.mission.gasp_based.ode.constraints.speed_constraints import SpeedConstraints from aviary.mission.gasp_based.ode.params import ParamPort from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase from aviary.subsystems.propulsion.propulsion_builder import PropulsionBuilderBase from aviary.variable_info.enums import AnalysisScheme, AlphaModes, SpeedType from aviary.variable_info.variables import Dynamic +from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs class ClimbODE(BaseODE): @@ -52,6 +52,14 @@ def setup(self): speed_inputs = ["mach"] speed_outputs = ["EAS", ("TAS", Dynamic.Mission.VELOCITY)] + if analysis_scheme is AnalysisScheme.SHOOTING: + add_SGM_required_inputs(self, { + 't_curr': {'units': 's'}, + Dynamic.Mission.DISTANCE: {'units': 'ft'}, + 'alt_trigger': {'units': self.options['alt_trigger_units'], 'val': 10e3}, + 'speed_trigger': {'units': self.options['speed_trigger_units'], 'val': 100}, + }) + # TODO: paramport self.add_subsystem("params", ParamPort(), promotes=["*"]) @@ -77,10 +85,6 @@ def setup(self): EAS_target = self.options["EAS_target"] mach_cruise = self.options["mach_cruise"] - constraint_args = {} - integration_states = [] - constraint_inputs = [] - mach_balance_group = self.add_subsystem( "mach_balance_group", subsys=om.Group(), promotes=["*"] ) @@ -129,13 +133,6 @@ def setup(self): flight_condition_group = mach_balance_group elif analysis_scheme is AnalysisScheme.SHOOTING: - constraint_args = {'analysis_scheme': AnalysisScheme.SHOOTING, - 'alt_trigger_units': self.options["alt_trigger_units"], - 'speed_trigger_units': self.options["speed_trigger_units"]} - - integration_states = ["t_curr", Dynamic.Mission.DISTANCE] - constraint_inputs = ["alt_trigger", "speed_trigger"] - lift_balance_group = self flight_condition_group = self @@ -176,15 +173,13 @@ def setup(self): lift_balance_group.add_subsystem( "climb_eom", - ClimbRates( - num_nodes=nn, - analysis_scheme=analysis_scheme), + ClimbRates(num_nodes=nn), promotes_inputs=[ Dynamic.Mission.MASS, Dynamic.Mission.VELOCITY, Dynamic.Mission.DRAG, - Dynamic.Mission.THRUST_TOTAL,] + - integration_states, + Dynamic.Mission.THRUST_TOTAL + ], promotes_outputs=[ Dynamic.Mission.ALTITUDE_RATE, Dynamic.Mission.DISTANCE_RATE, @@ -201,7 +196,7 @@ def setup(self): self.add_subsystem( "constraints", - FlightConstraints(num_nodes=nn, **constraint_args), + FlightConstraints(num_nodes=nn), promotes_inputs=[ "alpha", "rho", @@ -210,8 +205,7 @@ def setup(self): Dynamic.Mission.MASS, ("TAS", Dynamic.Mission.VELOCITY), ] - + ["aircraft:*"] - + constraint_inputs, + + ["aircraft:*"], promotes_outputs=["theta", "TAS_violation"], ) diff --git a/aviary/mission/gasp_based/ode/constraints/flight_constraints.py b/aviary/mission/gasp_based/ode/constraints/flight_constraints.py index e4d011283..f9f6782ea 100644 --- a/aviary/mission/gasp_based/ode/constraints/flight_constraints.py +++ b/aviary/mission/gasp_based/ode/constraints/flight_constraints.py @@ -2,7 +2,6 @@ import openmdao.api as om from aviary.constants import GRAV_ENGLISH_LBM -from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.functions import add_aviary_input from aviary.variable_info.variables import Aircraft, Dynamic @@ -19,22 +18,12 @@ class FlightConstraints(om.ExplicitComponent): """ def initialize(self): - self.options.declare("analysis_scheme", types=AnalysisScheme, - default=AnalysisScheme.COLLOCATION) self.options.declare("num_nodes", types=int) - self.options.declare("alt_trigger_units", default="ft") - self.options.declare("speed_trigger_units", default="kn") def setup(self): - analysis_scheme = self.options["analysis_scheme"] nn = self.options["num_nodes"] arange = np.arange(nn) - if analysis_scheme is AnalysisScheme.SHOOTING: - self.add_input( - "alt_trigger", units=self.options["alt_trigger_units"], val=10.e3) - self.add_input("speed_trigger", - units=self.options["speed_trigger_units"], val=100) self.add_input( Dynamic.Mission.MASS, val=np.ones(nn), diff --git a/aviary/mission/gasp_based/ode/descent_eom.py b/aviary/mission/gasp_based/ode/descent_eom.py index 957d872d1..0f1faa6e5 100644 --- a/aviary/mission/gasp_based/ode/descent_eom.py +++ b/aviary/mission/gasp_based/ode/descent_eom.py @@ -2,18 +2,14 @@ import openmdao.api as om from aviary.constants import GRAV_ENGLISH_LBM -from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.variables import Dynamic class DescentRates(om.ExplicitComponent): def initialize(self): self.options.declare("num_nodes", types=int) - self.options.declare("analysis_scheme", types=AnalysisScheme, default=AnalysisScheme.COLLOCATION, - desc="The analysis method that will be used to close the trajectory; for example collocation or time integration") def setup(self): - analysis_scheme = self.options["analysis_scheme"] nn = self.options["num_nodes"] arange = np.arange(nn) @@ -96,12 +92,6 @@ def setup(self): rows=arange, cols=arange) - if analysis_scheme is AnalysisScheme.SHOOTING: - self.add_input("t_curr", val=np.ones(nn), desc="time", units="s") - self.add_input( - Dynamic.Mission.DISTANCE, val=np.ones(nn), desc="distance traveled", units="ft" - ) - def compute(self, inputs, outputs): TAS = inputs[Dynamic.Mission.VELOCITY] diff --git a/aviary/mission/gasp_based/ode/descent_ode.py b/aviary/mission/gasp_based/ode/descent_ode.py index e109640bc..322bb3c13 100644 --- a/aviary/mission/gasp_based/ode/descent_ode.py +++ b/aviary/mission/gasp_based/ode/descent_ode.py @@ -2,22 +2,19 @@ import openmdao.api as om from dymos.models.atmosphere.atmos_1976 import USatm1976Comp -from aviary.variable_info.enums import AnalysisScheme, AlphaModes, SpeedType -from aviary.variable_info.variables import Mission, Dynamic - from aviary.mission.gasp_based.ode.base_ode import BaseODE from aviary.mission.gasp_based.ode.params import ParamPort from aviary.mission.gasp_based.ode.descent_eom import DescentRates from aviary.mission.gasp_based.flight_conditions import FlightConditions from aviary.mission.gasp_based.ode.base_ode import BaseODE -from aviary.mission.gasp_based.ode.constraints.flight_constraints import \ - FlightConstraints +from aviary.mission.gasp_based.ode.constraints.flight_constraints import FlightConstraints from aviary.mission.gasp_based.ode.constraints.speed_constraints import SpeedConstraints -from aviary.variable_info.enums import AnalysisScheme, SpeedType -from aviary.variable_info.variables import Dynamic +from aviary.variable_info.enums import AnalysisScheme, AlphaModes, SpeedType +from aviary.variable_info.variables import Mission, Dynamic from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase from aviary.subsystems.propulsion.propulsion_builder import PropulsionBuilderBase +from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs class DescentODE(BaseODE): @@ -34,8 +31,10 @@ def initialize(self): super().initialize() self.options.declare("input_speed_type", types=SpeedType, desc="Whether the speed is given as a equivalent airspeed, true airspeed, or mach number") - self.options.declare("alt_trigger_units") - self.options.declare("speed_trigger_units") + self.options.declare("alt_trigger_units", default='ft', + desc='The units that the altitude trigger is provided in') + self.options.declare("speed_trigger_units", default='kn', + desc='The units that the speed trigger is provided in.') self.options.declare( "mach_cruise", default=0, desc="targeted cruise mach number" ) @@ -60,6 +59,13 @@ def setup(self): speed_inputs = ["mach"] speed_outputs = ["EAS", ("TAS", Dynamic.Mission.VELOCITY)] + if analysis_scheme is AnalysisScheme.SHOOTING: + add_SGM_required_inputs(self, { + 't_curr': {'units': 's'}, + Dynamic.Mission.DISTANCE: {'units': 'ft'}, + 'alt_trigger': {'units': self.options['alt_trigger_units'], 'val': 10e3}, + 'speed_trigger': {'units': self.options['speed_trigger_units'], 'val': 100}, + }) # TODO: paramport self.add_subsystem("params", ParamPort(), promotes=["*"]) @@ -84,9 +90,6 @@ def setup(self): if analysis_scheme is AnalysisScheme.COLLOCATION: EAS_limit = self.options["EAS_limit"] mach_cruise = self.options["mach_cruise"] - constraint_args = {} - integration_states = [] - constraint_inputs = [] # Add a group to contain the balance @@ -144,12 +147,6 @@ def setup(self): ) elif analysis_scheme is AnalysisScheme.SHOOTING: - constraint_args = {'analysis_scheme': AnalysisScheme.SHOOTING, - 'alt_trigger_units': self.options["alt_trigger_units"], - 'speed_trigger_units': self.options["speed_trigger_units"]} - integration_states = ["t_curr", Dynamic.Mission.DISTANCE] - constraint_inputs = ["alt_trigger", "speed_trigger"] - lift_balance_group = self flight_condition_group.add_subsystem( @@ -170,16 +167,14 @@ def setup(self): lift_balance_group.add_subsystem( "descent_eom", - DescentRates( - num_nodes=nn, - analysis_scheme=analysis_scheme), + DescentRates(num_nodes=nn), promotes_inputs=[ Dynamic.Mission.MASS, Dynamic.Mission.VELOCITY, Dynamic.Mission.DRAG, Dynamic.Mission.THRUST_TOTAL, - "alpha",] + - integration_states, + "alpha", + ], promotes_outputs=[ Dynamic.Mission.ALTITUDE_RATE, Dynamic.Mission.DISTANCE_RATE, @@ -190,7 +185,7 @@ def setup(self): self.add_subsystem( "constraints", - FlightConstraints(num_nodes=nn, **constraint_args), + FlightConstraints(num_nodes=nn), promotes_inputs=[ Dynamic.Mission.MASS, "alpha", @@ -199,8 +194,7 @@ def setup(self): Dynamic.Mission.FLIGHT_PATH_ANGLE, ("TAS", Dynamic.Mission.VELOCITY), ] - + ["aircraft:*"] - + constraint_inputs, + + ["aircraft:*"], promotes_outputs=["theta", "TAS_violation"], ) diff --git a/aviary/mission/gasp_based/ode/flight_path_eom.py b/aviary/mission/gasp_based/ode/flight_path_eom.py index beaeca6fb..f68891fbd 100644 --- a/aviary/mission/gasp_based/ode/flight_path_eom.py +++ b/aviary/mission/gasp_based/ode/flight_path_eom.py @@ -1,7 +1,6 @@ import numpy as np import openmdao.api as om -from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.functions import add_aviary_input, add_aviary_output from aviary.variable_info.variables import Aircraft, Mission, Dynamic @@ -19,8 +18,6 @@ def __init__(self, **kwargs): def initialize(self): self.options.declare("num_nodes", types=int) - self.options.declare("analysis_scheme", types=AnalysisScheme, default=AnalysisScheme.COLLOCATION, - desc="The analysis method that will be used to close the trajectory; for example collocation or time integration") self.options.declare("ground_roll", types=bool, default=False, desc="True if the aircraft is confined to the ground. Removes altitude rate as an " "output and adjust the TAS rate equation.") @@ -28,7 +25,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] ground_roll = self.options["ground_roll"] - analysis_scheme = self.options["analysis_scheme"] self.add_input(Dynamic.Mission.MASS, val=np.ones(nn), desc="aircraft mass", units="lbm") @@ -51,12 +47,6 @@ def setup(self): add_aviary_input(self, Aircraft.Wing.INCIDENCE, val=0) - if analysis_scheme is AnalysisScheme.SHOOTING: - self.add_input("t_curr", val=np.ones(nn), desc="time", units="s") - self.add_input("distance_trigger", val=0, units="ft") - add_aviary_input(self, Dynamic.Mission.ALTITUDE, val=np.ones(nn), units="ft") - add_aviary_input(self, Dynamic.Mission.DISTANCE, val=np.ones(nn), units="ft") - self.add_output(Dynamic.Mission.VELOCITY_RATE, val=np.ones(nn), desc="TAS rate", units="ft/s**2", tags=['dymos.state_rate_source:velocity', 'dymos.state_units:kn']) diff --git a/aviary/mission/gasp_based/ode/flight_path_ode.py b/aviary/mission/gasp_based/ode/flight_path_ode.py index bf37220b1..5e4d99e95 100644 --- a/aviary/mission/gasp_based/ode/flight_path_ode.py +++ b/aviary/mission/gasp_based/ode/flight_path_ode.py @@ -10,8 +10,7 @@ from aviary.mission.gasp_based.ode.params import ParamPort from aviary.subsystems.propulsion.propulsion_builder import PropulsionBuilderBase from aviary.variable_info.variables import Aircraft, Dynamic, Mission -from aviary.mission.ode.specific_energy_rate import SpecificEnergyRate -from aviary.mission.ode.altitude_rate import AltitudeRate +from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs class FlightPathODE(BaseODE): @@ -75,11 +74,16 @@ def setup(self): EOM_inputs.append('alpha') if analysis_scheme is AnalysisScheme.SHOOTING: - EOM_inputs.append(Dynamic.Mission.ALTITUDE) - EOM_inputs.append('distance_trigger') - EOM_inputs.append('distance') - EOM_inputs.append('t_curr') - + SGM_required_inputs = { + 't_curr': {'units': 's'}, + 'distance_trigger': {'units': 'ft'}, + Dynamic.Mission.ALTITUDE: {'units': 'ft'}, + Dynamic.Mission.DISTANCE: {'units': 'ft'}, + } + if kwargs['method'] == 'cruise': + SGM_required_inputs[Dynamic.Mission.FLIGHT_PATH_ANGLE] = { + 'val': 0, 'units': 'deg'} + add_SGM_required_inputs(self, SGM_required_inputs) prop_group = om.Group() else: prop_group = self @@ -141,8 +145,10 @@ def setup(self): ], promotes_outputs=['required_lift'] ) - self.AddAlphaControl(alpha_mode=alpha_mode, target_load_factor=1, - atol=1e-6, rtol=1e-12, num_nodes=nn, print_level=print_level) + self.AddAlphaControl( + alpha_mode=alpha_mode, + target_load_factor=1, + atol=1e-6, rtol=1e-12, num_nodes=nn, print_level=print_level) for subsystem in core_subsystems: system = subsystem.build_mission(**kwargs) @@ -192,7 +198,7 @@ def setup(self): FlightPathEOM( num_nodes=nn, ground_roll=self.options['ground_roll'], - analysis_scheme=analysis_scheme), + ), promotes_inputs=EOM_inputs, promotes_outputs=[ Dynamic.Mission.VELOCITY_RATE, @@ -216,13 +222,19 @@ def setup(self): all_inputs=[ Dynamic.Mission.DISTANCE, Dynamic.Mission.THROTTLE, - 'required_lift', - 'load_factor', + Dynamic.Mission.THRUST_TOTAL, + 'required_thrust', Dynamic.Mission.ALTITUDE, + 'load_factor', + 'required_lift', + Dynamic.Mission.MASS, Dynamic.Mission.FLIGHT_PATH_ANGLE, + 'alpha', ], input_units={ + 'required_thrust': 'lbf', 'required_lift': 'lbf', + 'alpha': 'deg', Dynamic.Mission.FLIGHT_PATH_ANGLE: 'deg', }) self.add_subsystem( @@ -231,25 +243,6 @@ def setup(self): promotes_inputs=["*"],) self.set_input_defaults( Dynamic.Mission.DISTANCE, val=0, units='NM') - debug_comp = ['dummy_comp'] - - if analysis_scheme is AnalysisScheme.SHOOTING: - self.add_subsystem('mass_trigger', - om.ExecComp( - 'mass_trigger = OEM + payload + reserve_fuel + descent_fuel', - mass_trigger={'val': 0, 'units': 'lbm'}, - OEM={'val': 0, 'units': 'lbm'}, - payload={'val': 0, 'units': 'lbm'}, - reserve_fuel={'val': 0, 'units': 'lbm'}, - descent_fuel={'val': 0, 'units': 'lbm'}, - ), - promotes_inputs=[ - ('OEM', Aircraft.Design.OPERATING_MASS), - ('payload', Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS), - ('reserve_fuel', Mission.Design.RESERVE_FUEL), - 'descent_fuel', - ], - ) ParamPort.set_default_vals(self) if not self.options["clean"]: diff --git a/aviary/mission/gasp_based/ode/groundroll_eom.py b/aviary/mission/gasp_based/ode/groundroll_eom.py index 2e55fec26..fbe4a6702 100644 --- a/aviary/mission/gasp_based/ode/groundroll_eom.py +++ b/aviary/mission/gasp_based/ode/groundroll_eom.py @@ -85,10 +85,6 @@ def setup(self): ) self.declare_partials("alpha_rate", ["*"], val=0.0) - elif analysis_scheme is AnalysisScheme.SHOOTING: - self.add_input( - Dynamic.Mission.DISTANCE, val=np.ones(nn), desc="distance traveled", units="ft" - ) def compute(self, inputs, outputs): analysis_scheme = self.options["analysis_scheme"] diff --git a/aviary/mission/gasp_based/ode/groundroll_ode.py b/aviary/mission/gasp_based/ode/groundroll_ode.py index e2b64f814..65c4c0158 100644 --- a/aviary/mission/gasp_based/ode/groundroll_ode.py +++ b/aviary/mission/gasp_based/ode/groundroll_ode.py @@ -6,9 +6,11 @@ from aviary.mission.gasp_based.ode.groundroll_eom import GroundrollEOM from aviary.mission.gasp_based.ode.params import ParamPort from aviary.variable_info.variables import Aircraft, Dynamic, Mission +from aviary.variable_info.enums import AnalysisScheme from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase from aviary.variable_info.variable_meta_data import _MetaData from aviary.variable_info.variables_in import VariablesIn +from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs class GroundrollODE(BaseODE): @@ -38,6 +40,11 @@ def setup(self): core_subsystems = self.options['core_subsystems'] subsystem_options = self.options['subsystem_options'] + if analysis_scheme is AnalysisScheme.SHOOTING: + add_SGM_required_inputs(self, { + Dynamic.Mission.DISTANCE: {'units': 'ft'}, + }) + # TODO: paramport self.add_subsystem("params", ParamPort(), promotes=["*"]) @@ -82,8 +89,7 @@ def setup(self): src_indices=np.zeros(nn, dtype=int), ) - self.add_subsystem("groundroll_eom", GroundrollEOM(num_nodes=nn, analysis_scheme=analysis_scheme), - promotes=["*"]) + self.add_subsystem("groundroll_eom", GroundrollEOM(num_nodes=nn), promotes=["*"]) self.add_subsystem("exec", om.ExecComp(f"over_a = velocity / velocity_rate", velocity_rate={"units": "kn/s", diff --git a/aviary/mission/gasp_based/ode/rotation_eom.py b/aviary/mission/gasp_based/ode/rotation_eom.py index 3d544c505..bef250755 100644 --- a/aviary/mission/gasp_based/ode/rotation_eom.py +++ b/aviary/mission/gasp_based/ode/rotation_eom.py @@ -56,10 +56,6 @@ def setup(self): ) self.declare_partials("alpha_rate", ["*"], val=0) - elif analysis_scheme is AnalysisScheme.SHOOTING: - self.add_input( - Dynamic.Mission.DISTANCE, val=np.ones(nn), desc="distance traveled", units="ft" - ) def setup_partials(self): arange = np.arange(self.options["num_nodes"]) diff --git a/aviary/mission/gasp_based/ode/rotation_ode.py b/aviary/mission/gasp_based/ode/rotation_ode.py index f739ce825..4b445e96a 100644 --- a/aviary/mission/gasp_based/ode/rotation_ode.py +++ b/aviary/mission/gasp_based/ode/rotation_ode.py @@ -7,6 +7,7 @@ from aviary.mission.gasp_based.ode.rotation_eom import RotationEOM from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.variables import Aircraft, Dynamic, Mission +from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs class RotationODE(BaseODE): @@ -22,6 +23,11 @@ def setup(self): aviary_options = self.options['aviary_options'] core_subsystems = self.options['core_subsystems'] + if analysis_scheme is AnalysisScheme.SHOOTING: + add_SGM_required_inputs(self, { + Dynamic.Mission.DISTANCE: {'units': 'ft'}, + }) + # TODO: paramport self.add_subsystem("params", ParamPort(), promotes=["*"]) @@ -60,12 +66,11 @@ def setup(self): promotes_outputs=["alpha"], ) - self.add_subsystem("rotation_eom", RotationEOM( - num_nodes=nn, analysis_scheme=analysis_scheme), promotes=["*"]) + self.add_subsystem("rotation_eom", RotationEOM(num_nodes=nn), promotes=["*"]) ParamPort.set_default_vals(self) - self.set_input_defaults("t_init_flaps", val=47.5) - self.set_input_defaults("t_init_gear", val=37.3) + self.set_input_defaults("t_init_flaps", val=47.5, units='s') + self.set_input_defaults("t_init_gear", val=37.3, units='s') self.set_input_defaults("alpha", val=np.ones(nn), units="deg") self.set_input_defaults(Dynamic.Mission.FLIGHT_PATH_ANGLE, val=np.zeros(nn), units="deg") diff --git a/aviary/mission/gasp_based/ode/time_integration_base_classes.py b/aviary/mission/gasp_based/ode/time_integration_base_classes.py index 6919b6f71..dd381dba1 100644 --- a/aviary/mission/gasp_based/ode/time_integration_base_classes.py +++ b/aviary/mission/gasp_based/ode/time_integration_base_classes.py @@ -229,7 +229,6 @@ def __init__( self.dim_parameters = len(parameters) # TODO: add defensive checks to make sure dimensions match in both setup and # calls - if verbosity.value >= 2: if problem_name: problem_name = '_'+problem_name @@ -274,8 +273,6 @@ def compute_along_traj(self, ts, xs): self.prob.set_val(state_name, elem_val, units=self.states[state_name]['units']) - self.prob.run_model() - @property def control(self): return np.array( @@ -380,6 +377,8 @@ def update_equation_function(self, t, x, event_channels=None): return x def add_trigger(self, state, value, units=None, channel_name=None): + if isinstance(value, tuple): + units = value[1] if units is None: units = self.states[state]['units'] elif hasattr(self, units): @@ -410,6 +409,8 @@ def evaluate_trigger(self, trigger: event_trigger): if isinstance(trigger_value, str): if hasattr(self, trigger_value): trigger_value = getattr(self, trigger_value) + if isinstance(trigger_value, tuple): + trigger_value, trigger.units = trigger_value else: trigger_value = self.get_val( trigger_value, units=trigger.units).squeeze() @@ -571,17 +572,20 @@ def setup_params( self.traj_event_trigger_input = { event_trigger_input: { **dict(name="_".join([ - event_trigger_input[0].__class__.__name__, + ODEs[self.phase_names.index( + event_trigger_input[0])].__class__.__name__, event_trigger_input[1], trigger_suffix ])), **self.add_input( "_".join([ - event_trigger_input[0].__class__.__name__, + ODEs[self.phase_names.index( + event_trigger_input[0])].__class__.__name__, event_trigger_input[1], trigger_suffix ]), - units=event_trigger_input[0].states[event_trigger_input[1]]['units'], + units=ODEs[self.phase_names.index( + event_trigger_input[0])].states[event_trigger_input[1]]['units'], ) } for event_trigger_input in traj_event_trigger_input @@ -1123,3 +1127,15 @@ def co_state_rate(t, costate, *args): for param_deriv_val, param_deriv_name in zip(param_deriv, param_dict): J[output_name, param_deriv_name] = param_deriv_val self.costate_reses = costate_reses + + +class _killer_comp(om.ExplicitComponent): + ''' + This component will stop the execution of the integration during compute, + this is useful for debugging trajectories by getting the initial value + of variables for a phase and then exiting. + ''' + + def compute(self, inputs, outputs): + print(f'exit in {self.name}') + exit() diff --git a/aviary/mission/gasp_based/phases/time_integration_phases.py b/aviary/mission/gasp_based/phases/time_integration_phases.py index 7e9841374..25ab7eda5 100644 --- a/aviary/mission/gasp_based/phases/time_integration_phases.py +++ b/aviary/mission/gasp_based/phases/time_integration_phases.py @@ -22,8 +22,7 @@ class SGMGroundroll(SimuPyProblem): def __init__( self, phase_name='groundroll', - VR_value=143.1, - VR_units="kn", + VR_value=(143.1, 'kn'), ode_args={}, simupy_args={}, ): @@ -46,15 +45,7 @@ def __init__( self.phase_name = phase_name self.VR_value = VR_value - # self.VR_units = VR_units - self.event_channel_names = [Dynamic.Mission.VELOCITY] - self.num_events = len(self.event_channel_names) - # self.add_trigger(Dynamic.Mission.VELOCITY, "VR_value", units='ft/s') - - def event_equation_function(self, t, x): - self.time = t - self.state = x - return self.get_val(Dynamic.Mission.VELOCITY, units='ft/s') - self.VR_value + self.add_trigger(Dynamic.Mission.VELOCITY, "VR_value") class SGMRotation(SimuPyProblem): @@ -194,7 +185,7 @@ class SGMAscentCombined(SGMAscent): def __init__( self, phase_name='ascent_combined', - fuselage_pitch_max=0, + fuselage_pitch_max=(0, 'deg'), ode_args={}, simupy_args={}, ): @@ -276,7 +267,6 @@ def get_alpha(self, t, x): load_factor_val = ode.get_val("load_factor") fuselage_pitch_val = ode.get_val("fuselage_pitch", units="deg") velocity_rate_val = ode.get_val("velocity_rate") - if ( (load_factor_val > load_factor_max) and not np.isclose(load_factor_val, load_factor_max) @@ -385,6 +375,7 @@ def __init__( Dynamic.Mission.MASS: Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL}, **simupy_args, ) + self.phase_name = phase_name self.add_trigger("EAS", VC_value, units=VC_units) @@ -447,6 +438,7 @@ def __init__( Dynamic.Mission.MASS: Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL}, **simupy_args, ) + self.phase_name = phase_name self.add_trigger(Dynamic.Mission.ALTITUDE, "alt_trigger", units=self.alt_trigger_units) @@ -466,7 +458,8 @@ def __init__( alpha_mode=AlphaModes.DEFAULT, input_speed_type=SpeedType.MACH, input_speed_units="kn", - distance_trigger_units='ft', + distance_trigger=(-1, 'ft'), + mass_trigger=(0, 'lbm'), ode_args={}, simupy_args={}, ): @@ -477,7 +470,8 @@ def __init__( clean=True, **ode_args,) - self.distance_trigger_units = distance_trigger_units + self.distance_trigger = distance_trigger + self.mass_trigger = mass_trigger super().__init__( ode, @@ -504,9 +498,8 @@ def __init__( ) self.phase_name = phase_name - self.add_trigger(Dynamic.Mission.MASS, 'mass_trigger.mass_trigger', units='lbm') - self.add_trigger(Dynamic.Mission.DISTANCE, "distance_trigger", - units=self.distance_trigger_units) + self.add_trigger(Dynamic.Mission.DISTANCE, "distance_trigger") + self.add_trigger(Dynamic.Mission.MASS, 'mass_trigger') class SGMDescent(SimuPyProblem): diff --git a/aviary/mission/gasp_based/phases/time_integration_traj.py b/aviary/mission/gasp_based/phases/time_integration_traj.py index 332fc77da..9fe937cd0 100644 --- a/aviary/mission/gasp_based/phases/time_integration_traj.py +++ b/aviary/mission/gasp_based/phases/time_integration_traj.py @@ -1,5 +1,4 @@ import numpy as np -import openmdao.api as om from aviary.mission.gasp_based.ode.time_integration_base_classes import SGMTrajBase from aviary.mission.gasp_based.phases.time_integration_phases import SGMGroundroll, SGMRotation @@ -9,8 +8,6 @@ def initialize(self): super().initialize() self.options.declare("cruise_mach", default=0.8) self.options.declare("ode_args", types=dict, default=dict()) - self.options.declare("ode_args_pyc", types=dict, default=dict()) - self.options.declare("pyc_phases", default=list()) class FlexibleTraj(TimeIntegrationTrajBase): @@ -35,7 +32,8 @@ def setup(self): ODEs = [] for phase_name, phase_info in self.options['Phases'].items(): - next_phase = phase_info['ode'] + kwargs = phase_info.get('kwargs', {}) + next_phase = phase_info['builder'](**kwargs) next_phase.phase_name = phase_name ODEs.append(next_phase) @@ -57,16 +55,20 @@ def compute(self, inputs, outputs): for phase in self.ODEs: phase_name = phase.phase_name - vals_to_set = self.options['Phases'][phase_name]['vals_to_set'] + vals_to_set = self.options['Phases'][phase_name]['user_options'] if vals_to_set: for name, data in vals_to_set.items(): + var, units = data if name.startswith('attr:'): - setattr(phase, name.replace('attr:', ''), inputs[data['val']]) + if isinstance(var, str): + val = np.squeeze(self.convert2units(var, inputs[var], units)) + data = (val, units) + setattr(phase, name.replace('attr:', ''), data) elif name.startswith('rotation.'): phase.rotation.set_val(name.replace( - 'rotation.', ''), data['val'], units=data['units']) + 'rotation.', ''), var, units=units) else: - phase.set_val(name, data['val'], units=data['units']) + phase.set_val(name, var, units=units) ode_index = 0 sim_gen = self.compute_traj_loop(self.ODEs[0], inputs, outputs) diff --git a/aviary/mission/gasp_based/test/test_idle_descent_estimation.py b/aviary/mission/gasp_based/test/test_idle_descent_estimation.py index 376ecc4d6..2672abb1f 100644 --- a/aviary/mission/gasp_based/test/test_idle_descent_estimation.py +++ b/aviary/mission/gasp_based/test/test_idle_descent_estimation.py @@ -1,19 +1,21 @@ import unittest +import warnings +import importlib import openmdao.api as om -from aviary.interface.default_phase_info.two_dof_fiti import create_2dof_based_descent_phases +from aviary.interface.default_phase_info.two_dof_fiti_deprecated import create_2dof_based_descent_phases +from aviary.interface.default_phase_info.two_dof_fiti import descent_phases, add_default_sgm_args from openmdao.utils.assert_utils import assert_near_equal from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems -from aviary.mission.gasp_based.idle_descent_estimation import descent_range_and_fuel +from aviary.mission.gasp_based.idle_descent_estimation import descent_range_and_fuel, add_descent_estimation_as_submodel from aviary.subsystems.propulsion.utils import build_engine_deck -from aviary.variable_info.variables import Aircraft, Dynamic +from aviary.variable_info.variables import Aircraft, Dynamic, Settings from aviary.variable_info.enums import Verbosity from aviary.utils.process_input_decks import create_vehicle from aviary.utils.preprocessors import preprocess_propulsion -import importlib @unittest.skipUnless(importlib.util.find_spec("pyoptsparse") is not None, "pyoptsparse is not installed") @@ -21,7 +23,7 @@ class IdleDescentTestCase(unittest.TestCase): def setUp(self): input_deck = 'models/large_single_aisle_1/large_single_aisle_1_GwGm.csv' aviary_inputs, _ = create_vehicle(input_deck) - aviary_inputs.set_val('verbosity', Verbosity.QUIET) + aviary_inputs.set_val(Settings.VERBOSITY, Verbosity.QUIET) aviary_inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, val=28690, units="lbf") aviary_inputs.set_val(Dynamic.Mission.THROTTLE, val=0, units="unitless") @@ -38,14 +40,44 @@ def setUp(self): self.aviary_inputs = aviary_inputs self.tol = 1e-5 + add_default_sgm_args(descent_phases, self.ode_args) + self.phases = descent_phases + def test_case1(self): - results = descent_range_and_fuel(ode_args=self.ode_args)['refined_guess'] + results = descent_range_and_fuel(phases=self.phases)['refined_guess'] # Values obtained by running idle_descent_estimation assert_near_equal(results['distance_flown'], 91.8911599691433, self.tol) assert_near_equal(results['fuel_burned'], 236.73893823639082, self.tol) + def test_subproblem(self): + prob = om.Problem() + prob.model = om.Group() + + ivc = om.IndepVarComp() + ivc.add_output(Aircraft.Design.OPERATING_MASS, 97500, units='lbm') + ivc.add_output(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS, 36000, units='lbm') + prob.model.add_subsystem('IVC', ivc, promotes=['*']) + + add_descent_estimation_as_submodel( + prob, + phases=self.phases, + ode_args=self.ode_args, + cruise_alt=35000, + reserve_fuel=4500, + ) + + prob.setup() + # om.n2(prob, 'idle_descent_n2.html', show_browser=False) + warnings.filterwarnings('ignore', category=UserWarning) + prob.run_model() + warnings.filterwarnings('default', category=UserWarning) + + # Values obtained by running idle_descent_estimation + assert_near_equal(prob.get_val('descent_range', 'NM'), 98.38026813, self.tol) + assert_near_equal(prob.get_val('descent_fuel', 'lbm'), 250.84809336, self.tol) + if __name__ == "__main__": unittest.main() diff --git a/aviary/mission/phase_builder_base.py b/aviary/mission/phase_builder_base.py index 12056edf1..f8882d27c 100644 --- a/aviary/mission/phase_builder_base.py +++ b/aviary/mission/phase_builder_base.py @@ -523,7 +523,6 @@ def add_altitude_state(self, user_options, units='ft'): alt_defect_ref = user_options.get_val('alt_defect_ref', units=units) self.phase.add_state( Dynamic.Mission.ALTITUDE, - fix_initial=True, fix_final=False, lower=alt_lower, upper=alt_upper, diff --git a/aviary/models/N3CC/N3CC_data.py b/aviary/models/N3CC/N3CC_data.py index 34e8a095f..151f723c7 100644 --- a/aviary/models/N3CC/N3CC_data.py +++ b/aviary/models/N3CC/N3CC_data.py @@ -135,7 +135,7 @@ # Hydraulics # --------------------------- -inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 5000., 'lbf/ft**2') +inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 5000., 'psi') inputs.set_val(Aircraft.Hydraulics.MASS_SCALER, 0.95543) # Instruments diff --git a/aviary/models/N3CC/N3CC_generic_low_speed_polars_FLOPSinp.csv b/aviary/models/N3CC/N3CC_generic_low_speed_polars_FLOPSinp.csv index be0e4314f..4a9181ab7 100644 --- a/aviary/models/N3CC/N3CC_generic_low_speed_polars_FLOPSinp.csv +++ b/aviary/models/N3CC/N3CC_generic_low_speed_polars_FLOPSinp.csv @@ -85,7 +85,7 @@ aircraft:horizontal_tail:thickness_to_chord,0.115,unitless aircraft:horizontal_tail:vertical_tail_fraction,0,unitless aircraft:horizontal_tail:wetted_area_scaler,576.571192,unitless aircraft:hydraulics:mass_scaler,0.95543,unitless -aircraft:hydraulics:system_pressure,5000,lbf/ft**2 +aircraft:hydraulics:system_pressure,5000,psi aircraft:instruments:mass_scaler,1.66955,unitless aircraft:landing_gear:carrier_based,0,unitless aircraft:landing_gear:drag_coefficient,0.024,unitless diff --git a/aviary/models/large_single_aisle_1/large_single_aisle_1_FLOPS_data.py b/aviary/models/large_single_aisle_1/large_single_aisle_1_FLOPS_data.py index 632dc2169..c5fd70877 100644 --- a/aviary/models/large_single_aisle_1/large_single_aisle_1_FLOPS_data.py +++ b/aviary/models/large_single_aisle_1/large_single_aisle_1_FLOPS_data.py @@ -112,7 +112,7 @@ # Hydraulics # --------------------------- -inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'lbf/ft**2') +inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'psi') inputs.set_val(Aircraft.Hydraulics.MASS_SCALER, 1.0) # Instruments diff --git a/aviary/models/large_single_aisle_2/large_single_aisle_2_FLOPS_data.py b/aviary/models/large_single_aisle_2/large_single_aisle_2_FLOPS_data.py index 17aae96d6..c6c0f8e34 100644 --- a/aviary/models/large_single_aisle_2/large_single_aisle_2_FLOPS_data.py +++ b/aviary/models/large_single_aisle_2/large_single_aisle_2_FLOPS_data.py @@ -120,7 +120,7 @@ # Hydraulics # --------------------------- -inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'lbf/ft**2') +inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'psi') inputs.set_val(Aircraft.Hydraulics.MASS_SCALER, 1.0) # Instruments @@ -328,7 +328,7 @@ outputs.set_val(Aircraft.Hydraulics.MASS, 1075.3, 'lbm') -outputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'lbf/ft**2') +outputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'psi') outputs.set_val(Aircraft.Instruments.MASS, 484., 'lbm') diff --git a/aviary/models/large_single_aisle_2/large_single_aisle_2_altwt_FLOPS_data.py b/aviary/models/large_single_aisle_2/large_single_aisle_2_altwt_FLOPS_data.py index e4864361a..56130af1b 100644 --- a/aviary/models/large_single_aisle_2/large_single_aisle_2_altwt_FLOPS_data.py +++ b/aviary/models/large_single_aisle_2/large_single_aisle_2_altwt_FLOPS_data.py @@ -120,7 +120,7 @@ # Hydraulics # --------------------------- -inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'lbf/ft**2') +inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'psi') inputs.set_val(Aircraft.Hydraulics.MASS_SCALER, 1.0) # Instruments @@ -325,7 +325,7 @@ outputs.set_val(Aircraft.Hydraulics.MASS, 1361.15, 'lbm') -outputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'lbf/ft**2') +outputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'psi') outputs.set_val(Aircraft.Instruments.MASS, 484., 'lbm') diff --git a/aviary/models/large_single_aisle_2/large_single_aisle_2_detailwing_FLOPS_data.py b/aviary/models/large_single_aisle_2/large_single_aisle_2_detailwing_FLOPS_data.py index a2981ebb4..2aca006c4 100644 --- a/aviary/models/large_single_aisle_2/large_single_aisle_2_detailwing_FLOPS_data.py +++ b/aviary/models/large_single_aisle_2/large_single_aisle_2_detailwing_FLOPS_data.py @@ -113,7 +113,7 @@ # Hydraulics # --------------------------- -inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'lbf/ft**2') +inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'psi') inputs.set_val(Aircraft.Hydraulics.MASS_SCALER, 1.0) # Instruments diff --git a/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py b/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py index 86a7d4cf7..d45060497 100644 --- a/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py +++ b/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py @@ -113,7 +113,7 @@ # Hydraulics # --------------------------- -inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'lbf/ft**2') +inputs.set_val(Aircraft.Hydraulics.SYSTEM_PRESSURE, 3000., 'psi') inputs.set_val(Aircraft.Hydraulics.MASS_SCALER, 1.0) # Instruments diff --git a/aviary/models/test_aircraft/aircraft_for_bench_FwFm.csv b/aviary/models/test_aircraft/aircraft_for_bench_FwFm.csv index d18303b7f..e9b041851 100644 --- a/aviary/models/test_aircraft/aircraft_for_bench_FwFm.csv +++ b/aviary/models/test_aircraft/aircraft_for_bench_FwFm.csv @@ -89,7 +89,7 @@ aircraft:horizontal_tail:vertical_tail_fraction,0.0,unitless aircraft:horizontal_tail:wetted_area_scaler,1.0,unitless aircraft:horizontal_tail:wetted_area,592.65,ft**2 aircraft:hydraulics:mass_scaler,1.0,unitless -aircraft:hydraulics:system_pressure,3000.0,lbf/ft**2 +aircraft:hydraulics:system_pressure,3000,psi aircraft:instruments:mass_scaler,1.25,unitless aircraft:landing_gear:carrier_based,False,unitless aircraft:landing_gear:main_gear_mass_scaler,1.1,unitless diff --git a/aviary/models/test_aircraft/aircraft_for_bench_FwGm.csv b/aviary/models/test_aircraft/aircraft_for_bench_FwGm.csv index 32e9f40d5..39734a417 100644 --- a/aviary/models/test_aircraft/aircraft_for_bench_FwGm.csv +++ b/aviary/models/test_aircraft/aircraft_for_bench_FwGm.csv @@ -212,7 +212,7 @@ aircraft:horizontal_tail:mass_scaler,1.2,unitless aircraft:horizontal_tail:wetted_area_scaler,1.0,unitless aircraft:horizontal_tail:wetted_area,592.65,ft**2 aircraft:hydraulics:mass_scaler,1.0,unitless -aircraft:hydraulics:system_pressure,3000.0,lbf/ft**2 +aircraft:hydraulics:system_pressure,3000.0,psi aircraft:instruments:mass_scaler,1.25,unitless aircraft:landing_gear:carrier_based,False,unitless aircraft:landing_gear:main_gear_mass_scaler,1.1,unitless diff --git a/aviary/models/test_aircraft/aircraft_for_bench_GwFm.csv b/aviary/models/test_aircraft/aircraft_for_bench_GwFm.csv index f47eccb93..26ae35d6a 100644 --- a/aviary/models/test_aircraft/aircraft_for_bench_GwFm.csv +++ b/aviary/models/test_aircraft/aircraft_for_bench_GwFm.csv @@ -155,7 +155,7 @@ aircraft:horizontal_tail:wetted_area,592.65,ft**2 aircraft:hydraulics:flight_control_mass_coefficient,0.112,unitless aircraft:hydraulics:gear_mass_coefficient,0.14,unitless aircraft:hydraulics:mass_scaler,1.0,unitless -aircraft:hydraulics:system_pressure,3000.0,lbf/ft**2 +aircraft:hydraulics:system_pressure,3000.0,psi aircraft:instruments:mass_coefficient,0.0736,unitless aircraft:instruments:mass_scaler,1.25,unitless aircraft:landing_gear:carrier_based,False,unitless diff --git a/aviary/models/test_aircraft/aircraft_for_bench_solved2dof.csv b/aviary/models/test_aircraft/aircraft_for_bench_solved2dof.csv index e01b2a267..9b795b495 100644 --- a/aviary/models/test_aircraft/aircraft_for_bench_solved2dof.csv +++ b/aviary/models/test_aircraft/aircraft_for_bench_solved2dof.csv @@ -89,7 +89,7 @@ aircraft:horizontal_tail:vertical_tail_fraction,0.0,unitless aircraft:horizontal_tail:wetted_area_scaler,1.0,unitless aircraft:horizontal_tail:wetted_area,592.65,ft**2 aircraft:hydraulics:mass_scaler,1.0,unitless -aircraft:hydraulics:system_pressure,3000.0,lbf/ft**2 +aircraft:hydraulics:system_pressure,3000.0,psi aircraft:instruments:mass_scaler,1.25,unitless aircraft:landing_gear:carrier_based,False,unitless aircraft:landing_gear:main_gear_mass_scaler,1.1,unitless diff --git a/aviary/subsystems/mass/gasp_based/fixed.py b/aviary/subsystems/mass/gasp_based/fixed.py index 078acb91e..9b4b28914 100644 --- a/aviary/subsystems/mass/gasp_based/fixed.py +++ b/aviary/subsystems/mass/gasp_based/fixed.py @@ -285,7 +285,6 @@ def compute(self, inputs, outputs): outputs[Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS] = \ payload_mass = pax_mass * PAX - outputs["payload_mass_des"] = payload_mass outputs["payload_mass_max"] = pax_mass * PAX + cargo_mass diff --git a/aviary/utils/fortran_to_aviary.py b/aviary/utils/fortran_to_aviary.py index 9045ceb40..c48f9c72c 100644 --- a/aviary/utils/fortran_to_aviary.py +++ b/aviary/utils/fortran_to_aviary.py @@ -14,7 +14,6 @@ Example inputs: aircraft:fuselage:pressure_differential = .5, atm !DELP in GASP, but using atmospheres instead of psi ARNGE(1) = 3600 !target range in nautical miles -pyc_phases = taxi, groundroll, rotation, landing """ import csv @@ -612,7 +611,7 @@ def _setup_F2A_parser(parser): "--legacy_code", type=LegacyCode, help="Name of the legacy code the deck originated from", - choices=list(LegacyCode), + choices=set(LegacyCode), required=True ) parser.add_argument( @@ -629,17 +628,11 @@ def _setup_F2A_parser(parser): parser.add_argument( "-v", "--verbosity", - type=Verbosity, - choices=list(Verbosity), + type=int, + choices=Verbosity.values(), default=1, help="Set level of print statements", ) - # parser.add_argument( - # "-vv", - # "--very_verbose", - # action="store_true", - # help="Enable debug print statements", - # ) def _exec_F2A(args, user_args): diff --git a/aviary/utils/functions.py b/aviary/utils/functions.py index 51189b515..331b8d4c8 100644 --- a/aviary/utils/functions.py +++ b/aviary/utils/functions.py @@ -257,7 +257,8 @@ def setup(self): if ':' in variable_name: add_aviary_input(self, variable_name, units=units) else: - self.add_input(variable_name, units=units) + # using an arbitrary number that will stand out for unconnected variables + self.add_input(variable_name, units=units, val=1.23456) def compute(self, inputs, outputs): print_string = ['v'*20] diff --git a/aviary/utils/process_input_decks.py b/aviary/utils/process_input_decks.py index 672da5bed..5cbe8cf74 100644 --- a/aviary/utils/process_input_decks.py +++ b/aviary/utils/process_input_decks.py @@ -36,42 +36,68 @@ 'alternate': ProblemType.ALTERNATE, 'fallout': ProblemType.FALLOUT} -def create_vehicle(vehicle_deck='', verbosity=Verbosity.BRIEF, meta_data=_MetaData): +def create_vehicle(vehicle_deck='', meta_data=_MetaData, verbosity=None): """ Creates and initializes a vehicle with default or specified parameters. It sets up the aircraft values and initial guesses based on the input from the vehicle deck. Parameters ---------- - vehicle_deck (str): Path to the vehicle deck file. Default is an empty string. + vehicle_deck (str): + Path to the vehicle deck file. Default is an empty string. + meta_data (dict): + Variable metadata used when reading input file for unit validation, + default values, and other checks Returns ------- - tuple: Returns a tuple containing aircraft values and initial guesses. + (aircraft_values, initial_guesses): (tuple) + Returns a tuple containing aircraft values and initial guesses. """ aircraft_values = get_option_defaults(engine=False) # TODO remove all hardcoded GASP values here, find appropriate place for them - aircraft_values.set_val('verbosity', val=verbosity) aircraft_values.set_val('INGASP.JENGSZ', val=4) aircraft_values.set_val('test_mode', val=False) aircraft_values.set_val('use_surrogates', val=True) aircraft_values.set_val('mass_defect', val=10000, units='lbm') + # TODO problem_type should get set by get_option_defaults?? aircraft_values.set_val(Settings.PROBLEM_TYPE, val=ProblemType.SIZING) aircraft_values.set_val(Aircraft.Electrical.HAS_HYBRID_SYSTEM, val=False) + initial_guesses = { + # initial_guesses is a dictionary that contains values used to initialize the trajectory + 'actual_takeoff_mass': 0, + 'rotation_mass': 0, + 'operating_empty_mass': 0, + 'fuel_burn_per_passenger_mile': 0, + 'cruise_mass_final': 0, + 'flight_duration': 0, + 'time_to_climb': 0, + 'climb_range': 0, + 'reserves': 0 + } + if isinstance(vehicle_deck, AviaryValues): aircraft_values.update(vehicle_deck) - initial_guesses = {} else: vehicle_deck = get_path(vehicle_deck) aircraft_values, initial_guesses = parse_inputs( - vehicle_deck, aircraft_values, meta_data=meta_data) + vehicle_deck=vehicle_deck, aircraft_values=aircraft_values, initial_guesses=initial_guesses, meta_data=meta_data) + + # make sure verbosity is always set + # if verbosity set via parameter, use that + if verbosity is not None: + # Enum conversion here, so user can pass either number or actual Enum as parameter + aircraft_values.set_val(Settings.VERBOSITY, Verbosity(verbosity)) + # else, if verbosity not specified anywhere, use default of BRIEF + elif verbosity is None and Settings.VERBOSITY not in aircraft_values: + aircraft_values.set_val(Settings.VERBOSITY, Verbosity.BRIEF) return aircraft_values, initial_guesses -def parse_inputs(vehicle_deck, aircraft_values: AviaryValues(), meta_data=_MetaData): +def parse_inputs(vehicle_deck, aircraft_values: AviaryValues = None, initial_guesses=None, meta_data=_MetaData): """ Parses the input files and updates the aircraft values and initial guesses. The function reads the vehicle deck file, processes each line, and updates the aircraft_values object based on the data found. @@ -80,23 +106,19 @@ def parse_inputs(vehicle_deck, aircraft_values: AviaryValues(), meta_data=_MetaD ---------- vehicle_deck (str): The vehicle deck file path. aircraft_values (AviaryValues): An instance of AviaryValues to be updated. + initial_guesses: An initialized dictionary of trajectory values to be updated. Returns ------- tuple: Updated aircraft values and initial guesses. """ - initial_guesses = { - # initial_guesses is a dictionary that contains values used to initialize the trajectory - 'actual_takeoff_mass': 0, - 'rotation_mass': .99, - 'operating_empty_mass': 0, - 'fuel_burn_per_passenger_mile': 0.1, - 'cruise_mass_final': 0, - 'flight_duration': 0, - 'time_to_climb': 0, - 'climb_range': 0, - 'reserves': 0 - } + if aircraft_values is None: + aircraft_values = AviaryValues() + aircraft_values.set_val(Settings.VERBOSITY, Verbosity.BRIEF) + + if initial_guesses is None: + initial_guesses = {} + guess_names = list(initial_guesses.keys()) with open(vehicle_deck, newline='') as f_in: @@ -136,10 +158,22 @@ def parse_inputs(vehicle_deck, aircraft_values: AviaryValues(), meta_data=_MetaD elif var_name in guess_names: # all initial guesses take only a single value + # get values from supplied dictionary initial_guesses[var_name] = float(var_values[0]) continue - if aircraft_values.get_val('verbosity').value >= 2: + elif var_name.startswith('initial_guesses:'): + # get values labelled as initial_guesses in .csv input file + initial_guesses[var_name.split(':')[-1]] = float(var_values[0]) + continue + + elif ":" in var_name: + warnings.warn( + f"Variable '{var_name}' is not in meta_data nor in 'guess_names'. It will be ignored.", + UserWarning) + continue + + if aircraft_values.get_val(Settings.VERBOSITY).value >= 2: print('Unused:', var_name, var_values, comment) return aircraft_values, initial_guesses @@ -148,7 +182,7 @@ def parse_inputs(vehicle_deck, aircraft_values: AviaryValues(), meta_data=_MetaD # e.g. aero preprocessor, mass preprocessor, 2DOF preprocessor, etc. -def update_GASP_options(aircraft_values: AviaryValues()): +def update_GASP_options(aircraft_values: AviaryValues): """ Updates options based on the current values in aircraft_values. This function also handles special cases and prints debug information if the debug mode is active. @@ -183,7 +217,7 @@ def update_GASP_options(aircraft_values: AviaryValues()): aircraft_values.set_val( Aircraft.Wing.FOLD_DIMENSIONAL_LOCATION_SPECIFIED, val=False) - if aircraft_values.get_val('verbosity').value >= 2: + if aircraft_values.get_val(Settings.VERBOSITY).value >= 2: print('\nOptions') for key in get_keys(aircraft_values): val, units = aircraft_values.get_item(key) @@ -192,7 +226,7 @@ def update_GASP_options(aircraft_values: AviaryValues()): return aircraft_values -def update_dependent_options(aircraft_values: AviaryValues(), dependent_options): +def update_dependent_options(aircraft_values: AviaryValues, dependent_options): """ Updates options that are dependent on the value of an input variable or option. The function iterates through each dependent option and sets its value based on the current aircraft values. @@ -224,7 +258,7 @@ def update_dependent_options(aircraft_values: AviaryValues(), dependent_options) return aircraft_values -def initial_guessing(aircraft_values: AviaryValues(), initial_guesses): +def initial_guessing(aircraft_values: AviaryValues, initial_guesses): """ Sets initial guesses for various aircraft parameters based on the current problem type, aircraft values, and other factors. It calculates and sets values like takeoff mass, cruise mass, flight duration, etc. @@ -244,6 +278,9 @@ def initial_guessing(aircraft_values: AviaryValues(), initial_guesses): reserve_frac = aircraft_values.get_val( Aircraft.Design.RESERVE_FUEL_FRACTION, units='unitless') + if initial_guesses['fuel_burn_per_passenger_mile'] <= 0: + initial_guesses['fuel_burn_per_passenger_mile'] = 0.1 + reserves = initial_guesses['reserves'] if reserves < 0.0: raise ValueError( @@ -298,6 +335,8 @@ def initial_guessing(aircraft_values: AviaryValues(), initial_guesses): cruise_mass_final initial_guesses['cruise_mass_final'] = cruise_mass_final + if initial_guesses['rotation_mass'] <= 0: + initial_guesses['rotation_mass'] = 0.99 if initial_guesses['rotation_mass'] <= 1: # fraction of takeoff mass initial_guesses['rotation_mass'] = mission_mass * \ initial_guesses['rotation_mass'] @@ -333,7 +372,7 @@ def initial_guessing(aircraft_values: AviaryValues(), initial_guesses): initial_guesses['climb_range'] = initial_guesses['time_to_climb'] / \ (60 * 60) * (avg_speed_guess * np.cos(gamma_guess)) - if aircraft_values.get_val('verbosity').value >= 2: + if aircraft_values.get_val(Settings.VERBOSITY).value >= 2: print('\nInitial Guesses') for key, value in initial_guesses.items(): print(key, value) diff --git a/aviary/utils/test/test_csv_data_file.py b/aviary/utils/test/test_csv_data_file.py index 1847660b8..043a1cb31 100644 --- a/aviary/utils/test/test_csv_data_file.py +++ b/aviary/utils/test/test_csv_data_file.py @@ -1,12 +1,16 @@ +import os import unittest import warnings -from openmdao.utils.assert_utils import assert_near_equal +from openmdao.utils.assert_utils import assert_near_equal, assert_warning +from openmdao.utils.om_warnings import SetupWarning from openmdao.utils.testing_utils import use_tempdirs from aviary.utils.csv_data_file import write_data_file, read_data_file from aviary.utils.functions import get_path from aviary.utils.named_values import NamedValues, get_items, get_keys +from aviary.utils.process_input_decks import parse_inputs +from aviary.variable_info.options import get_option_defaults from aviary.variable_info.variable_meta_data import CoreMetaData, add_meta_data @@ -80,6 +84,24 @@ def test_aliases_csv(self): if 'Real Var' not in get_keys(data): raise RuntimeError("'Real Var' is not in data read from csv") + @use_tempdirs + def test_parse_input(self): + aircraft_values = get_option_defaults(engine=False) + # create a temperary csv file for testing non-existing variable name + file_name = "aircraft_for_invalid_var.csv" + with open(file_name, "w") as file: + file.write("test_string,0\n") # be ignored + file.write("aircraft:wing:mass_scalar,1,unitless\n") # raise a warning + file.write("aircraft:anti_icing:mass,551,lbm\n") # a good variable + vehicle_deck = get_path(file_name) + + msg = "Variable 'aircraft:wing:mass_scalar' is not in meta_data nor in 'guess_names'. It will be ignored." + with assert_warning(UserWarning, msg): + parse_inputs(vehicle_deck, aircraft_values) + + # remove the temperary csv file + os.remove(file_name) + def _compare_csv_results(self, data, comments): expected_data = self.data diff --git a/aviary/validation_cases/benchmark_tests/test_bench_FwGm.py b/aviary/validation_cases/benchmark_tests/test_bench_FwGm.py index 22b9f29b3..e062eb760 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_FwGm.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_FwGm.py @@ -27,13 +27,13 @@ def bench_test_swap_3_FwGm_IPOPT(self): # There are no truth values for these. assert_near_equal(prob.get_val(Mission.Design.GROSS_MASS), - 186418., tolerance=rtol) + 184533., tolerance=rtol) assert_near_equal(prob.get_val(Aircraft.Design.OPERATING_MASS), 104530., tolerance=rtol) assert_near_equal(prob.get_val(Mission.Summary.TOTAL_FUEL_MASS), - 42935., tolerance=rtol) + 42444., tolerance=rtol) assert_near_equal(prob.get_val('landing.' + Mission.Landing.GROUND_DISTANCE), 2528., tolerance=rtol) diff --git a/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py b/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py index b6f985c30..9e7d66ce3 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py @@ -33,7 +33,7 @@ def test_bench_GwGm(self): 95509, tolerance=rtol) assert_near_equal(prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), - 42529., tolerance=rtol) + 41856., tolerance=rtol) assert_near_equal(prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), 2634.8, tolerance=rtol) @@ -100,10 +100,13 @@ def test_bench_GwGm_SNOPT_lbm_s(self): @require_pyoptsparse(optimizer="IPOPT") def test_bench_GwGm_shooting(self): + from aviary.interface.default_phase_info.two_dof_fiti import phase_info, \ + phase_info_parameterization local_phase_info = deepcopy(phase_info) prob = run_aviary('models/test_aircraft/aircraft_for_bench_GwGm.csv', local_phase_info, optimizer='IPOPT', run_driver=False, - analysis_scheme=AnalysisScheme.SHOOTING, verbosity=Verbosity.QUIET) + analysis_scheme=AnalysisScheme.SHOOTING, verbosity=Verbosity.QUIET, + phase_info_parameterization=phase_info_parameterization) rtol = 0.01 @@ -120,7 +123,7 @@ def test_bench_GwGm_shooting(self): 43574., tolerance=rtol) assert_near_equal(prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), - 2634.8, tolerance=rtol) + 2623.4, tolerance=rtol) assert_near_equal(prob.get_val(Mission.Summary.RANGE, units='NM'), 3774.3, tolerance=rtol) @@ -135,5 +138,5 @@ def test_bench_GwGm_shooting(self): if __name__ == '__main__': # unittest.main() test = ProblemPhaseTestCase() + test.setUp() test.test_bench_GwGm_SNOPT_lbm_s() - test.test_bench_GwGm_shooting() diff --git a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py index 19be06cb0..f41b6c60d 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_multiengine.py @@ -169,6 +169,4 @@ def test_multiengine_dynamic(self): if __name__ == '__main__': - # unittest.main() - test = MultiengineTestcase() - test.test_multiengine_static() + unittest.main() diff --git a/aviary/variable_info/enums.py b/aviary/variable_info/enums.py index a37b2d7b3..4a6278540 100644 --- a/aviary/variable_info/enums.py +++ b/aviary/variable_info/enums.py @@ -34,6 +34,7 @@ class AlphaModes(Enum): REQUIRED_LIFT = auto() ALTITUDE_RATE = auto() CONSTANT_ALTITUDE = auto() + FLIGHT_PATH_ANGLE = auto() class AnalysisScheme(Enum): @@ -163,9 +164,12 @@ class SpeedType(Enum): TAS is true airspeed. MACH is mach ''' - EAS = auto() - TAS = auto() - MACH = auto() + EAS = 'EAS' + TAS = 'TAS' + MACH = 'mach' + + def __str__(self): + return self.value class ThrottleAllocation(Enum): @@ -195,3 +199,7 @@ class Verbosity(Enum): def __str__(self): return str(self.value) + + @classmethod + def values(cls): + return {c.value for c in cls} diff --git a/aviary/variable_info/variable_meta_data.py b/aviary/variable_info/variable_meta_data.py index 2e01d8d70..410f1ebf5 100644 --- a/aviary/variable_info/variable_meta_data.py +++ b/aviary/variable_info/variable_meta_data.py @@ -3498,7 +3498,7 @@ "FLOPS": 'WTIN.HYDPR', # ['&DEFINE.WTIN.HYDPR', 'WTS.HYDPR'], "LEAPS1": 'aircraft.inputs.L0_weights.hydraulic_sys_press' }, - units='lbf/ft**2', + units='psi', desc='hydraulic system pressure', default_value=3000.0, ) diff --git a/aviary/visualization/aircraft_3d_model.py b/aviary/visualization/aircraft_3d_model.py new file mode 100644 index 000000000..7ed7d7f66 --- /dev/null +++ b/aviary/visualization/aircraft_3d_model.py @@ -0,0 +1,892 @@ +from enum import IntEnum +import math +from string import Template +from dataclasses import dataclass +from typing import Iterator, List, Tuple + +import openmdao.api as om +import aviary.api as av + + +class WingType(IntEnum): + """ + Enum class used to define wing types. + + Attributes + ---------- + WING : int + The main wing. + HORIZONTAL_TAIL : int + The rear horizontal tail wing. + """ + + WING = 0 + HORIZONTAL_TAIL = 1 + + def __str__(self): + return self.name + + +class Axis(IntEnum): + """ + Enum class used to define which of the 3d axes. + + Attributes + ---------- + X : int + X axis. + Y : int + Y axis. + Z : int + Z axis. + """ + + X = 0 + Y = 1 + Z = 2 + + +@dataclass +class Point3D: + """ + A class to represent a point in 3D. + + Attributes + ---------- + X : int + X axis. + Y : int + Y axis. + Z : int + Z axis. + """ + + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + + def translated_copy(self, dx: float, dy: float, dz: float) -> "Point3D": + """Return a new Point3D that is a translation of this point by (dx, dy, dz).""" + return Point3D(self.x + dx, self.y + dy, self.z + dz) + + def reflected_copy(self, axis: Axis) -> "Point3D": + """Return a new Point3D that is a reflection of this point across one of the 3d axes.""" + x = self.x * (-1 if axis == Axis.X else 1) + y = self.y * (-1 if axis == Axis.Y else 1) + z = self.z * (-1 if axis == Axis.Z else 1) + return Point3D(x, y, z) + + def __str__(self) -> str: + """Return a string representation of the point in the format 'x y z'.""" + return f"{self.x} {self.y} {self.z}" + + +@dataclass +class Quad3D: + """ + A class to represent a quadrilateral in 3D. + + Attributes + ---------- + vertices : List + A list of the Point3Ds that define the quadrilateral. + """ + + vertices: List[Point3D] + + def __post_init__(self): + if len(self.vertices) != 4: + raise ValueError("A quadrilateral must have exactly four vertices.") + + def translated_copy(self, dx: float, dy: float, dz: float) -> "Quad3D": + """Return a new Quad3D that is a translation of this quad by (dx, dy, dz).""" + translated_vertices = [ + vertex.translated_copy(dx, dy, dz) for vertex in self.vertices + ] + return Quad3D(translated_vertices) + + def reflected_copy(self, axis: Axis) -> "Quad3D": + """Return a new Quad3D that is a reflection of this quad across one of the 3 axes.""" + reflected_vertices = [vertex.reflected_copy(axis) for vertex in self.vertices] + return Quad3D(reflected_vertices) + + def edges(self) -> Iterator[Tuple[Point3D, Point3D]]: + """Yield each pair of adjacent vertices as edges.""" + for i in range(4): + yield self.vertices[i], self.vertices[(i + 1) % 4] + + +def complete_solid(quad1, quad2): + """ + Given two sides of a wing of some kind, defined by a quad, + create additional quads to complete a solid/hexahedron. + + Parameters + ---------- + quad1 : Quad3D + Quadrilateral on one side of the wing. + quad2 : Quad3D + Quadrilateral on the other side of the wing. + + Returns + ------- + hexahedron + A list of Quad3Ds that define the six sides of the solid/hexadedron. + + """ + quads_to_complete_solid = [] + # loop over each of the 4 edges of the paired quads making new quads + # to complete the solid defined by the sides + for edge1, edge2 in zip(quad1.edges(), quad2.edges()): + # edges are defined by two vertices + quads_to_complete_solid.append( + Quad3D( + [ + edge1[0], + edge1[1], + edge2[1], + edge2[0], + ] + ) + ) + + return quads_to_complete_solid + + +def quad3d_to_triangle_entities(quad): + """ + Given a quadrilateral object, generate the HTML that defines the + triangles that generate that quad. + + Parameters + ---------- + quad : Quad3D + Quadrilateral input. + + Returns + ------- + html_text + The HTML that defines the quad using two a-triangles. + """ + vertices = quad.vertices + entities = f""" + + + + """ + + return entities + + +class AircraftModelReaderError(Exception): + """ + Exception thrown if there was an error trying to get data from the case recorder. + + Parameters + ---------- + msg : str + The message string. + + Attributes + ---------- + msg : str + The message string. + """ + + def __init__(self, msg): + super().__init__(msg) + self.msg = msg + + +class AircraftModelReader(object): + """ + Class used to read a case recorder file and provide methods to get Aviary variables. + + Parameters + ---------- + case_recorder_file : str + Path to the case recorder file. + + Attributes + ---------- + _case_recorder_file : str + Path to the case recorder file. + _cr : CaseReader + CaseReader object. + _problem_metadata : dict + Metadata about the problem, including the system hierachy and connections. + _final_case : str + Final Problem case from the case recorder file. + """ + + def __init__(self, case_recorder_file): + self._case_recorder_file = case_recorder_file + self._cr = None + self._problem_metadata = None + self._final_case = None + + def read_case_recorder_file(self): + """ + Read the given case recorder file. + + Parameters + ---------- + case_recorder_file : str + Path to the case recorder file. + """ + cr = om.CaseReader(self._case_recorder_file) + self._cr = cr + self._problem_metadata = cr.problem_metadata + + model_options = cr.list_model_options(out_stream=None) + self.aviary_options = model_options["root"]["aviary_options"] + + if "final" not in cr.list_cases(): + raise AircraftModelReaderError( + f"Case recorder file, {self._case_recorder_file} does not have expected case named 'final'" + ) + + self._final_case = cr.get_case("final") + + def _write_input_output_variables(self): + """ + Write out the input and output variables in the final case. For debugging. + """ + inputs = self._final_case.list_inputs( + val=True, + return_format="list", + prom_name=True, + hierarchical=False, + ) + outputs = self._final_case.list_outputs( + val=True, + return_format="list", + prom_name=True, + hierarchical=False, + ) + + def get_variable_from_case(self, var_prom_name, units=None): + """ + Get the value of a variable from the final case. + + Parameters + ---------- + var_prom_name : str + Promoted name of the variable. + units : str + Optional. Desired units of the value. + + Returns + ------- + value + Value of the variable. + """ + abs2prom = self._problem_metadata["abs2prom"] + for abs_name, prom_name in abs2prom["input"].items(): + if prom_name == var_prom_name: + val = self._final_case.get_val(abs_name, units=units) + return float(val) + + raise AircraftModelReaderError( + f"Promoted name {var_prom_name} not found in final case") + + def get_variable_from_aviary_options(self, var_prom_name): + """ + Get the value of a variable from the aviary options dict. + + Parameters + ---------- + var_prom_name : str + Promoted name of the variable. + + Returns + ------- + value + Value of the variable. + """ + item = self.aviary_options.get_item(var_prom_name) + if item is None: + raise AircraftModelReaderError( + f"Promoted name {var_prom_name} not found in aviary_options") + value, _units = item + return value + + +class Fuselage(object): + """ + Class used to represent the fuselage of the aircraft. + + Parameters + ---------- + reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + + Attributes + ---------- + _reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + _length : float + Length of the fuselage. + _radius : float + Radius of the fuselage. + """ + + def __init__(self, reader): + self._reader = reader + self._length = None + self._radius = None + + def read_variables(self): + try: + self._length = self._reader.get_variable_from_case( + "aircraft:fuselage:length", units="ft" + ) + self._radius = ( + self._reader.get_variable_from_case( + "aircraft:fuselage:avg_diameter", units="ft" + ) + / 2.0 + ) + except AircraftModelReaderError as e: + print(f"Warning: Unable to read fuselage variables due to the error: {e} ") + raise + + def get_aframe_markup(self): + return f""" + + + + + """ + + @property + def length(self): + return self._length + + @property + def radius(self): + return self._radius + + +class VerticalTail(object): + """ + Class used to represent the vertical tail of the aircraft. + + Parameters + ---------- + reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + fuselage : Fuselage + The Fuselage object used to represent the fuselage of the aircraft. + + Attributes + ---------- + _reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + _span : float + Span of the vertical tail. + _chord : float + Chord of the vertical tail. + _taper_ratio : float + Taper ratio of the vertical tail. + _thickness : float + Thickness of the vertical tail. + _fuselage : Fuselage + The Fuselage object used to represent the fuselage of the aircraft. + """ + + def __init__(self, reader, fuselage): + self._reader = reader + self._span = None + self._chord = None + self._taper_ratio = None + self._thickness = None + self._fuselage = fuselage + + def read_variables(self): + """ + Read the variables from the final case that are needed to define the vertical tail. + """ + try: + thickness_to_chord = self._reader.get_variable_from_case( + "aircraft:vertical_tail:thickness_to_chord" + ) + area = self._reader.get_variable_from_case( + "aircraft:vertical_tail:area", units="ft**2" + ) + aspect_ratio = self._reader.get_variable_from_case( + "aircraft:vertical_tail:aspect_ratio" + ) + self._taper_ratio = self._reader.get_variable_from_case( + "aircraft:vertical_tail:taper_ratio" + ) + # Calculate the span (b) using the formula b = sqrt(AR * S) + self._span = (aspect_ratio * area) ** 0.5 + # Calculate the chord (c) using the formula c = S / b + self._chord = area / self._span + self._thickness = thickness_to_chord * self._chord + except AircraftModelReaderError as e: + print( + f"Warning: Unable to read vertical tail variables due to the error: {e} " + ) + raise + + def get_aframe_markup(self): + """ + Get the A-Frame markup string. + + Returns + ------- + str + A-Frame markup defining the vertical tail. + """ + # the quad that defines the left side of the vertical tail + left_quad = Quad3D( + [ + # point 0 - trailing edge at root + Point3D( + self._fuselage.radius, + -self._fuselage.length / 2.0, + self._thickness / 2, + ), + # point 1 - leading edge at root + Point3D( + self._fuselage.radius, + -self._fuselage.length / 2.0 + self._chord, + self._thickness / 2, + ), + # point 2 - leading edge at tip + Point3D( + self._span + self._fuselage.radius, + -self._fuselage.length / 2.0 + self._chord * self._taper_ratio, + self._thickness / 2, + ), + # point 3 - trailing edge at tip + Point3D( + self._span + self._fuselage.radius, + -self._fuselage.length / 2.0, + self._thickness / 2, + ), + ] + ) + right_quad = left_quad.translated_copy(0, 0, -self._thickness) + quads_to_complete_solid = complete_solid(left_quad, right_quad) + + entities = "" + entities += f""" + + {quad3d_to_triangle_entities(left_quad)} + {quad3d_to_triangle_entities(right_quad)} + """ + + for quad in quads_to_complete_solid: + entities += f"{quad3d_to_triangle_entities(quad)}" + return entities + + +class HorizontalWing(object): + """ + Class used to represent a horizontal (main or tail) wing of the aircraft. + + Parameters + ---------- + reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + fuselage : Fuselage + The Fuselage object used to represent the fuselage of the aircraft. + wing_type : WingType + Enum indicating whether this is the main wing or the horizontal tail wing. + + Attributes + ---------- + _reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + _vertical_position : float + Position of the wing vertically on the fuselage. + _thickness : float + Thickness of the wing. + _chord : float + Chord of the wing. + _span : float + Span of the wing. + _sweep_angle : float + Sweep angle of the wing. + _chord_tip : float + Chord at the top of the wing. + _position_along_fuselage : float + Position of the wing along the fuselage. + _mounting_type : float + wing location on fuselage (0 = low wing, 1 = high wing, can be fractions). + _fuselage : Fuselage + The Fuselage object used to represent the fuselage of the aircraft. + _wing_type : WingType + Enum indicating whether this is the main wing or the horizontal tail wing. + """ + + def __init__(self, reader, fuselage, wing_type): + self._reader = reader + self._vertical_position = None + self._thickness = None + self._chord = None + self._span = None + self._sweep_angle = None + self._chord_tip = None + self._position_along_fuselage = None + self._mounting_type = None + self._fuselage = fuselage + self._wing_type = wing_type + + def read_variables(self): + """ + Read the variables from the final case that are needed to define the horizontal wing. + """ + if self._wing_type == WingType.WING: + wing_type_name = "wing" + elif self._wing_type == WingType.HORIZONTAL_TAIL: + wing_type_name = "horizontal_tail" + try: + aspect_ratio = self._reader.get_variable_from_case( + f"aircraft:{wing_type_name}:aspect_ratio" + ) + taper_ratio = self._reader.get_variable_from_case( + f"aircraft:{wing_type_name}:taper_ratio" + ) + area = self._reader.get_variable_from_case( + f"aircraft:{wing_type_name}:area", units="ft**2" + ) + try: + thickness_to_chord = self._reader.get_variable_from_case( + f"aircraft:{wing_type_name}:thickness_to_chord" + ) + except AircraftModelReaderError: # try this method if the first doesn't work + thickness_to_chord_root = self._reader.get_variable_from_case( + f"aircraft:{wing_type_name}:thickness_to_chord_root" + ) + thickness_to_chord_tip = self._reader.get_variable_from_case( + f"aircraft:{wing_type_name}:thickness_to_chord_tip" + ) + thickness_to_chord = ( + thickness_to_chord_root + thickness_to_chord_tip + ) / 2.0 + + self._span = (aspect_ratio * area) ** 0.5 + self._chord = area / self._span + self._thickness = thickness_to_chord * self._chord + self._sweep_angle = self._reader.get_variable_from_case( + av.Aircraft.Wing.SWEEP) + self._chord_tip = self._chord * taper_ratio + if self._wing_type == WingType.WING: + try: + mounting_type = self._reader.get_variable_from_case( + "aircraft:wing:mounting_type" + ) + except AircraftModelReaderError: + mounting_type = 0.0 + self._vertical_position = ( + 2.0 * (mounting_type - 0.5) * self._fuselage.radius + ) + self._position_along_fuselage = 0.0 + elif self._wing_type == WingType.HORIZONTAL_TAIL: + self._vertical_position = 0.0 + self._position_along_fuselage = ( + -self._fuselage.length / 2.0 + self._chord / 2.0 + ) + except AircraftModelReaderError as e: + print( + f"Warning: Unable to read horizontal wing of type '{self._wing_type}' variables due to the error: {e} " + ) + raise + + def get_aframe_markup(self): + """ + Get the A-Frame markup string. + + Returns + ------- + str + A-Frame markup defining the horizontal wing. + """ + sweep_angle_tan = math.tan(math.radians(self._sweep_angle)) + + entities = "" + + # the quad that defines the left side of the horizontal wing + left_top_quad = Quad3D( + [ + # point 0 - leading edge on centerline + Point3D( + self._vertical_position + self._thickness / 2, + self._position_along_fuselage + self._chord / 2.0, + 0.0, + ), + # point 1 - leading edge at tip + Point3D( + self._vertical_position + self._thickness / 2, + self._position_along_fuselage + + self._chord / 2.0 + - sweep_angle_tan * self._span / 2.0, + self._span / 2.0, + ), + # point 2 - trailing edge at tip + Point3D( + self._vertical_position + self._thickness / 2, + self._position_along_fuselage + + self._chord / 2.0 + - sweep_angle_tan * self._span / 2.0 + - self._chord_tip, + self._span / 2.0, + ), + # point 3 - trailing edge on centerline + Point3D( + self._vertical_position + self._thickness / 2, + self._position_along_fuselage - self._chord / 2.0, + 0.0, + ), + ] + ) + left_bottom_quad = left_top_quad.translated_copy(-self._thickness, 0, 0) + left_quads_to_complete_solid = complete_solid(left_top_quad, left_bottom_quad) + entities += f""" + + {quad3d_to_triangle_entities(left_top_quad)} + {quad3d_to_triangle_entities(left_bottom_quad)} + """ + for quad in left_quads_to_complete_solid: + entities += f"{quad3d_to_triangle_entities(quad)}" + + # Now the right wing + right_top_quad = left_top_quad.reflected_copy(Axis.Z) + right_bottom_quad = right_top_quad.translated_copy(-self._thickness, 0, 0) + right_quads_to_complete_solid = complete_solid( + right_top_quad, right_bottom_quad + ) + entities += f""" + + {quad3d_to_triangle_entities(right_top_quad)} + {quad3d_to_triangle_entities(right_bottom_quad)} + """ + + for quad in right_quads_to_complete_solid: + entities += f"{quad3d_to_triangle_entities(quad)}" + + return entities + + @property + def span(self): + return self._span + + @property + def position_along_fuselage(self): + return self._position_along_fuselage + + @property + def chord(self): + return self._chord + + @property + def sweep_angle(self): + return self._sweep_angle + + @property + def vertical_position(self): + return self._vertical_position + + +class Engines(object): + """ + Class used to represent the engines of the aircraft. + + Parameters + ---------- + reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + fuselage : Fuselage + Fuselage object. + wing : HorizontalWing + The main wing object. + + Attributes + ---------- + _reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + _fuselage : Fuselage + Fuselage object. + _wing : HorizontalWing + Main wing object. + _num_wing_engines : int + Number of engines. + _engine_diameter : float + Diameter of engines. + _engine_length : float + Length of engines. + _engine_locations_on_wing : float + Location of engine on wing. Fraction of span. + _has_propellers : bool + Does the engine have propellers. + """ + + def __init__(self, reader, fuselage, wing): + self._reader = reader + self._fuselage = fuselage + self._wing = wing + self._num_wing_engines = None + self._engine_diameter = None + self._engine_length = None + self._engine_locations_on_wing = None + self._has_propellers = None + + def read_variables(self): + """ + Read the variables from the final case that are needed to define the engines. + """ + try: + self._num_wing_engines = self._reader.get_variable_from_aviary_options( + av.Aircraft.Engine.NUM_WING_ENGINES + ) + self._engine_diameter = self._reader.get_variable_from_case( + av.Aircraft.Nacelle.AVG_DIAMETER, units="ft" + ) + self._engine_length = self._reader.get_variable_from_case( + av.Aircraft.Nacelle.AVG_LENGTH, units="ft" + ) + self._engine_locations_on_wing = ( + self._reader.get_variable_from_aviary_options( + av.Aircraft.Engine.WING_LOCATIONS + ) + ) + try: + self._has_propellers = self._reader.get_variable_from_case( + av.Aircraft.Engine.HAS_PROPELLERS + ) + except AircraftModelReaderError as e2: + self._has_propellers = False + except AircraftModelReaderError as e: + print(f"Warning: Unable to read engine variables due to the error: {e} ") + raise + + def get_aframe_markup(self): + """ + Get the A-Frame markup string. + + Returns + ------- + str + A-Frame markup defining the engines. + """ + wing_span = self._wing.span + entities = "" + + for engine_location in self._engine_locations_on_wing: + distance_from_fuselage = engine_location * wing_span/2.0 + distance_along_fuselage = self._wing.position_along_fuselage + self._wing.chord / 2. - \ + distance_from_fuselage * \ + math.tan(math.radians(self._wing.sweep_angle)) + self._engine_length/2. + distance_above_fuselage = self._wing.vertical_position - self._engine_diameter / 2. + entities += f""" + + + + """ + if self._has_propellers: + propeller_blade_radius = self._engine_diameter / \ + 10.0 # arbitrary fraction of engine diameter + propeller_blade_length = self._engine_diameter * 2.0 + entities += f""" + + + + + + + """ + + return entities + + +class Aircraft3DModel(object): + """ + Class used to represent the 3D model of the aircraft. The A-Frame library + is used to draw the 3D model in the Web page. + + Parameters + ---------- + case_recorder_file : str + Path to the case recorder file. + + Attributes + ---------- + _reader : AircraftModelReader + The AircraftModelReader object used to read aviary variable values. + _entities : str + HTML representing all the entities defining the aircraft 3D model. + _camera_entity : str + HTML representing all the camera in the scene. + """ + + def __init__(self, case_recorder_file): + self._reader = AircraftModelReader(case_recorder_file) + self._reader.read_case_recorder_file() + + # Used for debugging. Uncomment to print out the input and output variables + # in the final case + # self.model_reader._write_input_output_variables() + + self._entities = "" + self._camera_entity = "" + + def read_variables(self): + self.fuselage = Fuselage(self._reader) + self.fuselage.read_variables() + self.wing = HorizontalWing(self._reader, self.fuselage, WingType.WING) + self.wing.read_variables() + self.engines = Engines(self._reader, self.fuselage, self.wing) + self.engines.read_variables() + self.horizontal_tail = HorizontalWing( + self._reader, self.fuselage, WingType.HORIZONTAL_TAIL + ) + self.horizontal_tail.read_variables() + self.vertical_tail = VerticalTail(self._reader, self.fuselage) + self.vertical_tail.read_variables() + + def get_aframe_markup(self): + self._entities += self.fuselage.get_aframe_markup() + self._entities += self.wing.get_aframe_markup() + self._entities += self.engines.get_aframe_markup() + self._entities += self.horizontal_tail.get_aframe_markup() + self._entities += self.vertical_tail.get_aframe_markup() + + def get_camera_entity(self, fuselage_length): + y_camera = fuselage_length / 2.0 + z_camera = fuselage_length + self._camera_entity = f""" + + """ + + def write_file(self, aircraft_3d_template_filepath, outfilepath): + with open(aircraft_3d_template_filepath, "r", encoding="utf-8") as f: + aircraft_3d_template = f.read() + + with open(outfilepath, "w") as f: + template = Template(aircraft_3d_template) + + s = template.substitute( + entities=self._entities, camera_entity=self._camera_entity + ) + f.write(s) diff --git a/aviary/visualization/assets/aircraft_3d_file_template.html b/aviary/visualization/assets/aircraft_3d_file_template.html new file mode 100644 index 000000000..60e6404da --- /dev/null +++ b/aviary/visualization/assets/aircraft_3d_file_template.html @@ -0,0 +1,29 @@ + + + + + Designed Aircraft 3D Model + + + + + + + $camera_entity + + + + + + + + $entities + + + + + + + + + diff --git a/aviary/visualization/assets/aviary_airlines.png b/aviary/visualization/assets/aviary_airlines.png new file mode 100644 index 0000000000000000000000000000000000000000..073ebbbfbb6112f6b5a405358aa55cce2ef44f96 GIT binary patch literal 64403 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 OpOT!KY~ceFzyAO)oB8Sh literal 0 HcmV?d00001 diff --git a/aviary/visualization/dashboard.py b/aviary/visualization/dashboard.py index 635dd4eea..da047e348 100644 --- a/aviary/visualization/dashboard.py +++ b/aviary/visualization/dashboard.py @@ -5,6 +5,13 @@ import pathlib import shutil import importlib.util +from string import Template +from dataclasses import dataclass +from typing import ( + List, + Iterator, + Tuple, +) # Use typing.List and typing.Tuple for compatibility import numpy as np from bokeh.palettes import Category10 @@ -21,6 +28,9 @@ # If get_free_port is unavailable, the default port will be used def get_free_port(): return 5000 +from openmdao.utils.om_warnings import issue_warning + +from aviary.visualization.aircraft_3d_model import Aircraft3DModel # support getting this function from OpenMDAO post movement of the function to utils # but also support its old location @@ -226,7 +236,6 @@ def create_aviary_variables_table_data_nested(script_name, recorder_file): """ cr = om.CaseReader(recorder_file) - print(f"r.list_c with {cr=}") if "final" not in cr.list_cases(): return None @@ -383,9 +392,40 @@ def convert_case_recorder_file_to_df(recorder_file_name): return df -# The main script that generates all the tabs in the dashboard + +def create_aircraft_3d_file(recorder_file, reports_dir, outfilepath): + """ + Create the HTML file with the display of the aircraft design + in 3D using the A-Frame library. + + Parameters + ---------- + recorder_file : str + Name of the case recorder file. + reports_dir : str + Path of the directory containing the reports from the run. + outfilepath : str + The path to the location where the file should be created. + """ + # Get the location of the HTML template file for this HTML file + aviary_dir = pathlib.Path(importlib.util.find_spec("aviary").origin).parent + aircraft_3d_template_filepath = aviary_dir.joinpath( + "visualization/assets/aircraft_3d_file_template.html" + ) + + # texture for the aircraft. Need to copy it to the reports directory + # next to the HTML file + shutil.copy( + aviary_dir.joinpath("visualization/assets/aviary_airlines.png"), + f"{reports_dir}/aviary_airlines.png", + ) + + aircraft_3d_model = Aircraft3DModel(recorder_file) + + aircraft_3d_model.write_file(aircraft_3d_template_filepath, outfilepath) +# The main script that generates all the tabs in the dashboard def dashboard(script_name, problem_recorder, driver_recorder, port): """ Generate the dashboard app display. @@ -561,9 +601,6 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): ihvplot.panel(), ) ) - optimization_tabs_list.append( - ("Desvars, cons, opt", optimization_plot_pane) - ) else: optimization_plot_pane = pn.pane.Markdown( f"# Recorder file '{driver_recorder}' does not have Driver case recordings" @@ -572,13 +609,13 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): optimization_plot_pane = pn.pane.Markdown( f"# Recorder file '{driver_recorder}' not found") - optimization_plot_pane = pn.Column( + optimization_plot_pane_with_doc = pn.Column( pn.pane.HTML(f"

    Plot of design variables, constraints, and objectives

    ", styles={'text-align': documentation_text_align}), optimization_plot_pane ) optimization_tabs_list.append( - ("Desvars, cons, opt", optimization_plot_pane) + ("Desvars, cons, opt", optimization_plot_pane_with_doc) ) ####### Results Tab ####### @@ -610,6 +647,7 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): # Make the Aviary variables table pane if problem_recorder: if os.path.exists(problem_recorder): + # Make dir reports/script_name/aviary_vars if needed aviary_vars_dir = pathlib.Path(f"reports/{script_name}/aviary_vars") aviary_vars_dir.mkdir(parents=True, exist_ok=True) @@ -628,16 +666,20 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): # copy script.js file to reports/script_name/aviary_vars/index.html. # mod the script.js file to point at the json file # create the json file and put it in reports/script_name/aviary_vars/aviary_vars.json - create_aviary_variables_table_data_nested( - script_name, problem_recorder - ) # create the json file - aviary_vars_pane = create_report_frame( - "html", f"{reports_dir}/aviary_vars/index.html", ''' - A table of outputs of the model with features for filtering, and copying values - ''' - ) - - results_tabs_list.append(("Aviary Variables", aviary_vars_pane)) + try: + create_aviary_variables_table_data_nested( + script_name, problem_recorder + ) # create the json file + + aviary_vars_pane = create_report_frame( + "html", f"{reports_dir}/aviary_vars/index.html", + "Table showing Aviary variables" + ) + results_tabs_list.append(("Aviary Variables", aviary_vars_pane)) + except Exception as e: + issue_warning( + f"Unable do create Aviary Variables tab in dashboard due to the error: {str(e)}" + ) # Timeseries Mission Output Report mission_timeseries_pane = create_csv_frame( @@ -651,6 +693,25 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): ("Timeseries Mission Output Report", mission_timeseries_pane) ) + # Aircraft 3d model display + if problem_recorder: + if os.path.exists(problem_recorder): + + try: + create_aircraft_3d_file( + problem_recorder, reports_dir, f"{reports_dir}/aircraft_3d.html" + ) + aircraft_3d_pane = create_report_frame( + "html", f"{reports_dir}/aircraft_3d.html", + "3D model view of designed aircraft" + ) + if aircraft_3d_pane: + results_tabs_list.append(("Aircraft 3d model", aircraft_3d_pane)) + except Exception as e: + issue_warning( + f"Unable to create aircraft 3D model display due to error {e}" + ) + ####### Subsystems Tab ####### subsystem_tabs_list = [] diff --git a/setup.py b/setup.py index 187b610d5..4bcf1d4a8 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ version=__version__, packages=find_packages(), install_requires=[ - "openmdao>=3.27.0", + "openmdao>=3.33.0", "dymos>=1.8.1", "hvplot", "numpy", From 6dc196c175e572983ae07a9fa18805c74b05e8fa Mon Sep 17 00:00:00 2001 From: Jason Kirk <110835404+jkirk5@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:08:41 -0400 Subject: [PATCH 19/22] Apply suggestions from code review Co-authored-by: crecine <51181861+crecine@users.noreply.github.com> --- .../mission/gasp_based/test/test_idle_descent_estimation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aviary/mission/gasp_based/test/test_idle_descent_estimation.py b/aviary/mission/gasp_based/test/test_idle_descent_estimation.py index 2672abb1f..97d93432f 100644 --- a/aviary/mission/gasp_based/test/test_idle_descent_estimation.py +++ b/aviary/mission/gasp_based/test/test_idle_descent_estimation.py @@ -27,8 +27,8 @@ def setUp(self): aviary_inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, val=28690, units="lbf") aviary_inputs.set_val(Dynamic.Mission.THROTTLE, val=0, units="unitless") - engine = build_engine_deck(aviary_options=aviary_inputs)[0] - preprocess_propulsion(aviary_inputs, [engine]) + engine = build_engine_deck(aviary_options=aviary_inputs) + preprocess_propulsion(aviary_inputs, engine) default_mission_subsystems = get_default_mission_subsystems( 'GASP', build_engine_deck(aviary_inputs)) From acb4cd621d57c3b3cdc534ded0d3c658912792f4 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 17 Jun 2024 18:19:27 -0400 Subject: [PATCH 20/22] removed bad import --- aviary/interface/default_phase_info/two_dof_fiti.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aviary/interface/default_phase_info/two_dof_fiti.py b/aviary/interface/default_phase_info/two_dof_fiti.py index ce06c54db..8e5b552a5 100644 --- a/aviary/interface/default_phase_info/two_dof_fiti.py +++ b/aviary/interface/default_phase_info/two_dof_fiti.py @@ -3,7 +3,6 @@ from aviary.mission.gasp_based.phases.time_integration_phases import SGMGroundroll, \ SGMRotation, SGMAscentCombined, SGMAccel, SGMClimb, SGMCruise, SGMDescent from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings -from aviary.variable_info.variable_meta_data import _MetaData as Mission # defaults for 2DOF based forward in time integeration phases cruise_alt = 35e3, @@ -172,7 +171,6 @@ def phase_info_parameterization(phase_info, post_mission_info, aviary_inputs: Av Modified phase_info that has been changed to match the new mission parameters """ - range_cruise = aviary_inputs.get_item(Mission.Design.RANGE) alt_cruise = aviary_inputs.get_item(Mission.Design.CRUISE_ALTITUDE) gross_mass = aviary_inputs.get_item(Mission.Design.GROSS_MASS) From f0110bbe6b64211daa8b5327bad1a8256acf4db0 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 17 Jun 2024 21:42:04 -0400 Subject: [PATCH 21/22] minor tweak --- aviary/subsystems/propulsion/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aviary/subsystems/propulsion/utils.py b/aviary/subsystems/propulsion/utils.py index 76632d6a1..41485c87e 100644 --- a/aviary/subsystems/propulsion/utils.py +++ b/aviary/subsystems/propulsion/utils.py @@ -128,8 +128,7 @@ def build_engine_deck(aviary_options: AviaryValues): # Build a single engine deck, currently ignoring vectorization # of AviaryValues (use first index) engine_options = AviaryValues() - for entry in Aircraft.Engine.__dict__: - var = getattr(Aircraft.Engine, entry) + for var in Aircraft.Engine.__dict__.values(): # check if this variable exist with useable metadata try: units = _MetaData[var]['units'] From 9e0057ad29309b80c76120a7ffcaf919711ac300 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 17 Jun 2024 22:41:25 -0400 Subject: [PATCH 22/22] merge fixes --- .../default_phase_info/height_energy_fiti.py | 16 +--------------- .../interface/default_phase_info/two_dof_fiti.py | 4 +++- .../phases/test/test_time_integration_phases.py | 2 +- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/aviary/interface/default_phase_info/height_energy_fiti.py b/aviary/interface/default_phase_info/height_energy_fiti.py index 5854f5712..082b87202 100644 --- a/aviary/interface/default_phase_info/height_energy_fiti.py +++ b/aviary/interface/default_phase_info/height_energy_fiti.py @@ -1,26 +1,12 @@ from aviary.mission.flops_based.phases.time_integration_phases import SGMDetailedTakeoff, \ SGMHeightEnergy, SGMDetailedLanding -from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder -from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder -from aviary.subsystems.mass.mass_builder import CoreMassBuilder -from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData from aviary.variable_info.variables import Dynamic, Mission -from aviary.variable_info.enums import SpeedType, AlphaModes, LegacyCode +from aviary.variable_info.enums import SpeedType, AlphaModes from aviary.interface.default_phase_info.two_dof_fiti import add_default_sgm_args -FLOPS = LegacyCode.FLOPS - -prop = CorePropulsionBuilder('core_propulsion', BaseMetaData) -mass = CoreMassBuilder('core_mass', BaseMetaData, FLOPS) -aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, FLOPS) -geom = CoreGeometryBuilder('core_geometry', BaseMetaData, FLOPS) - -default_premission_subsystems = [prop, geom, mass, aero] -default_mission_subsystems = [aero, prop] - cruise_mach = .8, cruise_alt = 35e3, diff --git a/aviary/interface/default_phase_info/two_dof_fiti.py b/aviary/interface/default_phase_info/two_dof_fiti.py index fc6b7ac7e..8de3b0a82 100644 --- a/aviary/interface/default_phase_info/two_dof_fiti.py +++ b/aviary/interface/default_phase_info/two_dof_fiti.py @@ -1,7 +1,9 @@ from aviary.variable_info.enums import SpeedType, AlphaModes from aviary.mission.gasp_based.phases.time_integration_phases import SGMGroundroll, \ SGMRotation, SGMAscentCombined, SGMAccel, SGMClimb, SGMCruise, SGMDescent -from aviary.variable_info.variables import Aircraft +from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.variable_info.enums import Verbosity # defaults for 2DOF based forward in time integeration phases cruise_alt = 35e3, diff --git a/aviary/mission/flops_based/phases/test/test_time_integration_phases.py b/aviary/mission/flops_based/phases/test/test_time_integration_phases.py index 27d42e9b5..daec19787 100644 --- a/aviary/mission/flops_based/phases/test/test_time_integration_phases.py +++ b/aviary/mission/flops_based/phases/test/test_time_integration_phases.py @@ -7,7 +7,7 @@ SGMHeightEnergy, SGMDetailedTakeoff, SGMDetailedLanding from aviary.subsystems.premission import CorePreMission from aviary.utils.functions import set_aviary_initial_values -from aviary.variable_info.enums import Verbosity, EquationsOfMotion +from aviary.variable_info.enums import EquationsOfMotion from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings from aviary.variable_info.variables_in import VariablesIn