From da4eeafd21b5a0ef13ba71e144522e317174c988 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Wed, 4 Mar 2026 11:01:14 -0500 Subject: [PATCH 01/29] fixes for electric test case --- aviary/mission/two_dof/ode/landing_ode.py | 49 +++++++++++++------ .../electrified_phase_info.py | 24 +++++---- ...h_electrified_large_turboprop_freighter.py | 1 - .../test_bench_large_turboprop_freighter.py | 2 +- 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/aviary/mission/two_dof/ode/landing_ode.py b/aviary/mission/two_dof/ode/landing_ode.py index 39bce265fb..bbe1b120d9 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, @@ -34,13 +35,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, @@ -64,14 +91,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), @@ -97,11 +119,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) @@ -139,7 +157,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=[ @@ -159,7 +177,6 @@ def setup(self): aero_builder.build_mission(num_nodes=1, aviary_inputs=aviary_options, **kwargs), 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/models/aircraft/large_turboprop_freighter/electrified_phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py index ec9e6dd0ba..9be4677d11 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py @@ -1,11 +1,8 @@ -from aviary.variable_info.enums import SpeedType, 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'}}, 'user_options': { @@ -246,18 +243,19 @@ 'throttle': ([0.956, 0.956], 'unitless'), }, }, - 'electric_cruise': { + 'cruise': { 'user_options': { + 'phase_builder': 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': { @@ -269,7 +267,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/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 2cd1b67b57..896ee708a0 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 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 295617f5e2..254bca45d8 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 @@ -59,8 +59,8 @@ def build_and_run_problem(self, mission_method): 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') From 978ad0dc16bd90461ba3881fbf3b78e01eb53659 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Wed, 4 Mar 2026 14:45:50 -0500 Subject: [PATCH 02/29] removed hardcoded design vars from motor model --- .../propulsion/motor/motor_builder.py | 18 +++++++++--------- .../propulsion/test/test_turboprop_model.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/aviary/subsystems/propulsion/motor/motor_builder.py b/aviary/subsystems/propulsion/motor/motor_builder.py index 1ad82d5882..2fe889423b 100644 --- a/aviary/subsystems/propulsion/motor/motor_builder.py +++ b/aviary/subsystems/propulsion/motor/motor_builder.py @@ -84,16 +84,16 @@ def build_mission(self, num_nodes, aviary_inputs): # return constraints - def get_design_vars(self): - DVs = { - Aircraft.Engine.SCALE_FACTOR: { - 'units': 'unitless', - 'lower': 0.001, - 'upper': None, - }, - } + # def get_design_vars(self): + # DVs = { + # Aircraft.Engine.SCALE_FACTOR: { + # 'units': 'unitless', + # 'lower': 0.001, + # 'upper': None, + # }, + # } - return DVs + # return DVs def get_parameters(self): params = { diff --git a/aviary/subsystems/propulsion/test/test_turboprop_model.py b/aviary/subsystems/propulsion/test/test_turboprop_model.py index 47dc3c5081..67d1369e91 100644 --- a/aviary/subsystems/propulsion/test/test_turboprop_model.py +++ b/aviary/subsystems/propulsion/test/test_turboprop_model.py @@ -261,7 +261,7 @@ def test_electroprop(self): motor_model = MotorBuilder() - self.prepare_model(test_points, motor_model, input_rpm=True) + self.prepare_model(test_points, motor_model) 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') From 34c4f0e5efddc0ab970c26d5d0677949e83892f1 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 5 Mar 2026 11:08:46 -0500 Subject: [PATCH 03/29] minor updates to electrified turboprop test case --- ...h_electrified_large_turboprop_freighter.py | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) 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 896ee708a0..723f029f78 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 @@ -40,18 +40,18 @@ def build_and_run_problem(self, mission_method): # set up electric propulsion # TODO make separate input file for electroprop freighter? - scale_factor = 17.77 # target is ~32 kN*m torque + # 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') # 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.Engine.SCALE_FACTOR, scale_factor) # 11.87) + # options.set_val( + # Aircraft.Engine.REFERENCE_SLS_THRUST, + # options.get_val(Aircraft.Engine.SCALED_SLS_THRUST, 'lbf') / scale_factor, + # 'lbf', + # ) options.set_val(Aircraft.Battery.PACK_ENERGY_DENSITY, 1000, 'kW*h/kg') motor = MotorBuilder( @@ -86,22 +86,24 @@ 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', verbosity=0) prob.add_design_variables() - # prob.model.add_design_var( - # Aircraft.Engine.SCALE_FACTOR, - # units='unitless', - # lower=1, - # upper=25, - # ref=10, - # ) + 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=0, upper=10000) prob.add_objective() prob.setup() - prob.run_aviary_problem() self.assertTrue(prob.result.success) + prob.run_aviary_problem() + @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') From 50fa7b754066b7e320f17fa68201771cb6ed9878 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 17 Mar 2026 13:46:22 -0400 Subject: [PATCH 04/29] jason_t --- .../subsystems/propulsion/propeller/hamilton_standard.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aviary/subsystems/propulsion/propeller/hamilton_standard.py b/aviary/subsystems/propulsion/propeller/hamilton_standard.py index b963f44f94..e7c52552b6 100644 --- a/aviary/subsystems/propulsion/propeller/hamilton_standard.py +++ b/aviary/subsystems/propulsion/propeller/hamilton_standard.py @@ -75,6 +75,13 @@ def _unint(xa, ya, x): c4 = (rb * d2 * d3) / (p5 * p3) y = ya[jx1] * c1 + ya[jx1 + 1] * c2 + ya[jx1 + 2] * c3 + ya[jx1 + 3] * c4 + # we don't want y to be an array + try: + y = y[0] + # floats/ints will give TypeError, numpy versions give IndexError + except (TypeError, IndexError): + pass + return y, extrap_flag From 13bdce1e13c11551244df221e6568b7e9c4e0394 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 17 Mar 2026 13:50:55 -0400 Subject: [PATCH 05/29] cleanup --- .../subsystems/propulsion/propeller/hamilton_standard.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/aviary/subsystems/propulsion/propeller/hamilton_standard.py b/aviary/subsystems/propulsion/propeller/hamilton_standard.py index e7c52552b6..b963f44f94 100644 --- a/aviary/subsystems/propulsion/propeller/hamilton_standard.py +++ b/aviary/subsystems/propulsion/propeller/hamilton_standard.py @@ -75,13 +75,6 @@ def _unint(xa, ya, x): c4 = (rb * d2 * d3) / (p5 * p3) y = ya[jx1] * c1 + ya[jx1 + 1] * c2 + ya[jx1 + 2] * c3 + ya[jx1 + 3] * c4 - # we don't want y to be an array - try: - y = y[0] - # floats/ints will give TypeError, numpy versions give IndexError - except (TypeError, IndexError): - pass - return y, extrap_flag From 63a8d360a63ec955b2ee95b0b784b96d45fe8958 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 17 Mar 2026 14:31:50 -0400 Subject: [PATCH 06/29] debugging --- aviary/models/aircraft/large_turboprop_freighter/phase_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index 3b6dc1c414..e1fab66ae8 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py @@ -203,7 +203,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'), From 03e1d67e0519497996590b70db50535c587581bc Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 17 Mar 2026 16:48:52 -0400 Subject: [PATCH 07/29] debugging --- .../large_turboprop_freighter_GASP.csv | 2 +- .../aircraft/large_turboprop_freighter/phase_info.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) 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 d918e3cad6..ec93ef5ec7 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 @@ -49,7 +49,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 e1fab66ae8..d055956b4f 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py @@ -141,6 +141,7 @@ 'velocity_ref': (200, 'kn'), 'velocity_ref0': (0, 'kn'), 'time_duration_ref': (10, 's'), + 'time_duration_bounds': ((5.0, 1000.0), 's'), 'mass_bounds': ((0, None), 'lbm'), 'mass_ref': (150_000, 'lbm'), 'mass_defect_ref': (150_000, 'lbm'), @@ -148,8 +149,8 @@ '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'), @@ -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': { From 602e985f4c6117fd8298c2dab8ec02244ee0f583 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Thu, 19 Mar 2026 08:39:45 -0400 Subject: [PATCH 08/29] Debugging --- aviary/mission/two_dof_problem_configurator.py | 6 +++++- .../large_turboprop_freighter_GASP.csv | 1 + .../models/aircraft/large_turboprop_freighter/phase_info.py | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/aviary/mission/two_dof_problem_configurator.py b/aviary/mission/two_dof_problem_configurator.py index 917a4decb4..d3bebb2321 100644 --- a/aviary/mission/two_dof_problem_configurator.py +++ b/aviary/mission/two_dof_problem_configurator.py @@ -529,7 +529,11 @@ def link_phases(self, aviary_group, phases, connect_directly=True): ('initial_time_slack', Mission.Takeoff.ASCENT_T_INITIAL), ], ) - aviary_group.add_constraint('ascent_initial_time_slack_constraint.con_val', ref=30.0) + aviary_group.add_constraint( + 'ascent_initial_time_slack_constraint.con_val', + equals=0.0, + ref=30.0, + ) # imitate input_initial for taxi -> groundroll eq = aviary_group.add_subsystem('taxi_groundroll_mass_constraint', om.EQConstraintComp()) 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 ec93ef5ec7..85aefe3a29 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 @@ -10,6 +10,7 @@ settings:mass_method, GASP # AIRCRAFT # ############ 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 diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index d055956b4f..e5944de616 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py @@ -141,7 +141,7 @@ 'velocity_ref': (200, 'kn'), 'velocity_ref0': (0, 'kn'), 'time_duration_ref': (10, 's'), - 'time_duration_bounds': ((5.0, 1000.0), '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'), @@ -153,7 +153,7 @@ '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'), @@ -254,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'), From 2d240432ce3d15597ccc3a1ca89d6a36a2a08ff4 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 24 Mar 2026 09:04:05 -0400 Subject: [PATCH 09/29] Lower drag for the turboprop problem --- .../large_turboprop_freighter_GASP.csv | 6 ++++-- .../benchmark_tests/test_bench_large_turboprop_freighter.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) 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 3a57f5f92b..230bc42877 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,8 +9,10 @@ settings:mass_method, GASP ############ # AIRCRAFT # ############ -aircraft:design:subsonic_drag_coeff_factor, 1.447, unitless -# aircraft:design:subsonic_drag_coeff_factor, 1.33, 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 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 254bca45d8..8a7a89a659 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,7 +52,7 @@ 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('IPOPT', max_iter=100, verbosity=0) prob.add_design_variables() prob.add_objective() prob.setup() From e2dbb965d0b8b32b60beb185eddc9d56280c5221 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 3 Apr 2026 15:55:21 -0400 Subject: [PATCH 10/29] updates for latest merge --- .gitignore | 2 -- .../propulsion/motor/motor_builder.py | 14 ++++++++++++++ .../subsystems/propulsion/turboprop_model.py | 10 +++++----- ...h_electrified_large_turboprop_freighter.py | 19 ++++++++++--------- .../test_bench_large_turboprop_freighter.py | 6 +++++- 5 files changed, 34 insertions(+), 17 deletions(-) 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/subsystems/propulsion/motor/motor_builder.py b/aviary/subsystems/propulsion/motor/motor_builder.py index 1850e4cc0c..d306239462 100644 --- a/aviary/subsystems/propulsion/motor/motor_builder.py +++ b/aviary/subsystems/propulsion/motor/motor_builder.py @@ -94,6 +94,20 @@ def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_option # return DVs + # def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options=None): + # if Aircraft.Engine.FIXED_RPM not in aviary_inputs: + # controls_dict = { + # Dynamic.Vehicle.Propulsion.RPM: { + # 'units': 'RPM', + # 'lower': 0.0, + # 'upper': 6000, + # 'control_type': 'polynomial', + # 'order': 3, + # 'opt': True, + # }, + # } + # return controls_dict + def get_parameters(self, aviary_inputs=None, user_options=None, subsystem_options=None): params = { Aircraft.Engine.SCALE_FACTOR: { diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index ca4ea5e9e0..96305175fe 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -123,7 +123,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 +131,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,7 +141,7 @@ 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=['*'], ) @@ -571,7 +571,7 @@ 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) @@ -596,7 +596,7 @@ def configure(self): shp_input_list.remove(Dynamic.Vehicle.Propulsion.RPM) else: rpm_ivc.add_output( - 'AIRCRAFT_DATA_OVERRIDE:' + Dynamic.Vehicle.Propulsion.RPM, 1.0, units='rpm' + 'INTERNAL_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 in shp_output_list: 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 1e1b3b97f1..77cfe9dfb9 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 @@ -12,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): @@ -28,6 +28,10 @@ def build_and_run_problem(self, mission_method): elif mission_method == '2DOF': phase_info = deepcopy(two_dof_phase_info) + # remove descent until RPM can be controlled for that phase + del phase_info['desc1'] + del phase_info['desc2'] + # Build problem prob = AviaryProblem(verbosity=0) @@ -60,7 +64,6 @@ def build_and_run_problem(self, mission_method): ) motor = MotorBuilder( - options=options, name='motor', ) @@ -73,8 +76,6 @@ 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) @@ -92,7 +93,7 @@ def build_and_run_problem(self, mission_method): prob.check_and_preprocess_inputs() prob.build_model() - prob.add_driver('SNOPT', verbosity=0) + prob.add_driver('SNOPT', verbosity=1) prob.add_design_variables() prob.model.add_design_var( Aircraft.Engine.SCALE_FACTOR, @@ -106,18 +107,18 @@ def build_and_run_problem(self, mission_method): prob.setup() - self.assertTrue(prob.result.success) - prob.run_aviary_problem() @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') + self.assertTrue(prob.result.success) # TODO asserts 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 26b0fd734e..d807facc1d 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,7 +52,7 @@ def build_and_run_problem(self, mission_method): prob.check_and_preprocess_inputs() prob.build_model() - prob.add_driver('IPOPT', max_iter=100, verbosity=0) + prob.add_driver('IPOPT', max_iter=100, verbosity=1) prob.add_design_variables() prob.add_objective() prob.setup() @@ -64,11 +64,15 @@ def build_and_run_problem(self, mission_method): 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 From 8d3b18b892ee42525c29f70c42b3f270b4062e5d Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 3 Apr 2026 16:14:00 -0400 Subject: [PATCH 11/29] moved motor data file to correct location --- .../{ => engines}/motors/electric_motor_1800Nm_6000rpm.csv | 0 .../test_bench_electrified_large_turboprop_freighter.py | 2 +- .../benchmark_tests/test_bench_large_turboprop_freighter.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename aviary/models/{ => engines}/motors/electric_motor_1800Nm_6000rpm.csv (100%) 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/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 77cfe9dfb9..07839ba2c0 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 @@ -93,7 +93,7 @@ def build_and_run_problem(self, mission_method): prob.check_and_preprocess_inputs() prob.build_model() - prob.add_driver('SNOPT', verbosity=1) + prob.add_driver('SNOPT', max_iter=20, verbosity=1) prob.add_design_variables() prob.model.add_design_var( Aircraft.Engine.SCALE_FACTOR, 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 d807facc1d..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,7 +52,7 @@ def build_and_run_problem(self, mission_method): prob.check_and_preprocess_inputs() prob.build_model() - prob.add_driver('IPOPT', max_iter=100, verbosity=1) + prob.add_driver('SNOPT', max_iter=20, verbosity=1) prob.add_design_variables() prob.add_objective() prob.setup() From d8b557ce3c61db92c177ab0981a0a3ab1c3dffcd Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 7 Apr 2026 13:03:17 -0400 Subject: [PATCH 12/29] A few small fixes. --- aviary/core/aviary_group.py | 2 +- aviary/mission/two_dof/ode/simple_cruise_ode.py | 4 ++-- aviary/subsystems/propulsion/motor/model/motor_map.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 9402c37a20..c6f521e9f8 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/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/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), From a9ab29324e53e95154c5b4af54c5be917ae120aa Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 8 Apr 2026 10:31:27 -0400 Subject: [PATCH 13/29] Added pack initial guess. --- .../test_bench_electrified_large_turboprop_freighter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 07839ba2c0..4119d61e46 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 @@ -102,11 +102,14 @@ def build_and_run_problem(self, mission_method): upper=2, ref=1, ) - prob.model.add_design_var(Aircraft.Battery.PACK_MASS, units='lbm', lower=0, upper=10000) + prob.model.add_design_var(Aircraft.Battery.PACK_MASS, units='lbm', lower=10, upper=10000) prob.add_objective() prob.setup() + # initial guess for pack mass. + prob.set_val(Aircraft.Battery.PACK_MASS, val=1000.0, units='lbm') + prob.run_aviary_problem() @unittest.skip('Skipping until subsystems with states can be used in 2DOF cruise') From 87f3c42d14f2c00d602708d19087caad12229c3d Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 13 Apr 2026 10:33:07 -0400 Subject: [PATCH 14/29] typehints for subsystembuilder --- aviary/subsystems/subsystem_builder.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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. From 9e8d05fec62384beb5b786682da4dfcf8d9a1c41 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 13 Apr 2026 11:20:01 -0400 Subject: [PATCH 15/29] builder updates --- aviary/interface/reports.py | 5 +- .../propulsion/gearbox/gearbox_builder.py | 6 +- .../propulsion/motor/motor_builder.py | 43 ++- .../propulsion/propeller/propeller_builder.py | 6 +- .../propulsion/test/test_turboprop_model.py | 150 +++++-- .../subsystems/propulsion/turboprop_model.py | 365 ++++++++++++++++-- aviary/subsystems/subsystem_builder.py | 2 +- ...h_electrified_large_turboprop_freighter.py | 3 + 8 files changed, 497 insertions(+), 83 deletions(-) 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/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/motor_builder.py b/aviary/subsystems/propulsion/motor/motor_builder.py index d306239462..7d27f814a7 100644 --- a/aviary/subsystems/propulsion/motor/motor_builder.py +++ b/aviary/subsystems/propulsion/motor/motor_builder.py @@ -94,19 +94,26 @@ def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_option # return DVs - # def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options=None): - # if Aircraft.Engine.FIXED_RPM not in aviary_inputs: - # controls_dict = { - # Dynamic.Vehicle.Propulsion.RPM: { - # 'units': 'RPM', - # 'lower': 0.0, - # 'upper': 6000, - # 'control_type': 'polynomial', - # 'order': 3, - # 'opt': True, - # }, - # } - # return controls_dict + 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') + else: + rpm_limit = 6000 + controls_dict = { + Dynamic.Vehicle.Propulsion.RPM: { + 'units': 'rpm', + 'lower': 0.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 = { @@ -138,7 +145,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): """ @@ -150,8 +157,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/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..c1682cecad 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,9 +185,17 @@ 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) + + 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. @@ -229,7 +235,11 @@ 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') @@ -245,9 +255,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 +304,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,14 +316,23 @@ 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) + + 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. @@ -317,6 +348,11 @@ def test_electroprop(self): shp_file = get_path('electric_motor_1800Nm_6000rpm.csv') options.set_val(Aircraft.Engine.Motor.DATA_FILE, shp_file) + options.set_val( + Aircraft.Engine.FIXED_RPM, + 1455.13090827, + units='rpm', + ) self.prepare_model(options, test_points, shp_model=MotorBuilder(), input_rpm=True) @@ -340,15 +376,75 @@ 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_control_rpm_electoprop(self): + # based on test_electroprop, but simulating RPM as a dymos control + test_points = [(0.6, 25000, 1), (0.6, 25000, 1), (0.6, 25000, 1)] + + options = get_option_defaults() + + shp_file = get_path('electric_motor_1800Nm_6000rpm.csv') + options.set_val(Aircraft.Engine.Motor.DATA_FILE, shp_file) + + 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') + + 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() + + shp_expected = [183.91156919, 275.86735378, 367.82313837] + prop_thrust_expected = total_thrust_expected = [87.15691683, 164.22051903, 184.42047241] + electric_power_expected = [164.66385039, 233.10905408, 303.31010941] + + expected_values = { + Dynamic.Vehicle.Propulsion.RPM: ( + rpm_control, + 'rpm', + 1e-8, + ), + 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, + ), + } - 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) + 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. diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index 96305175fe..545f14cdbb 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__( @@ -233,7 +246,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 +259,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 +272,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_states( + 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 +415,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 +472,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_states( + 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_states( + 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_states( + 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_states( + 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): + if self.shaft_power_model is not None: + self.shaft_power_model.report(prob, reports_folder) + if self.gearbox_model is not None: + self.gearbox_model.report(prob, reports_folder) + if self.propeller_model is not None: + self.propeller_model.report(prob, reports_folder) + class TurbopropMission(om.Group): def initialize(self): @@ -331,9 +591,21 @@ def setup(self): kwargs = self.options['kwargs'] 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: @@ -556,7 +828,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') @@ -576,39 +851,73 @@ def configure(self): ) 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( - 'INTERNAL_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) + else: + self.connect( + 'rpm_source.RPM', + f'{gearbox_model.name}.{Dynamic.Vehicle.Propulsion.RPM}_in', + ) gearbox_input_list.remove(Dynamic.Vehicle.Propulsion.RPM + '_in') + if 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: shp_inputs.append(var) diff --git a/aviary/subsystems/subsystem_builder.py b/aviary/subsystems/subsystem_builder.py index 3c9608cacb..1ec8f0434a 100644 --- a/aviary/subsystems/subsystem_builder.py +++ b/aviary/subsystems/subsystem_builder.py @@ -484,7 +484,7 @@ def build_post_mission( """ return None - def report(self, prob, reports_folder, **kwargs): + def report(self, prob, reports_folder): """ Generates report file for this subsystem. If this subsystem doesn't need a report, do nothing. 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 4119d61e46..0aff32764e 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 @@ -111,6 +111,9 @@ def build_and_run_problem(self, mission_method): prob.set_val(Aircraft.Battery.PACK_MASS, val=1000.0, units='lbm') prob.run_aviary_problem() + import openmdao.api as om + + om.n2(prob, show_browser=False) @unittest.skip('Skipping until subsystems with states can be used in 2DOF cruise') def test_bench_2DOF(self): From 90d18e0abf6236b6b594e156af88c891c329cf81 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 13 Apr 2026 11:41:32 -0400 Subject: [PATCH 16/29] returned kwargs to reports --- aviary/subsystems/propulsion/turboprop_model.py | 8 ++++---- aviary/subsystems/subsystem_builder.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index 545f14cdbb..fcc0a78fdd 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -561,13 +561,13 @@ def get_post_mission_bus_variables(self, aviary_inputs=None, mission_info=None): ) return new_bus - def report(self, prob, reports_folder): + def report(self, prob, reports_folder, **kwargs): if self.shaft_power_model is not None: - self.shaft_power_model.report(prob, reports_folder) + self.shaft_power_model.report(prob, reports_folder, **kwargs) if self.gearbox_model is not None: - self.gearbox_model.report(prob, reports_folder) + self.gearbox_model.report(prob, reports_folder, **kwargs) if self.propeller_model is not None: - self.propeller_model.report(prob, reports_folder) + self.propeller_model.report(prob, reports_folder, **kwargs) class TurbopropMission(om.Group): diff --git a/aviary/subsystems/subsystem_builder.py b/aviary/subsystems/subsystem_builder.py index 1ec8f0434a..3c9608cacb 100644 --- a/aviary/subsystems/subsystem_builder.py +++ b/aviary/subsystems/subsystem_builder.py @@ -484,7 +484,7 @@ def build_post_mission( """ return None - def report(self, prob, reports_folder): + def report(self, prob, reports_folder, **kwargs): """ Generates report file for this subsystem. If this subsystem doesn't need a report, do nothing. From d63a59501b3e79c1d37aac76abd844791d29dc96 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 13 Apr 2026 14:55:55 -0400 Subject: [PATCH 17/29] rpm control electroprop wip --- aviary/subsystems/performance/performance_builder.py | 2 +- aviary/subsystems/propulsion/motor/motor_builder.py | 4 ++-- .../test_bench_electrified_large_turboprop_freighter.py | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) 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/motor/motor_builder.py b/aviary/subsystems/propulsion/motor/motor_builder.py index 7d27f814a7..bf1616d258 100644 --- a/aviary/subsystems/propulsion/motor/motor_builder.py +++ b/aviary/subsystems/propulsion/motor/motor_builder.py @@ -99,14 +99,14 @@ def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options= 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') + 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': 0.0, - 'upper': rpm_limit, + 'upper': 6000, 'control_type': 'polynomial', 'order': 3, 'opt': True, 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 0aff32764e..c7fe87bfc7 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 @@ -39,6 +39,7 @@ 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') @@ -47,7 +48,8 @@ def build_and_run_problem(self, mission_method): # 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.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) @@ -93,6 +95,7 @@ def build_and_run_problem(self, mission_method): prob.check_and_preprocess_inputs() prob.build_model() + prob.add_driver('SNOPT', max_iter=20, verbosity=1) prob.add_design_variables() prob.model.add_design_var( @@ -106,7 +109,9 @@ def build_and_run_problem(self, mission_method): prob.add_objective() prob.setup() + import openmdao.api as om + om.n2(prob, show_browser=False) # initial guess for pack mass. prob.set_val(Aircraft.Battery.PACK_MASS, val=1000.0, units='lbm') From af211efd6ebf3b70f664b8dcb4f446c12dd2d126 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Tue, 14 Apr 2026 12:16:01 -0400 Subject: [PATCH 18/29] debugging motor RPM control --- .../propulsion/motor/motor_builder.py | 29 ++++++++++++------- ...h_electrified_large_turboprop_freighter.py | 11 +++++-- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/aviary/subsystems/propulsion/motor/motor_builder.py b/aviary/subsystems/propulsion/motor/motor_builder.py index bf1616d258..53e21dcd64 100644 --- a/aviary/subsystems/propulsion/motor/motor_builder.py +++ b/aviary/subsystems/propulsion/motor/motor_builder.py @@ -105,8 +105,8 @@ def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options= controls_dict = { Dynamic.Vehicle.Propulsion.RPM: { 'units': 'rpm', - 'lower': 0.0, - 'upper': 6000, + 'lower': 1.0, + 'upper': rpm_limit, 'control_type': 'polynomial', 'order': 3, 'opt': True, @@ -125,16 +125,23 @@ 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 - # }, - # } + def get_initial_guesses(self, aviary_inputs=None, user_options=None, subsystem_options=None): + 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 + return initial_guess_dict def get_mass_names(self, aviary_inputs=None): """ 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 c7fe87bfc7..d84795710b 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 @@ -29,8 +29,10 @@ def build_and_run_problem(self, mission_method): phase_info = deepcopy(two_dof_phase_info) # remove descent until RPM can be controlled for that phase - del phase_info['desc1'] - del phase_info['desc2'] + # del phase_info['desc1'] + # del phase_info['desc2'] + + del phase_info['descent'] # Build problem prob = AviaryProblem(verbosity=0) @@ -109,6 +111,7 @@ def build_and_run_problem(self, mission_method): prob.add_objective() prob.setup() + prob.final_setup() import openmdao.api as om om.n2(prob, show_browser=False) @@ -119,6 +122,7 @@ def build_and_run_problem(self, mission_method): import openmdao.api as om om.n2(prob, show_browser=False) + return prob @unittest.skip('Skipping until subsystems with states can be used in 2DOF cruise') def test_bench_2DOF(self): @@ -129,6 +133,7 @@ def test_bench_2DOF(self): @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 @@ -136,4 +141,4 @@ def test_bench_energy(self): if __name__ == '__main__': # unittest.main() test = LargeElectrifiedTurbopropFreighterBenchmark() - test.build_and_run_problem('2DOF') + test.build_and_run_problem('energy') From e3bd33604034e83f73aed9cff856c4a9e44bc7aa Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 14 Apr 2026 14:14:45 -0400 Subject: [PATCH 19/29] Fixed a couple of bugs --- aviary/subsystems/propulsion/turboprop_model.py | 10 +++++----- aviary/utils/math.py | 6 +++++- ...test_bench_electrified_large_turboprop_freighter.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index fcc0a78fdd..9688374082 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -310,7 +310,7 @@ def get_states(self, aviary_inputs=None, user_options=None, subsystem_options=No return states def get_controls(self, aviary_inputs=None, user_options=None, subsystem_options=None): - controls = super().get_states( + controls = super().get_controls( aviary_inputs=aviary_inputs, user_options=user_options, subsystem_options=subsystem_options, @@ -473,14 +473,14 @@ def get_design_vars(self, aviary_inputs=None): return desvars def get_initial_guesses(self, aviary_inputs=None, user_options=None, subsystem_options=None): - guesses = super().get_states( + 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_states( + extra_guesses = self.shaft_power_model.get_initial_guesses( aviary_inputs=aviary_inputs, user_options=user_options, subsystem_options=subsystem_options, @@ -488,7 +488,7 @@ def get_initial_guesses(self, aviary_inputs=None, user_options=None, subsystem_o guesses.update(extra_guesses) if self.gearbox_model is not None: - extra_guesses = self.gearbox_model.get_states( + extra_guesses = self.gearbox_model.get_initial_guesses( aviary_inputs=aviary_inputs, user_options=user_options, subsystem_options=subsystem_options, @@ -496,7 +496,7 @@ def get_initial_guesses(self, aviary_inputs=None, user_options=None, subsystem_o guesses.update(extra_guesses) if self.propeller_model is not None: - extra_guesses = self.propeller_model.get_states( + extra_guesses = self.propeller_model.get_initial_guesses( aviary_inputs=aviary_inputs, user_options=user_options, subsystem_options=subsystem_options, 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 d84795710b..e5bc411559 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 @@ -32,7 +32,7 @@ def build_and_run_problem(self, mission_method): # del phase_info['desc1'] # del phase_info['desc2'] - del phase_info['descent'] + # del phase_info['descent'] # Build problem prob = AviaryProblem(verbosity=0) From a86057edf8b7761b62a32487cfebc614589730f6 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 14 Apr 2026 16:34:44 -0400 Subject: [PATCH 20/29] Fix an indentation error in the Hamilton Standard interps that affected extrapolation. --- .../propulsion/propeller/hamilton_standard.py | 197 +++++++++--------- ...h_electrified_large_turboprop_freighter.py | 5 +- 2 files changed, 102 insertions(+), 100 deletions(-) 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/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 e5bc411559..460bb28121 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 @@ -98,7 +98,7 @@ def build_and_run_problem(self, mission_method): prob.build_model() - prob.add_driver('SNOPT', max_iter=20, verbosity=1) + prob.add_driver('SNOPT', max_iter=50, verbosity=1) prob.add_design_variables() prob.model.add_design_var( Aircraft.Engine.SCALE_FACTOR, @@ -121,7 +121,8 @@ def build_and_run_problem(self, mission_method): prob.run_aviary_problem() import openmdao.api as om - om.n2(prob, show_browser=False) + # om.n2(prob, show_browser=False) + # prob.model.list_vars(units=True, print_arrays=True) return prob @unittest.skip('Skipping until subsystems with states can be used in 2DOF cruise') From da484f59fbaa09746d579fe37e030a29c904244c Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Tue, 14 Apr 2026 16:41:59 -0400 Subject: [PATCH 21/29] propeller RPM connection bug fixed --- aviary/subsystems/propulsion/turboprop_model.py | 11 ++++++++++- ...est_bench_electrified_large_turboprop_freighter.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index 9688374082..5918bfe410 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -911,7 +911,16 @@ def configure(self): gearbox_input_list.remove(Dynamic.Vehicle.Propulsion.RPM + '_in') - if Dynamic.Vehicle.Propulsion.RPM in propeller_input_list: + # 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}', 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 e5bc411559..8c82805500 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 @@ -104,7 +104,7 @@ def build_and_run_problem(self, mission_method): Aircraft.Engine.SCALE_FACTOR, units='unitless', lower=0.25, - upper=2, + upper=5, ref=1, ) prob.model.add_design_var(Aircraft.Battery.PACK_MASS, units='lbm', lower=10, upper=10000) From c1ce2bc61688819035888f169b35cf03228b9f8a Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 16 Apr 2026 13:01:20 -0400 Subject: [PATCH 22/29] RPM & throttle pairing in motor model --- .../electrified_phase_info.py | 6 + .../propulsion/motor/model/motor_mission.py | 54 ++++++-- .../propulsion/motor/motor_builder.py | 74 +++++------ .../motor/test/test_motor_mission.py | 9 +- .../propulsion/test/test_turboprop_model.py | 115 ++++++++++++------ .../subsystems/propulsion/turboprop_model.py | 17 ++- ...h_electrified_large_turboprop_freighter.py | 45 ++----- aviary/variable_info/functions.py | 2 + aviary/variable_info/variable_meta_data.py | 2 + 9 files changed, 194 insertions(+), 130 deletions(-) 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 3b6dc1c414..82036ca5d6 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py @@ -17,12 +17,14 @@ 'no_descent': True, 'mass_ref': (154000, 'lbm'), 'throttle_enforcement': 'control', + 'throttle_bounds': ((0.001, 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': { @@ -38,12 +40,14 @@ 'altitude_bounds': ((20_000.0, 22_000.0), 'ft'), 'mass_ref': (154000, 'lbm'), 'throttle_enforcement': 'control', + 'throttle_bounds': ((0.001, 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': { @@ -62,9 +66,11 @@ 'mass_ref': (154000, 'lbm'), 'no_climb': True, 'throttle_enforcement': 'control', + 'throttle_bounds': ((0.001, 1), 'unitless'), 'time_initial_bounds': ((80, 1056.5), 'min'), 'time_duration_bounds': ((29.0, 128.0), 'min'), }, + 'initial_guesses': {'throttle': ([0.1, 0.1], 'unitless')}, }, 'post_mission': { 'include_landing': False, diff --git a/aviary/subsystems/propulsion/motor/model/motor_mission.py b/aviary/subsystems/propulsion/motor/model/motor_mission.py index 336fcaedfa..d1a9696499 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,51 @@ 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'] + rpm_design = self.options[Aircraft.Engine.RPM_DESIGN][0] + fixed_rpm = self.options[Aircraft.Engine.FIXED_RPM][0] ivc = om.IndepVarComp() ivc.add_output('max_throttle', val=np.ones(nn), units='unitless') + ivc.add_output('max_RPM', val=np.ones(nn) * rpm_design, units='rpm') self.add_subsystem('ivc', ivc, promotes=['*']) motor_group = om.Group() + # 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 * throttle', + RPM={'val': np.zeros(nn), 'units': 'rpm'}, + max_RPM={'val': np.zeros(nn), 'units': 'rpm'}, + throttle={'val': np.zeros(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 +63,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 +79,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') @@ -84,7 +124,7 @@ def setup(self): promotes_inputs=[ (Dynamic.Vehicle.Propulsion.THROTTLE, 'max_throttle'), Aircraft.Engine.SCALE_FACTOR, - Dynamic.Vehicle.Propulsion.RPM, + (Dynamic.Vehicle.Propulsion.RPM, 'max_RPM'), ], promotes_outputs=[ ( @@ -98,15 +138,15 @@ def setup(self): motor_group_max.add_subsystem( 'power_comp_max', om.ExecComp( - 'max_power = max_torque * pi * RPM / 30', + 'max_power = max_torque * pi * max_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'}, + max_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), + 'max_RPM', ], promotes_outputs=[('max_power', Dynamic.Vehicle.Propulsion.SHAFT_POWER_MAX)], ) @@ -121,4 +161,4 @@ def setup(self): ], ) - self.set_input_defaults(Dynamic.Vehicle.Propulsion.RPM, val=np.ones(nn), units='rpm') + # 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 53e21dcd64..15750155ea 100644 --- a/aviary/subsystems/propulsion/motor/motor_builder.py +++ b/aviary/subsystems/propulsion/motor/motor_builder.py @@ -94,26 +94,26 @@ def build_mission(self, num_nodes, aviary_inputs, user_options, subsystem_option # 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_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 = { @@ -125,23 +125,23 @@ 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 = {} - 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_initial_guesses(self, aviary_inputs=None, user_options=None, subsystem_options=None): + # 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): """ diff --git a/aviary/subsystems/propulsion/motor/test/test_motor_mission.py b/aviary/subsystems/propulsion/motor/test/test_motor_mission.py index 86baf52314..e38e67d5e9 100644 --- a/aviary/subsystems/propulsion/motor/test/test_motor_mission.py +++ b/aviary/subsystems/propulsion/motor/test/test_motor_mission.py @@ -12,25 +12,26 @@ from aviary.utils.functions import get_path +@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) @@ -47,7 +48,7 @@ def test_motor_mission(self): 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] + max_shp_expected = [1266.69015793, 1266.69015793, 1266.69015793] power = [0.0, 330.3403723894654, 1327.7674611398375] assert_near_equal(torque, torque_expected, tolerance=1e-9) diff --git a/aviary/subsystems/propulsion/test/test_turboprop_model.py b/aviary/subsystems/propulsion/test/test_turboprop_model.py index c1682cecad..0b634e3423 100644 --- a/aviary/subsystems/propulsion/test/test_turboprop_model.py +++ b/aviary/subsystems/propulsion/test/test_turboprop_model.py @@ -197,8 +197,7 @@ def test_case_1(self): 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. + # 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) @@ -244,8 +243,6 @@ def test_case_2(self): 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' ) @@ -334,20 +331,19 @@ def test_case_3(self): 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. + # 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, @@ -356,8 +352,6 @@ def test_electroprop(self): 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( @@ -388,19 +382,20 @@ def test_electroprop(self): 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. + # 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_electoprop(self): - # based on test_electroprop, but simulating RPM as a dymos control - test_points = [(0.6, 25000, 1), (0.6, 25000, 1), (0.6, 25000, 1)] + 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') self.prepare_model(options, test_points, shp_model=MotorBuilder(), input_rpm=True) @@ -412,33 +407,20 @@ def test_control_rpm_electoprop(self): 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() - shp_expected = [183.91156919, 275.86735378, 367.82313837] - prop_thrust_expected = total_thrust_expected = [87.15691683, 164.22051903, 184.42047241] - electric_power_expected = [164.66385039, 233.10905408, 303.31010941] - + 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.RPM: ( - rpm_control, - 'rpm', - 1e-8, - ), 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, - ), + Dynamic.Vehicle.Propulsion.ELECTRIC_POWER_IN: (electric_power_expected, 'kW', 2e-7), } for var_name, (expected, units, tol) in expected_values.items(): @@ -446,11 +428,63 @@ def test_control_rpm_electoprop(self): 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. + # 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) + + 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): @@ -489,3 +523,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 5918bfe410..748e3cd226 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -95,7 +95,7 @@ def __init__( 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, @@ -122,7 +122,7 @@ def needs_mission_solver(self, aviary_inputs, subsystem_options): # 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: + 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 @@ -161,7 +161,9 @@ def build_pre_mission(self, aviary_inputs, subsystem_options) -> om.Group: 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, @@ -815,7 +817,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: @@ -908,8 +914,7 @@ def configure(self): 'rpm_source.RPM', f'{gearbox_model.name}.{Dynamic.Vehicle.Propulsion.RPM}_in', ) - - gearbox_input_list.remove(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 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 ad4609bf57..78c4fa500a 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 @@ -28,12 +28,6 @@ def build_and_run_problem(self, mission_method): elif mission_method == '2DOF': phase_info = deepcopy(two_dof_phase_info) - # remove descent until RPM can be controlled for that phase - # del phase_info['desc1'] - # del phase_info['desc2'] - - # del phase_info['descent'] - # Build problem prob = AviaryProblem(verbosity=0) @@ -48,19 +42,13 @@ def build_and_run_problem(self, mission_method): # 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.SCALE_FACTOR, 2) options.set_val(Aircraft.Engine.RPM_DESIGN, 6000, '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.REFERENCE_SLS_THRUST, - # options.get_val(Aircraft.Engine.SCALED_SLS_THRUST, 'lbf') / scale_factor, - # 'lbf', - # ) options.set_val(Aircraft.Battery.PACK_ENERGY_DENSITY, 1000, 'kW*h/kg') options.set_val( @@ -80,18 +68,6 @@ def build_and_run_problem(self, mission_method): ) prob.load_external_subsystems([electroprop]) - # 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() @@ -100,28 +76,23 @@ def build_and_run_problem(self, mission_method): prob.add_driver('SNOPT', max_iter=50, verbosity=1) prob.add_design_variables() - prob.model.add_design_var( - Aircraft.Engine.SCALE_FACTOR, - units='unitless', - lower=0.25, - upper=5, - ref=1, - ) + # 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, upper=10000) prob.add_objective() prob.setup() - prob.final_setup() - import openmdao.api as om - om.n2(prob, show_browser=False) # initial guess for pack mass. prob.set_val(Aircraft.Battery.PACK_MASS, val=1000.0, units='lbm') prob.run_aviary_problem() - import openmdao.api as om - # om.n2(prob, show_browser=False) # prob.model.list_vars(units=True, print_arrays=True) return prob 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 588ece73c3..1a8c386478 100644 --- a/aviary/variable_info/variable_meta_data.py +++ b/aviary/variable_info/variable_meta_data.py @@ -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( From 2f25d363fa8b3fa0a58f4745455dcedc7880ac10 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 16 Apr 2026 13:22:01 -0400 Subject: [PATCH 23/29] removed redundant max calc from motor --- .../propulsion/motor/model/motor_mission.py | 69 +++---------------- .../motor/test/test_motor_mission.py | 10 +-- ...h_electrified_large_turboprop_freighter.py | 16 ++--- 3 files changed, 19 insertions(+), 76 deletions(-) diff --git a/aviary/subsystems/propulsion/motor/model/motor_mission.py b/aviary/subsystems/propulsion/motor/model/motor_mission.py index d1a9696499..80218d7d15 100644 --- a/aviary/subsystems/propulsion/motor/model/motor_mission.py +++ b/aviary/subsystems/propulsion/motor/model/motor_mission.py @@ -19,13 +19,12 @@ def setup(self): 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_throttle', val=np.ones(nn), units='unitless') ivc.add_output('max_RPM', val=np.ones(nn) * rpm_design, units='rpm') - self.add_subsystem('ivc', ivc, promotes=['*']) - - motor_group = om.Group() + 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: @@ -34,16 +33,16 @@ def setup(self): 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) + # 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 * throttle', - RPM={'val': np.zeros(nn), 'units': 'rpm'}, - max_RPM={'val': np.zeros(nn), 'units': 'rpm'}, - throttle={'val': np.zeros(nn), 'units': 'unitless'}, + 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)], @@ -112,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, 'max_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 * max_RPM / 30', - max_power={'val': np.ones(nn), 'units': 'kW'}, - max_torque={'val': np.ones(nn), 'units': 'kN*m'}, - max_RPM={'val': np.ones(nn), 'units': 'rpm'}, - has_diag_partials=True, - ), - promotes_inputs=[ - ('max_torque', Dynamic.Vehicle.Propulsion.TORQUE_MAX), - 'max_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/test/test_motor_mission.py b/aviary/subsystems/propulsion/motor/test/test_motor_mission.py index e38e67d5e9..3eb0011cc0 100644 --- a/aviary/subsystems/propulsion/motor/test/test_motor_mission.py +++ b/aviary/subsystems/propulsion/motor/test/test_motor_mission.py @@ -6,10 +6,10 @@ 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 @@ -38,24 +38,18 @@ def test_motor_mission(self): 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 = [1266.69015793, 1266.69015793, 1266.69015793] 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/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 78c4fa500a..8d0c30848b 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 @@ -76,15 +76,15 @@ def build_and_run_problem(self, mission_method): prob.add_driver('SNOPT', max_iter=50, 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.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, upper=10000) - prob.add_objective() + prob.add_objective('mass') prob.setup() From 570bdc23854845836d465734c524a932372e9ca7 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 16 Apr 2026 14:34:48 -0400 Subject: [PATCH 24/29] remove obsolete kwargs references in turboprop model --- .../subsystems/propulsion/turboprop_model.py | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/aviary/subsystems/propulsion/turboprop_model.py b/aviary/subsystems/propulsion/turboprop_model.py index 748e3cd226..3c44630516 100644 --- a/aviary/subsystems/propulsion/turboprop_model.py +++ b/aviary/subsystems/propulsion/turboprop_model.py @@ -88,7 +88,7 @@ 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) @@ -121,7 +121,7 @@ def needs_mission_solver(self, aviary_inputs=None, subsystem_options=None): 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? + # 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 @@ -170,7 +170,7 @@ def build_mission( propeller_model=self.propeller_model, gearbox_model=self.gearbox_model, aviary_inputs=self.options, - kwargs=subsystem_options, + subsystem_options=subsystem_options, ) return turboprop_group @@ -180,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) @@ -205,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) @@ -580,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): @@ -590,7 +592,7 @@ 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'] if Aircraft.Engine.FIXED_RPM in aviary_inputs: @@ -611,52 +613,52 @@ def setup(self): # 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) From 99f52bae4bfd4769c017cbeee4c3d55ba8e970e5 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 16 Apr 2026 14:34:56 -0400 Subject: [PATCH 25/29] fix turboprop test --- aviary/subsystems/propulsion/test/test_turboprop_model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aviary/subsystems/propulsion/test/test_turboprop_model.py b/aviary/subsystems/propulsion/test/test_turboprop_model.py index 0b634e3423..fedd97d970 100644 --- a/aviary/subsystems/propulsion/test/test_turboprop_model.py +++ b/aviary/subsystems/propulsion/test/test_turboprop_model.py @@ -396,6 +396,7 @@ def test_electroprop_calc_RPM(self): 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) self.prepare_model(options, test_points, shp_model=MotorBuilder(), input_rpm=True) @@ -445,6 +446,7 @@ def test_control_rpm_turboprop(self): options = get_option_defaults() options.set_val(Aircraft.Engine.DATA_FILE, filename) + options.delete(Aircraft.Engine.FIXED_RPM) self.prepare_model(options, test_points) From cb6cbc8994dc5198e8341a1a508fe324c64686fd Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 16 Apr 2026 15:02:17 -0400 Subject: [PATCH 26/29] experimental motor fixes --- .../electrified_phase_info.py | 8 +++--- .../propulsion/motor/model/motor_mission.py | 2 +- ...h_electrified_large_turboprop_freighter.py | 26 ++++++++++++++----- 3 files changed, 24 insertions(+), 12 deletions(-) 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 82036ca5d6..a367c98086 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py @@ -17,7 +17,7 @@ 'no_descent': True, 'mass_ref': (154000, 'lbm'), 'throttle_enforcement': 'control', - 'throttle_bounds': ((0.001, 1), 'unitless'), + 'throttle_bounds': ((0.15, 1), 'unitless'), 'time_initial_bounds': ((0.0, 0.0), 'min'), 'time_duration_bounds': ((24.0, 128.0), 'min'), }, @@ -40,7 +40,7 @@ 'altitude_bounds': ((20_000.0, 22_000.0), 'ft'), 'mass_ref': (154000, 'lbm'), 'throttle_enforcement': 'control', - 'throttle_bounds': ((0.001, 1), 'unitless'), + 'throttle_bounds': ((0.15, 1), 'unitless'), 'time_initial_bounds': ((24.0, 128.0), 'min'), 'time_duration_bounds': ((56.5, 1000.0), 'min'), }, @@ -66,11 +66,11 @@ 'mass_ref': (154000, 'lbm'), 'no_climb': True, 'throttle_enforcement': 'control', - 'throttle_bounds': ((0.001, 1), 'unitless'), + '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.1, 0.1], 'unitless')}, + 'initial_guesses': {'throttle': ([0.15, 0.15], 'unitless')}, }, 'post_mission': { 'include_landing': False, diff --git a/aviary/subsystems/propulsion/motor/model/motor_mission.py b/aviary/subsystems/propulsion/motor/model/motor_mission.py index 80218d7d15..c4135f495f 100644 --- a/aviary/subsystems/propulsion/motor/model/motor_mission.py +++ b/aviary/subsystems/propulsion/motor/model/motor_mission.py @@ -39,7 +39,7 @@ def setup(self): motor_group.add_subsystem( 'rpm_calc', om.ExecComp( - 'RPM = max_RPM * throttle', + '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'}, 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 8d0c30848b..d330fdfeb4 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 @@ -28,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) @@ -42,14 +45,14 @@ def build_and_run_problem(self, mission_method): # set up electric propulsion # TODO make separate input file for electroprop freighter? - options.set_val(Aircraft.Engine.SCALE_FACTOR, 2) - options.set_val(Aircraft.Engine.RPM_DESIGN, 6000, 'rpm') # max RPM of motor map + 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.Battery.PACK_ENERGY_DENSITY, 1000, 'kW*h/kg') + options.set_val(Aircraft.Battery.PACK_ENERGY_DENSITY, 550, 'kW*h/kg') options.set_val( Aircraft.Engine.Motor.DATA_FILE, get_path('electric_motor_1800Nm_6000rpm.csv') @@ -74,7 +77,7 @@ def build_and_run_problem(self, mission_method): prob.build_model() - prob.add_driver('SNOPT', max_iter=50, verbosity=1) + prob.add_driver('SNOPT', max_iter=100, verbosity=1) prob.add_design_variables() prob.model.add_design_var( Aircraft.Engine.SCALE_FACTOR, @@ -83,13 +86,22 @@ def build_and_run_problem(self, mission_method): upper=2, ref=1, ) - prob.model.add_design_var(Aircraft.Battery.PACK_MASS, units='lbm', lower=10, upper=10000) - prob.add_objective('mass') + prob.model.add_design_var( + Aircraft.Battery.PACK_MASS, units='lbm', lower=10, upper=10_000, ref=1_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=1000.0, units='lbm') + prob.set_val(Aircraft.Battery.PACK_MASS, val=100.0, units='lbm') prob.run_aviary_problem() From 9b885810cf2dd5c21af35c466815b84e6f40693f Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 16 Apr 2026 17:52:13 -0400 Subject: [PATCH 27/29] electric turboprop model updates --- .../large_turboprop_freighter/electrified_phase_info.py | 6 +++--- .../test_bench_electrified_large_turboprop_freighter.py | 8 +++++--- aviary/variable_info/variable_meta_data.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) 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 a367c98086..2f4f2c861f 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py @@ -17,7 +17,7 @@ 'no_descent': True, 'mass_ref': (154000, 'lbm'), 'throttle_enforcement': 'control', - 'throttle_bounds': ((0.15, 1), 'unitless'), + 'throttle_bounds': ((0.2, 1), 'unitless'), 'time_initial_bounds': ((0.0, 0.0), 'min'), 'time_duration_bounds': ((24.0, 128.0), 'min'), }, @@ -40,7 +40,7 @@ 'altitude_bounds': ((20_000.0, 22_000.0), 'ft'), 'mass_ref': (154000, 'lbm'), 'throttle_enforcement': 'control', - 'throttle_bounds': ((0.15, 1), 'unitless'), + 'throttle_bounds': ((0.2, 1), 'unitless'), 'time_initial_bounds': ((24.0, 128.0), 'min'), 'time_duration_bounds': ((56.5, 1000.0), 'min'), }, @@ -70,7 +70,7 @@ 'time_initial_bounds': ((80, 1056.5), 'min'), 'time_duration_bounds': ((29.0, 128.0), 'min'), }, - 'initial_guesses': {'throttle': ([0.15, 0.15], 'unitless')}, + 'initial_guesses': {'throttle': ([0.5, 0.15], 'unitless')}, }, 'post_mission': { 'include_landing': False, 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 d330fdfeb4..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 @@ -43,6 +43,8 @@ def build_and_run_problem(self, mission_method): 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? options.set_val(Aircraft.Engine.SCALE_FACTOR, 1) @@ -52,7 +54,7 @@ def build_and_run_problem(self, mission_method): # 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.Battery.PACK_ENERGY_DENSITY, 550, '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') @@ -87,7 +89,7 @@ def build_and_run_problem(self, mission_method): ref=1, ) prob.model.add_design_var( - Aircraft.Battery.PACK_MASS, units='lbm', lower=10, upper=10_000, ref=1_000 + Aircraft.Battery.PACK_MASS, units='lbm', lower=10_000, upper=75_000, ref=10_000 ) final_phase_name = prob.model.regular_phases[-1] @@ -101,7 +103,7 @@ def build_and_run_problem(self, mission_method): prob.setup() # initial guess for pack mass. - prob.set_val(Aircraft.Battery.PACK_MASS, val=100.0, units='lbm') + prob.set_val(Aircraft.Battery.PACK_MASS, val=25_000.0, units='lbm') prob.run_aviary_problem() diff --git a/aviary/variable_info/variable_meta_data.py b/aviary/variable_info/variable_meta_data.py index bcd325fe4a..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, ) From 985e19ae55b3b3e4b06fcbb6ee4148ab629275ba Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 17 Apr 2026 11:14:00 -0400 Subject: [PATCH 28/29] fixed mistake in phase info user options --- .../large_turboprop_freighter/electrified_phase_info.py | 6 +++--- .../models/aircraft/large_turboprop_freighter/phase_info.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) 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 2f4f2c861f..02c78a92d5 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/electrified_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': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, @@ -28,7 +28,7 @@ }, }, 'cruise': { - 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': 'true'}}, + 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, @@ -51,7 +51,7 @@ }, }, 'descent': { - 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': 'true'}}, + 'subsystem_options': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index e5944de616..40f5712355 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': {'core_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': {'core_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': {'core_aerodynamics': {'method': 'cruise', 'solve_alpha': True}}, 'user_options': { 'num_segments': 5, 'order': 3, From 50f4cf619fb3729cb050f3f7d70fc051b52e77be Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 17 Apr 2026 16:23:00 -0400 Subject: [PATCH 29/29] update subsystem name typo --- .../large_turboprop_freighter/electrified_phase_info.py | 6 +++--- .../models/aircraft/large_turboprop_freighter/phase_info.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) 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 02c78a92d5..26a63e99d8 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/electrified_phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/electrified_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, @@ -28,7 +28,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, @@ -51,7 +51,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, diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index 40f5712355..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,