diff --git a/.gitignore b/.gitignore index 3a8bb5d15e..5aa5eb092f 100644 --- a/.gitignore +++ b/.gitignore @@ -143,8 +143,6 @@ aviary/reports/ #VSCode user settings .vscode/ -#VSCode extensions -.continue/ # Built docs _build/ diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index b457429cf6..753b2e9110 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -32,7 +32,6 @@ def register_custom_reports(): class_name='AviaryProblem', method='run_driver', pre_or_post='post', - # **kwargs ) register_report( @@ -148,7 +147,7 @@ def sizing_results(prob: AviaryProblem): prob.save_results(report_file) -def subsystem_report(prob: AviaryProblem, **kwargs): +def subsystem_report(prob: AviaryProblem): """ Loops through all subsystem builders in the AviaryProblem calls their write_report method. All generated report files are placed in the "reports/subsystem_reports" folder. @@ -176,7 +175,7 @@ def subsystem_report(prob: AviaryProblem, **kwargs): subsystems = model.subsystems # TODO: redo for multimissions for subsystem in subsystems: - subsystem.report(prob, reports_folder, **kwargs) + subsystem.report(prob, reports_folder) def mission_report(prob: AviaryProblem, **kwargs): diff --git a/aviary/mission/two_dof/ode/landing_ode.py b/aviary/mission/two_dof/ode/landing_ode.py index 1e2f923cb4..03e328c023 100644 --- a/aviary/mission/two_dof/ode/landing_ode.py +++ b/aviary/mission/two_dof/ode/landing_ode.py @@ -1,4 +1,5 @@ import numpy as np +import openmdao.api as om from aviary.mission.two_dof.ode.landing_eom import ( GlideConditionComponent, @@ -35,13 +36,38 @@ def setup(self): promotes_outputs=[Mission.Landing.INITIAL_ALTITUDE], ) + alias_comp = om.ExecComp( + 'alt=airport_alt', + alt={ + 'val': np.zeros(1), + 'units': 'ft', + }, + airport_alt={'val': np.zeros(1), 'units': 'ft'}, + ) + + alias_comp.add_expr( + 'mach=landing_mach', + mach={'val': np.zeros(1), 'units': 'unitless'}, + landing_mach={'val': np.zeros(1), 'units': 'unitless'}, + ) + self.add_subsystem( - name='atmosphere', - subsys=Atmosphere(num_nodes=1, input_speed_type=SpeedType.MACH), + 'alias_landing_phase', + alias_comp, promotes_inputs=[ - (Dynamic.Mission.ALTITUDE, Mission.Landing.INITIAL_ALTITUDE), - (Dynamic.Atmosphere.MACH, Mission.Landing.INITIAL_MACH), + ('airport_alt', Mission.Landing.AIRPORT_ALTITUDE), + ('landing_mach', Mission.Landing.INITIAL_MACH), ], + promotes_outputs=[ + ('alt', Dynamic.Mission.ALTITUDE), + ('mach', Dynamic.Atmosphere.MACH), + ], + ) + + self.add_subsystem( + name='atmosphere', + subsys=Atmosphere(num_nodes=1, input_speed_type=SpeedType.MACH), + promotes_inputs=[Dynamic.Mission.ALTITUDE, Dynamic.Atmosphere.MACH], promotes_outputs=[ Dynamic.Atmosphere.DENSITY, Dynamic.Atmosphere.SPEED_OF_SOUND, @@ -72,14 +98,9 @@ def setup(self): aero_system, promotes_inputs=[ '*', - ( - Dynamic.Mission.ALTITUDE, - Mission.Landing.INITIAL_ALTITUDE, - ), Dynamic.Atmosphere.DENSITY, Dynamic.Atmosphere.SPEED_OF_SOUND, Dynamic.Atmosphere.DYNAMIC_VISCOSITY, - ('airport_alt', Mission.Landing.AIRPORT_ALTITUDE), (Dynamic.Atmosphere.MACH, Mission.Landing.INITIAL_MACH), Dynamic.Atmosphere.DYNAMIC_PRESSURE, ('flap_defl', Aircraft.Wing.FLAP_DEFLECTION_LANDING), @@ -108,11 +129,7 @@ def setup(self): propulsion_mission = self.add_subsystem( subsystem.name, propulsion_system, - promotes_inputs=[ - '*', - (Dynamic.Mission.ALTITUDE, Mission.Landing.AIRPORT_ALTITUDE), - (Dynamic.Atmosphere.MACH, Mission.Landing.INITIAL_MACH), - ], + promotes_inputs=['*'], promotes_outputs=[(Dynamic.Vehicle.Propulsion.THRUST_TOTAL, 'thrust_idle')], ) propulsion_mission.set_input_defaults(Dynamic.Vehicle.Propulsion.THROTTLE, 0.0) @@ -150,7 +167,7 @@ def setup(self): name='atmosphere_td', subsys=Atmosphere(num_nodes=1), promotes_inputs=[ - (Dynamic.Mission.ALTITUDE, Mission.Landing.AIRPORT_ALTITUDE), + Dynamic.Mission.ALTITUDE, (Dynamic.Mission.VELOCITY, 'TAS_touchdown'), ], promotes_outputs=[ @@ -175,7 +192,6 @@ def setup(self): ), promotes_inputs=[ '*', - (Dynamic.Mission.ALTITUDE, Mission.Landing.AIRPORT_ALTITUDE), (Dynamic.Atmosphere.DENSITY, 'rho_td'), (Dynamic.Atmosphere.SPEED_OF_SOUND, 'sos_td'), (Dynamic.Atmosphere.DYNAMIC_VISCOSITY, 'viscosity_td'), diff --git a/aviary/mission/two_dof/ode/simple_cruise_ode.py b/aviary/mission/two_dof/ode/simple_cruise_ode.py index 40fc236a4f..2612ddefaf 100644 --- a/aviary/mission/two_dof/ode/simple_cruise_ode.py +++ b/aviary/mission/two_dof/ode/simple_cruise_ode.py @@ -84,8 +84,8 @@ def setup(self): bal = om.BalanceComp( name=Dynamic.Vehicle.Propulsion.THROTTLE, val=np.ones(nn), - upper=1.0, - lower=0.0, + # upper=1.0, + # lower=0.0, units='unitless', lhs_name=Dynamic.Vehicle.Propulsion.THRUST_TOTAL, rhs_name=Dynamic.Vehicle.DRAG, diff --git a/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py index 49510abeda..26a63e99d8 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py @@ -1,13 +1,10 @@ -from aviary.variable_info.enums import SpeedType, PhaseType, ThrottleAllocation +from aviary.variable_info.enums import PhaseType, SpeedType # Energy method energy_phase_info = { - 'pre_mission': { - 'include_takeoff': False, - 'optimize_mass': True, - }, + 'pre_mission': {'include_takeoff': False, 'optimize_mass': True}, 'climb': { - 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': 'true'}}, + 'subsystem_options': {'aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, @@ -20,16 +17,18 @@ 'no_descent': True, 'mass_ref': (154000, 'lbm'), 'throttle_enforcement': 'control', + 'throttle_bounds': ((0.2, 1), 'unitless'), 'time_initial_bounds': ((0.0, 0.0), 'min'), 'time_duration_bounds': ((24.0, 128.0), 'min'), }, 'initial_guesses': { 'altitude': ([100.0, 21_000.0], 'ft'), 'mach': ([0.17, 0.475], 'unitless'), + 'throttle': ([1, 1], 'unitless'), }, }, 'cruise': { - 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': 'true'}}, + 'subsystem_options': {'aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, @@ -41,16 +40,18 @@ 'altitude_bounds': ((20_000.0, 22_000.0), 'ft'), 'mass_ref': (154000, 'lbm'), 'throttle_enforcement': 'control', + 'throttle_bounds': ((0.2, 1), 'unitless'), 'time_initial_bounds': ((24.0, 128.0), 'min'), 'time_duration_bounds': ((56.5, 1000.0), 'min'), }, 'initial_guesses': { 'altitude': ([21_000, 21_000.0], 'ft'), 'mach': ([0.475, 0.475], 'unitless'), + 'throttle': ([1, 1], 'unitless'), }, }, 'descent': { - 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': 'true'}}, + 'subsystem_options': {'aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, @@ -65,9 +66,11 @@ 'mass_ref': (154000, 'lbm'), 'no_climb': True, 'throttle_enforcement': 'control', + 'throttle_bounds': ((0.15, 1), 'unitless'), 'time_initial_bounds': ((80, 1056.5), 'min'), 'time_duration_bounds': ((29.0, 128.0), 'min'), }, + 'initial_guesses': {'throttle': ([0.5, 0.15], 'unitless')}, }, 'post_mission': { 'include_landing': False, @@ -253,19 +256,19 @@ 'throttle': ([0.956, 0.956], 'unitless'), }, }, - 'electric_cruise': { + 'cruise': { 'user_options': { - 'phase_type': PhaseType.BREGUET_RANGE, + 'phase_type': PhaseType.SIMPLE_CRUISE, 'alt_cruise': (21_000, 'ft'), 'mach_cruise': 0.475, + 'mass_bounds': ((0, None), 'lbm'), + 'mass_ref': (150_000, 'lbm'), + 'time_duration_bounds': ((0.0, 15.0), 'h'), + 'time_duration_ref': (8, 'h'), }, 'initial_guesses': { - # [Initial mass, delta mass] for special cruise phase. - 'mass': ([150_000.0, -35_000], 'lbm'), - 'initial_distance': (100.0e3, 'ft'), - 'initial_time': (1_000.0, 's'), - 'altitude': (21_000, 'ft'), - 'mach': (0.475, 'unitless'), + 'mass': ([150_000.0, 115000], 'lbm'), + 'time': ([1516.0, 2100.0], 's'), }, }, 'desc1': { @@ -277,7 +280,7 @@ 'input_speed_type': SpeedType.MACH, 'time_duration_bounds': ((300.0, 1800.0), 's'), 'time_duration_ref': (1000, 's'), - 'altitude_initial': (21_000, 'ft'), + # 'altitude_initial': (21_000, 'ft'), 'altitude_final': (10_000, 'ft'), 'altitude_bounds': ((10000.0, 21_000.0), 'ft'), 'altitude_ref': (20_000, 'ft'), diff --git a/aviary/models/aircraft/large_turboprop_freighter/large_turboprop_freighter_GASP.csv b/aviary/models/aircraft/large_turboprop_freighter/large_turboprop_freighter_GASP.csv index 4b9d34f15b..bf68c60cd7 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/large_turboprop_freighter_GASP.csv +++ b/aviary/models/aircraft/large_turboprop_freighter/large_turboprop_freighter_GASP.csv @@ -9,7 +9,10 @@ settings:mass_method, GASP ############ # AIRCRAFT # ############ -aircraft:design:subsonic_drag_coeff_factor, 1.447, unitless +# This was the original value in this file. We needed to decrease the drag +# to get a good optimization. +# aircraft:design:subsonic_drag_coeff_factor, 1.447, unitless +aircraft:design:subsonic_drag_coeff_factor, 1.33, unitless # Design aircraft:design:cg_delta, 0.25, unitless @@ -49,7 +52,7 @@ aircraft:crew_and_payload:water_mass_per_occupant, 0, lbm # Engine/Propulsion # setting electrical mass may not do anything -aircraft:electrical:mass, 300, lbm +#aircraft:electrical:mass, 300, lbm aircraft:engine:additional_mass_fraction, 0.34, unitless aircraft:engine:data_file, models/engines/turboshaft_4465hp.csv aircraft:engine:mass_scaler, 1, unitless diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index 3b6dc1c414..78930456ce 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py @@ -4,7 +4,7 @@ energy_phase_info = { 'pre_mission': {'include_takeoff': False, 'optimize_mass': True}, 'climb': { - 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': 'true'}}, + 'subsystem_options': {'aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, @@ -26,7 +26,7 @@ }, }, 'cruise': { - 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': 'true'}}, + 'subsystem_options': {'aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, @@ -47,7 +47,7 @@ }, }, 'descent': { - 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': 'true'}}, + 'subsystem_options': {'aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, @@ -141,6 +141,7 @@ 'velocity_ref': (200, 'kn'), 'velocity_ref0': (0, 'kn'), 'time_duration_ref': (10, 's'), + 'time_duration_bounds': ((5.0, 25.0), 's'), 'mass_bounds': ((0, None), 'lbm'), 'mass_ref': (150_000, 'lbm'), 'mass_defect_ref': (150_000, 'lbm'), @@ -148,11 +149,11 @@ 'distance_ref': (1e4, 'ft'), 'distance_defect_ref': (1e4, 'ft'), 'altitude_bounds': ((0.0, 700.0), 'ft'), - 'altitude_ref': (1000, 'ft'), - 'altitude_defect_ref': (1000, 'ft'), + 'altitude_ref': (500, 'ft'), + 'altitude_defect_ref': (500, 'ft'), 'altitude_final': (500, 'ft'), 'altitude_constraint_ref': (500, 'ft'), - 'flight_path_angle_bounds': ((-10.0, 20.0), 'rad'), + 'flight_path_angle_bounds': ((0.0, 20.0), 'rad'), 'flight_path_angle_ref': (57.2958, 'deg'), 'flight_path_angle_defect_ref': (57.2958, 'deg'), 'flight_path_angle_initial': (0.0, 'deg'), @@ -203,7 +204,7 @@ 'order': 3, 'EAS_target': (250, 'kn'), 'mach_target': 0.475, - 'time_duration_bounds': ((30, 300), 's'), + 'time_duration_bounds': ((30, 400), 's'), 'time_duration_ref': (1000, 's'), 'altitude_final': (10.0e3, 'ft'), 'altitude_bounds': ((400.0, 11_000.0), 'ft'), @@ -212,7 +213,7 @@ 'mass_ref': (150_000, 'lbm'), 'mass_defect_ref': (150_000, 'lbm'), 'distance_bounds': ((0, 500.0), 'NM'), - 'distance_ref': (10, 'NM'), + 'distance_ref': (20, 'NM'), 'distance_ref0': (0, 'NM'), }, 'initial_guesses': { @@ -253,6 +254,8 @@ 'cruise': { 'user_options': { 'phase_type': PhaseType.SIMPLE_CRUISE, + 'num_segments': 1, + 'order': 3, 'alt_cruise': (21_000, 'ft'), 'mach_cruise': 0.475, 'mass_bounds': ((0, None), 'lbm'), diff --git a/aviary/models/motors/electric_motor_1800Nm_6000rpm.csv b/aviary/models/engines/motors/electric_motor_1800Nm_6000rpm.csv similarity index 100% rename from aviary/models/motors/electric_motor_1800Nm_6000rpm.csv rename to aviary/models/engines/motors/electric_motor_1800Nm_6000rpm.csv diff --git a/aviary/subsystems/energy/battery_builder.py b/aviary/subsystems/energy/battery_builder.py index e3e424b3d7..381ad7e010 100644 --- a/aviary/subsystems/energy/battery_builder.py +++ b/aviary/subsystems/energy/battery_builder.py @@ -33,8 +33,10 @@ class BatteryBuilder(SubsystemBuilder): def build_pre_mission(self, aviary_inputs=None, subsystem_options=None): return SizeBattery(aviary_inputs=aviary_inputs) - def get_mass_names(self, aviary_inputs=None): - return [Aircraft.Battery.MASS] + # Special Case: Aviary directly looks for battery mass as part of mass buildup, rather then + # letting it get summed with other external subsystem masses + # def get_mass_names(self, aviary_inputs=None): + # return [Aircraft.Battery.MASS] def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_options) -> om.Group: battery_group = om.Group() diff --git a/aviary/subsystems/performance/performance_builder.py b/aviary/subsystems/performance/performance_builder.py index 973b45a998..27bb31f2a4 100644 --- a/aviary/subsystems/performance/performance_builder.py +++ b/aviary/subsystems/performance/performance_builder.py @@ -12,7 +12,7 @@ class PerformanceBuilder(SubsystemBuilder): Initializes the PerformanceBuilder object with a given name. """ - _default_name = 'mass' + _default_name = 'performance' class CorePerformanceBuilder(PerformanceBuilder): diff --git a/aviary/subsystems/propulsion/gearbox/gearbox_builder.py b/aviary/subsystems/propulsion/gearbox/gearbox_builder.py index d9f0fcb0c1..cc27be84f6 100644 --- a/aviary/subsystems/propulsion/gearbox/gearbox_builder.py +++ b/aviary/subsystems/propulsion/gearbox/gearbox_builder.py @@ -105,10 +105,10 @@ def get_mass_names(self, aviary_inputs=None): def get_timeseries(self, aviary_inputs=None, user_options=None, subsystem_options=None): return [ - Dynamic.Vehicle.Propulsion.SHAFT_POWER + '_out', + # Dynamic.Vehicle.Propulsion.SHAFT_POWER + '_out', # Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out', - Dynamic.Vehicle.Propulsion.RPM + '_out', - Dynamic.Vehicle.Propulsion.TORQUE + '_out', + # Dynamic.Vehicle.Propulsion.RPM + '_out', + # Dynamic.Vehicle.Propulsion.TORQUE + '_out', # Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL, ] diff --git a/aviary/subsystems/propulsion/motor/model/motor_map.py b/aviary/subsystems/propulsion/motor/model/motor_map.py index 9bc229236c..54922d5b75 100644 --- a/aviary/subsystems/propulsion/motor/model/motor_map.py +++ b/aviary/subsystems/propulsion/motor/model/motor_map.py @@ -57,7 +57,7 @@ def setup(self): ) efficiency_units = units_dict['efficiency'] - motor = om.MetaModelStructuredComp(method='slinear', vec_size=n, extrapolate=False) + motor = om.MetaModelStructuredComp(method='slinear', vec_size=n, extrapolate=True) motor.add_input( Dynamic.Vehicle.Propulsion.RPM, val=np.ones(n), diff --git a/aviary/subsystems/propulsion/motor/model/motor_mission.py b/aviary/subsystems/propulsion/motor/model/motor_mission.py index 336fcaedfa..c4135f495f 100644 --- a/aviary/subsystems/propulsion/motor/model/motor_mission.py +++ b/aviary/subsystems/propulsion/motor/model/motor_mission.py @@ -2,6 +2,7 @@ import openmdao.api as om from aviary.subsystems.propulsion.motor.model.motor_map import MotorMap +from aviary.variable_info.functions import add_aviary_option from aviary.variable_info.variables import Aircraft, Dynamic @@ -10,24 +11,50 @@ class MotorMission(om.Group): def initialize(self): self.options.declare('num_nodes', types=int) + add_aviary_option(self, Aircraft.Engine.RPM_DESIGN, units='rpm') + add_aviary_option(self, Aircraft.Engine.FIXED_RPM, val=0.0, units='rpm') def setup(self): nn = self.options['num_nodes'] - - ivc = om.IndepVarComp() - ivc.add_output('max_throttle', val=np.ones(nn), units='unitless') - - self.add_subsystem('ivc', ivc, promotes=['*']) + rpm_design = self.options[Aircraft.Engine.RPM_DESIGN][0] + fixed_rpm = self.options[Aircraft.Engine.FIXED_RPM][0] motor_group = om.Group() + ivc = om.IndepVarComp() + ivc.add_output('max_RPM', val=np.ones(nn) * rpm_design, units='rpm') + + motor_group.add_subsystem('ivc', ivc, promotes=['*']) + + # NOTE: this relies on the option default for this group for FIXED_RPM being 0.0 + if fixed_rpm != 0.0: + use_fixed_rpm = True + else: + use_fixed_rpm = False + + if not use_fixed_rpm: + # Adjust RPM with throttle. This is because motor model does not capture the physics of + # RPM with changing torque and shaft power, and prevents very high RPM with little to no + # motor power (aka free energy spinning the shaft) + motor_group.add_subsystem( + 'rpm_calc', + om.ExecComp( + 'RPM = max_RPM * power(throttle, 0.5)', + RPM={'val': np.ones(nn), 'units': 'rpm'}, + max_RPM={'val': np.ones(nn), 'units': 'rpm'}, + throttle={'val': np.ones(nn), 'units': 'unitless'}, + ), + promotes_inputs=[('throttle', Dynamic.Vehicle.Propulsion.THROTTLE), 'max_RPM'], + promotes_outputs=[('RPM', Dynamic.Vehicle.Propulsion.RPM)], + ) + motor_group.add_subsystem( 'motor_map', MotorMap(num_nodes=nn), promotes_inputs=[ Dynamic.Vehicle.Propulsion.THROTTLE, Aircraft.Engine.SCALE_FACTOR, - Dynamic.Vehicle.Propulsion.RPM, + # Dynamic.Vehicle.Propulsion.RPM, ], promotes_outputs=[ Dynamic.Vehicle.Propulsion.TORQUE, @@ -35,6 +62,13 @@ def setup(self): ], ) + if use_fixed_rpm: + motor_group.promotes('motor_map', inputs=[Dynamic.Vehicle.Propulsion.RPM]) + else: + motor_group.connect( + Dynamic.Vehicle.Propulsion.RPM, f'motor_map.{Dynamic.Vehicle.Propulsion.RPM}' + ) + motor_group.add_subsystem( 'power_comp', om.ExecComp( @@ -44,10 +78,15 @@ def setup(self): RPM={'val': np.ones(nn), 'units': 'rad/s'}, has_diag_partials=True, ), # fixed RPM system - promotes_inputs=[('RPM', Dynamic.Vehicle.Propulsion.RPM)], + # promotes_inputs=[('RPM', Dynamic.Vehicle.Propulsion.RPM)], promotes_outputs=[('shaft_power', Dynamic.Vehicle.Propulsion.SHAFT_POWER)], ) + if use_fixed_rpm: + motor_group.promotes('power_comp', inputs=[('RPM', Dynamic.Vehicle.Propulsion.RPM)]) + else: + motor_group.connect(Dynamic.Vehicle.Propulsion.RPM, 'power_comp.RPM') + # Can't promote torque as an input, as it will create a feedback loop with # propulsion mux component. Connect it here instead motor_group.connect(Dynamic.Vehicle.Propulsion.TORQUE, 'power_comp.torque') @@ -72,53 +111,3 @@ def setup(self): self.add_subsystem( 'motor_group', motor_group, promotes_inputs=['*'], promotes_outputs=['*'] ) - - # Determine the maximum power available at this flight condition - # this is used for excess power constraints - motor_group_max = om.Group() - - # these two groups are the same as those above - motor_group_max.add_subsystem( - 'motor_map_max', - MotorMap(num_nodes=nn), - promotes_inputs=[ - (Dynamic.Vehicle.Propulsion.THROTTLE, 'max_throttle'), - Aircraft.Engine.SCALE_FACTOR, - Dynamic.Vehicle.Propulsion.RPM, - ], - promotes_outputs=[ - ( - Dynamic.Vehicle.Propulsion.TORQUE, - Dynamic.Vehicle.Propulsion.TORQUE_MAX, - ), - 'efficiency', - ], - ) - - motor_group_max.add_subsystem( - 'power_comp_max', - om.ExecComp( - 'max_power = max_torque * pi * RPM / 30', - max_power={'val': np.ones(nn), 'units': 'kW'}, - max_torque={'val': np.ones(nn), 'units': 'kN*m'}, - RPM={'val': np.ones(nn), 'units': 'rpm'}, - has_diag_partials=True, - ), - promotes_inputs=[ - ('max_torque', Dynamic.Vehicle.Propulsion.TORQUE_MAX), - ('RPM', Dynamic.Vehicle.Propulsion.RPM), - ], - promotes_outputs=[('max_power', Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX)], - ) - - self.add_subsystem( - 'motor_group_max', - motor_group_max, - promotes_inputs=['*', 'max_throttle'], - promotes_outputs=[ - Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX, - Dynamic.Vehicle.Propulsion.TORQUE_MAX, - ], - ) - - self.set_input_defaults(Dynamic.Vehicle.Propulsion.RPM, val=np.ones(nn), units='rpm') diff --git a/aviary/subsystems/propulsion/motor/motor_builder.py b/aviary/subsystems/propulsion/motor/motor_builder.py index 640b4c991b..15750155ea 100644 --- a/aviary/subsystems/propulsion/motor/motor_builder.py +++ b/aviary/subsystems/propulsion/motor/motor_builder.py @@ -83,16 +83,37 @@ def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_option # return constraints - def get_design_vars(self, aviary_inputs=None): - DVs = { - Aircraft.Engine.SCALE_FACTOR: { - 'units': 'unitless', - 'lower': 0.001, - 'upper': None, - }, - } + # def get_design_vars(self, aviary_inputs=None): + # DVs = { + # Aircraft.Engine.SCALE_FACTOR: { + # 'units': 'unitless', + # 'lower': 0.001, + # 'upper': None, + # }, + # } - return DVs + # return DVs + + # def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options=None): + # controls_dict = {} + # if aviary_inputs is not None: + # if Aircraft.Engine.FIXED_RPM not in aviary_inputs: + # if Aircraft.Engine.RPM_DESIGN in aviary_inputs: + # rpm_limit = aviary_inputs.get_val(Aircraft.Engine.RPM_DESIGN, 'rpm')[0] + # else: + # rpm_limit = 6000 + # controls_dict = { + # Dynamic.Vehicle.Propulsion.RPM: { + # 'units': 'rpm', + # 'lower': 1.0, + # 'upper': rpm_limit, + # 'control_type': 'polynomial', + # 'order': 3, + # 'opt': True, + # }, + # } + + # return controls_dict def get_parameters(self, aviary_inputs=None, user_options=None, subsystem_options=None): params = { @@ -105,15 +126,22 @@ def get_parameters(self, aviary_inputs=None, user_options=None, subsystem_option return params # def get_initial_guesses(self, aviary_inputs=None, user_options=None, subsystem_options=None): - # initial_guess_dict = { - # Aircraft.Motor.RPM: { - # 'units': 'rpm', - # 'type': 'parameter', - # 'val': 4000.0, # based on our map - # }, - # } - - # return initial_guess_dict + # initial_guess_dict = {} + # if aviary_inputs is not None: + # if Aircraft.Engine.FIXED_RPM not in aviary_inputs: + # if Aircraft.Engine.RPM_DESIGN in aviary_inputs: + # rpm_guess = aviary_inputs.get_val(Aircraft.Engine.RPM_DESIGN, 'rpm')[0] + # else: + # rpm_guess = 6000 + # initial_guess_dict = { + # f'{Dynamic.Vehicle.Propulsion.RPM}_control': { + # 'units': 'rpm', + # 'type': 'control', + # 'val': rpm_guess, + # }, + # } + + # return initial_guess_dict def get_mass_names(self, aviary_inputs=None): """ @@ -124,7 +152,7 @@ def get_mass_names(self, aviary_inputs=None): mass_names : list A list of names for the motor subsystem. """ - return [Aircraft.Engine.Motor.MASS, Aircraft.Engine.Gearbox.MASS] + return [Aircraft.Engine.Motor.MASS] def get_timeseries(self, aviary_inputs=None, user_options=None, subsystem_options=None): """ @@ -136,8 +164,8 @@ def get_timeseries(self, aviary_inputs=None, user_options=None, subsystem_option A list of variable names for the motor subsystem. """ return [ - Dynamic.Vehicle.Propulsion.TORQUE, - Dynamic.Vehicle.Propulsion.SHAFT_POWER, - Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX, - Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN, + # Dynamic.Vehicle.Propulsion.TORQUE, + # Dynamic.Vehicle.Propulsion.SHAFT_POWER, + # Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX, + # Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN, ] diff --git a/aviary/subsystems/propulsion/motor/test/test_motor_mission.py b/aviary/subsystems/propulsion/motor/test/test_motor_mission.py index 86baf52314..5f820633c7 100644 --- a/aviary/subsystems/propulsion/motor/test/test_motor_mission.py +++ b/aviary/subsystems/propulsion/motor/test/test_motor_mission.py @@ -6,55 +6,57 @@ from openmdao.utils.testing_utils import use_tempdirs from aviary.subsystems.propulsion.motor.model.motor_mission import MotorMission -from aviary.variable_info.variables import Aircraft, Dynamic from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.functions import setup_model_options from aviary.utils.functions import get_path +from aviary.variable_info.functions import setup_model_options +from aviary.variable_info.variables import Aircraft, Dynamic +@use_tempdirs class TestMotorMission(unittest.TestCase): - @use_tempdirs def test_motor_mission(self): nn = 3 filename = 'electric_motor_1800Nm_6000rpm.csv' options = AviaryValues() options.set_val(Aircraft.Engine.Motor.DATA_FILE, get_path(filename)) + options.set_val(Aircraft.Engine.RPM_DESIGN, 6000, 'rpm') + options.set_val( + Aircraft.Engine.FIXED_RPM, 6000, 'rpm' + ) # set fixed RPM so we can manually set it prob = om.Problem() - prob.model.add_subsystem('motor_map', MotorMission(num_nodes=nn), promotes=['*']) + prob.model.add_subsystem('motor_mission', MotorMission(num_nodes=nn), promotes=['*']) setup_model_options(prob, options) + prob.model.set_input_defaults( + Dynamic.Vehicle.Propulsion.RPM, val=np.ones(nn) * 6000, units='rpm' + ) + prob.setup(force_alloc_complex=True) prob.set_val(Dynamic.Vehicle.Propulsion.THROTTLE, np.linspace(0, 1, nn)) prob.set_val(Dynamic.Vehicle.Propulsion.RPM, np.linspace(0, 6000, nn)) - # prob.set_val('torque_unscaled', np.linspace(0, 1800, nn), 'N*m') + prob.set_val(Aircraft.Engine.SCALE_FACTOR, 1.12) prob.run_model() torque = prob.get_val(Dynamic.Vehicle.Propulsion.TORQUE, 'N*m') - max_torque = prob.get_val(Dynamic.Vehicle.Propulsion.TORQUE_MAX, 'N*m') efficiency = prob.get_val('efficiency') shp = prob.get_val(Dynamic.Vehicle.Propulsion.SHAFT_POWER) - max_shp = prob.get_val(Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX) power = prob.get_val(Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN, 'kW') torque_expected = np.array([0.0, 900.0, 1800.0]) * 1.12 - max_torque_expected = [2016, 2016, 2016] eff_expected = [0.871, 0.958625, 0.954] shp_expected = [0.0, 316.67253948185123, 1266.690157927405] - max_shp_expected = [0.0, 633.3450789637025, 1266.690157927405] power = [0.0, 330.3403723894654, 1327.7674611398375] assert_near_equal(torque, torque_expected, tolerance=1e-9) - assert_near_equal(max_torque, max_torque_expected, tolerance=1e-9) assert_near_equal(efficiency, eff_expected, tolerance=1e-9) assert_near_equal(shp, shp_expected, tolerance=1e-9) - assert_near_equal(max_shp, max_shp_expected, tolerance=1e-9) assert_near_equal(power, power, tolerance=1e-9) partial_data = prob.check_partials(out_stream=None, method='cs') diff --git a/aviary/subsystems/propulsion/propeller/hamilton_standard.py b/aviary/subsystems/propulsion/propeller/hamilton_standard.py index ac7261f2db..987abe2319 100644 --- a/aviary/subsystems/propulsion/propeller/hamilton_standard.py +++ b/aviary/subsystems/propulsion/propeller/hamilton_standard.py @@ -134,108 +134,109 @@ def _biquad(T, i, xi, yi): else: jx1 = jn - 2 ra_x = (T[jn] - x) / (T[jn] - T[jn - 1]) - rb_x = 1.0 - ra_x - # return here from search of x - lmt = kx - jx = jx1 - # The following code puts x values in xc blocks - for j in range(4): - xc[j] = T[jx1 + j] - # get coeff. in x sense - # coefficient routine - input x,x1,x2,x3,x4,ra_x,rb_x - p1 = xc[1] - xc[0] - p2 = xc[2] - xc[1] - p3 = xc[3] - xc[2] - p4 = p1 + p2 - p5 = p2 + p3 - d1 = x - xc[0] - d2 = x - xc[1] - d3 = x - xc[2] - d4 = x - xc[3] - cx1 = ra_x / p1 * d2 / p4 * d3 - cx2 = -ra_x / p1 * d1 / p2 * d3 + rb_x / p2 * d3 / p5 * d4 - cx3 = ra_x / p2 * d1 / p4 * d2 - rb_x / p2 * d2 / p3 * d4 - cx4 = rb_x / p5 * d2 / p3 * d3 - # return to main body - - # return here with coeff. test for univariate or bivariate - if ny == 0: - z = 0.0 - jy = jx + nx - z = cx1 * T[jy] + cx2 * T[jy + 1] + cx3 * T[jy + 2] + cx4 * T[jy + 3] + rb_x = 1.0 - ra_x + + # return here from search of x + lmt = kx + jx = jx1 + # The following code puts x values in xc blocks + for j in range(4): + xc[j] = T[jx1 + j] + # get coeff. in x sense + # coefficient routine - input x,x1,x2,x3,x4,ra_x,rb_x + p1 = xc[1] - xc[0] + p2 = xc[2] - xc[1] + p3 = xc[3] - xc[2] + p4 = p1 + p2 + p5 = p2 + p3 + d1 = x - xc[0] + d2 = x - xc[1] + d3 = x - xc[2] + d4 = x - xc[3] + cx1 = ra_x / p1 * d2 / p4 * d3 + cx2 = -ra_x / p1 * d1 / p2 * d3 + rb_x / p2 * d3 / p5 * d4 + cx3 = ra_x / p2 * d1 / p4 * d2 - rb_x / p2 * d2 / p3 * d4 + cx4 = rb_x / p5 * d2 / p3 * d3 + # return to main body + + # return here with coeff. test for univariate or bivariate + if ny == 0: + z = 0.0 + jy = jx + nx + z = cx1 * T[jy] + cx2 * T[jy + 1] + cx3 * T[jy + 2] + cx4 * T[jy + 3] + else: + # bivariate table + y = yi + j3 = j2 + 1 + j4 = j3 + ny - 1 + # search in y sense + # jy1 = subscript of 1st y + # search routine - input j3,j4,y + # - output ra_y,rb_y,ky,,jy1 + ky = 0 + ifnd_y = 0 + for j in range(j3, j4 + 1): + if T[j] >= y: + ifnd_y = 1 + break + if ifnd_y == 0: + # off high end + y = T[j4] + ky = 2 + # use last 4 points and curve B + jy1 = j4 - 3 + ra_y = 0.0 else: - # bivariate table - y = yi - j3 = j2 + 1 - j4 = j3 + ny - 1 - # search in y sense - # jy1 = subscript of 1st y - # search routine - input j3,j4,y - # - output ra_y,rb_y,ky,,jy1 - ky = 0 - ifnd_y = 0 - for j in range(j3, j4 + 1): - if T[j] >= y: - ifnd_y = 1 - break - if ifnd_y == 0: - # off high end - y = T[j4] - ky = 2 - # use last 4 points and curve B - jy1 = j4 - 3 - ra_y = 0.0 + # test for off low end, first interval + if j < j3 + 1: + if T[j] != y: + ky = 1 + y = T[j3] + if j <= j3 + 1: + jy1 = j3 + ra_y = 1.0 else: - # test for off low end, first interval - if j < j3 + 1: - if T[j] != y: - ky = 1 - y = T[j3] - if j <= j3 + 1: - jy1 = j3 - ra_y = 1.0 + # test for last interval + if j == j4: + jy1 = j4 - 3 + ra_y = 0.0 else: - # test for last interval - if j == j4: - jy1 = j4 - 3 - ra_y = 0.0 - else: - jy1 = j - 2 - ra_y = (T[j] - y) / (T[j] - T[j - 1]) - rb_y = 1.0 - ra_y - - lmt = lmt + 3 * ky - # interpolate in y sense - # subscript - base, num. of col., num. of y's - jy = (j4 + 1) + (jx - i - 2) * ny + (jy1 - j3) - yt = [0, 0, 0, 0] - for m in range(4): - jx = jy - yt[m] = cx1 * T[jx] + cx2 * T[jx + ny] + cx3 * T[jx + 2 * ny] + cx4 * T[jx + 3 * ny] - jy = jy + 1 - - # the following code puts y values in yc block - yc = [0, 0, 0, 0] - for j in range(4): - yc[j] = T[jy1] - jy1 = jy1 + 1 - # get coeff. in y sense - # coefficient routine - input y, y1, y2, y3, y4, ra_y, rb_y - p1 = yc[1] - yc[0] - p2 = yc[2] - yc[1] - p3 = yc[3] - yc[2] - p4 = p1 + p2 - p5 = p2 + p3 - d1 = y - yc[0] - d2 = y - yc[1] - d3 = y - yc[2] - d4 = y - yc[3] - cy1 = ra_y / p1 * d2 / p4 * d3 - cy2 = -ra_y / p1 * d1 / p2 * d3 + rb_y / p2 * d3 / p5 * d4 - cy3 = ra_y / p2 * d1 / p4 * d2 - rb_y / p2 * d2 / p3 * d4 - cy4 = rb_y / p5 * d2 / p3 * d3 - z = cy1 * yt[0] + cy2 * yt[1] + cy3 * yt[2] + cy4 * yt[3] + jy1 = j - 2 + ra_y = (T[j] - y) / (T[j] - T[j - 1]) + rb_y = 1.0 - ra_y + + lmt = lmt + 3 * ky + # interpolate in y sense + # subscript - base, num. of col., num. of y's + jy = (j4 + 1) + (jx - i - 2) * ny + (jy1 - j3) + yt = [0, 0, 0, 0] + for m in range(4): + jx = jy + yt[m] = cx1 * T[jx] + cx2 * T[jx + ny] + cx3 * T[jx + 2 * ny] + cx4 * T[jx + 3 * ny] + jy = jy + 1 + + # the following code puts y values in yc block + yc = [0, 0, 0, 0] + for j in range(4): + yc[j] = T[jy1] + jy1 = jy1 + 1 + # get coeff. in y sense + # coefficient routine - input y, y1, y2, y3, y4, ra_y, rb_y + p1 = yc[1] - yc[0] + p2 = yc[2] - yc[1] + p3 = yc[3] - yc[2] + p4 = p1 + p2 + p5 = p2 + p3 + d1 = y - yc[0] + d2 = y - yc[1] + d3 = y - yc[2] + d4 = y - yc[3] + cy1 = ra_y / p1 * d2 / p4 * d3 + cy2 = -ra_y / p1 * d1 / p2 * d3 + rb_y / p2 * d3 / p5 * d4 + cy3 = ra_y / p2 * d1 / p4 * d2 - rb_y / p2 * d2 / p3 * d4 + cy4 = rb_y / p5 * d2 / p3 * d3 + z = cy1 * yt[0] + cy2 * yt[1] + cy3 * yt[2] + cy4 * yt[3] return z, lmt diff --git a/aviary/subsystems/propulsion/propeller/propeller_builder.py b/aviary/subsystems/propulsion/propeller/propeller_builder.py index 68f4bfee2f..0c778f6215 100644 --- a/aviary/subsystems/propulsion/propeller/propeller_builder.py +++ b/aviary/subsystems/propulsion/propeller/propeller_builder.py @@ -143,9 +143,9 @@ def get_mass_names(self, aviary_inputs=None): def get_timeseries(self, aviary_inputs=None, user_options=None, subsystem_options=None): return [ - Dynamic.Vehicle.Propulsion.SHAFT_POWER, + # Dynamic.Vehicle.Propulsion.SHAFT_POWER, # Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX + '_out', - Dynamic.Vehicle.Propulsion.RPM, - Dynamic.Vehicle.Propulsion.TORQUE, + # Dynamic.Vehicle.Propulsion.RPM, + # Dynamic.Vehicle.Propulsion.TORQUE, # Mission.Constraints.GEARBOX_SHAFT_POWER_RESIDUAL, ] diff --git a/aviary/subsystems/propulsion/test/test_turboprop_model.py b/aviary/subsystems/propulsion/test/test_turboprop_model.py index c65d059328..d791616c98 100644 --- a/aviary/subsystems/propulsion/test/test_turboprop_model.py +++ b/aviary/subsystems/propulsion/test/test_turboprop_model.py @@ -1,5 +1,4 @@ import unittest -from pathlib import Path import numpy as np import openmdao.api as om @@ -11,13 +10,13 @@ from aviary.subsystems.propulsion.propeller.propeller_performance import PropellerPerformance from aviary.subsystems.propulsion.turboprop_model import TurbopropModel from aviary.subsystems.subsystem_builder import SubsystemBuilder +from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import get_path from aviary.utils.preprocessors import preprocess_propulsion from aviary.variable_info.enums import SpeedType from aviary.variable_info.functions import setup_model_options from aviary.variable_info.options import get_option_defaults from aviary.variable_info.variables import Aircraft, Dynamic, Mission -from aviary.utils.aviary_values import AviaryValues @use_tempdirs @@ -48,12 +47,6 @@ def prepare_model( options.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, 0.08) options.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) options.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') - options.set_val( - Aircraft.Engine.FIXED_RPM, - 1455.13090827, - units='rpm', - ) - options.set_val( Aircraft.Engine.Propeller.COMPUTE_INSTALLATION_LOSS, val=True, @@ -165,6 +158,11 @@ def test_case_1(self): val=True, units='unitless', ) + options.set_val( + Aircraft.Engine.FIXED_RPM, + 1455.13090827, + units='rpm', + ) options.set_val(Aircraft.Engine.Propeller.NUM_BLADES, val=4, units='unitless') options.set_val('speed_type', SpeedType.MACH) @@ -186,12 +184,19 @@ def test_case_1(self): self.prob.run_model() results = self.get_results() - assert_near_equal(results[0], truth_vals[0], tolerance=1.5e-10) - assert_near_equal(results[1], truth_vals[1], tolerance=1.5e-10) - assert_near_equal(results[2], truth_vals[2], tolerance=1.5e-10) - # because Hamilton Standard model uses fd method, the following may not be - # accurate. + expected_values = { + 'point_0 (idle)': truth_vals[0], + 'point_1 (SLS)': truth_vals[1], + 'point_2 (TOC)': truth_vals[2], + } + + for point_name, expected in expected_values.items(): + with self.subTest(var=point_name): + idx = list(expected_values.keys()).index(point_name) + assert_near_equal(results[idx], expected, tolerance=1.5e-10) + + # because Hamilton Standard model uses fd method, the following may not be accurate. partial_data = self.prob.check_partials(out_stream=None, form='central') assert_check_partials(partial_data, atol=0.2, rtol=0.2) @@ -228,13 +233,15 @@ def test_case_2(self): options = get_option_defaults() options.set_val(Aircraft.Engine.DATA_FILE, filename) - + options.set_val( + Aircraft.Engine.FIXED_RPM, + 1455.13090827, + units='rpm', + ) self.prepare_model(options, test_points) 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(Dynamic.Mission.PERCENT_ROTOR_RPM_CORRECTED, - # np.array([1,1,0.7]), units='unitless') self.prob.set_val( Aircraft.Engine.Propeller.INTEGRATED_LIFT_COEFFICIENT, 0.5, units='unitless' ) @@ -244,9 +251,17 @@ def test_case_2(self): self.prob.run_model() results = self.get_results() - assert_near_equal(results[0], truth_vals[0], tolerance=1.5e-10) - assert_near_equal(results[1], truth_vals[1], tolerance=1.5e-10) - assert_near_equal(results[2], truth_vals[2], tolerance=1.5e-10) + + expected_values = { + 'point_0 (M=0.001, alt=0, idle)': truth_vals[0], + 'point_1 (M=0, alt=0, SLS)': truth_vals[1], + 'point_2 (M=0.6, alt=25k, TOC)': truth_vals[2], + } + + for point_name, expected in expected_values.items(): + with self.subTest(var=point_name): + idx = list(expected_values.keys()).index(point_name) + assert_near_equal(results[idx], expected, tolerance=1.5e-10) partial_data = self.prob.check_partials(out_stream=None, form='central') assert_check_partials(partial_data, atol=0.15, rtol=0.15) @@ -285,7 +300,11 @@ def test_case_3(self): options = get_option_defaults() options.set_val(Aircraft.Engine.DATA_FILE, filename) - + options.set_val( + Aircraft.Engine.FIXED_RPM, + 1455.13090827, + units='rpm', + ) self.prepare_model(options, test_points) self.prob.set_val(Aircraft.Engine.Propeller.DIAMETER, 10.5, units='ft') @@ -293,34 +312,45 @@ def test_case_3(self): 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() results = self.get_results() - assert_near_equal(results[0], truth_vals[0], tolerance=1.5e-10) - assert_near_equal(results[1], truth_vals[1], tolerance=1.5e-10) - assert_near_equal(results[2], truth_vals[2], tolerance=1.5e-10) - # Note: There isn't much point in checking the partials of a component - # that computes them with FD. + expected_values = { + 'point_0 (idle)': truth_vals[0], + 'point_1 (SLS)': truth_vals[1], + 'point_2 (TOC)': truth_vals[2], + } + + for point_name, expected in expected_values.items(): + with self.subTest(var=point_name): + idx = list(expected_values.keys()).index(point_name) + assert_near_equal(results[idx], expected, tolerance=1.5e-10) + + # NOTE: There isn't much point in checking the partials of a component that computes them with FD. partial_data = self.prob.check_partials(out_stream=None, form='forward', step=1.01e-6) assert_check_partials(partial_data, atol=1e10, rtol=1e-3) - def test_electroprop(self): - # test case using electric motor and default HS prop model. + def test_electroprop_fixed_RPM(self): + # test case using electric motor and default HS prop model and fixed RPM. test_points = [(0, 0, 0), (0, 0, 1), (0.6, 25000, 1)] - num_nodes = len(test_points) options = get_option_defaults() shp_file = get_path('electric_motor_1800Nm_6000rpm.csv') options.set_val(Aircraft.Engine.Motor.DATA_FILE, shp_file) + options.set_val(Aircraft.Engine.RPM_DESIGN, 6000, 'rpm') + options.set_val( + Aircraft.Engine.FIXED_RPM, + 1455.13090827, + units='rpm', + ) self.prepare_model(options, test_points, shp_model=MotorBuilder(), input_rpm=True) - self.prob.set_val(Dynamic.Vehicle.Propulsion.RPM, np.ones(num_nodes) * 2000.0, units='rpm') - 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( @@ -339,21 +369,123 @@ def test_electroprop(self): ] electric_power_expected = [0.0, 303.31014553, 303.31014553] - 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('thrust_summation.propeller_thrust', units='lbf') - electric_power = self.prob.get_val(Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN, units='kW') + expected_values = { + Dynamic.Vehicle.Propulsion.SHAFT_POWER: (shp_expected, 'hp', 1e-8), + Dynamic.Vehicle.Propulsion.THRUST: (total_thrust_expected, 'lbf', 1e-8), + 'thrust_summation.propeller_thrust': (prop_thrust_expected, 'lbf', 1e-8), + Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN: (electric_power_expected, 'kW', 2e-7), + } + + for var_name, (expected, units, tol) in expected_values.items(): + with self.subTest(var=var_name): + actual = self.prob.get_val(var_name, units=units) + assert_near_equal(actual, expected, tolerance=tol) + + # NOTE: There isn't much point in checking the partials of a component that computes them + # with FD. + partial_data = self.prob.check_partials(out_stream=None, form='forward', step=1.01e-6) + assert_check_partials(partial_data, atol=1e10, rtol=1e-3) + + def test_electroprop_calc_RPM(self): + # test case using electric motor and default HS prop model and RPM that scales with throttle. + test_points = [(0, 0, 0.01), (0, 0, 0.5), (0, 0, 1)] + + options = get_option_defaults() + + shp_file = get_path('electric_motor_1800Nm_6000rpm.csv') + options.set_val(Aircraft.Engine.Motor.DATA_FILE, shp_file) + options.set_val(Aircraft.Engine.RPM_DESIGN, 6000, 'rpm') + options.delete(Aircraft.Engine.FIXED_RPM) - assert_near_equal(shp, shp_expected, tolerance=1e-8) - assert_near_equal(total_thrust, total_thrust_expected, tolerance=1e-8) - assert_near_equal(prop_thrust, prop_thrust_expected, tolerance=1e-8) - assert_near_equal(electric_power, electric_power_expected, tolerance=2e-7) + self.prepare_model(options, test_points, shp_model=MotorBuilder(), input_rpm=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') - # Note: There isn't much point in checking the partials of a component - # that computes them with FD. + self.prob.run_model() + + shp_expected = [1.51665999, 536.2202822, 1516.659991] + prop_thrust_expected = total_thrust_expected = [ + 103.76048662, + 5188.02430387, + 10376.04860512, + ] + electric_power_expected = [1.29959593, 415.69603238, 1185.50666173] + expected_values = { + Dynamic.Vehicle.Propulsion.SHAFT_POWER: (shp_expected, 'hp', 1e-8), + Dynamic.Vehicle.Propulsion.THRUST: (total_thrust_expected, 'lbf', 1e-8), + 'thrust_summation.propeller_thrust': (prop_thrust_expected, 'lbf', 1e-8), + Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN: (electric_power_expected, 'kW', 2e-7), + } + + for var_name, (expected, units, tol) in expected_values.items(): + with self.subTest(var=var_name): + actual = self.prob.get_val(var_name, units=units) + assert_near_equal(actual, expected, tolerance=tol) + + # NOTE: There isn't much point in checking the partials of a component that computes them + # with FD. partial_data = self.prob.check_partials(out_stream=None, form='forward', step=1.01e-6) assert_check_partials(partial_data, atol=1e10, rtol=1e-3) + def test_control_rpm_turboprop(self): + # based on test_case_2, but simulating RPM as a dymos control + filename = get_path('models/engines/turboshaft_1120hp.csv') + test_points = [(0.001, 0, 0), (0, 0, 1), (0.6, 25000, 1)] + truth_vals = [ + (111.99507922, 37.507376, 910.70245568, 948.20983168, -195.78762), + (1119.99609612, 136.3, 2752.73508087, 2889.03508087, -644), + (778.21130479, 21.3, 558.33650216, 579.63650216, -839.7), + ] + + options = get_option_defaults() + options.set_val(Aircraft.Engine.DATA_FILE, filename) + options.delete(Aircraft.Engine.FIXED_RPM) + + self.prepare_model(options, test_points) + + 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') + + rpm_control = np.array([0.5, 0.75, 1.0]) * 1455.13090827 + self.prob.set_val( + f'{Dynamic.Vehicle.Propulsion.RPM}_control', + val=rpm_control, + units='rpm', + ) + + self.prob.run_model() + + results = self.get_results() + + expected_values = { + 'point_0 (M=0.001, alt=0, idle)': truth_vals[0], + 'point_1 (M=0, alt=0, SLS)': truth_vals[1], + 'point_2 (M=0.6, alt=25k, TOC)': truth_vals[2], + } + + for point_name, expected in expected_values.items(): + with self.subTest(var=point_name): + idx = list(expected_values.keys()).index(point_name) + assert_near_equal(results[idx], expected, tolerance=1.5e-10) + + with self.subTest(var='rotations_per_minute'): + actual = self.prob.get_val(Dynamic.Vehicle.Propulsion.RPM, units='rpm') + assert_near_equal(actual, rpm_control, tolerance=1e-8) + + partial_data = self.prob.check_partials(out_stream=None, form='central') + assert_check_partials(partial_data, atol=0.15, rtol=0.15) + class ExamplePropModel(SubsystemBuilder): def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_options): @@ -392,3 +524,6 @@ def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_option if __name__ == '__main__': unittest.main() + # test = TurbopropMissionTest() + # test.setUp() + # test.test_electroprop_calc_RPM() diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index ca4ea5e9e0..3c44630516 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -16,8 +16,9 @@ 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), a gearbox + model (default is simple gearbox), and a model for propeller performance (default is Hamilton + Standard). Attributes ---------- @@ -28,20 +29,32 @@ class TurbopropModel(EngineModel): shaft_power_model : SubsystemBuilder () Subsystem builder for the shaft power generating component. If None, an EngineDeck built using provided options is used. + gearbox_model : SubsystemBuilder () + Subsystem builder used for the gearbox. If None, the simple gearbox model is used. propeller_model : SubsystemBuilder () 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. Methods ------- - build_pre_mission - build_mission - build_post_mission - get_val - set_val - update + - build_pre_mission + - build_mission + - build_post_mission + - needs_mission_solver + - get_states + - get_controls + - get_parameters + - get_constraints + - get_bus_variables + - get_pre_mission_bus_variables + - get_linked_variables + - get_design_vars + - get_initial_guesses + - get_mass_names + - get_val + - get_item + - set_val + - update """ def __init__( @@ -75,14 +88,14 @@ def __init__( # 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 + # TODO where can we bring in include_constraints? subsystem_options 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') - def needs_mission_solver(self, aviary_inputs, subsystem_options): + def needs_mission_solver(self, aviary_inputs=None, subsystem_options=None): if self.shaft_power_model is not None: shp_solver = self.shaft_power_model.needs_mission_solver( aviary_inputs=aviary_inputs, @@ -108,8 +121,8 @@ def needs_mission_solver(self, aviary_inputs, subsystem_options): 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, subsystem_options) -> om.Group: + # this breaks - look into "nested" subsystem options with separate dict per turboprop subsystem? + def build_pre_mission(self, aviary_inputs=None, subsystem_options=None): shp_model = self.shaft_power_model propeller_model = self.propeller_model gearbox_model = self.gearbox_model @@ -123,7 +136,7 @@ def build_pre_mission(self, aviary_inputs, subsystem_options) -> om.Group: ) if shp_model_pre_mission is not None: turboprop_group.add_subsystem( - shp_model_pre_mission.name, subsys=shp_model_pre_mission, promotes=['*'] + shp_model.name, subsys=shp_model_pre_mission, promotes=['*'] ) gearbox_model_pre_mission = gearbox_model.build_pre_mission( @@ -131,7 +144,7 @@ def build_pre_mission(self, aviary_inputs, subsystem_options) -> om.Group: ) if gearbox_model_pre_mission is not None: turboprop_group.add_subsystem( - gearbox_model_pre_mission.name, + gearbox_model.name, subsys=gearbox_model_pre_mission, promotes=['*'], ) @@ -141,21 +154,23 @@ def build_pre_mission(self, aviary_inputs, subsystem_options) -> om.Group: ) if propeller_model_pre_mission is not None: turboprop_group.add_subsystem( - propeller_model_pre_mission.name, + propeller_model.name, subsys=propeller_model_pre_mission, promotes=['*'], ) return turboprop_group - def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_options): + def build_mission( + self, num_nodes, aviary_inputs=None, user_options=None, subsystem_options=None + ): turboprop_group = TurbopropMission( num_nodes=num_nodes, shaft_power_model=self.shaft_power_model, propeller_model=self.propeller_model, gearbox_model=self.gearbox_model, aviary_inputs=self.options, - kwargs=subsystem_options, + subsystem_options=subsystem_options, ) return turboprop_group @@ -165,20 +180,20 @@ def mission_inputs(self, aviary_inputs=None, user_options=None, subsystem_option # 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] + # if self.shaft_power_model.name in subsystem_options: + # subsys_args = subsystem_options[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] + # if self.gearbox_model.name in subsystem_options: + # subsys_args = subsystem_options[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] + # if self.propeller_model.name in subsystem_options: + # subsys_args = subsystem_options[self.propeller_model.name] # else: # subsys_args = {} # inputs += self.propeller_model.mission_inputs(**subsys_args) @@ -190,20 +205,20 @@ def mission_outputs(self, aviary_inputs=None, user_options=None, subsystem_optio # 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] + # if self.shaft_power_model.name in subsystem_options: + # subsys_args = subsystem_options[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] + # if self.gearbox_model.name in subsystem_options: + # subsys_args = subsystem_options[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] + # if self.propeller_model.name in subsystem_options: + # subsys_args = subsystem_options[self.propeller_model.name] # else: # subsys_args = {} # outputs += self.propeller_model.mission_outputs(**subsys_args) @@ -233,7 +248,6 @@ def build_post_mission( turboprop_group.add_subsystem( shp_model.name, subsys=shp_model_post_mission, - aviary_options=aviary_inputs, ) gearbox_model_post_mission = gearbox_model.build_post_mission( @@ -247,7 +261,6 @@ def build_post_mission( turboprop_group.add_subsystem( gearbox_model.name, subsys=gearbox_model_post_mission, - aviary_options=aviary_inputs, ) propeller_model_post_mission = propeller_model.build_post_mission( @@ -261,11 +274,116 @@ def build_post_mission( turboprop_group.add_subsystem( propeller_model.name, subsys=propeller_model_post_mission, - aviary_options=aviary_inputs, ) return turboprop_group + def get_states(self, aviary_inputs=None, user_options=None, subsystem_options=None): + states = super().get_states( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + + if self.shaft_power_model is not None: + extra_states = self.shaft_power_model.get_states( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + states.update(extra_states) + + if self.gearbox_model is not None: + extra_states = self.gearbox_model.get_states( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + states.update(extra_states) + + if self.propeller_model is not None: + extra_states = self.propeller_model.get_states( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + states.update(extra_states) + + return states + + def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options=None): + controls = super().get_controls( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + + if self.shaft_power_model is not None: + extra_controls = self.shaft_power_model.get_controls( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + controls.update(extra_controls) + + if self.gearbox_model is not None: + extra_controls = self.gearbox_model.get_controls( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + controls.update(extra_controls) + + if self.propeller_model is not None: + extra_controls = self.propeller_model.get_controls( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + controls.update(extra_controls) + + # RPM control needs special handling, which is done automatically in configure step of + # Mission group + if Dynamic.Vehicle.Propulsion.RPM in controls: + controls[f'{Dynamic.Vehicle.Propulsion.RPM}_control'] = controls.pop( + Dynamic.Vehicle.Propulsion.RPM + ) + + return controls + + def get_constraints(self, aviary_inputs=None, user_options=None, subsystem_options=None): + constraints = super().get_constraints( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) # calls from EngineModel + + if self.shaft_power_model is not None: + extra_constraints = self.shaft_power_model.get_constraints( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + constraints.update(extra_constraints) + + if self.gearbox_model is not None: + extra_constraints = self.gearbox_model.get_constraints( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + constraints.update(extra_constraints) + + if self.propeller_model is not None: + extra_constraints = self.propeller_model.get_constraints( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + constraints.update(extra_constraints) + + return constraints + def get_parameters(self, aviary_inputs=None, user_options=None, subsystem_options=None): params = super().get_parameters( aviary_inputs=aviary_inputs, @@ -299,6 +417,53 @@ def get_parameters(self, aviary_inputs=None, user_options=None, subsystem_option return params + def get_linked_variables(self, aviary_inputs=None): + linked_vars = super().get_linked_variables() # calls from EngineModel + if self.shaft_power_model is not None: + linked_vars += self.shaft_power_model.get_linked_variables(aviary_inputs=aviary_inputs) + if self.gearbox_model is not None: + linked_vars += self.gearbox_model.get_linked_variables(aviary_inputs=aviary_inputs) + if self.propeller_model is not None: + linked_vars += self.propeller_model.get_linked_variables(aviary_inputs=aviary_inputs) + + return linked_vars + + def get_bus_variables(self, aviary_inputs=None): + busvars = super().get_bus_variables() # calls from EngineModel + if self.shaft_power_model is not None: + busvars.update(self.shaft_power_model.get_bus_variables(aviary_inputs=aviary_inputs)) + if self.gearbox_model is not None: + busvars.update(self.gearbox_model.get_bus_variables(aviary_inputs=aviary_inputs)) + if self.propeller_model is not None: + busvars.update(self.propeller_model.get_bus_variables(aviary_inputs=aviary_inputs)) + return busvars + + def get_pre_mission_bus_variables(self, aviary_inputs=None, mission_info=None): + bus_vars = super().get_pre_mission_bus_variables( + aviary_inputs=aviary_inputs, + mission_info=mission_info, + ) # calls from EngineModel + + if self.shaft_power_model is not None: + extra_bus_vars = self.shaft_power_model.get_pre_mission_bus_variables( + aviary_inputs=aviary_inputs, mission_info=mission_info + ) + bus_vars.update(extra_bus_vars) + + if self.gearbox_model is not None: + extra_bus_vars = self.gearbox_model.get_pre_mission_bus_variables( + aviary_inputs=aviary_inputs, mission_info=mission_info + ) + bus_vars.update(extra_bus_vars) + + if self.propeller_model is not None: + extra_bus_vars = self.propeller_model.get_pre_mission_bus_variables( + aviary_inputs=aviary_inputs, mission_info=mission_info + ) + bus_vars.update(extra_bus_vars) + + return bus_vars + def get_design_vars(self, aviary_inputs=None): desvars = super().get_design_vars() # calls from EngineModel if self.shaft_power_model is not None: @@ -309,6 +474,103 @@ def get_design_vars(self, aviary_inputs=None): desvars.update(self.propeller_model.get_design_vars(aviary_inputs=aviary_inputs)) return desvars + def get_initial_guesses(self, aviary_inputs=None, user_options=None, subsystem_options=None): + guesses = super().get_initial_guesses( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + + if self.shaft_power_model is not None: + extra_guesses = self.shaft_power_model.get_initial_guesses( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + guesses.update(extra_guesses) + + if self.gearbox_model is not None: + extra_guesses = self.gearbox_model.get_initial_guesses( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + guesses.update(extra_guesses) + + if self.propeller_model is not None: + extra_guesses = self.propeller_model.get_initial_guesses( + aviary_inputs=aviary_inputs, + user_options=user_options, + subsystem_options=subsystem_options, + ) + guesses.update(extra_guesses) + + return guesses + + def get_mass_names(self, aviary_inputs=None): + mass_names = super().get_mass_names() # calls from EngineModel + if self.shaft_power_model is not None: + mass_names += self.shaft_power_model.get_mass_names(aviary_inputs=aviary_inputs) + if self.gearbox_model is not None: + mass_names += self.gearbox_model.get_mass_names(aviary_inputs=aviary_inputs) + if self.propeller_model is not None: + mass_names += self.propeller_model.get_mass_names(aviary_inputs=aviary_inputs) + return mass_names + + def preprocess_inputs(self, aviary_inputs=None): + new_inputs = super().preprocess_inputs(aviary_inputs) + if self.shaft_power_model is not None: + new_inputs.update(self.shaft_power_model.preprocess_inputs(aviary_inputs=new_inputs)) + if self.gearbox_model is not None: + new_inputs.update(self.gearbox_model.get_mass_names(aviary_inputs=new_inputs)) + if self.propeller_model is not None: + new_inputs.update(self.propeller_model.get_mass_names(aviary_inputs=new_inputs)) + return new_inputs + + def get_timeseries(self, aviary_inputs=None, user_options=None, subsystem_options=None): + new_timeseries = super().get_timeseries( + aviary_inputs, user_options, subsystem_options + ) # calls from EngineModel + if self.shaft_power_model is not None: + new_timeseries += self.shaft_power_model.get_timeseries( + aviary_inputs, user_options, subsystem_options + ) + if self.gearbox_model is not None: + new_timeseries += self.gearbox_model.get_timeseries( + aviary_inputs, user_options, subsystem_options + ) + if self.propeller_model is not None: + new_timeseries += self.propeller_model.get_timeseries( + aviary_inputs, user_options, subsystem_options + ) + return new_timeseries + + def get_post_mission_bus_variables(self, aviary_inputs=None, mission_info=None): + new_bus = super().get_post_mission_bus_variables( + aviary_inputs, mission_info + ) # calls from EngineModel + if self.shaft_power_model is not None: + new_bus.update( + self.shaft_power_model.get_post_mission_bus_variables(aviary_inputs, mission_info) + ) + if self.gearbox_model is not None: + new_bus.update( + self.gearbox_model.get_post_mission_bus_variables(aviary_inputs, mission_info) + ) + if self.propeller_model is not None: + new_bus.update( + self.propeller_model.get_post_mission_bus_variables(aviary_inputs, mission_info) + ) + return new_bus + + def report(self, prob, reports_folder, **kwargs): + if self.shaft_power_model is not None: + self.shaft_power_model.report(prob, reports_folder, **kwargs) + if self.gearbox_model is not None: + self.gearbox_model.report(prob, reports_folder, **kwargs) + if self.propeller_model is not None: + self.propeller_model.report(prob, reports_folder, **kwargs) + class TurbopropMission(om.Group): def initialize(self): @@ -318,7 +580,9 @@ def initialize(self): self.options.declare('shaft_power_model', desc='shaft power generation model') self.options.declare('propeller_model', desc='propeller model') self.options.declare('gearbox_model', desc='gearbox model') - self.options.declare('kwargs', desc='kwargs for turboprop mission model') + self.options.declare( + 'subsystem_options', desc='subsystem options for turboprop mission model' + ) self.options.declare('aviary_inputs', desc='aviary inputs for turboprop mission model') def setup(self): @@ -328,61 +592,73 @@ def setup(self): shp_model = self.options['shaft_power_model'] propeller_model = self.options['propeller_model'] gearbox_model = self.options['gearbox_model'] - kwargs = self.options['kwargs'] + subsystem_options = self.options['subsystem_options'] 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 - self.add_subsystem('fixed_rpm_source', subsys=om.IndepVarComp()) + if Aircraft.Engine.FIXED_RPM 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('rpm_source', subsys=om.IndepVarComp()) + else: + # passthrough component so RPM can be used as a control + self.add_subsystem( + 'rpm_source', + subsys=om.ExecComp( + 'RPM=RPM_control', + RPM={'val': np.zeros(num_nodes), 'units': 'rpm'}, + RPM_control={'val': np.zeros(num_nodes), 'units': 'rpm'}, + has_diag_partials=True, + ), + ) # Shaft Power Model try: - subsystem_options = kwargs[shp_model.name] + shp_options = subsystem_options[shp_model.name] except (AttributeError, KeyError): - subsystem_options = {} + shp_options = {} shp_model_mission = shp_model.build_mission( num_nodes=num_nodes, aviary_inputs=aviary_inputs, user_options={}, - subsystem_options=subsystem_options, + subsystem_options=shp_options, ) if shp_model_mission is not None: self.add_subsystem(shp_model.name, subsys=shp_model_mission) # Gearbox Model try: - gearbox_kwargs = kwargs[gearbox_model.name] + gearbox_options = subsystem_options[gearbox_model.name] except (AttributeError, KeyError): - gearbox_kwargs = {} + gearbox_options = {} if gearbox_model is not None: gearbox_model_mission = gearbox_model.build_mission( num_nodes=num_nodes, aviary_inputs=aviary_inputs, user_options={}, - subsystem_options=gearbox_kwargs, + subsystem_options=gearbox_options, ) if gearbox_model_mission is not None: self.add_subsystem(gearbox_model.name, subsys=gearbox_model_mission) # Propeller Model try: - propeller_kwargs = kwargs[propeller_model.name] + propeller_options = subsystem_options[propeller_model.name] except (AttributeError, KeyError): - propeller_kwargs = {} + propeller_options = {} propeller_model_mission = propeller_model.build_mission( num_nodes=num_nodes, aviary_inputs=aviary_inputs, user_options={}, - subsystem_options=propeller_kwargs, + subsystem_options=propeller_options, ) if isinstance(propeller_model, PropellerBuilder): # use the Hamilton Standard model try: - propeller_kwargs = kwargs['hamilton_standard'] + propeller_options = subsystem_options['hamilton_standard'] except KeyError: - propeller_kwargs = {} + propeller_options = {} self.add_subsystem(propeller_model.name, propeller_model_mission) @@ -543,7 +819,11 @@ def configure(self): for var in shp_output_list.copy(): # 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: + if ( + var in gearbox_output_list + or var + '_out' in gearbox_output_list + and var != Dynamic.Vehicle.Propulsion.RPM + ): 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: @@ -556,7 +836,10 @@ def configure(self): ######################## # 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 no fixed RPM requested, then check if SHP model outputs it - if not, then make promotions/ + # connections from passthrough component, which will get added as a control by the turboprop + # model if any sub-builders request it as a control + rpm_source_comp = self._get_subsystem('rpm_source') if Aircraft.Engine.FIXED_RPM in aviary_inputs: fixed_rpm = aviary_inputs.get_val(Aircraft.Engine.FIXED_RPM, units='rpm') @@ -571,43 +854,85 @@ def configure(self): shp_outputs.append( ( Dynamic.Vehicle.Propulsion.RPM, - 'AIRCRAFT_DATA_OVERRIDE:' + Dynamic.Vehicle.Propulsion.RPM, + 'INTERNAL_OVERRIDE:' + Dynamic.Vehicle.Propulsion.RPM, ) ) shp_output_list.remove(Dynamic.Vehicle.Propulsion.RPM) - rpm_ivc.add_output( + rpm_source_comp.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: + # If gearbox exists, don't promote RPM from source because gearbox will modify it + # If no gearbox, just promote RPM and let OM connect it everywhere + if has_gearbox and f'{Dynamic.Vehicle.Propulsion.RPM}_in' in gearbox_input_list: self.connect( - f'fixed_rpm_source.{Dynamic.Vehicle.Propulsion.RPM}', + f'rpm_source.{Dynamic.Vehicle.Propulsion.RPM}', f'{gearbox_model.name}.{Dynamic.Vehicle.Propulsion.RPM}_in', ) - gearbox_input_list.remove(Dynamic.Vehicle.Propulsion.RPM + '_in') + gearbox_input_list.remove(f'{Dynamic.Vehicle.Propulsion.RPM}_in') else: - self.promotes('fixed_rpm_source', ['*']) + self.promotes('rpm_source', ['*']) # models such as motor take RPM as input if Dynamic.Vehicle.Propulsion.RPM in shp_input_list: self.connect( - f'fixed_rpm_source.{Dynamic.Vehicle.Propulsion.RPM}', + f'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 and Dynamic.Vehicle.Propulsion.RPM + '_in' in gearbox_input_list: + if Dynamic.Vehicle.Propulsion.RPM not in shp_output_list: + # There is no defined source for RPM, so if any component needs it, we use the + # passthrough component so an "external" source, like a Dymos control, can be connected + if ( + Dynamic.Vehicle.Propulsion.RPM in shp_input_list + or ( + has_gearbox and f'{Dynamic.Vehicle.Propulsion.RPM}_in' in gearbox_input_list + ) + or Dynamic.Vehicle.Propulsion.RPM in propeller_input_list + ): + # expose RPM input so Dymos can control it + self.promotes( + 'rpm_source', + inputs=[('RPM_control', f'{Dynamic.Vehicle.Propulsion.RPM}_control')], + ) + # Go through all sub-systems and connect RPM inputs as needed + if Dynamic.Vehicle.Propulsion.RPM in shp_input_list: + self.connect( + 'rpm_source.RPM', + f'{shp_model.name}.{Dynamic.Vehicle.Propulsion.RPM}', + ) + shp_input_list.remove(Dynamic.Vehicle.Propulsion.RPM) + + if has_gearbox and f'{Dynamic.Vehicle.Propulsion.RPM}_in' in gearbox_input_list: if Dynamic.Vehicle.Propulsion.RPM in shp_output_list: self.connect( f'{shp_model.name}.{Dynamic.Vehicle.Propulsion.RPM}', f'{gearbox_model.name}.{Dynamic.Vehicle.Propulsion.RPM}_in', ) - + gearbox_input_list.remove(f'{Dynamic.Vehicle.Propulsion.RPM}_in') shp_output_list.remove(Dynamic.Vehicle.Propulsion.RPM) - - gearbox_input_list.remove(Dynamic.Vehicle.Propulsion.RPM + '_in') + else: + self.connect( + 'rpm_source.RPM', + f'{gearbox_model.name}.{Dynamic.Vehicle.Propulsion.RPM}_in', + ) + gearbox_input_list.remove(Dynamic.Vehicle.Propulsion.RPM + '_in') + + # only connect RPM source to propeller if: 1. SHP or gearbox do not output it, and + # 2. propeller has it as an input + if ( + Dynamic.Vehicle.Propulsion.RPM not in shp_output_list + and ( + has_gearbox + and f'{Dynamic.Vehicle.Propulsion.RPM}_out' not in gearbox_output_list + ) + and Dynamic.Vehicle.Propulsion.RPM in propeller_input_list + ): + self.connect( + 'rpm_source.RPM', + f'{propeller_model.name}.{Dynamic.Vehicle.Propulsion.RPM}', + ) + propeller_input_list.remove(Dynamic.Vehicle.Propulsion.RPM) # All other shp model inputs/outputs that don't interact with other components will be promoted for var in shp_input_list: diff --git a/aviary/subsystems/subsystem_builder.py b/aviary/subsystems/subsystem_builder.py index 1f765e3198..3c9608cacb 100644 --- a/aviary/subsystems/subsystem_builder.py +++ b/aviary/subsystems/subsystem_builder.py @@ -1,5 +1,8 @@ from abc import ABC +from openmdao.core.system import System + +from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.variable_meta_data import _MetaData @@ -46,7 +49,7 @@ def needs_mission_solver(self, aviary_inputs, subsystem_options): """ return True - def build_pre_mission(self, aviary_inputs, subsystem_options=None): + def build_pre_mission(self, aviary_inputs, subsystem_options=None) -> None | System: """ Build an OpenMDAO System for the pre-mission computations of the subsystem. @@ -72,7 +75,7 @@ def build_pre_mission(self, aviary_inputs, subsystem_options=None): def get_states(self, aviary_inputs=None, user_options=None, subsystem_options=None): """ - Return a dictionary of dynamic states defined by this subsystem. (Optional) + Return a dictionary of dynamic states defined by this subsystem (Optional). Required for subsystems with mission-based dynamics. @@ -104,7 +107,7 @@ def get_states(self, aviary_inputs=None, user_options=None, subsystem_options=No def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options=None): """ - Return a dictionary of control variables for the subsystem. (Optional) + Return a dictionary of control variables for the subsystem (Optional). Parameters ---------- @@ -131,7 +134,7 @@ def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options= def get_parameters(self, aviary_inputs=None, user_options=None, subsystem_options=None): """ - Return a dictionary of parameters for the subsystem. (Optional) + Return a dictionary of parameters for the subsystem (Optional). A parameter is a value that does not vary over the trajectory. Adding a variable name to this list promotes the input to the top of the Aviary model, where it is either implicitly @@ -243,7 +246,9 @@ def get_pre_mission_bus_variables(self, aviary_inputs=None, mission_info=None): """ return {} - def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_options): + def build_mission( + self, num_nodes, aviary_inputs, user_options, subsystem_options + ) -> None | System: """ Build an OpenMDAO System for the mission computations of the subsystem. @@ -375,7 +380,7 @@ def get_mass_names(self, aviary_inputs=None): """ return [] - def preprocess_inputs(self, aviary_inputs=None): + def preprocess_inputs(self, aviary_inputs=None) -> AviaryValues: """ Preprocess the inputs to the subsystem, returning a modified AviaryValues object. @@ -452,7 +457,7 @@ def build_post_mission( mission_info=None, subsystem_options=None, phase_mission_bus_lengths=None, - ): + ) -> None | System: """ Build an OpenMDAO System for the post-mission computations of the subsystem. diff --git a/aviary/utils/math.py b/aviary/utils/math.py index 0a0541a3e0..c90e3f0211 100644 --- a/aviary/utils/math.py +++ b/aviary/utils/math.py @@ -35,7 +35,11 @@ def sigmoidX(x, x0, mu=1.0): y = np.zeros(n_size, dtype=dtype) # avoid overflow in squared term, underflow seems to be ok calc_idx = np.where((x.real - x0) / mu > -320) - y[calc_idx] = 1 / (1 + np.exp(-(x[calc_idx] - x0) / mu)) + + if isinstance(x0, np.ndarray) and len(x0) == n_size: + y[calc_idx] = 1 / (1 + np.exp(-(x[calc_idx] - x0[calc_idx]) / mu)) + else: + y[calc_idx] = 1 / (1 + np.exp(-(x[calc_idx] - x0) / mu)) else: if isinstance(x, float): dtype = float diff --git a/aviary/utils/preprocessors.py b/aviary/utils/preprocessors.py index 64d302b13e..f81b17f30d 100644 --- a/aviary/utils/preprocessors.py +++ b/aviary/utils/preprocessors.py @@ -728,14 +728,23 @@ def preprocess_propulsion( vec.append(default_value) else: # save value from aviary_options - if isiterable(aviary_val): + if isiterable(aviary_val) and len(aviary_val) > 1: if multidimensional: vec.extend(aviary_val) else: - # if aviary_val is an iterable, just grab val for this engine - vec.append(aviary_val[i]) + try: + # check variable exists at this index - if not, use default + aviary_val[i] + except IndexError: + vec.append(default_value) + else: + # if aviary_val is an iterable, just grab val for this engine + vec.append(aviary_val[i]) else: - vec.append(aviary_val) + if isiterable(aviary_val): + vec.extend(aviary_val) + else: + vec.append(aviary_val) else: # save value from EngineModel if isiterable(engine_val) and multidimensional: diff --git a/aviary/validation_cases/benchmark_tests/test_battery_in_a_mission.py b/aviary/validation_cases/benchmark_tests/test_battery_in_a_mission.py index 12c0ba2f46..d0e7e4b54b 100644 --- a/aviary/validation_cases/benchmark_tests/test_battery_in_a_mission.py +++ b/aviary/validation_cases/benchmark_tests/test_battery_in_a_mission.py @@ -11,7 +11,7 @@ @use_tempdirs class TestBatteryMission(unittest.TestCase): - """Test the setup and run optimization model with a battery subsystem.""" + """Test the setup and optimization of a model with a battery subsystem.""" def setUp(self): self.phase_info = { @@ -96,81 +96,91 @@ def test_subsystems_in_a_mission(self): prob.run_aviary_problem() self.assertTrue(prob.result.success) - electric_energy_used_cruise2 = prob.get_val( - f'traj.cruise2.timeseries.{av.Dynamic.Vehicle.CUMULATIVE_ELECTRIC_ENERGY_USED}', - units='kW*h', - ) - soc_cruise1 = prob.get_val( - f'traj.cruise1.timeseries.{av.Dynamic.Vehicle.BATTERY_STATE_OF_CHARGE}', - ) - soc_cruise2 = prob.get_val( - f'traj.cruise2.timeseries.{av.Dynamic.Vehicle.BATTERY_STATE_OF_CHARGE}', - ) - fuel_burned = prob.get_val(av.Mission.FUEL, units='lbm') - - # Check outputs - # indirectly check mission trajectory by checking total fuel/electric split - assert_near_equal(electric_energy_used_cruise2[-1], 38.61409623, 1.0e-7) - assert_near_equal(fuel_burned, 1254.14075517, 1.0e-7) - # check battery state-of-charge over mission - - assert_near_equal( - soc_cruise1.ravel(), - [ - 0.99999578, - 0.98775148, - 0.97085772, - 0.96551112, - 0.96551112, - 0.94050841, - 0.90601344, - 0.89509679, - 0.89509679, - 0.86537875, - 0.82437910, - 0.81140410, - 0.81140410, - 0.78641512, - 0.75193908, - 0.74102841, - 0.74102841, - 0.72879540, - 0.71191722, - 0.70657555, - ], - 1e-6, + # Check fuel/electric energy split + cumulative_energy_var = ( + f'traj.cruise2.timeseries.{av.Dynamic.Vehicle.CUMULATIVE_ELECTRIC_ENERGY_USED}' ) + expected_scalar_values = { + cumulative_energy_var: (38.46817379, 'kW*h'), + av.Mission.FUEL: (1249.64666191, 'lbm'), + } - assert_near_equal( - soc_cruise2.ravel(), - [ - 0.70657555, - 0.69434404, - 0.67746793, - 0.67212691, - 0.67212691, - 0.64715033, - 0.61269139, - 0.60178614, - 0.60178614, - 0.57209914, - 0.53114231, - 0.51818085, - 0.51818085, - 0.49321797, - 0.45877792, - 0.44787865, - 0.44787865, - 0.43565841, - 0.41879785, - 0.41346175, - ], - 1e-6, - ) + for var_name, (expected, units) in expected_scalar_values.items(): + with self.subTest(var=var_name): + actual = prob.get_val(var_name, units=units) + if actual.size > 1: + actual = actual[-1] + assert_near_equal(actual, expected, 1.0e-7) + + # Check battery state-of-charge over mission + soc_cruise1_var = f'traj.cruise1.timeseries.{av.Dynamic.Vehicle.BATTERY_STATE_OF_CHARGE}' + soc_cruise2_var = f'traj.cruise2.timeseries.{av.Dynamic.Vehicle.BATTERY_STATE_OF_CHARGE}' + + expected_soc_values = { + soc_cruise1_var: ( + [ + 0.9999957806265609, + 0.9877977965127587, + 0.9709679386644526, + 0.9656415564989798, + 0.9656415564989798, + 0.9407334200010511, + 0.906368905403873, + 0.8954935308182879, + 0.8954935308182879, + 0.8658878715348804, + 0.8250432325811342, + 0.8121172826623315, + 0.8121172826623315, + 0.7872227709968601, + 0.752877052113554, + 0.7420076253955674, + 0.7420076253955674, + 0.729820851948888, + 0.7130064610124855, + 0.7076849737044318, + ], + None, + ), + soc_cruise2_var: ( + [ + 0.7076849737044318, + 0.6954996910206249, + 0.6786873568305393, + 0.6733665204268243, + 0.6733665204268243, + 0.6484843167073028, + 0.6141555768684874, + 0.6032915231366659, + 0.6032915231366659, + 0.5737166802223987, + 0.532914551421322, + 0.5200020533696793, + 0.5200020533696793, + 0.49513344752347366, + 0.460823466208866, + 0.44996534857628184, + 0.44996534857628184, + 0.4377912370406455, + 0.4209942619176692, + 0.41567827333310314, + ], + None, + ), + } + + for var_name, (expected, units) in expected_soc_values.items(): + with self.subTest(var=var_name): + actual = prob.get_val(var_name).ravel() + assert_near_equal(actual, expected, 1e-6) @unittest.skip('Not converging in CI for unknown reasons - requires additional investigation') @require_pyoptsparse(optimizer='SNOPT') def test_subsystems_in_a_mission_2dof(self): + """ + Does not actually use electric power but tests that externally added states are successfully + added to all mission phases. + """ phase_info = deepcopy(twodof_phase_info) prob = av.AviaryProblem(verbosity=0) @@ -207,4 +217,5 @@ def test_subsystems_in_a_mission_2dof(self): if __name__ == '__main__': # unittest.main() test = TestBatteryMission() - test.test_subsystems_in_a_mission_2dof() + test.setUp() + test.test_subsystems_in_a_mission() diff --git a/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py b/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py index 5a45008bdf..7ae7e3ebcb 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py @@ -32,7 +32,7 @@ def check_values(self, prob): (Mission.GROSS_MASS, 'lbm'): 171414.17171104, (Mission.OPERATING_MASS, 'lbm'): 94986.583699, (Mission.TOTAL_FUEL, 'lbm'): 40451.68735078, - (Mission.Landing.GROUND_DISTANCE, 'ft'): 2657.88663983, + (Mission.Landing.GROUND_DISTANCE, 'ft'): 2655.0906835, (Mission.RANGE, 'NM'): 3675.0, (Mission.FINAL_MASS, 'lbm'): 136087.98897716, } @@ -58,8 +58,6 @@ def test_bench_GwGm_IPOPT(self): self.assertTrue(prob.result.success) - rtol = 1e-3 - # There are no truth values for these. self.check_values(prob) @@ -75,8 +73,6 @@ def test_bench_GwGm_SNOPT(self): self.assertTrue(prob.result.success) - rtol = 1e-3 - # There are no truth values for these. self.check_values(prob) @@ -111,7 +107,7 @@ def test_bench_GwGm_IPOPT_Breguet_Cruise(self): if __name__ == '__main__': - # unittest.main() - test = ProblemPhaseTestCase() - test.setUp() - test.test_bench_GwGm_IPOPT_Breguet_Cruise() + unittest.main() + # test = ProblemPhaseTestCase() + # test.setUp() + # test.test_bench_GwGm_IPOPT_Breguet_Cruise() diff --git a/aviary/validation_cases/benchmark_tests/test_bench_electrified_large_turboprop_freighter.py b/aviary/validation_cases/benchmark_tests/test_bench_electric_large_turboprop_freighter.py similarity index 60% rename from aviary/validation_cases/benchmark_tests/test_bench_electrified_large_turboprop_freighter.py rename to aviary/validation_cases/benchmark_tests/test_bench_electric_large_turboprop_freighter.py index d31a40399f..c9733c1b66 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_electrified_large_turboprop_freighter.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_electric_large_turboprop_freighter.py @@ -2,7 +2,6 @@ from copy import deepcopy from numpy.testing import assert_almost_equal -from openmdao.utils.testing_utils import use_tempdirs from openmdao.utils.testing_utils import require_pyoptsparse, use_tempdirs from aviary.core.aviary_problem import AviaryProblem @@ -13,15 +12,15 @@ from aviary.subsystems.energy.battery_builder import BatteryBuilder from aviary.subsystems.propulsion.motor.motor_builder import MotorBuilder from aviary.subsystems.propulsion.turboprop_model import TurbopropModel -from aviary.utils.process_input_decks import create_vehicle from aviary.utils.functions import get_path -from aviary.variable_info.variables import Aircraft, Mission, Settings +from aviary.utils.process_input_decks import create_vehicle +from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings -@use_tempdirs +# @use_tempdirs @require_pyoptsparse(optimizer='IPOPT') # TODO need to add asserts with "truth" values -class LargeElectrifiedTurbopropFreighterBenchmark(unittest.TestCase): +class LargeElectricTurbopropFreighterBenchmark(unittest.TestCase): def build_and_run_problem(self, mission_method): if mission_method == 'energy': phase_info = deepcopy(energy_phase_info) @@ -29,6 +28,9 @@ def build_and_run_problem(self, mission_method): elif mission_method == '2DOF': phase_info = deepcopy(two_dof_phase_info) + # del phase_info['climb'] + # del phase_info['descent'] + # Build problem prob = AviaryProblem(verbosity=0) @@ -36,32 +38,29 @@ def build_and_run_problem(self, mission_method): options, _ = create_vehicle( 'models/aircraft/large_turboprop_freighter/large_turboprop_freighter_GASP.csv' ) + options.set_val(Settings.PROBLEM_TYPE, 'SIZING') if mission_method == 'energy': options.set_val(Settings.EQUATIONS_OF_MOTION, 'energy_state') + options.set_val(Aircraft.CrewPayload.CARGO_MASS, 0, 'lbm') + # set up electric propulsion # TODO make separate input file for electroprop freighter? - scale_factor = 17.77 # target is ~32 kN*m torque - options.set_val(Aircraft.Engine.RPM_DESIGN, 6000, 'rpm') # max RPM of motor map - options.set_val(Aircraft.Engine.FIXED_RPM, 6000, 'rpm') + options.set_val(Aircraft.Engine.SCALE_FACTOR, 1) + options.set_val(Aircraft.Engine.RPM_DESIGN, 6_000, 'rpm') # max RPM of motor map + options.delete(Aircraft.Engine.FIXED_RPM) + # options.set_val(Aircraft.Engine.FIXED_RPM, 6000, 'rpm') # match propeller RPM of gas turboprop options.set_val(Aircraft.Engine.Gearbox.GEAR_RATIO, 5.88) options.set_val(Aircraft.Engine.Gearbox.EFFICIENCY, 1.0) - options.set_val(Aircraft.Engine.SCALE_FACTOR, scale_factor) # 11.87) - options.set_val( - Aircraft.Engine.SCALED_SLS_THRUST, - options.get_val(Aircraft.Engine.REFERENCE_SLS_THRUST, 'lbf') * scale_factor, - 'lbf', - ) - options.set_val(Aircraft.Battery.PACK_ENERGY_DENSITY, 1000, 'kW*h/kg') + options.set_val(Aircraft.Battery.PACK_ENERGY_DENSITY, 1_000, 'W*h/kg') options.set_val( Aircraft.Engine.Motor.DATA_FILE, get_path('electric_motor_1800Nm_6000rpm.csv') ) motor = MotorBuilder( - options=options, name='motor', ) @@ -74,53 +73,58 @@ def build_and_run_problem(self, mission_method): ) prob.load_external_subsystems([electroprop]) - prob.aviary_inputs.set_val(Settings.VERBOSITY, 0) - - # if mission_method == 'energy': - # # FLOPS aero specific stuff? Best guesses for values here - # prob.aviary_inputs.set_val(Mission.Constraints.MAX_MACH, 0.5) - # prob.aviary_inputs.set_val(Aircraft.Wing.AREA, 1744.59, 'ft**2') - # # prob.aviary_inputs.set_val(Aircraft.Wing.ASPECT_RATIO, 10.078) - # prob.aviary_inputs.set_val( - # Aircraft.Wing.THICKNESS_TO_CHORD, 0.1500 - # ) # average between root and chord T/C - # prob.aviary_inputs.set_val(Aircraft.Fuselage.MAX_WIDTH, 4.3, 'm') - # prob.aviary_inputs.set_val(Aircraft.Fuselage.MAX_HEIGHT, 3.95, 'm') - # prob.aviary_inputs.set_val(Aircraft.Fuselage.REF_DIAMETER, 4.125, 'm') - prob.load_external_subsystems([BatteryBuilder()]) prob.check_and_preprocess_inputs() prob.build_model() - prob.add_driver('IPOPT', max_iter=0, verbosity=0) + + prob.add_driver('SNOPT', max_iter=100, verbosity=1) prob.add_design_variables() - # prob.model.add_design_var( - # Aircraft.Engine.SCALE_FACTOR, - # units='unitless', - # lower=1, - # upper=25, - # ref=10, - # ) - prob.add_objective() + prob.model.add_design_var( + Aircraft.Engine.SCALE_FACTOR, + units='unitless', + lower=0.25, + upper=2, + ref=1, + ) + prob.model.add_design_var( + Aircraft.Battery.PACK_MASS, units='lbm', lower=10_000, upper=75_000, ref=10_000 + ) + + final_phase_name = prob.model.regular_phases[-1] + prob.model.add_objective( + f'traj.{final_phase_name}.timeseries.cumulative_electric_energy_used', + index=-1, + ref=1e6, + ) + # prob.add_objective('mass') prob.setup() + # initial guess for pack mass. + prob.set_val(Aircraft.Battery.PACK_MASS, val=25_000.0, units='lbm') + prob.run_aviary_problem() - self.assertTrue(prob.result.success) + + # prob.model.list_vars(units=True, print_arrays=True) + return prob @unittest.skip('Skipping until subsystems with states can be used in 2DOF cruise') def test_bench_2DOF(self): prob = self.build_and_run_problem('2DOF') + self.assertTrue(prob.result.success) # TODO asserts @unittest.skip('Skipping due to convergence issues (possible drag too low in descent?)') def test_bench_energy(self): prob = self.build_and_run_problem('energy') + prob.model.list_vars() + self.assertTrue(prob.result.success) # TODO asserts if __name__ == '__main__': # unittest.main() - test = LargeElectrifiedTurbopropFreighterBenchmark() + test = LargeElectricTurbopropFreighterBenchmark() test.build_and_run_problem('2DOF') diff --git a/aviary/validation_cases/benchmark_tests/test_bench_hybrid_large_turboprop_freighter.py b/aviary/validation_cases/benchmark_tests/test_bench_hybrid_large_turboprop_freighter.py new file mode 100644 index 0000000000..7cea4bd926 --- /dev/null +++ b/aviary/validation_cases/benchmark_tests/test_bench_hybrid_large_turboprop_freighter.py @@ -0,0 +1,149 @@ +import unittest +from copy import deepcopy + +from numpy.testing import assert_almost_equal +from openmdao.utils.testing_utils import require_pyoptsparse, use_tempdirs + +from aviary.core.aviary_problem import AviaryProblem +from aviary.models.aircraft.large_turboprop_freighter.electrified_phase_info import ( + energy_phase_info, + two_dof_phase_info, +) +from aviary.subsystems.energy.battery_builder import BatteryBuilder +from aviary.subsystems.propulsion.motor.motor_builder import MotorBuilder +from aviary.subsystems.propulsion.turboprop_model import TurbopropModel +from aviary.utils.aviary_values import AviaryValues +from aviary.utils.functions import get_path +from aviary.utils.process_input_decks import create_vehicle +from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings + + +# @use_tempdirs +@require_pyoptsparse(optimizer='IPOPT') +# TODO need to add asserts with "truth" values +class LargeHybridTurbopropFreighterBenchmark(unittest.TestCase): + def build_and_run_problem(self, mission_method): + if mission_method == 'energy': + phase_info = deepcopy(energy_phase_info) + + elif mission_method == '2DOF': + phase_info = deepcopy(two_dof_phase_info) + + # del phase_info['climb'] + # del phase_info['descent'] + + # Build problem + prob = AviaryProblem(verbosity=0) + + # load inputs from .csv to build engine + options, _ = create_vehicle( + 'models/aircraft/large_turboprop_freighter/large_turboprop_freighter_GASP.csv' + ) + options.set_val(Settings.PROBLEM_TYPE, 'SIZING') + + if mission_method == 'energy': + options.set_val(Settings.EQUATIONS_OF_MOTION, 'energy_state') + + options.set_val(Aircraft.CrewPayload.CARGO_MASS, 0, 'lbm') + + # build up electroprop + electroprop_options = AviaryValues() + electroprop_options.set_val(Aircraft.Engine.SCALE_FACTOR, 1) + electroprop_options.set_val( + Aircraft.Engine.RPM_DESIGN, 6_000, 'rpm' + ) # max RPM of motor map + # electroprop_options.delete(Aircraft.Engine.FIXED_RPM) + options.set_val(Aircraft.Engine.FIXED_RPM, 6000, 'rpm') + electroprop_options.set_val(Aircraft.Engine.Gearbox.GEAR_RATIO, 5.88) + electroprop_options.set_val(Aircraft.Engine.Gearbox.EFFICIENCY, 1.0) + electroprop_options.set_val(Aircraft.Battery.PACK_ENERGY_DENSITY, 1_000, 'W*h/kg') + + # Setup partial engine set + electroprop_options.set_val(Aircraft.Engine.NUM_ENGINES, 2) + electroprop_options.set_val(Aircraft.Engine.NUM_WING_ENGINES, 2) + electroprop_options.set_val(Aircraft.Engine.WING_LOCATIONS, [0.57]) + + electroprop_options.set_val( + Aircraft.Engine.Motor.DATA_FILE, get_path('electric_motor_1800Nm_6000rpm.csv') + ) + + motor = MotorBuilder( + name='motor', + ) + + electroprop = TurbopropModel( + 'electroprop', options=electroprop_options, shaft_power_model=motor + ) + + # build fuel turboprop + turboprop_options = options.deepcopy() + + # Setup partial engine set + turboprop_options.set_val(Aircraft.Engine.NUM_ENGINES, 2) + turboprop_options.set_val(Aircraft.Engine.NUM_WING_ENGINES, 2) + turboprop_options.set_val(Aircraft.Engine.WING_LOCATIONS, [0.19]) + + turboprop = TurbopropModel('turboprop', options=turboprop_options) + + # load_inputs needs to be updated to accept an already existing aviary options + prob.load_inputs( + options, # "models/aircraft/large_turboprop_freighter/large_turboprop_freighter_GASP.csv", + phase_info, + ) + prob.load_external_subsystems([turboprop, electroprop]) + + prob.load_external_subsystems([BatteryBuilder()]) + + prob.check_and_preprocess_inputs() + + prob.build_model() + + prob.add_driver('SNOPT', max_iter=100, verbosity=1) + prob.add_design_variables() + prob.model.add_design_var( + Aircraft.Engine.SCALE_FACTOR, + units='unitless', + lower=0.25, + upper=2, + ref=1, + ) + prob.model.add_design_var( + Aircraft.Battery.PACK_MASS, units='lbm', lower=10_000, upper=75_000, ref=10_000 + ) + + final_phase_name = prob.model.regular_phases[-1] + prob.model.add_objective( + f'traj.{final_phase_name}.timeseries.cumulative_electric_energy_used', + index=-1, + ref=1e6, + ) + # prob.add_objective('mass') + + prob.setup() + + # initial guess for pack mass. + prob.set_val(Aircraft.Battery.PACK_MASS, val=25_000.0, units='lbm') + + prob.run_aviary_problem() + + # prob.model.list_vars(units=True, print_arrays=True) + return prob + + @unittest.skip('Skipping until subsystems with states can be used in 2DOF cruise') + def test_bench_2DOF(self): + prob = self.build_and_run_problem('2DOF') + self.assertTrue(prob.result.success) + # TODO asserts + + @unittest.skip('Skipping due to convergence issues (possible drag too low in descent?)') + def test_bench_energy(self): + prob = self.build_and_run_problem('energy') + prob.model.list_vars() + self.assertTrue(prob.result.success) + # TODO asserts + + +if __name__ == '__main__': + # unittest.main() + test = LargeHybridTurbopropFreighterBenchmark() + test.build_and_run_problem('energy') diff --git a/aviary/validation_cases/benchmark_tests/test_bench_large_turboprop_freighter.py b/aviary/validation_cases/benchmark_tests/test_bench_large_turboprop_freighter.py index 5227c30591..049c22e412 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_large_turboprop_freighter.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_large_turboprop_freighter.py @@ -15,7 +15,7 @@ @use_tempdirs -@require_pyoptsparse(optimizer='IPOPT') +@require_pyoptsparse(optimizer='SNOPT') # TODO need to add asserts with "truth" values, only verifying no errors here class LargeTurbopropFreighterBenchmark(unittest.TestCase): def build_and_run_problem(self, mission_method): @@ -52,23 +52,27 @@ def build_and_run_problem(self, mission_method): prob.check_and_preprocess_inputs() prob.build_model() - prob.add_driver('IPOPT', max_iter=0, verbosity=0) + prob.add_driver('SNOPT', max_iter=20, verbosity=1) prob.add_design_variables() prob.add_objective() prob.setup() prob.run_aviary_problem() + self.assertTrue(prob.result.success) return prob - # self.assertTrue(prob.result.success) def test_bench_2DOF(self): prob = self.build_and_run_problem('2DOF') + self.assertTrue(prob.result.success) + # TODO asserts @unittest.skip('Skipping due to convergence issues (possible drag too low in descent?)') def test_bench_energy(self): prob = self.build_and_run_problem('energy') + self.assertTrue(prob.result.success) + # TODO asserts diff --git a/aviary/variable_info/functions.py b/aviary/variable_info/functions.py index 886b51d9a6..3ea66a4e8e 100644 --- a/aviary/variable_info/functions.py +++ b/aviary/variable_info/functions.py @@ -606,6 +606,8 @@ def setup_model_options( Aircraft.Engine.FUEL_FLOW_SCALER_LINEAR_TERM, ] opt_names_units = [ + Aircraft.Engine.RPM_DESIGN, + Aircraft.Engine.FIXED_RPM, Aircraft.Engine.REFERENCE_SLS_THRUST, Aircraft.Engine.CONSTANT_FUEL_CONSUMPTION, ] diff --git a/aviary/variable_info/variable_meta_data.py b/aviary/variable_info/variable_meta_data.py index 368e6c9f3a..c759971efd 100644 --- a/aviary/variable_info/variable_meta_data.py +++ b/aviary/variable_info/variable_meta_data.py @@ -274,7 +274,7 @@ 'GASP': 'INGASP.ENGYDEN', 'FLOPS': None, }, - units='kW*h/kg', + units='W*h/kg', desc='specific energy density of the battery pack', default_value=1.0, ) @@ -1962,6 +1962,7 @@ 'engine model or chosen by optimizer. Typically used when pairing a motor or ' 'turboshaft using a fixed operating RPM with a propeller.', multivalue=True, + option=True, ) add_meta_data( @@ -2350,6 +2351,7 @@ units='rpm', desc='the designed output RPM from the engine for fixed-RPM shafts', multivalue=True, + option=True, ) add_meta_data(