diff --git a/.gitignore b/.gitignore index c5ae4b4934..9999cfc686 100644 --- a/.gitignore +++ b/.gitignore @@ -144,8 +144,6 @@ aviary/reports/ #VSCode user settings .vscode/ -#VSCode extensions -.continue/ # Built docs _build/ diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 69f19ca325..7858b27855 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -135,7 +135,7 @@ def configure(self): # Temporarily add extra stuff here, probably patched soon # add a check for traj using hasattr for pre-mission tests. - if mission_method is ENERGY_STATE and hasattr(self, 'traj'): + if hasattr(self, 'traj'): # Set a more appropriate solver for dymos when the phases are linked. if MPI and isinstance(self.traj.phases.linear_solver, om.PETScKrylov): # When any phase is connected with input_initial = True, dymos puts 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..c2797be669 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,39 @@ 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'}, + has_diag_partials=True, + ) + + 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 +99,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 +130,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 +168,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 +193,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..fb7bf5773c 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 97d850cfef..d457c0b157 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/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..3eb0011cc0 100644 --- a/aviary/subsystems/propulsion/motor/test/test_motor_mission.py +++ b/aviary/subsystems/propulsion/motor/test/test_motor_mission.py @@ -6,55 +6,50 @@ 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') 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.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(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 0fa1537f21..fedd97d970 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 @@ -49,12 +48,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, @@ -166,6 +159,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) @@ -187,12 +185,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) @@ -229,13 +234,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' ) @@ -245,9 +252,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) @@ -286,7 +301,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') @@ -294,34 +313,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( @@ -340,21 +370,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 = [0.151665999, 379.164998, 1516.65999] + prop_thrust_expected = total_thrust_expected = [ + 8.20491214, + 2594.01215, + 10376.0486, + ] + electric_power_expected = [0.129792551, 294.946761, 1185.50666] + 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): @@ -393,3 +525,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/validation_cases/benchmark_tests/test_bench_electrified_large_turboprop_freighter.py b/aviary/validation_cases/benchmark_tests/test_bench_electrified_large_turboprop_freighter.py index d31a40399f..86f9412e71 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_electrified_large_turboprop_freighter.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_electrified_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,12 +12,12 @@ 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): @@ -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.build_and_run_problem('2DOF') + 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..f2fcf7cfa8 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 @@ -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 424a958cec..b062e4c1c5 100644 --- a/aviary/variable_info/functions.py +++ b/aviary/variable_info/functions.py @@ -607,6 +607,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 69158dadb6..1821b140a5 100644 --- a/aviary/variable_info/variable_meta_data.py +++ b/aviary/variable_info/variable_meta_data.py @@ -298,7 +298,7 @@ 'FLOPS': None, 'LEAPS1': 'aircraft.inputs.L0_battery.energy_density', }, - units='kW*h/kg', + units='W*h/kg', desc='specific energy density of the battery pack', default_value=1.0, ) @@ -2102,6 +2102,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( @@ -2508,6 +2509,7 @@ units='rpm', desc='the designed output RPM from the engine for fixed-RPM shafts', multivalue=True, + option=True, ) add_meta_data(