diff --git a/aviary/subsystems/propulsion/engine_deck.py b/aviary/subsystems/propulsion/engine_deck.py index 3b14e9edb..a7c5d2b91 100644 --- a/aviary/subsystems/propulsion/engine_deck.py +++ b/aviary/subsystems/propulsion/engine_deck.py @@ -145,6 +145,12 @@ class EngineDeck(EngineModel): update """ + # EngineDecks using GLOBAL_THROTTLE = False will have unique maximum throttle levels per flight + # condition (not always 1) - max engine values must be handled manually inside this component + # TODO this can be updated so that if GLOBAL_THROTTLE = True, the max engine components in + # build_mission() are skipped, and this flag is set to False. + compute_max_values = True + def __init__( self, name='engine_deck', @@ -335,12 +341,12 @@ def _set_variable_flags(self): def _setup(self, data): """ Read in and process engine data. - - Check data consistency. - - Convert altitudes to geometric. - - Sort and pack data. - - Determine reference thrust. - - Normalize throttles & hybrid throttles. - - Fill flight idle points if requested. + - Check data consistency + - Convert altitudes to geometric (optional) + - Sort and pack data + - Determine reference thrust (optional) + - Normalize throttles & hybrid throttles + - Fill flight idle points (optional) """ self._read_data(data) @@ -904,7 +910,7 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs) -> om.Group: # reduced data set? if self.use_thrust or self.use_shaft_power: if self.global_throttle or (self.global_hybrid_throttle and self.use_hybrid_throttle): - # create IndepVarComp to pass maximum throttle is to max thrust interpolator + # create IndepVarComp to pass maximum throttle to max thrust interpolator fixed_throttles = om.IndepVarComp() if self.global_throttle: fixed_throttles.add_output( @@ -985,7 +991,7 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs) -> om.Group: max_thrust_engine = om.MetaModelSemiStructuredComp( method=interp_method, extrapolate=False, vec_size=num_nodes ) - + # TODO engine could have other inputs!! Don't hardcode these if interp_sort == 'altitude': max_thrust_engine.add_input( Dynamic.Mission.ALTITUDE, @@ -1085,23 +1091,38 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs) -> om.Group: if self.use_thrust or self.use_shaft_power: if self.global_throttle or (self.global_hybrid_throttle and self.use_hybrid_throttle): engine_group.add_subsystem( - 'fixed_max_throttles', fixed_throttles, promotes_outputs=['*'] + 'fixed_max_throttles', + fixed_throttles, # , promotes_outputs=['*'] ) - - if not ( - self.global_throttle or (self.global_hybrid_throttle and self.use_hybrid_throttle) - ): + else: engine_group.add_subsystem( 'interp_max_throttles', interp_throttles, promotes_inputs=['*'], - promotes_outputs=['*'], + # promotes_outputs=['*'], ) engine_group.add_subsystem( - 'max_interpolation', max_thrust_engine, promotes_inputs=['*'] + 'max_interpolation', + max_thrust_engine, + promotes_inputs=[ + Dynamic.Atmosphere.MACH, + Dynamic.Mission.ALTITUDE, + ], # , promotes_inputs=['*'] ) + # manually connect max throttles - do not promote them out of group + if self.global_throttle or (self.global_hybrid_throttle and self.use_hybrid_throttle): + engine_group.connect( + 'fixed_max_throttles.throttle_max', + 'max_interpolation.throttle_max', + ) + else: + engine_group.connect( + 'interp_max_throttles.throttle_max', + 'max_interpolation.throttle_max', + ) + if uncorrect_shp: engine_group.add_subsystem( 'uncorrect_max_shaft_power', @@ -1171,6 +1192,14 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs) -> om.Group: return engine_group + def mission_inputs(self, **kwargs): + inputs = [inp.value for inp in self.inputs] + return inputs + + def mission_outputs(self, **kwargs): + outputs = [out.value for out in self.outputs] + return outputs + def get_parameters(self): params = { Aircraft.Engine.SCALE_FACTOR: { diff --git a/aviary/subsystems/propulsion/engine_model.py b/aviary/subsystems/propulsion/engine_model.py index 0059f3b5b..225436797 100644 --- a/aviary/subsystems/propulsion/engine_model.py +++ b/aviary/subsystems/propulsion/engine_model.py @@ -39,6 +39,10 @@ class EngineModel(SubsystemBuilder): """ default_name = 'engine_model' + # Flag that sets if this engine computes maximum values (e.g. max thrust, shaft power) for a + # given flight condition. If False, during mission Aviary will create a duplicate copy of the + # engine that is given max throttle and hybrid throttle (1.0) to compute max values. + compute_max_values = False def __init__( self, name: str = None, options: AviaryValues = None, meta_data: dict = None, **kwargs diff --git a/aviary/subsystems/propulsion/gearbox/gearbox_builder.py b/aviary/subsystems/propulsion/gearbox/gearbox_builder.py index adc59562b..f1c72a7da 100644 --- a/aviary/subsystems/propulsion/gearbox/gearbox_builder.py +++ b/aviary/subsystems/propulsion/gearbox/gearbox_builder.py @@ -32,6 +32,13 @@ def build_mission(self, num_nodes, aviary_inputs): """Builds an OpenMDAO system for the mission computations of the subsystem.""" return GearboxMission(num_nodes=num_nodes) + def mission_inputs(self, **kwargs): + inputs = [Aircraft.Engine.Gearbox.GEAR_RATIO, Aircraft.Engine.Gearbox.EFFICIENCY] + return inputs + + def mission_outputs(self, **kwargs): + return [] + def get_design_vars(self): """ Design vars are only tested to see if they exist in pre_mission @@ -82,11 +89,11 @@ def get_parameters(self, aviary_inputs=None, phase_info=None): 'units': 'unitless', 'static_target': True, }, - Aircraft.Engine.Gearbox.SHAFT_POWER_DESIGN: { - 'val': 1.0, - 'units': 'kW', - 'static_target': True, - }, + # Aircraft.Engine.Gearbox.SHAFT_POWER_DESIGN: { + # 'val': 1.0, + # 'units': 'kW', + # 'static_target': True, + # }, } return parameters @@ -97,21 +104,22 @@ def get_mass_names(self): def get_timeseries(self): return [ Dynamic.Vehicle.Propulsion.SHAFT_POWER + '_out', - Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out', + # Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out', Dynamic.Vehicle.Propulsion.RPM + '_out', Dynamic.Vehicle.Propulsion.TORQUE + '_out', - Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL, + # Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL, ] def get_constraints(self): if self.include_constraints: - constraints = { - Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL: { - 'lower': 0.0, - 'type': 'path', - 'units': 'kW', - } - } + constraints = {} + # constraints = { + # Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL: { + # 'lower': 0.0, + # 'type': 'path', + # 'units': 'kW', + # } + # } else: constraints = {} return constraints diff --git a/aviary/subsystems/propulsion/gearbox/model/gearbox_mission.py b/aviary/subsystems/propulsion/gearbox/model/gearbox_mission.py index f7b41b946..ffed88dc3 100644 --- a/aviary/subsystems/propulsion/gearbox/model/gearbox_mission.py +++ b/aviary/subsystems/propulsion/gearbox/model/gearbox_mission.py @@ -69,46 +69,46 @@ def setup(self): # Determine the maximum power available at this flight condition # this is used for excess power constraints - self.add_subsystem( - 'shaft_power_max_comp', - om.ExecComp( - 'shaft_power_out = shaft_power_in * efficiency', - shaft_power_in={'val': np.ones(n), 'units': 'kW'}, - shaft_power_out={'val': np.ones(n), 'units': 'kW'}, - efficiency={'val': 1.0, 'units': 'unitless'}, - has_diag_partials=True, - ), - promotes_inputs=[ - ('shaft_power_in', Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_in'), - ('efficiency', Aircraft.Engine.Gearbox.EFFICIENCY), - ], - promotes_outputs=[ - ('shaft_power_out', Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out') - ], - ) + # self.add_subsystem( + # 'shaft_power_max_comp', + # om.ExecComp( + # 'shaft_power_out = shaft_power_in * efficiency', + # shaft_power_in={'val': np.ones(n), 'units': 'kW'}, + # shaft_power_out={'val': np.ones(n), 'units': 'kW'}, + # efficiency={'val': 1.0, 'units': 'unitless'}, + # has_diag_partials=True, + # ), + # promotes_inputs=[ + # ('shaft_power_in', Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_in'), + # ('efficiency', Aircraft.Engine.Gearbox.EFFICIENCY), + # ], + # promotes_outputs=[ + # ('shaft_power_out', Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out') + # ], + # ) - # We must ensure the design shaft power that was provided to pre-mission is - # larger than the maximum shaft power that could be drawn by the mission. - # Note this is a larger value than the actual maximum shaft power drawn during - # the mission because the aircraft might need to climb to avoid obstacles at - # anytime during the mission - self.add_subsystem( - 'shaft_power_residual', - om.ExecComp( - 'shaft_power_residual = shaft_power_design - shaft_power_max', - shaft_power_max={'val': np.ones(n), 'units': 'kW'}, - shaft_power_design={'val': 1.0, 'units': 'kW'}, - shaft_power_residual={'val': np.ones(n), 'units': 'kW'}, - has_diag_partials=True, - ), - promotes_inputs=[ - ('shaft_power_max', Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_in'), - ('shaft_power_design', Aircraft.Engine.Gearbox.SHAFT_POWER_DESIGN), - ], - promotes_outputs=[ - ( - 'shaft_power_residual', - Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL, - ) - ], - ) + # # We must ensure the design shaft power that was provided to pre-mission is + # # larger than the maximum shaft power that could be drawn by the mission. + # # Note this is a larger value than the actual maximum shaft power drawn during + # # the mission because the aircraft might need to climb to avoid obstacles at + # # anytime during the mission + # self.add_subsystem( + # 'shaft_power_residual', + # om.ExecComp( + # 'shaft_power_residual = shaft_power_design - shaft_power_max', + # shaft_power_max={'val': np.ones(n), 'units': 'kW'}, + # shaft_power_design={'val': 1.0, 'units': 'kW'}, + # shaft_power_residual={'val': np.ones(n), 'units': 'kW'}, + # has_diag_partials=True, + # ), + # promotes_inputs=[ + # ('shaft_power_max', Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_in'), + # ('shaft_power_design', Aircraft.Engine.Gearbox.SHAFT_POWER_DESIGN), + # ], + # promotes_outputs=[ + # ( + # 'shaft_power_residual', + # Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL, + # ) + # ], + # ) diff --git a/aviary/subsystems/propulsion/gearbox/test/test_gearbox.py b/aviary/subsystems/propulsion/gearbox/test/test_gearbox.py index 59dd091ad..e71838b1c 100644 --- a/aviary/subsystems/propulsion/gearbox/test/test_gearbox.py +++ b/aviary/subsystems/propulsion/gearbox/test/test_gearbox.py @@ -54,9 +54,9 @@ def test_gearbox_mission(self): prob.set_val(av.Dynamic.Vehicle.Propulsion.RPM + '_in', [5000, 6195, 6195], units='rpm') prob.set_val(av.Dynamic.Vehicle.Propulsion.SHAFT_POWER + '_in', [100, 200, 375], units='hp') - prob.set_val( - av.Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_in', [375, 300, 375], units='hp' - ) + # prob.set_val( + # av.Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_in', [375, 300, 375], units='hp' + # ) prob.set_val(av.Aircraft.Engine.Gearbox.GEAR_RATIO, 12.6, units=None) prob.set_val(av.Aircraft.Engine.Gearbox.EFFICIENCY, 0.98, units=None) @@ -65,17 +65,17 @@ def test_gearbox_mission(self): shaft_power = prob.get_val(av.Dynamic.Vehicle.Propulsion.SHAFT_POWER + '_out', 'hp') rpm = prob.get_val(av.Dynamic.Vehicle.Propulsion.RPM + '_out', 'rpm') torque = prob.get_val(av.Dynamic.Vehicle.Propulsion.TORQUE + '_out', 'ft*lbf') - shaft_power_max = prob.get_val(av.Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out', 'hp') + # shaft_power_max = prob.get_val(av.Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out', 'hp') shaft_power_expected = [98.0, 196.0, 367.5] rpm_expected = [396.82539683, 491.66666667, 491.66666667] torque_expected = [1297.0620786, 2093.72409783, 3925.73268342] - shaft_power_max_expected = [367.5, 294.0, 367.5] + # shaft_power_max_expected = [367.5, 294.0, 367.5] assert_near_equal(shaft_power, shaft_power_expected, tolerance=1e-6) assert_near_equal(rpm, rpm_expected, tolerance=1e-6) assert_near_equal(torque, torque_expected, tolerance=1e-6) - assert_near_equal(shaft_power_max, shaft_power_max_expected, tolerance=1e-6) + # assert_near_equal(shaft_power_max, shaft_power_max_expected, tolerance=1e-6) partial_data = prob.check_partials(out_stream=None, method='cs') assert_check_partials(partial_data, atol=1e-9, rtol=1e-9) diff --git a/aviary/subsystems/propulsion/propeller/propeller_builder.py b/aviary/subsystems/propulsion/propeller/propeller_builder.py index 6a9bbc139..625ba0cdf 100644 --- a/aviary/subsystems/propulsion/propeller/propeller_builder.py +++ b/aviary/subsystems/propulsion/propeller/propeller_builder.py @@ -37,10 +37,30 @@ def build_mission(self, num_nodes, aviary_inputs): num_nodes=num_nodes, aviary_options=aviary_inputs, propeller_data=self.data ) + def mission_inputs(self, **kwargs): + inputs = [ + Dynamic.Atmosphere.MACH, + Aircraft.Engine.Propeller.TIP_SPEED_MAX, + Aircraft.Engine.Propeller.TIP_MACH_MAX, + Dynamic.Atmosphere.DENSITY, + Dynamic.Mission.VELOCITY, + Aircraft.Engine.Propeller.DIAMETER, + Aircraft.Engine.Propeller.ACTIVITY_FACTOR, + Aircraft.Engine.Propeller.INTEGRATED_LIFT_COEFFICIENT, + Aircraft.Nacelle.AVG_DIAMETER, + Dynamic.Atmosphere.SPEED_OF_SOUND, + Dynamic.Vehicle.Propulsion.RPM, + ] + return inputs + + def mission_outputs(self, **kwargs): + outputs = [Dynamic.Vehicle.Propulsion.THRUST] + return outputs + def get_design_vars(self): """ Design vars are only tested to see if they exist in pre_mission - Returns a dictionary of design variables for the gearbox subsystem, where the keys are the + Returns a dictionary of design variables for the propeller subsystem, where the keys are the names of the design variables, and the values are dictionaries that contain the units for the design variable, the lower and upper bounds for the design variable, and any additional keyword arguments required by OpenMDAO for the design variable. @@ -117,13 +137,13 @@ def get_parameters(self, aviary_inputs=None, phase_info=None): return parameters def get_mass_names(self): - return [Aircraft.Engine.Gearbox.MASS] + return [] def get_timeseries(self): return [ - Dynamic.Vehicle.Propulsion.SHAFT_POWER + '_out', - Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out', - Dynamic.Vehicle.Propulsion.RPM + '_out', - Dynamic.Vehicle.Propulsion.TORQUE + '_out', - Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL, + Dynamic.Vehicle.Propulsion.SHAFT_POWER, + # Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out', + Dynamic.Vehicle.Propulsion.RPM, + Dynamic.Vehicle.Propulsion.TORQUE, + # Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL, ] diff --git a/aviary/subsystems/propulsion/propulsion_mission.py b/aviary/subsystems/propulsion/propulsion_mission.py index 0414d87cb..626a89de4 100644 --- a/aviary/subsystems/propulsion/propulsion_mission.py +++ b/aviary/subsystems/propulsion/propulsion_mission.py @@ -3,11 +3,14 @@ import numpy as np import openmdao.api as om +from aviary.subsystems.propulsion.utils import EngineModelVariables, max_variables from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.functions import add_aviary_option from aviary.variable_info.variables import Aircraft, Dynamic, Settings +# TODO it should be possible to completely remove need for EngineModelVariables in this file if +# max_variables is modified in this file, where the keys are changed to their Enum values class PropulsionMission(om.Group): """ Group that tracks all engine models used during mission analysis. Accounts for @@ -41,13 +44,32 @@ def setup(self): engine_options = self.options['engine_options'] num_engine_type = len(engine_models) + # Create IndepVarComp to pass maximum throttle to max thrust interpolator + # Only needed if any engine doesn't compute max thrust + if any(not engine.compute_max_values for engine in engine_models): + fixed_throttles = om.IndepVarComp() + fixed_throttles.add_output( + 'throttle_max', + val=np.ones(nn), + units='unitless', + desc='Engine maximum throttle', + ) + fixed_throttles.add_output( + 'hybrid_throttle_max', + val=np.ones(nn), + units='unitless', + desc='Engine maximum hybrid throttle', + ) + self.add_subsystem('max_throttles', fixed_throttles, promotes_outputs=['*']) + if num_engine_type > 1: # We need a component to add parameters to problem. Dymos can't find it when # it is already sliced across several components. # TODO is this problem fixable from dymos end (introspection includes parameters)? # create set of params - # TODO get_parameters() should have access to aviary options + phase info + # TODO get_parameters() should have access to aviary options + phase info here, pass + # via options into this component from subsystem builder? param_dict = {} # save parameters for use in configure() parameters = self.parameters = set() @@ -57,10 +79,10 @@ def setup(self): parameters.update(param_dict.keys()) - # if params exist, create execcomp, fill with placeholder equations - if len(parameters) != 0: - comp = om.ExecComp(has_diag_partials=True) + comp = om.ExecComp(has_diag_partials=True) + # if params exist, fill with placeholder equations + if len(parameters) != 0: for i, param in enumerate(parameters): # try to find units information try: @@ -92,11 +114,31 @@ def setup(self): kwargs = {} if engine.name in engine_options: kwargs = engine_options[engine.name] - self.add_subsystem( - engine.name, - subsys=engine.build_mission(num_nodes=nn, aviary_inputs=options, **kwargs), - promotes_inputs=['*'], - ) + if engine.compute_max_values: + engine_model = engine.build_mission( + num_nodes=nn, aviary_inputs=options, **kwargs + ) + else: + base_comp = engine.build_mission(num_nodes=nn, aviary_inputs=options, **kwargs) + engine_model = om.Group() + + max_comp = engine.build_mission(num_nodes=nn, aviary_inputs=options, **kwargs) + + input_aliases = [(Dynamic.Vehicle.Propulsion.THROTTLE, 'throttle_max')] + if engine.use_hybrid_throttle: + input_aliases.append( + (Dynamic.Vehicle.Propulsion.HYBRID_THROTTLE, 'hybrid_throttle_max') + ) + + engine_model.add_subsystem( + 'max', + max_comp, + promotes_inputs=input_aliases, + ) + + engine_model.add_subsystem('base', base_comp) + + self.add_subsystem(engine.name, subsys=engine_model, promotes_inputs=['*']) # split vectorized throttles and connect to the correct engine model self.promotes( @@ -105,6 +147,15 @@ def setup(self): src_indices=om.slicer[:, i], ) + # NOTE if only some engine use hybrid throttle, source vector will have an + # index for that engine that is unused, will this confuse optimizer? + if engine.use_hybrid_throttle: + self.promotes( + engine.name, + inputs=[Dynamic.Vehicle.Propulsion.HYBRID_THROTTLE], + src_indices=om.slicer[:, i], + ) + # loop through params and slice as needed params = engine.get_parameters() for param in params: @@ -114,24 +165,34 @@ def setup(self): src_indices=om.slicer[i], ) - # TODO if only some engine use hybrid throttle, source vector will have an - # index for that engine that is unused, will this confuse optimizer? - if engine.use_hybrid_throttle: - self.promotes( - engine.name, - inputs=[Dynamic.Vehicle.Propulsion.HYBRID_THROTTLE], - src_indices=om.slicer[:, i], - ) else: engine = engine_models[0] kwargs = {} if engine.name in engine_options: kwargs = engine_options[engine.name] - self.add_subsystem( - engine.name, - subsys=engine.build_mission(num_nodes=nn, aviary_inputs=options, **kwargs), - promotes_inputs=['*'], - ) + if engine.compute_max_values: + engine_model = engine.build_mission(num_nodes=nn, aviary_inputs=options, **kwargs) + else: + base_comp = engine.build_mission(num_nodes=nn, aviary_inputs=options, **kwargs) + engine_model = om.Group() + + max_comp = engine.build_mission(num_nodes=nn, aviary_inputs=options, **kwargs) + + input_aliases = [(Dynamic.Vehicle.Propulsion.THROTTLE, 'throttle_max')] + if engine.use_hybrid_throttle: + input_aliases.append( + (Dynamic.Vehicle.Propulsion.HYBRID_THROTTLE, 'hybrid_throttle_max') + ) + + engine_model.add_subsystem( + 'max', + max_comp, + promotes_inputs=input_aliases, + ) + + engine_model.add_subsystem('base', base_comp) + + self.add_subsystem(engine.name, subsys=engine_model, promotes_inputs=['*']) self.promotes(engine.name, inputs=[Dynamic.Vehicle.Propulsion.THROTTLE]) @@ -192,6 +253,16 @@ def setup(self): axis=1, units='hp', ) + perf_mux.add_var( + Dynamic.Vehicle.Propulsion.RPM, + val=0, + shape=(nn,), + axis=1, + units='rpm', + ) + perf_mux.add_var( + Dynamic.Vehicle.Propulsion.PROPELLER_TIP_SPEED, val=0, shape=(nn,), axis=1, units='ft/s' + ) # perf_mux.add_var( # 'exit_area_unscaled', # shape=(nn,), @@ -212,7 +283,13 @@ def configure(self): # Handle checking each EngineModel for compatible outputs with # vectorize_performance component and connecting those outputs - # TODO this list shouldn't be hardcoded so it can be extended by users + # Can't make this list dynamic this way because it must perfectly match the + # "vectorize_performance" comp, and we don't have enough info to dynamically filter inputs + # from the list. Can only have engine outputs sent to "vectorize_performance" + + # from aviary.utils.test_utils.variable_test import get_names_from_hierarchy + # supported_outputs = get_names_from_hierarchy(Dynamic.Vehicle.Propulsion) + supported_outputs = [ Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN, Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE, @@ -222,6 +299,8 @@ def configure(self): Dynamic.Vehicle.Propulsion.TEMPERATURE_T4, Dynamic.Vehicle.Propulsion.THRUST, Dynamic.Vehicle.Propulsion.THRUST_MAX, + Dynamic.Vehicle.Propulsion.RPM, + Dynamic.Vehicle.Propulsion.PROPELLER_TIP_SPEED, ] engine_models = self.options['engine_models'] @@ -237,43 +316,158 @@ def configure(self): comp_list = [self._get_subsystem(engine) for engine in engine_names] - # dictionaries of outputs for each engine in prop mission + # dictionaries of outputs for each engine in mission + input_dict = {} output_dict = {} - # Dictionary of all unique inputs/outputs from all new components, keys are - # units for each var - unique_outputs = {} - # idx to be used for slicing inputs in next round of improvements + # TODO Use mission_inputs(), mission_outputs() to promote variables from engines. Use idx + # for slicing inputs, re-use vectorization & mux comp for engine-vectorized vars? for idx, comp in enumerate(comp_list): - # identify outputs to connect to muxcomp - comp_outputs = comp.list_outputs( - return_format='dict', units=True, out_stream=out_stream, all_procs=True - ) - # grab only outputs that have been promoted out of component - promoted_outputs = [ - key for key in comp_outputs if '.' not in comp_outputs[key]['prom_name'] - ] - output_dict[comp.name] = dict( - (comp_outputs[key]['prom_name'], comp_outputs[key]) for key in promoted_outputs - ) - unique_outputs.update( - [ - ( - comp_outputs[key]['prom_name'], - comp_outputs[key]['units'], + engine = engine_models[idx] + + if not engine.compute_max_values: + base_comp = comp._get_subsystem('base') + max_comp = comp._get_subsystem('max') + + # Get all inputs and outputs from components # + + # inputs are for later promotion + base_inputs = base_comp.list_inputs( + return_format='dict', units=False, out_stream=out_stream, all_procs=True + ) + max_inputs = max_comp.list_inputs( + return_format='dict', units=False, out_stream=out_stream, all_procs=True + ) + + # outputs to connect to muxcomp + base_outputs = base_comp.list_outputs( + return_format='dict', units=False, out_stream=out_stream, all_procs=True + ) + max_outputs = max_comp.list_outputs( + return_format='dict', units=False, out_stream=out_stream, all_procs=True + ) + + # Build dictionaries of unique, top-level promoted inputs/outputs per component # + + base_promoted_inputs = [ + key for key in base_inputs if '.' not in max_inputs[key]['prom_name'] + ] + input_dict[comp.name] = dict( + (base_inputs[key]['prom_name'], base_inputs[key]) + for key in base_promoted_inputs + ) + + max_promoted_inputs = [ + key for key in max_inputs if '.' not in max_inputs[key]['prom_name'] + ] + input_dict[comp.name].update( + dict( + (max_inputs[key]['prom_name'], max_inputs[key]) + for key in max_promoted_inputs ) - for key in promoted_outputs + ) + + # don't grab "max" variables that might be floating around in the base component + base_promoted_outputs = [ + key + for key in base_outputs + if '.' not in base_outputs[key]['prom_name'] + and max_outputs[key]['prom_name'] not in [var for var in max_variables.values()] ] - ) + output_dict[comp.name] = dict( + (base_outputs[key]['prom_name'], base_outputs[key]) + for key in base_promoted_outputs + ) + + # only grab variables from max component that have a "max" counterpart to alias + max_promoted_outputs = [ + key + for key in max_outputs + if '.' not in max_outputs[key]['prom_name'] + and max_outputs[key]['prom_name'] in [var.value for var in max_variables.keys()] + ] + output_dict[comp.name].update( + dict( + ( + max_variables[EngineModelVariables(max_outputs[key]['prom_name'])], + max_outputs[key], + ) + for key in max_promoted_outputs + ) + ) + + # Promote the correct inputs and outputs from each component to top of group # + + # first promote the correct outputs for each component to top of group, save list + if base_promoted_outputs: + promoted_outputs = list( + set([max_outputs[key]['prom_name'] for key in base_promoted_outputs]) + ) + comp.promotes('base', outputs=promoted_outputs) + else: + promoted_outputs = [] + if max_promoted_outputs: + aliased_outputs = [] + for key in max_promoted_outputs: + aliased_outputs.append( + ( + max_outputs[key]['prom_name'], + max_variables[EngineModelVariables(max_outputs[key]['prom_name'])], + ) + ) + comp.promotes('max', outputs=aliased_outputs) + + # Promote all inputs that aren't also outputs of this component - this happens when + # a component is a group that promotes an output from an interior component to the + # top level so another component inside can receive it as an input + if base_promoted_inputs: + promoted_inputs = list( + set( + [ + base_inputs[key]['prom_name'] + for key in base_promoted_inputs + if base_inputs[key]['prom_name'] not in promoted_outputs + ] + ) + ) + comp.promotes('base', inputs=promoted_inputs) + if max_promoted_inputs: + # Purposefully using promoted_outputs here, as internal input/output matches + # won't appear in aliased_outputs and components are otherwise perfect matches + # Throttles are already aliased, so do not promote it here + promoted_inputs = list( + set( + [ + max_inputs[key]['prom_name'] + for key in max_promoted_inputs + if max_inputs[key]['prom_name'] not in promoted_outputs + and max_inputs[key]['prom_name'] + is not Dynamic.Vehicle.Propulsion.THROTTLE + and max_inputs[key]['prom_name'] + is not Dynamic.Vehicle.Propulsion.HYBRID_THROTTLE + ] + ) + ) + comp.promotes('max', inputs=promoted_inputs) + + else: + # identify outputs to connect to muxcomp + comp_outputs = comp.list_outputs( + return_format='dict', units=True, out_stream=out_stream, all_procs=True + ) + # grab outputs that have been promoted to top of system + promoted_outputs = [ + key for key in comp_outputs if '.' not in comp_outputs[key]['prom_name'] + ] + output_dict[comp.name] = dict( + (comp_outputs[key]['prom_name'], comp_outputs[key]) for key in promoted_outputs + ) - # add variables to the mux component and make connections to individual - # component outputs - # if num_engine_type > 1: - for output in unique_outputs: - if output in supported_outputs: - # self.vectorize_performance.add_var(output, units=unique_outputs[output]) + # add variables to the mux component and make connections to individual component outputs + for i, comp in enumerate(output_dict): + for output in output_dict[comp]: # promote/alias outputs for each comp that has relevant outputs - for i, comp in enumerate(output_dict): + if output in supported_outputs: if output in output_dict[comp]: # if this component provides the output, connect it to the correct mux input self.connect( @@ -294,8 +488,7 @@ def configure(self): # input_dict = engine_comp.list_inputs( # return_format='dict', units=True, out_stream=None, all_procs=True # ) - # # TODO this catches not fully promoted variables are caught - is this - # # wanted? + # # TODO this catches not fully promoted variables - is this wanted? # input_list = list( # set( # input_dict[key]['prom_name'] diff --git a/aviary/subsystems/propulsion/test/test_custom_engine_model.py b/aviary/subsystems/propulsion/test/test_custom_engine_model.py index b9e1db744..6de77ffa7 100644 --- a/aviary/subsystems/propulsion/test/test_custom_engine_model.py +++ b/aviary/subsystems/propulsion/test/test_custom_engine_model.py @@ -60,12 +60,12 @@ def setup(self): units='lbf', desc='Current net thrust produced (scaled)', ) - self.add_output( - Dynamic.Vehicle.Propulsion.THRUST_MAX, - shape=nn, - units='lbf', - desc='Current net thrust produced (scaled)', - ) + # self.add_output( + # Dynamic.Vehicle.Propulsion.THRUST_MAX, + # shape=nn, + # units='lbf', + # desc='Current net thrust produced (scaled)', + # ) self.add_output( Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE, shape=nn, @@ -100,7 +100,7 @@ def compute(self, inputs, outputs): # calculate outputs outputs[Dynamic.Vehicle.Propulsion.THRUST] = 10000.0 * combined_throttle - outputs[Dynamic.Vehicle.Propulsion.THRUST_MAX] = 10000.0 + # outputs[Dynamic.Vehicle.Propulsion.THRUST_MAX] = 10000.0 outputs[Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE] = -10.0 * combined_throttle outputs[Dynamic.Vehicle.Propulsion.TEMPERATURE_T4] = 2800.0 diff --git a/aviary/subsystems/propulsion/test/test_propulsion_mission.py b/aviary/subsystems/propulsion/test/test_propulsion_mission.py index 1e916a85e..cfad058bd 100644 --- a/aviary/subsystems/propulsion/test/test_propulsion_mission.py +++ b/aviary/subsystems/propulsion/test/test_propulsion_mission.py @@ -4,19 +4,25 @@ import openmdao import openmdao.api as om from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal +from openmdao.utils.testing_utils import use_tempdirs from packaging import version +from aviary.subsystems.atmosphere.atmosphere import Atmosphere from aviary.subsystems.propulsion.engine_deck import EngineDeck from aviary.subsystems.propulsion.propulsion_mission import PropulsionMission, PropulsionSum +from aviary.subsystems.propulsion.test.test_custom_engine_model import SimpleTestEngine +from aviary.subsystems.propulsion.turboprop_model import TurbopropModel from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import get_path from aviary.utils.preprocessors import preprocess_propulsion from aviary.validation_cases.validation_tests import get_flops_inputs +from aviary.variable_info.enums import SpeedType from aviary.variable_info.functions import setup_model_options from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings +@use_tempdirs class PropulsionMissionTest(unittest.TestCase): def setUp(self): self.prob = om.Problem() @@ -49,10 +55,6 @@ def test_case_1(self): engine = EngineDeck(options=options) preprocess_propulsion(options, [engine]) - self.prob.model = PropulsionMission( - num_nodes=nn, aviary_options=options, engine_models=[engine] - ) - IVC = om.IndepVarComp(Dynamic.Atmosphere.MACH, np.linspace(0, 0.8, nn), units='unitless') IVC.add_output(Dynamic.Mission.ALTITUDE, np.linspace(0, 40000, nn), units='ft') IVC.add_output( @@ -62,6 +64,12 @@ def test_case_1(self): ) self.prob.model.add_subsystem('IVC', IVC, promotes=['*']) + self.prob.model.add_subsystem( + 'propulsion', + PropulsionMission(num_nodes=nn, aviary_options=options, engine_models=[engine]), + promotes=['*'], + ) + setup_model_options(self.prob, options) self.prob.setup(force_alloc_complex=True) @@ -206,26 +214,20 @@ def test_case_multiengine(self): preprocess_propulsion(options, engine_models=engine_models) model = self.prob.model - prop = PropulsionMission( - num_nodes=20, - aviary_options=options, - engine_models=engine_models, - ) - model.add_subsystem('propulsion', prop, promotes=['*']) - self.prob.model.add_subsystem( + model.add_subsystem( Dynamic.Atmosphere.MACH, om.IndepVarComp(Dynamic.Atmosphere.MACH, np.linspace(0, 0.85, nn), units='unitless'), promotes=['*'], ) - self.prob.model.add_subsystem( + model.add_subsystem( Dynamic.Mission.ALTITUDE, om.IndepVarComp(Dynamic.Mission.ALTITUDE, np.linspace(0, 40000, nn), units='ft'), promotes=['*'], ) throttle = np.linspace(1.0, 0.6, nn) - self.prob.model.add_subsystem( + model.add_subsystem( Dynamic.Vehicle.Propulsion.THROTTLE, om.IndepVarComp( Dynamic.Vehicle.Propulsion.THROTTLE, @@ -235,6 +237,13 @@ def test_case_multiengine(self): promotes=['*'], ) + prop = PropulsionMission( + num_nodes=nn, + aviary_options=options, + engine_models=engine_models, + ) + model.add_subsystem('propulsion', prop, promotes=['*']) + setup_model_options(self.prob, options, engine_models=engine_models) self.prob.setup(force_alloc_complex=True) @@ -284,6 +293,215 @@ def test_case_multiengine(self): partial_data = self.prob.check_partials(out_stream=None, method='cs') assert_check_partials(partial_data, atol=1e-10, rtol=1e-10) + def test_case_no_max_thrust(self): + # replaces the engine model with a fake one that does not compute maximum thrust + nn = 5 + + options = get_flops_inputs('LargeSingleAisle2FLOPS') + options.set_val(Settings.VERBOSITY, 0) + + engine = SimpleTestEngine() + preprocess_propulsion(options, engine_models=[engine]) + + model = self.prob.model + + model.add_subsystem( + Dynamic.Atmosphere.MACH, + om.IndepVarComp(Dynamic.Atmosphere.MACH, np.linspace(0, 0.85, nn), units='unitless'), + promotes=['*'], + ) + + model.add_subsystem( + Dynamic.Mission.ALTITUDE, + om.IndepVarComp(Dynamic.Mission.ALTITUDE, np.linspace(0, 40000, nn), units='ft'), + promotes=['*'], + ) + + model.add_subsystem( + Dynamic.Vehicle.Propulsion.THROTTLE, + om.IndepVarComp( + Dynamic.Vehicle.Propulsion.THROTTLE, + np.linspace(1.0, 0.6, nn), + units='unitless', + ), + promotes=['*'], + ) + + prop = PropulsionMission( + num_nodes=nn, + aviary_options=options, + engine_models=[engine], + ) + model.add_subsystem('propulsion', prop, promotes=['*']) + + setup_model_options(self.prob, options, engine_models=[engine]) + + self.prob.setup(force_alloc_complex=True) + + self.prob.run_model() + + thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST_TOTAL, units='lbf') + max_thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST_MAX_TOTAL, units='lbf') + + expected_thrust = np.array([40000, 38000, 36000, 34000, 32000]) + + expected_max_thrust = np.array([40000, 40000, 40000, 40000, 40000]) + + assert_near_equal(thrust, expected_thrust, tolerance=1e-10) + assert_near_equal(max_thrust, expected_max_thrust, tolerance=1e-10) + + def test_case_no_max_thrust_multiengine(self): + # Takes the multiengine test case and replaces an engine with one that does not compute max + # thrust + nn = 5 + + options = get_flops_inputs('LargeSingleAisle2FLOPS') + options.set_val(Settings.VERBOSITY, 0) + options.set_val(Aircraft.Engine.GLOBAL_THROTTLE, True) + + engine = build_engine_deck(options) + + engine2 = SimpleTestEngine() + engine2.name = 'engine2' + engine_models = [engine, engine2] + preprocess_propulsion(options, engine_models=engine_models) + + model = self.prob.model + + model.add_subsystem( + Dynamic.Atmosphere.MACH, + om.IndepVarComp(Dynamic.Atmosphere.MACH, np.linspace(0, 0.85, nn), units='unitless'), + promotes=['*'], + ) + + model.add_subsystem( + Dynamic.Mission.ALTITUDE, + om.IndepVarComp(Dynamic.Mission.ALTITUDE, np.linspace(0, 40000, nn), units='ft'), + promotes=['*'], + ) + throttle = np.linspace(1.0, 0.6, nn) + model.add_subsystem( + Dynamic.Vehicle.Propulsion.THROTTLE, + om.IndepVarComp( + Dynamic.Vehicle.Propulsion.THROTTLE, + np.vstack((throttle, throttle)).transpose(), + units='unitless', + ), + promotes=['*'], + ) + + prop = PropulsionMission( + num_nodes=nn, + aviary_options=options, + engine_models=engine_models, + ) + model.add_subsystem('propulsion', prop, promotes=['*']) + + setup_model_options(self.prob, options, engine_models=engine_models) + + self.prob.setup(force_alloc_complex=True) + self.prob.set_val(Aircraft.Engine.SCALE_FACTOR, [0.975, 0.975], units='unitless') + + self.prob.run_model() + + thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST_TOTAL, units='lbf') + max_thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST_MAX_TOTAL, units='lbf') + + expected_thrust = np.array( + [91795.1077032, 66538.67800773, 49882.20467434, 42078.10820403, 34897.24086484] + ) + expected_max_thrust = np.array( + [91795.1077032, 75448.88753979, 62863.25377679, 56217.19584678, 50659.15299758] + ) + + assert_near_equal(thrust, expected_thrust, tolerance=1e-10) + assert_near_equal(max_thrust, expected_max_thrust, tolerance=1e-10) + + def test_case_no_max_thrust_turboprop(self): + # replaces the engine with a turboprop + nn = 5 + + options = get_flops_inputs('LargeSingleAisle2FLOPS') + options.set_val(Settings.VERBOSITY, 0) + + options.set_val(Aircraft.Engine.FIXED_RPM, 1455.13090827, units='rpm') + options.set_val( + Aircraft.Engine.Propeller.COMPUTE_INSTALLATION_LOSS, + val=True, + units='unitless', + ) + options.set_val(Aircraft.Engine.Propeller.NUM_BLADES, val=4, units='unitless') + + # turboprop using turboshaft engine deck and hamilton standard propeller model + engine = TurbopropModel(options=options, shaft_power_model=None, propeller_model=None) + preprocess_propulsion(options, [engine]) + preprocess_propulsion(options, engine_models=[engine]) + + model = self.prob.model + + model.add_subsystem( + Dynamic.Atmosphere.MACH, + om.IndepVarComp(Dynamic.Atmosphere.MACH, np.linspace(0, 0.55, nn), units='unitless'), + promotes=['*'], + ) + + model.add_subsystem( + Dynamic.Mission.ALTITUDE, + om.IndepVarComp(Dynamic.Mission.ALTITUDE, np.linspace(0, 20000, nn), units='ft'), + promotes=['*'], + ) + + model.add_subsystem( + 'atmosphere', Atmosphere(num_nodes=nn, input_speed_type=SpeedType.MACH), promotes=['*'] + ) + + model.add_subsystem( + Dynamic.Vehicle.Propulsion.THROTTLE, + om.IndepVarComp( + Dynamic.Vehicle.Propulsion.THROTTLE, + np.linspace(1.0, 0.6, nn), + units='unitless', + ), + promotes=['*'], + ) + + prop = PropulsionMission( + num_nodes=nn, + aviary_options=options, + engine_models=[engine], + ) + model.add_subsystem('propulsion', prop, promotes=['*']) + + setup_model_options(self.prob, options, engine_models=[engine]) + + self.prob.setup(force_alloc_complex=True) + + self.prob.set_val(Aircraft.Engine.Propeller.DIAMETER, 10.5, units='ft') + self.prob.set_val(Aircraft.Engine.Propeller.ACTIVITY_FACTOR, 114.0, units='unitless') + self.prob.set_val( + Aircraft.Engine.Propeller.INTEGRATED_LIFT_COEFFICIENT, 0.5, units='unitless' + ) + self.prob.set_val(Aircraft.Engine.Propeller.TIP_SPEED_MAX, 800, units='ft/s') + + self.prob.run_model() + + thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST_TOTAL, units='lbf') + max_thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST_MAX_TOTAL, units='lbf') + + expected_thrust = np.array( + [42630.03616545, 34593.86400241, 20003.65216215, 12039.90789131, 8385.52777847] + ) + + expected_max_thrust = np.array( + [42630.03616545, 41028.46387, 29561.74, 22590.025, 20411.435] + ) + + assert_near_equal(thrust, expected_thrust, tolerance=1e-10) + assert_near_equal(max_thrust, expected_max_thrust, tolerance=1e-10) + if __name__ == '__main__': unittest.main() + # test = PropulsionMissionTest() + # test.setUp() + # test.test_case_no_max_thrust_turboprop() diff --git a/aviary/subsystems/propulsion/test/test_turboprop_model.py b/aviary/subsystems/propulsion/test/test_turboprop_model.py index 036eb55e8..17d2434dc 100644 --- a/aviary/subsystems/propulsion/test/test_turboprop_model.py +++ b/aviary/subsystems/propulsion/test/test_turboprop_model.py @@ -95,9 +95,9 @@ def prepare_model( def get_results(self, point_names=None, display_results=False): shp = self.prob.get_val(Dynamic.Vehicle.Propulsion.SHAFT_POWER, units='hp') total_thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST, units='lbf') - prop_thrust = self.prob.get_val('propeller_thrust', units='lbf') - tailpipe_thrust = self.prob.get_val('turboshaft_thrust', units='lbf') - max_thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST_MAX, units='lbf') + prop_thrust = self.prob.get_val('thrust_adder.propeller_thrust', units='lbf') + tailpipe_thrust = self.prob.get_val('thrust_adder.turboshaft_thrust', units='lbf') + # max_thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST_MAX, units='lbf') fuel_flow = self.prob.get_val( Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE, units='lbm/h' ) @@ -110,7 +110,7 @@ def get_results(self, point_names=None, display_results=False): tailpipe_thrust[n], prop_thrust[n], total_thrust[n], - max_thrust[n], + # max_thrust[n], fuel_flow[n], ) ) @@ -128,7 +128,7 @@ def test_case_1(self): 37.699999999999996, 610.3580810058977, 648.0580810058977, - 4184.157517016291, + # 4184.157517016291, -195.79999999999995, ), ( @@ -136,7 +136,7 @@ def test_case_1(self): 136.29999999999967, 4047.857517016292, 4184.157517016291, - 4184.157517016291, + # 4184.157517016291, -643.9999999999998, ), ( @@ -144,7 +144,7 @@ def test_case_1(self): 21.30000000000001, 558.2951237599805, 579.5951237599804, - 579.5951237599804, + # 579.5951237599804, -839.7000000000685, ), ] @@ -188,13 +188,20 @@ def test_case_2(self): filename = get_path('models/engines/turboshaft_1120hp.csv') test_points = [(0.001, 0, 0), (0, 0, 1), (0.6, 25000, 1)] truth_vals = [ - (111.99470752, 37.507376, 610.74316698, 648.25054298, 4174.71028286, -195.78762), + ( + 111.99470752, + 37.507376, + 610.74316698, + 648.25054298, + # 4174.71028286, + -195.78762, + ), ( 1119.992378878607, 136.29999999999967, 4047.857517016292, 4184.157517016291, - 4184.157517016291, + # 4184.157517016291, -643.9999999999998, ), ( @@ -202,7 +209,7 @@ def test_case_2(self): 21.30000000000001, 558.2951237599805, 579.5951237599804, - 579.5951237599804, + # 579.5951237599804, -839.7000000000685, ), ] @@ -222,9 +229,9 @@ def test_case_2(self): self.prob.run_model() results = self.get_results() - assert_near_equal(results[0], truth_vals[0], tolerance=1.5e-12) - assert_near_equal(results[1], truth_vals[1], tolerance=1.5e-12) - assert_near_equal(results[2], truth_vals[2], tolerance=1.5e-12) + assert_near_equal(results[0], truth_vals[0], tolerance=1e-11) + assert_near_equal(results[1], truth_vals[1], tolerance=1e-11) + assert_near_equal(results[2], truth_vals[2], tolerance=1e-11) partial_data = self.prob.check_partials(out_stream=None, form='central') assert_check_partials(partial_data, atol=0.15, rtol=0.15) @@ -240,7 +247,7 @@ def test_case_3(self): 0.0, 610.3580810058977, 610.3580810058977, - 4047.857517016292, + # 4047.857517016292, -195.79999999999995, ), ( @@ -248,7 +255,7 @@ def test_case_3(self): 0.0, 4047.857517016292, 4047.857517016292, - 4047.857517016292, + # 4047.857517016292, -643.9999999999998, ), ( @@ -256,7 +263,7 @@ def test_case_3(self): 0.0, 558.2951237599805, 558.2951237599805, - 558.2951237599805, + # 558.2951237599805, -839.7000000000685, ), ] @@ -312,7 +319,7 @@ def test_electroprop(self): shp = self.prob.get_val(Dynamic.Vehicle.Propulsion.SHAFT_POWER, units='hp') total_thrust = self.prob.get_val(Dynamic.Vehicle.Propulsion.THRUST, units='lbf') - prop_thrust = self.prob.get_val('propeller_thrust', units='lbf') + prop_thrust = self.prob.get_val('thrust_adder.propeller_thrust', units='lbf') electric_power = self.prob.get_val(Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN, units='kW') assert_near_equal(shp, shp_expected, tolerance=1e-8) diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index 01a16b85b..fa9b0491b 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -16,8 +16,8 @@ class TurbopropModel(EngineModel): """ - EngineModel that combines a model for shaft power generation (default is EngineDeck) - and a model for propeller performance (default is Hamilton Standard). + EngineModel that combines a model for shaft power generation (default is EngineDeck) and a model + for propeller performance (default is Hamilton Standard). Attributes ---------- @@ -26,14 +26,13 @@ class TurbopropModel(EngineModel): options : AviaryValues () Inputs and options related to engine model. shaft_power_model : SubsystemBuilder () - Subsystem builder for the shaft power generating component. If None, an - EngineDeck built using provided options is used. + Subsystem builder for the shaft power generating component. If None, an EngineDeck built + using provided options is used. propeller_model : SubsystemBuilder () - Subsystem builder for the propeller. If None, the Hamilton Standard methodology - is used to model the propeller. + Subsystem builder for the propeller. If None, the Hamilton Standard methodology is used to + model the propeller. gearbox_model : SubsystemBuilder () - Subsystem builder used for the gearbox. If None, the simple gearbox model is - used. + Subsystem builder used for the gearbox. If None, the simple gearbox model is used. Methods ------- @@ -72,27 +71,43 @@ def __init__( }, ) - # TODO No reason gearbox model needs to be required. All connections can - # be handled in configure - need to figure out when user wants gearbox without - # having to directly pass builder + # TODO No reason gearbox model needs to be required. All connections can be handled in + # configure - need to figure out when user wants gearbox without having to directly + # pass builder if gearbox_model is None: - # TODO where can we bring in include_constraints? kwargs in init is an option, - # but that still requires the L2 interface + # TODO where can we bring in include_constraints? kwargs in init is an option, but that + # still requires the L2 interface self.gearbox_model = GearboxBuilder(name='gearbox', include_constraints=True) if propeller_model is None: self.propeller_model = PropellerBuilder(name='propeller') - # BUG if using both custom subsystems that happen to share a kwarg but - # need different values, this breaks + def needs_mission_solver(self, aviary_inputs): + if self.shaft_power_model is not None: + shp_solver = self.shaft_power_model.needs_mission_solver() + else: + shp_solver = False + if self.gearbox_model is not None: + gearbox_solver = self.gearbox_model.needs_mission_solver() + else: + gearbox_solver = False + if self.propeller_model is not None: + prop_solver = self.propeller_model.needs_mission_solver() + else: + prop_solver = False + mission_solver = np.any([shp_solver, gearbox_solver, prop_solver]) + return mission_solver + + # BUG if using multiple custom subsystems that happen to share a kwarg but need different values, + # this breaks - look into "nested" kwargs with separate dict per turboprop subsystem? def build_pre_mission(self, aviary_inputs, **kwargs) -> om.Group: shp_model = self.shaft_power_model propeller_model = self.propeller_model gearbox_model = self.gearbox_model turboprop_group = om.Group() - # TODO engine scaling for turboshafts requires EngineSizing to be refactored to - # accept target scaling variable as an option, skipping for now + # TODO engine scaling for turboshafts requires EngineSizing to be refactored to accept + # target scaling variable as an option, skipping for now if not isinstance(shp_model, EngineDeck): shp_model_pre_mission = shp_model.build_pre_mission(self.options, **kwargs) if shp_model_pre_mission is not None: @@ -130,15 +145,71 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs): return turboprop_group + def mission_inputs(self, **kwargs): + # NOTE what may be an input/output for an individual subsystem may not be an overall + # input/output for the TurbopropModel as a whole, commenting this out for now + # inputs = [] + # if self.shaft_power_model is not None: + # if self.shaft_power_model.name in kwargs: + # subsys_args = kwargs[self.shaft_power_model.name] + # else: + # subsys_args = {} + # inputs += self.shaft_power_model.mission_inputs(**subsys_args) + # if self.gearbox_model is not None: + # if self.gearbox_model.name in kwargs: + # subsys_args = kwargs[self.gearbox_model.name] + # else: + # subsys_args = {} + # inputs += self.gearbox_model.mission_inputs(**subsys_args) + # if self.propeller_model is not None: + # if self.propeller_model.name in kwargs: + # subsys_args = kwargs[self.propeller_model.name] + # else: + # subsys_args = {} + # inputs += self.propeller_model.mission_inputs(**subsys_args) + # return list(set(inputs)) + return [] + + def mission_outputs(self, **kwargs): + # NOTE what may be an input/output for an individual subsystem may not be an overall + # input/output for the TurbopropModel as a whole, commenting this out for now + # outputs = [] + # if self.shaft_power_model is not None: + # if self.shaft_power_model.name in kwargs: + # subsys_args = kwargs[self.shaft_power_model.name] + # else: + # subsys_args = {} + # outputs += self.shaft_power_model.mission_outputs(**subsys_args) + # if self.gearbox_model is not None: + # if self.gearbox_model.name in kwargs: + # subsys_args = kwargs[self.gearbox_model.name] + # else: + # subsys_args = {} + # outputs += self.gearbox_model.mission_outputs(**subsys_args) + # if self.propeller_model is not None: + # if self.propeller_model.name in kwargs: + # subsys_args = kwargs[self.propeller_model.name] + # else: + # subsys_args = {} + # outputs += self.propeller_model.mission_outputs(**subsys_args) + # return list(set(outputs)) + return [] + def build_post_mission(self, aviary_inputs, phase_data, phase_mission_bus_lengths, **kwargs): shp_model = self.shaft_power_model gearbox_model = self.gearbox_model propeller_model = self.propeller_model turboprop_group = om.Group() + if self.shaft_power_model.name in kwargs: + subsys_args = kwargs[self.shaft_power_model.name] + else: + subsys_args = {} + shp_model_post_mission = shp_model.build_post_mission( - aviary_inputs, phase_data, phase_mission_bus_lengths, **kwargs + aviary_inputs, phase_data, phase_mission_bus_lengths, **subsys_args ) + if shp_model_post_mission is not None: turboprop_group.add_subsystem( shp_model.name, @@ -146,12 +217,18 @@ def build_post_mission(self, aviary_inputs, phase_data, phase_mission_bus_length aviary_options=aviary_inputs, ) + if self.gearbox_model.name in kwargs: + subsys_args = kwargs[self.gearbox_model.name] + else: + subsys_args = {} + gearbox_model_post_mission = gearbox_model.build_post_mission( aviary_inputs, phase_data, phase_mission_bus_lengths, - **kwargs, + **subsys_args, ) + if gearbox_model_post_mission is not None: turboprop_group.add_subsystem( gearbox_model.name, @@ -159,12 +236,18 @@ def build_post_mission(self, aviary_inputs, phase_data, phase_mission_bus_length aviary_options=aviary_inputs, ) + if self.propeller_model.name in kwargs: + subsys_args = kwargs[self.propeller_model.name] + else: + subsys_args = {} + propeller_model_post_mission = propeller_model.build_post_mission( aviary_inputs, phase_data, phase_mission_bus_lengths, - **kwargs, + **subsys_args, ) + if propeller_model_post_mission is not None: turboprop_group.add_subsystem( propeller_model.name, @@ -174,7 +257,7 @@ def build_post_mission(self, aviary_inputs, phase_data, phase_mission_bus_length return turboprop_group - def get_parameters(self): + def get_parameters(self, **kwargs): params = super().get_parameters() # calls from EngineModel if self.shaft_power_model is not None: params.update(self.shaft_power_model.get_parameters()) @@ -207,20 +290,17 @@ def initialize(self): self.options.declare('aviary_inputs', desc='aviary inputs for turboprop mission model') def setup(self): - # All promotions for configurable components in this group are handled during - # configure() + # All promotions for configurable components in this group are handled during configure() - # save num_nodes for use in configure() - self.num_nodes = num_nodes = self.options['num_nodes'] + num_nodes = self.options['num_nodes'] shp_model = self.options['shaft_power_model'] propeller_model = self.options['propeller_model'] gearbox_model = self.options['gearbox_model'] kwargs = self.options['kwargs'] - # save aviary_inputs for use in configure() - self.aviary_inputs = aviary_inputs = self.options['aviary_inputs'] + aviary_inputs = self.options['aviary_inputs'] - # NOTE: this subsystem is a empty component that has fixed RPM added as an output - # in configure() if provided in aviary_inputs + # NOTE this subsystem is a empty component that has fixed RPM added as an output in + # configure() if provided in aviary_inputs self.add_subsystem('fixed_rpm_source', subsys=om.IndepVarComp()) # Shaft Power Model @@ -250,86 +330,23 @@ def setup(self): except (AttributeError, KeyError): propeller_kwargs = {} - propeller_group = om.Group() + # propeller_group = om.Group() propeller_model_mission = propeller_model.build_mission( num_nodes, aviary_inputs, **propeller_kwargs ) if isinstance(propeller_model, PropellerBuilder): # use the Hamilton Standard model - # only promote top-level inputs to avoid conflicts with max group - prop_inputs = [ - Dynamic.Atmosphere.MACH, - Aircraft.Engine.Propeller.TIP_SPEED_MAX, - Aircraft.Engine.Propeller.TIP_MACH_MAX, - Dynamic.Atmosphere.DENSITY, - Dynamic.Mission.VELOCITY, - Aircraft.Engine.Propeller.DIAMETER, - Aircraft.Engine.Propeller.ACTIVITY_FACTOR, - Aircraft.Engine.Propeller.INTEGRATED_LIFT_COEFFICIENT, - Aircraft.Nacelle.AVG_DIAMETER, - Dynamic.Atmosphere.SPEED_OF_SOUND, - Dynamic.Vehicle.Propulsion.RPM, - ] try: propeller_kwargs = kwargs['hamilton_standard'] except KeyError: propeller_kwargs = {} - propeller_group.add_subsystem( - 'propeller_model_base', - propeller_model_mission, - promotes=['*'], - ) - - propeller_model_mission_max = propeller_model.build_mission( - num_nodes, aviary_inputs, **propeller_kwargs - ) - propeller_group.add_subsystem( - 'propeller_model_max', - propeller_model_mission_max, - promotes_inputs=[ - *prop_inputs, - ( - Dynamic.Vehicle.Propulsion.SHAFT_POWER, - Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX, - ), - ], - promotes_outputs=[ - (Dynamic.Vehicle.Propulsion.THRUST, Dynamic.Vehicle.Propulsion.THRUST_MAX) - ], - ) - - self.add_subsystem(propeller_model.name, propeller_group) + self.add_subsystem(propeller_model.name, propeller_model_mission) else: if propeller_model_mission is not None: - propeller_group.add_subsystem( - propeller_model.name + '_base', - subsys=propeller_model_mission, - promotes_inputs=['*'], - promotes_outputs=[Dynamic.Vehicle.Propulsion.THRUST], - ) - - propeller_model_mission_max = propeller_model.build_mission( - num_nodes, aviary_inputs, **propeller_kwargs - ) - propeller_group.add_subsystem( - propeller_model.name + '_max', - subsys=propeller_model_mission_max, - promotes_inputs=[ - '*', - ( - Dynamic.Vehicle.Propulsion.SHAFT_POWER, - Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX, - ), - ], - promotes_outputs=[ - (Dynamic.Vehicle.Propulsion.THRUST, Dynamic.Vehicle.Propulsion.THRUST_MAX) - ], - ) - - self.add_subsystem(propeller_model.name, propeller_group) + self.add_subsystem(propeller_model.name, subsys=propeller_model_mission) thrust_adder = om.ExecComp( 'turboprop_thrust=turboshaft_thrust+propeller_thrust', @@ -339,47 +356,32 @@ def setup(self): has_diag_partials=True, ) - max_thrust_adder = om.ExecComp( - 'turboprop_thrust_max=turboshaft_thrust_max+propeller_thrust_max', - turboprop_thrust_max={'val': np.zeros(num_nodes), 'units': 'lbf'}, - turboshaft_thrust_max={'val': np.zeros(num_nodes), 'units': 'lbf'}, - propeller_thrust_max={'val': np.zeros(num_nodes), 'units': 'lbf'}, - has_diag_partials=True, - ) - self.add_subsystem( 'thrust_adder', subsys=thrust_adder, - promotes_inputs=['*'], + # promotes_inputs=['*'], promotes_outputs=[('turboprop_thrust', Dynamic.Vehicle.Propulsion.THRUST)], ) - self.add_subsystem( - 'max_thrust_adder', - subsys=max_thrust_adder, - promotes_inputs=['*'], - promotes_outputs=[('turboprop_thrust_max', Dynamic.Vehicle.Propulsion.THRUST_MAX)], - ) - def configure(self): """ - Correctly connect variables between shaft power model, gearbox, and propeller, - aliasing names if they are present in both sets of connections. + Correctly connect variables between shaft power model, gearbox, and propeller. - If a gearbox is present, inputs to the gearbox are usually done via connection, - while outputs from the gearbox are promoted. This prevents intermediate values - from "leaking" out of the model and getting incorrectly connected to outside - components. It is assumed only the gearbox has variables like this. + When an input in a component is present as an output in a upstream component, the two are + connected rather than promoted. This prevents intermediate values from "leaking" out + of the model and getting incorrectly connected to outside components. All other inputs + and outputs are promoted. - Set up fixed RPM value if requested by user, which overrides any RPM defined by - shaft power model + Set up fixed RPM value if requested by user, which overrides any RPM defined by shaft power + model """ has_gearbox = self.options['gearbox_model'] is not None + num_nodes = self.options['num_nodes'] + aviary_inputs = self.options['aviary_inputs'] # TODO this list shouldn't be hardcoded - it should mirror propulsion_mission list - # Don't promote inputs that are in this list - shaft power should be an output - # of this system, also having it as an input causes feedback loop problem at - # the propulsion level + # Don't promote inputs that are in this list - shaft power should be an output of this + # system, also having it as an input causes feedback loop problem at the propulsion level skipped_inputs = [ Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN, Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE, @@ -392,10 +394,11 @@ def configure(self): ] # Build lists of inputs/outputs for each component as needed: - # "_input_list" or "_output_list" are all variables that still need to be - # connected or promoted. This list is pared down as each variable is handled. - # "_inputs" or "_outputs" is a list that tracks all the pomotions needed for a - # given component, which is done at the end as a bulk promote. + # "_input_list" or "_output_list" are all variables that still need to be connected or + # promoted. This list is pared down as each variable is handled. + + # "_inputs" or "_outputs" is a list that tracks all the promotions needed for a given + # component, which is done at the very end. shp_model = self._get_subsystem(self.options['shaft_power_model'].name) shp_input_dict = shp_model.list_inputs( @@ -418,8 +421,8 @@ def configure(self): if '.' not in shp_output_dict[key]['prom_name'] ) ) - # always promote all shaft power model inputs w/o aliasing - shp_inputs = ['*'] + + shp_inputs = [] shp_outputs = [] if has_gearbox: @@ -427,10 +430,9 @@ def configure(self): gearbox_input_dict = gearbox_model.list_inputs( return_format='dict', units=True, out_stream=None, all_procs=True ) - # Assumption is made that variables with '_out' should never be promoted or - # connected as top-level input to gearbox. This is necessary because - # Aviary gearbox uses things like shp_out internally, like when computing - # torque output, so "shp_out" is an input to that internal component + # Filter out variables with '_out' from input promotion list. This is necessary because + # Aviary gearbox uses things like shp_out as an input internally, but for the gearbox + # subsystem as a whole, it is an output. gearbox_input_list = list( set( gearbox_input_dict[key]['prom_name'] @@ -439,7 +441,7 @@ def configure(self): and '_out' not in gearbox_input_dict[key]['prom_name'] ) ) - gearbox_inputs = [] + gearbox_output_dict = gearbox_model.list_outputs( return_format='dict', units=True, out_stream=None, all_procs=True ) @@ -450,13 +452,18 @@ def configure(self): if '.' not in gearbox_output_dict[key]['prom_name'] ) ) - gearbox_outputs = [] + + gearbox_inputs = [] + gearbox_outputs = [] propeller_model_name = self.options['propeller_model'].name propeller_model = self._get_subsystem(propeller_model_name) propeller_input_dict = propeller_model.list_inputs( return_format='dict', units=True, out_stream=None, all_procs=True ) + propeller_output_dict = propeller_model.list_outputs( + return_format='dict', units=True, out_stream=None, all_procs=True + ) propeller_input_list = list( set( propeller_input_dict[key]['prom_name'] @@ -464,55 +471,59 @@ def configure(self): if '.' not in propeller_input_dict[key]['prom_name'] ) ) + propeller_output_list = list( + set( + propeller_output_dict[key]['prom_name'] + for key in propeller_output_dict + if '.' not in propeller_output_dict[key]['prom_name'] + ) + ) propeller_inputs = [] # always promote all propeller model outputs w/o aliasing except thrust - propeller_outputs = [ - '*', - (Dynamic.Vehicle.Propulsion.THRUST, 'propeller_thrust'), - (Dynamic.Vehicle.Propulsion.THRUST_MAX, 'propeller_thrust_max'), - ] + propeller_outputs = [] ######################### # SHP MODEL CONNECTIONS # ######################### # Everything not explicitly handled here gets promoted later on - # Thrust outputs are directly promoted with alias (this is a special case) + # Thrust outputs are directly connected to thrust adder comp (this is a special case) if Dynamic.Vehicle.Propulsion.THRUST in shp_output_list: - shp_outputs.append((Dynamic.Vehicle.Propulsion.THRUST, 'turboshaft_thrust')) + self.connect( + f'{shp_model.name}.{Dynamic.Vehicle.Propulsion.THRUST}', + 'thrust_adder.turboshaft_thrust', + ) shp_output_list.remove(Dynamic.Vehicle.Propulsion.THRUST) - if Dynamic.Vehicle.Propulsion.THRUST_MAX in shp_output_list: - shp_outputs.append((Dynamic.Vehicle.Propulsion.THRUST_MAX, 'turboshaft_thrust_max')) - shp_output_list.remove(Dynamic.Vehicle.Propulsion.THRUST_MAX) - # Gearbox connections if has_gearbox: + # Cover for several edge cases: connect shp_out to gearbox_in, duplicate shp_out and + # gearbox_out for var in shp_output_list.copy(): - # Check for case: var is output from shp_model, connects to gearbox, then - # gets updated by gearbox + # Check if var is output from both shp_model and gearbox model # RPM has special handling, so skip it here + if var in gearbox_output_list or var + '_out' in gearbox_output_list: + shp_output_list.remove(var) + # if var is shp_output and gearbox input, connect on shp -> gearbox side if var + '_in' in gearbox_input_list and var != Dynamic.Vehicle.Propulsion.RPM: - # if var is in gearbox input and output, connect on shp -> gearbox - # side - if var in gearbox_output_list or var + '_out' in gearbox_output_list: - shp_outputs.append((var, var + '_gearbox')) - shp_output_list.remove(var) - gearbox_inputs.append((var + '_in', var + '_gearbox')) - gearbox_input_list.remove(var + '_in') - # otherwise it gets promoted, which will get done later - - # If fixed RPM is requested by the user, use that value. Override RPM output - # from shaft power model if present, warning user + gearbox_input_list.remove(var + '_in') + self.connect(f'{shp_model.name}.{var}', f'{gearbox_model.name}.{var}_in') + # otherwise it gets promoted, which will get done later + + ######################## + # RPM SPECIAL HANDLING # + ######################## + # If fixed RPM is requested by the user, use that value. Override RPM output from shaft + # power model if present, warning user rpm_ivc = self._get_subsystem('fixed_rpm_source') - if Aircraft.Engine.FIXED_RPM in self.aviary_inputs: - fixed_rpm = self.aviary_inputs.get_val(Aircraft.Engine.FIXED_RPM, units='rpm') + if Aircraft.Engine.FIXED_RPM in aviary_inputs: + fixed_rpm = aviary_inputs.get_val(Aircraft.Engine.FIXED_RPM, units='rpm') if Dynamic.Vehicle.Propulsion.RPM in shp_output_list: - if self.aviary_inputs.get_val(Settings.VERBOSITY) >= Verbosity.BRIEF: + if aviary_inputs.get_val(Settings.VERBOSITY) >= Verbosity.BRIEF: warnings.warn( - 'Overriding RPM value outputted by EngineModel' - f'{shp_model.name} with fixed RPM of {fixed_rpm}' + f'Overriding RPM value outputted by EngineModel {shp_model.name} with fixed ' + f'RPM of {fixed_rpm} rpm' ) shp_outputs.append( @@ -523,40 +534,42 @@ def configure(self): ) shp_output_list.remove(Dynamic.Vehicle.Propulsion.RPM) - fixed_rpm_nn = np.ones(self.num_nodes) * fixed_rpm - - rpm_ivc.add_output(Dynamic.Vehicle.Propulsion.RPM, fixed_rpm_nn, units='rpm') - if has_gearbox: - self.promotes('fixed_rpm_source', [(Dynamic.Vehicle.Propulsion.RPM, 'fixed_rpm')]) - gearbox_inputs.append((Dynamic.Vehicle.Propulsion.RPM + '_in', 'fixed_rpm')) + rpm_ivc.add_output( + Dynamic.Vehicle.Propulsion.RPM, np.ones(num_nodes) * fixed_rpm, units='rpm' + ) + if has_gearbox and Dynamic.Vehicle.Propulsion.RPM + '_in' in gearbox_input_list: + self.connect( + f'fixed_rpm_source.{Dynamic.Vehicle.Propulsion.RPM}', + f'{gearbox_model.name}.{Dynamic.Vehicle.Propulsion.RPM}_in', + ) gearbox_input_list.remove(Dynamic.Vehicle.Propulsion.RPM + '_in') else: self.promotes('fixed_rpm_source', ['*']) # models such as motor take RPM as input if Dynamic.Vehicle.Propulsion.RPM in shp_input_list: - shp_inputs.append((Dynamic.Vehicle.Propulsion.RPM, 'fixed_rpm')) + self.connect( + f'fixed_rpm_source.{Dynamic.Vehicle.Propulsion.RPM}', + f'{shp_model.name}.{Dynamic.Vehicle.Propulsion.RPM}', + ) + shp_input_list.remove(Dynamic.Vehicle.Propulsion.RPM) else: rpm_ivc.add_output( 'AIRCRAFT_DATA_OVERRIDE:' + Dynamic.Vehicle.Propulsion.RPM, 1.0, units='rpm' ) - if has_gearbox: + if has_gearbox and Dynamic.Vehicle.Propulsion.RPM + '_in' in gearbox_input_list: if Dynamic.Vehicle.Propulsion.RPM in shp_output_list: - shp_outputs.append( - ( - Dynamic.Vehicle.Propulsion.RPM, - Dynamic.Vehicle.Propulsion.RPM + '_gearbox', - ) + self.connect( + f'{shp_model.name}.{Dynamic.Vehicle.Propulsion.RPM}', + f'{gearbox_model.name}.{Dynamic.Vehicle.Propulsion.RPM}_in', ) + shp_output_list.remove(Dynamic.Vehicle.Propulsion.RPM) - gearbox_inputs.append( - ( - Dynamic.Vehicle.Propulsion.RPM + '_in', - Dynamic.Vehicle.Propulsion.RPM + '_gearbox', - ) - ) + gearbox_input_list.remove(Dynamic.Vehicle.Propulsion.RPM + '_in') - # All other shp model outputs that don't interact with gearbox will be promoted + # All other shp model inputs/outputs that don't interact with other components will be promoted + for var in shp_input_list: + shp_inputs.append(var) for var in shp_output_list: shp_outputs.append(var) @@ -564,38 +577,34 @@ def configure(self): # GEARBOX MODEL CONNECTIONS # ############################# if has_gearbox: - # Promote all inputs which don't come from shp model (those got connected), - # don't promote ones in skip list + # Promote all inputs which don't come from shp model (those got connected), don't + # promote ones in skip list for var in gearbox_input_list.copy(): - if var not in skipped_inputs: + if var not in skipped_inputs and var[-3:] != '_in': gearbox_inputs.append(var) - # DO NOT promote inputs in skip list - always skip + # vars in skipped_inputs should never exist in either input list gearbox_input_list.remove(var) - # gearbox outputs can always get promoted + # gearbox outputs always get promoted, also connect to propeller for var in propeller_input_list.copy(): - if var in gearbox_output_list and var in propeller_input_list: - gearbox_outputs.append((var, var)) + # connect variables with exact name match to propeller + if var in gearbox_output_list: + self.connect(var, f'{propeller_model.name}.{var}') + propeller_input_list.remove(var) + + # NOTE a technically better way is to find "_in" and "_out" pairs beforehand and use + # that list instead, to avoid catching coincidentally named outputs + # alias outputs with '_out' to their "base" names, connect matching propeller inputs + for var in gearbox_output_list.copy(): + if var[-4:] == '_out': + new_var = var[:-4] + gearbox_outputs.append((var, new_var)) gearbox_output_list.remove(var) - # connect variables in skip list to propeller - if var in skipped_inputs: - self.connect( - var, - propeller_model.name + '.' + var, - ) - - # alias outputs with 'out' to match with propeller - if var + '_out' in gearbox_output_list and var in propeller_input_list: - gearbox_outputs.append((var + '_out', var)) - gearbox_output_list.remove(var + '_out') - # connect variables in skip list to propeller - if var in skipped_inputs: - self.connect( - var, - propeller_model.name + '.' + var, - ) - - # inputs/outputs that didn't need special handling will get promoted + if new_var in propeller_input_list: + self.connect(new_var, f'{propeller_model.name}.{new_var}') + propeller_input_list.remove(new_var) + + # inputs/outputs that didn't need special handling get promoted for var in gearbox_input_list: gearbox_inputs.append(var) for var in gearbox_output_list: @@ -604,17 +613,24 @@ def configure(self): ############################### # PROPELLER MODEL CONNECTIONS # ############################### + if Dynamic.Vehicle.Propulsion.THRUST in propeller_output_list: + self.connect( + f'{propeller_model.name}.{Dynamic.Vehicle.Propulsion.THRUST}', + 'thrust_adder.propeller_thrust', + ) + propeller_output_list.remove(Dynamic.Vehicle.Propulsion.THRUST) + # we will promote all inputs not in skip list - for var in propeller_input_list.copy(): + for var in propeller_input_list: if var not in skipped_inputs: propeller_inputs.append(var) - propeller_input_list.remove(var) + for var in propeller_output_list: + propeller_outputs.append(var) ############## # PROMOTIONS # ############## - # bulk promote desired inputs and outputs for each subsystem we have been - # tracking + # bulk promote desired inputs and outputs for each subsystem we have been tracking self.promotes(shp_model.name, inputs=shp_inputs, outputs=shp_outputs) if has_gearbox: diff --git a/aviary/subsystems/propulsion/utils.py b/aviary/subsystems/propulsion/utils.py index 50d31b192..ada72490b 100644 --- a/aviary/subsystems/propulsion/utils.py +++ b/aviary/subsystems/propulsion/utils.py @@ -20,7 +20,7 @@ class EngineModelVariables(Enum): - """Define constants that map to supported variable names in an engine model.""" + """Define constants that map to supported variable names in an engine deck.""" MACH = Dynamic.Atmosphere.MACH ALTITUDE = Dynamic.Mission.ALTITUDE diff --git a/aviary/variable_info/variable_meta_data.py b/aviary/variable_info/variable_meta_data.py index 75273cc1d..0ab94222b 100644 --- a/aviary/variable_info/variable_meta_data.py +++ b/aviary/variable_info/variable_meta_data.py @@ -2182,6 +2182,7 @@ multivalue=True, ) +# TODO if altitude is more robust, then we can modify how EngineDeck sorts things add_meta_data( Aircraft.Engine.INTERPOLATION_SORT, meta_data=_MetaData,