From 60918bdce09cf7162ff995aa3aac8dc228dd9a36 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 1 Aug 2025 12:33:04 -0400 Subject: [PATCH 01/22] test_balaned_field is starting to come together. --- .../phases/detailed_takeoff_phases.py | 307 ++++++++++++++---- .../phases/test/test_balanced_field.py | 194 +++++++++++ aviary/mission/initial_guess_builders.py | 33 +- 3 files changed, 460 insertions(+), 74 deletions(-) create mode 100644 aviary/mission/flops_based/phases/test/test_balanced_field.py diff --git a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py index f91ba7fcf6..f3e41cbc4e 100644 --- a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py +++ b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py @@ -42,6 +42,7 @@ """ from collections import namedtuple +from platform import node import dymos as dm import openmdao.api as om @@ -259,11 +260,199 @@ def build_phase(self, aviary_options=None): Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' ) + phase.add_calc_expr('v_to_go = velocity - (dV1 + v_stall)', + v={'units': 'kn'}, + dv1={'units': 'kn'}, + v_to_go={'units': 'kn'}) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + + phase.options['auto_order'] = True + phase.nonlinear_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0E-6, rtol=1.0E-6, debug_print=True) + phase.nonlinear_solver.linesearch = om.BoundsEnforceLS() + phase.linear_solver = om.DirectSolver() + return phase def make_default_transcription(self): """Return a transcription object to be used by default in build_phase.""" transcription = dm.Radau(num_segments=3, order=3, compressed=True) + transcription = dm.PicardShooting(num_segments=1, nodes_per_seg=10) + + return transcription + + def _extra_ode_init_kwargs(self): + """Return extra kwargs required for initializing the ODE.""" + return {'climbing': False, 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT} + + +TakeoffBrakeReleaseToDecisionSpeed._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK) +) + +@_init_initial_guess_meta_data +class TakeoffBrakeReleaseToEngineFailure(PhaseBuilderBase): + """ + Define a phase builder for the first phase of takeoff, from brake release to decision + speed, the maximum speed at which takeoff can be safely brought to full stop using + zero thrust while braking. + + Attributes + ---------- + name : str ('takeoff_brake_release') + object label + + user_options : AviaryValues () + state/path constraint values and flags + + supported options: + - max_duration (1000.0, 's') + - time_duration_ref (10.0, 's') + - distance_max (1000.0, 'ft') + - max_velocity (100.0, 'ft/s') + + initial_guesses : AviaryValues () + state/path beginning values to be set on the problem + + supported options: + - time + - range + - velocity + - mass + - throttle + - angle_of_attack + + ode_class : type (None) + advanced: the type of system defining the ODE + + transcription : "Dymos transcription object" (None) + advanced: an object providing the transcription technique of the + optimal control problem + + default_name : str + class attribute: derived type customization point; the default value + for name + + default_ode_class : type + class attribute: derived type customization point; the default value + for ode_class used by build_phase + + Methods + ------- + build_phase + make_default_transcription + """ + + __slots__ = () + + default_name = 'takeoff_to_engine_failure' + default_ode_class = TakeoffODE + default_options_class = TakeoffBrakeReleaseToDecisionSpeedOptions + + def build_phase(self, aviary_options=None): + """ + Return a new phase object for analysis using these constraints. + + If ode_class is None, default_ode_class is used. + + If transcription is None, the return value from calling + make_default_transcription is used. + + Parameters + ---------- + aviary_options : AviaryValues (empty) + collection of Aircraft/Mission specific options + + Returns + ------- + dymos.Phase + """ + phase: dm.Phase = super().build_phase(aviary_options) + + user_options: AviaryValues = self.user_options + + max_duration, units = user_options['max_duration'] + duration_ref = user_options.get_val('time_duration_ref', units) + + phase.set_time_options( + fix_initial=True, + duration_bounds=(1, max_duration), + duration_ref=duration_ref, + units=units, + ) + + distance_max, units = user_options['distance_max'] + + phase.add_state( + Dynamic.Mission.DISTANCE, + fix_initial=True, + lower=0, + ref=distance_max, + defect_ref=distance_max, + units=units, + upper=distance_max, + rate_source=Dynamic.Mission.DISTANCE_RATE, + ) + + max_velocity, units = user_options['max_velocity'] + + phase.add_state( + Dynamic.Mission.VELOCITY, + fix_initial=True, + lower=0, + ref=max_velocity, + defect_ref=max_velocity, + units=units, + upper=max_velocity, + rate_source=Dynamic.Mission.VELOCITY_RATE, + ) + + phase.add_state( + Dynamic.Vehicle.MASS, + fix_initial=True, + fix_final=False, + lower=0.0, + upper=1e9, + ref=5e4, + units='kg', + rate_source=Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE_TOTAL, + targets=Dynamic.Vehicle.MASS, + ) + + # TODO: Energy phase places this under an if num_engines > 0. + # phase.add_control( + # Dynamic.Vehicle.Propulsion.THROTTLE, + # targets=Dynamic.Vehicle.Propulsion.THROTTLE, + # units='unitless', + # opt=False, + # ) + + # phase.add_parameter(Dynamic.Vehicle.Propulsion.THROTTLE, + # targets=Dynamic.Vehicle.Propulsion.THROTTLE, + # val=1.0, opt=False, units='unitless') + phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') + + phase.add_parameter('v_ef', val=100.0, opt=False, units='kn') + phase.add_calc_expr('v_to_go = v - v_ef', + v={'units': 'kn'}, + v_ef={'units': 'kn'}, + v_to_go={'units': 'kn'}) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + + phase.add_timeseries_output( + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + output_name=Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + units='lbf', + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' + ) + + return phase + + def make_default_transcription(self): + """Return a transcription object to be used by default in build_phase.""" + transcription = dm.PicardShooting(num_segments=1, nodes_per_seg=11) return transcription @@ -3046,99 +3235,99 @@ def _add_phases(self, aviary_options: AviaryValues): self._add_phase(self._brake_release_to_decision_speed, aviary_options) - self._add_phase(self._decision_speed_to_rotate, aviary_options) + # self._add_phase(self._decision_speed_to_rotate, aviary_options) - self._add_phase(self._rotate_to_liftoff, aviary_options) + # self._add_phase(self._rotate_to_liftoff, aviary_options) - self._add_phase(self._liftoff_to_obstacle, aviary_options) + # self._add_phase(self._liftoff_to_obstacle, aviary_options) - obstacle_to_mic_p2 = self._obstacle_to_mic_p2 + # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 - if obstacle_to_mic_p2 is not None: - self._add_phase(obstacle_to_mic_p2, aviary_options) + # if obstacle_to_mic_p2 is not None: + # self._add_phase(obstacle_to_mic_p2, aviary_options) - self._add_phase(self._mic_p2_to_engine_cutback, aviary_options) + # self._add_phase(self._mic_p2_to_engine_cutback, aviary_options) - self._add_phase(self._engine_cutback, aviary_options) + # self._add_phase(self._engine_cutback, aviary_options) - self._add_phase(self._engine_cutback_to_mic_p1, aviary_options) + # self._add_phase(self._engine_cutback_to_mic_p1, aviary_options) - self._add_phase(self._mic_p1_to_climb, aviary_options) + # self._add_phase(self._mic_p1_to_climb, aviary_options) - decision_speed_to_brake = self._decision_speed_to_brake + # decision_speed_to_brake = self._decision_speed_to_brake - if decision_speed_to_brake is not None: - self._add_phase(decision_speed_to_brake, aviary_options) + # if decision_speed_to_brake is not None: + # self._add_phase(decision_speed_to_brake, aviary_options) - self._add_phase(self._brake_to_abort, aviary_options) + # self._add_phase(self._brake_to_abort, aviary_options) def _link_phases(self): traj: dm.Trajectory = self._traj - brake_release_name = self._brake_release_to_decision_speed.name - decision_speed_name = self._decision_speed_to_rotate.name + # brake_release_name = self._brake_release_to_decision_speed.name + # decision_speed_name = self._decision_speed_to_rotate.name - basic_vars = ['time', 'distance', 'velocity', 'mass'] + # basic_vars = ['time', 'distance', 'velocity', 'mass'] - traj.link_phases([brake_release_name, decision_speed_name], vars=basic_vars) + # traj.link_phases([brake_release_name, decision_speed_name], vars=basic_vars) - rotate_name = self._rotate_to_liftoff.name + # rotate_name = self._rotate_to_liftoff.name - ext_vars = basic_vars + ['angle_of_attack'] + # ext_vars = basic_vars + ['angle_of_attack'] - traj.link_phases([decision_speed_name, rotate_name], vars=ext_vars) + # traj.link_phases([decision_speed_name, rotate_name], vars=ext_vars) - liftoff_name = self._liftoff_to_obstacle.name + # liftoff_name = self._liftoff_to_obstacle.name - traj.link_phases([rotate_name, liftoff_name], vars=ext_vars) + # traj.link_phases([rotate_name, liftoff_name], vars=ext_vars) - obstacle_to_mic_p2 = self._obstacle_to_mic_p2 + # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 - if obstacle_to_mic_p2 is not None: - obstacle_to_mic_p2_name = obstacle_to_mic_p2.name - mic_p2_to_engine_cutback_name = self._mic_p2_to_engine_cutback.name - engine_cutback_name = self._engine_cutback.name - engine_cutback_to_mic_p1_name = self._engine_cutback_to_mic_p1.name - mic_p1_to_climb_name = self._mic_p1_to_climb.name + # if obstacle_to_mic_p2 is not None: + # obstacle_to_mic_p2_name = obstacle_to_mic_p2.name + # mic_p2_to_engine_cutback_name = self._mic_p2_to_engine_cutback.name + # engine_cutback_name = self._engine_cutback.name + # engine_cutback_to_mic_p1_name = self._engine_cutback_to_mic_p1.name + # mic_p1_to_climb_name = self._mic_p1_to_climb.name - acoustics_vars = ext_vars + [Dynamic.Mission.FLIGHT_PATH_ANGLE, 'altitude'] + # acoustics_vars = ext_vars + [Dynamic.Mission.FLIGHT_PATH_ANGLE, 'altitude'] - traj.link_phases([liftoff_name, obstacle_to_mic_p2_name], vars=acoustics_vars) + # traj.link_phases([liftoff_name, obstacle_to_mic_p2_name], vars=acoustics_vars) - traj.link_phases( - [obstacle_to_mic_p2_name, mic_p2_to_engine_cutback_name], vars=acoustics_vars - ) + # traj.link_phases( + # [obstacle_to_mic_p2_name, mic_p2_to_engine_cutback_name], vars=acoustics_vars + # ) - traj.link_phases( - [mic_p2_to_engine_cutback_name, engine_cutback_name], vars=acoustics_vars - ) + # traj.link_phases( + # [mic_p2_to_engine_cutback_name, engine_cutback_name], vars=acoustics_vars + # ) - traj.link_phases( - [engine_cutback_name, engine_cutback_to_mic_p1_name], vars=acoustics_vars - ) + # traj.link_phases( + # [engine_cutback_name, engine_cutback_to_mic_p1_name], vars=acoustics_vars + # ) - traj.link_phases( - [engine_cutback_to_mic_p1_name, mic_p1_to_climb_name], vars=acoustics_vars - ) + # traj.link_phases( + # [engine_cutback_to_mic_p1_name, mic_p1_to_climb_name], vars=acoustics_vars + # ) - decision_speed_to_brake = self._decision_speed_to_brake + # decision_speed_to_brake = self._decision_speed_to_brake - if decision_speed_to_brake is not None: - brake_name = decision_speed_to_brake.name - abort_name = self._brake_to_abort.name + # if decision_speed_to_brake is not None: + # brake_name = decision_speed_to_brake.name + # abort_name = self._brake_to_abort.name - traj.link_phases([brake_release_name, brake_name], vars=basic_vars) - traj.link_phases([brake_name, abort_name], vars=basic_vars) + # traj.link_phases([brake_release_name, brake_name], vars=basic_vars) + # traj.link_phases([brake_name, abort_name], vars=basic_vars) - traj.add_linkage_constraint( - phase_a=abort_name, - var_a='distance', - loc_a='final', - phase_b=liftoff_name, - var_b='distance', - loc_b='final', - ref=self._balanced_field_ref, - ) + # traj.add_linkage_constraint( + # phase_a=abort_name, + # var_a='distance', + # loc_a='final', + # phase_b=liftoff_name, + # var_b='distance', + # loc_b='final', + # ref=self._balanced_field_ref, + # ) def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): name = phase_builder.name diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field.py b/aviary/mission/flops_based/phases/test/test_balanced_field.py new file mode 100644 index 0000000000..8c1a4af8d5 --- /dev/null +++ b/aviary/mission/flops_based/phases/test/test_balanced_field.py @@ -0,0 +1,194 @@ +import aviary.api as av +import warnings +import unittest +import dymos as dm +import openmdao.api as om +from aviary.api import Dynamic, Mission +from aviary.core.aviary_group import AviaryGroup +from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData +from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import inputs +from aviary.utils.preprocessors import preprocess_options + +class TestTakeoffToEngineFailureTest(unittest.TestCase): + """Test takeoff phase builder.""" + + def test_case1(self): + + aviary_options = inputs.deepcopy() + + # This builder can be used for both takeoff and landing phases + aero_builder = av.CoreAerodynamicsBuilder(name='low_speed_aero', code_origin=av.LegacyCode.FLOPS) + + # fmt: off + takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } + } + # fmt: off + + # when using spoilers, add a few more options + takeoff_spoiler_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT), + 'spoiler_lift_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT), + } + } + + # We also need propulsion analysis for takeoff and landing. No additional configuration + # is needed for this builder + engines = [av.build_engine_deck(aviary_options)] + preprocess_options(aviary_options, engine_models=engines) + prop_builder = av.CorePropulsionBuilder(engine_models=engines) + + takeoff_brake_release_user_options = av.AviaryValues() + + takeoff_brake_release_user_options.set_val('max_duration', val=60.0, units='s') + takeoff_brake_release_user_options.set_val('time_duration_ref', val=60.0, units='s') + takeoff_brake_release_user_options.set_val('distance_max', val=7500.0, units='ft') + takeoff_brake_release_user_options.set_val('max_velocity', val=167.85, units='kn') + + takeoff_brake_release_initial_guesses = av.AviaryValues() + + takeoff_brake_release_initial_guesses.set_val('time', [0.0, 30.0], 's') + takeoff_brake_release_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') + takeoff_brake_release_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + takeoff_brake_release_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + takeoff_brake_release_initial_guesses.set_val('throttle', 1.0) + takeoff_brake_release_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + takeoff_brake_release_builder = av.DetailedTakeoffBrakeReleaseToDecisionSpeedPhaseBuilder( + 'takeoff_brake_release', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=takeoff_brake_release_user_options, + initial_guesses=takeoff_brake_release_initial_guesses, + ) + + from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( + takeoff_decision_speed_builder, + takeoff_engine_cutback_builder, + takeoff_engine_cutback_to_mic_p1_builder, + takeoff_liftoff_builder, + takeoff_liftoff_user_options, + takeoff_mic_p1_to_climb_builder, + takeoff_mic_p2_builder, + takeoff_mic_p2_to_engine_cutback_builder, + takeoff_rotate_builder, + ) + from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems + from aviary.variable_info.functions import setup_model_options + + takeoff_trajectory_builder = av.DetailedTakeoffTrajectoryBuilder('detailed_takeoff') + + takeoff_trajectory_builder.set_brake_release_to_decision_speed(takeoff_brake_release_builder) + + # takeoff_trajectory_builder.set_decision_speed_to_rotate(takeoff_decision_speed_builder) + + # takeoff_trajectory_builder.set_rotate_to_liftoff(takeoff_rotate_builder) + + # takeoff_trajectory_builder.set_liftoff_to_obstacle(takeoff_liftoff_builder) + + # takeoff_trajectory_builder.set_obstacle_to_mic_p2(takeoff_mic_p2_builder) + + # takeoff_trajectory_builder.set_mic_p2_to_engine_cutback(takeoff_mic_p2_to_engine_cutback_builder) + + # takeoff_trajectory_builder.set_engine_cutback(takeoff_engine_cutback_builder) + + # takeoff_trajectory_builder.set_engine_cutback_to_mic_p1(takeoff_engine_cutback_to_mic_p1_builder) + + # takeoff_trajectory_builder.set_mic_p1_to_climb(takeoff_mic_p1_to_climb_builder) + + takeoff = om.Problem() # model=AviaryGroup(aviary_options=aviary_options, aviary_metadata=BaseMetaData)) + + # default subsystems + default_premission_subsystems = get_default_premission_subsystems('FLOPS', engines) + + # Upstream pre-mission analysis for aero + takeoff.model.add_subsystem( + 'core_subsystems', + av.CorePreMission( + aviary_options=aviary_options, + subsystems=default_premission_subsystems, + ), + promotes_inputs=['*'], + promotes_outputs=['*'], + ) + + # Instantiate the trajectory and add the phases + traj = dm.Trajectory() + takeoff.model.add_subsystem('traj', traj) + + takeoff_trajectory_builder.build_trajectory( + aviary_options=aviary_options, model=takeoff.model, traj=traj + ) + + distance_max, units = takeoff_liftoff_user_options.get_item('distance_max') + # liftoff = takeoff_trajectory_builder.get_phase('takeoff_liftoff') + + # liftoff.add_objective(Dynamic.Mission.DISTANCE, loc='final', ref=distance_max, units=units) + + # Insert a constraint for a fake decision speed, until abort is added. + takeoff.model.add_constraint( + 'traj.takeoff_brake_release.states:velocity', equals=149.47, units='kn', ref=150.0, indices=[-1] + ) + + # takeoff.model.add_constraint( + # 'traj.takeoff_decision_speed.states:velocity', + # equals=155.36, + # units='kn', + # ref=159.0, + # indices=[-1], + # ) + + varnames = [ + av.Aircraft.Wing.AREA, + av.Aircraft.Wing.ASPECT_RATIO, + av.Aircraft.Wing.SPAN, + ] + av.set_aviary_input_defaults(takeoff.model, varnames, aviary_options) + + setup_model_options(takeoff, aviary_options) + + # suppress warnings: + # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' + with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + takeoff.setup(check=True) + + av.set_aviary_initial_values(takeoff, aviary_options) + + takeoff_trajectory_builder.apply_initial_guesses(takeoff, 'traj') + + takeoff.final_setup() + + takeoff.run_model() + + # takeoff.model.run_apply_nonlinear() + # takeoff.model.list_vars(print_arrays=True) + # om.n2(takeoff.model) + + + +if __name__ == '__main__': + unittest.main() diff --git a/aviary/mission/initial_guess_builders.py b/aviary/mission/initial_guess_builders.py index 402a879191..33a6118da3 100644 --- a/aviary/mission/initial_guess_builders.py +++ b/aviary/mission/initial_guess_builders.py @@ -52,14 +52,23 @@ def apply_initial_guess( # - to interpolate, or not to interpolate: that is the question # - the solution should probably be a value decoration (a wrapper) that is # both lightweight and easy to check and unpack - if isinstance(val, np.ndarray) or (isinstance(val, Sequence) and not isinstance(val, str)): - val = phase.interp(self.key, val) - - try: - prob.set_val(complete_key, val, units) - except KeyError: - complete_key = complete_key.replace('polynomial_controls', 'controls') - prob.set_val(complete_key, val, units) + if self.key in phase.state_options: + phase.set_state_val(self.key, val, units=units) + elif self.key in phase.control_options: + phase.set_control_val(self.key, val, units=units) + elif self.key in phase.parameter_options: + phase.set_parameter_val(self.key, val, units=units) + else: + prob.set_val(complete_key, val, units=units) + + # if isinstance(val, np.ndarray) or (isinstance(val, Sequence) and not isinstance(val, str)): + # val = phase.interp(self.key, val) + + # try: + # prob.set_val(complete_key, val, units) + # except KeyError: + # complete_key = complete_key.replace('polynomial_controls', 'controls') + # prob.set_val(complete_key, val, units) def _get_complete_key(self, traj_name, phase_name): """Compose the complete key for setting the initial guess.""" @@ -166,11 +175,5 @@ def __init__(self, key='time'): def apply_initial_guess( self, prob: om.Problem, traj_name, phase: dm.Phase, phase_name, val, units ): - _ = phase - - name = f'{traj_name}.{phase_name}.t_initial' t_initial, t_duration = val - prob.set_val(name, t_initial, units) - - name = f'{traj_name}.{phase_name}.t_duration' - prob.set_val(name, t_duration, units) + phase.set_integ_var_val(initial=t_initial, duration=t_duration, units=units) From e5597df0c93871b6cb97a012c05f139e332e27f6 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Tue, 5 Aug 2025 08:59:35 -0400 Subject: [PATCH 02/22] 2 phases working --- aviary/api.py | 1 + .../phases/detailed_takeoff_phases.py | 411 ++++++++++++++++-- .../phases/test/test_balanced_field.py | 108 +++-- aviary/variable_info/variables.py | 1 + 4 files changed, 449 insertions(+), 72 deletions(-) diff --git a/aviary/api.py b/aviary/api.py index 28f543f05a..cfa26ce244 100644 --- a/aviary/api.py +++ b/aviary/api.py @@ -148,6 +148,7 @@ LandingNoseDownToStop as DetailedLandingNoseDownToStopPhaseBuilder, ) from aviary.mission.flops_based.phases.detailed_takeoff_phases import ( + DetailedTakeoffPhaseBuilder, TakeoffBrakeReleaseToDecisionSpeed as DetailedTakeoffBrakeReleaseToDecisionSpeedPhaseBuilder, TakeoffDecisionSpeedToRotate as DetailedTakeoffDecisionSpeedToRotatePhaseBuilder, TakeoffDecisionSpeedBrakeDelay as DetailedTakeoffDecisionSpeedBrakeDelayPhaseBuilder, diff --git a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py index f3e41cbc4e..62851765be 100644 --- a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py +++ b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py @@ -46,6 +46,7 @@ import dymos as dm import openmdao.api as om +from openmdao.solvers.solver import NonlinearSolver, LinearSolver from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE from aviary.mission.initial_guess_builders import ( @@ -89,7 +90,7 @@ def _init_initial_guess_meta_data(cls: PhaseBuilderBase): return cls -class TakeoffBrakeReleaseToDecisionSpeedOptions(AviaryOptionsDictionary): +class DetailedTakeoffPhaseOptions(AviaryOptionsDictionary): def declare_options(self): self.declare( name='max_duration', @@ -110,6 +111,280 @@ def declare_options(self): name='max_velocity', default=100.0, units='ft/s', desc='Upper bound for velocity.' ) + self.declare( + name='terminal_speed', + types=(float, int, str), + allow_none=True, + default=None, + desc='true airspeed at which this phase terminates. One of "VEF", "VR", "V1", or a literal value in knots.', + ) + + self.declare( + name='pitch_control', + values=['alpha_fixed', 'alpha_rate'], + default='alpha_fixed', + desc='Specifies whether alpha is controled as a fixed value or as a state with a fixed rate.', + ) + + self.declare( + name='nonlinear_solver', + types=(NonlinearSolver,), + allow_none=True, + default=None, + desc='Nonlinear solver applied to the phase if it needs to solve for the terminal speed condition.', + ) + + self.declare( + name='linear_solver', + types=(LinearSolver,), + allow_none=True, + default=None, + desc='Linear solver applied to the phase if it needs to solve for the terminal speed condition.', + ) + + +@_init_initial_guess_meta_data +class DetailedTakeoffPhaseBuilder(PhaseBuilderBase): + """ + Define a phase builder for detailed takeoff phases. + + Attributes + ---------- + name : str ('takeoff_brake_release') + object label + + user_options : AviaryValues () + state/path constraint values and flags + + supported options: + - max_duration (1000.0, 's') + - time_duration_ref (10.0, 's') + - distance_max (1000.0, 'ft') + - max_velocity (100.0, 'ft/s') + + initial_guesses : AviaryValues () + state/path beginning values to be set on the problem + + supported options: + - time + - range + - velocity + - mass + - throttle + - angle_of_attack + + ode_class : type (None) + advanced: the type of system defining the ODE + + transcription : "Dymos transcription object" (None) + advanced: an object providing the transcription technique of the + optimal control problem + + default_name : str + class attribute: derived type customization point; the default value + for name + + default_ode_class : type + class attribute: derived type customization point; the default value + for ode_class used by build_phase + + Methods + ------- + build_phase + make_default_transcription + """ + + __slots__ = () + + default_name = 'detailed_takeoff' + default_ode_class = TakeoffODE + default_options_class = DetailedTakeoffPhaseOptions + + def build_phase(self, aviary_options=None): + """ + Return a new phase object for analysis using these constraints. + + If ode_class is None, default_ode_class is used. + + If transcription is None, the return value from calling + make_default_transcription is used. + + Parameters + ---------- + aviary_options : AviaryValues (empty) + collection of Aircraft/Mission specific options + + Returns + ------- + dymos.Phase + """ + phase: dm.Phase = super().build_phase(aviary_options) + + user_options: AviaryValues = self.user_options + + max_duration, units = user_options['max_duration'] + duration_ref = user_options.get_val('time_duration_ref', units) + + phase.set_time_options( + fix_initial=True, + duration_bounds=(0.1, max_duration), + duration_ref=duration_ref, + units=units, + ) + + distance_max, units = user_options['distance_max'] + + phase.add_state( + Dynamic.Mission.DISTANCE, + fix_initial=True, + lower=0, + ref=distance_max, + defect_ref=distance_max, + units=units, + upper=distance_max, + rate_source=Dynamic.Mission.DISTANCE_RATE, + ) + + max_velocity, units = user_options['max_velocity'] + + phase.add_state( + Dynamic.Mission.VELOCITY, + fix_initial=True, + lower=0, + ref=max_velocity, + defect_ref=max_velocity, + units=units, + upper=max_velocity, + rate_source=Dynamic.Mission.VELOCITY_RATE, + ) + + phase.add_state( + Dynamic.Vehicle.MASS, + fix_initial=True, + fix_final=False, + lower=0.0, + upper=1e9, + ref=5e4, + units='kg', + rate_source=Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE_TOTAL, + targets=Dynamic.Vehicle.MASS, + ) + + # TODO: Energy phase places this under an if num_engines > 0. + phase.add_control( + Dynamic.Vehicle.Propulsion.THROTTLE, + targets=Dynamic.Vehicle.Propulsion.THROTTLE, + units='unitless', + opt=False, + ) + + if user_options['pitch_control'] == 'alpha_fixed': + phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') + elif user_options['pitch_control'] == 'alpha_rate': + phase.add_state( + Dynamic.Vehicle.ANGLE_OF_ATTACK, + fix_initial=True, + fix_final=False, + lower=-14, + upper=14, + ref=1.0, + units='deg', + rate_source=Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, + targets=Dynamic.Vehicle.ANGLE_OF_ATTACK, + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + output_name=Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + units='lbf', + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' + ) + + # Define velocity to go based on definition of terminal speed, if applicable. + terminal_speed = user_options['terminal_speed'] + if terminal_speed == 'V1': + # Propagate until speed is the decision speed. + # In balanced field applications, dV1 will be + # set such that a rejected takeoff and a + # takeoff to a 35 ft altitude for obstacle clearance + # require the same range. + phase.add_calc_expr( + 'v_to_go = velocity - (dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif terminal_speed == 'VR': + # Propagate until speed is the rotation speed. + phase.add_calc_expr( + 'v_to_go = velocity - (dVR + dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + dVR={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif terminal_speed == 'VEF': + # Propagate until engine failure. + # Note we expect dVEF to be negative here. + # In balanced field applications, dVEF will be set + # such that it occurs {pilot_reaction_time} seconds + # before V1. + phase.add_calc_expr( + 'v_to_go = velocity - (dVEF + dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + dVEF={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif isinstance(terminal_speed, (float, int)): + # Propagate until velocity is the desired literal value. + phase.add_boundary_balance( + param='t_duration', name='velocity', tgt_val=terminal_speed, loc='final' + ) + phase.options['auto_order'] = True + elif terminal_speed is None: + # Propagate for t_duration + pass + else: + raise ValueError( + f'Unrecognized value for terminal_speed ({terminal_speed}).' + 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' + ) + + if v_to_go_calc: + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + phase.options['auto_order'] = True + + if user_options['nonlinear_solver'] is not None: + phase.nonlinear_solver = user_options['nonlinear_solver'] + + if user_options['linear_solver'] is not None: + phase.linear_solver = user_options['linear_solver'] + + return phase + + def make_default_transcription(self): + """Return a transcription object to be used by default in build_phase.""" + transcription = dm.Radau(num_segments=3, order=3, compressed=True) + transcription = dm.PicardShooting(num_segments=1, nodes_per_seg=10) + + return transcription + + def _extra_ode_init_kwargs(self): + """Return extra kwargs required for initializing the ODE.""" + return {'climbing': False, 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT} + + +DetailedTakeoffPhaseBuilder._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK) +) + @_init_initial_guess_meta_data class TakeoffBrakeReleaseToDecisionSpeed(PhaseBuilderBase): @@ -168,7 +443,7 @@ class attribute: derived type customization point; the default value default_name = 'takeoff_brake_release' default_ode_class = TakeoffODE - default_options_class = TakeoffBrakeReleaseToDecisionSpeedOptions + default_options_class = DetailedTakeoffPhaseOptions def build_phase(self, aviary_options=None): """ @@ -197,7 +472,7 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=True, - duration_bounds=(1, max_duration), + duration_bounds=(0.1, max_duration), duration_ref=duration_ref, units=units, ) @@ -260,16 +535,69 @@ def build_phase(self, aviary_options=None): Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' ) - phase.add_calc_expr('v_to_go = velocity - (dV1 + v_stall)', - v={'units': 'kn'}, - dv1={'units': 'kn'}, - v_to_go={'units': 'kn'}) - phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + # Define velocity to go based on definition of terminal speed, if applicable. + terminal_speed = user_options['terminal_speed'] + if terminal_speed == 'V1': + # Propagate until speed is the decision speed. + # In balanced field applications, dV1 will be + # set such that a rejected takeoff and a + # takeoff to a 35 ft altitude for obstacle clearance + # require the same range. + phase.add_calc_expr( + 'v_to_go = velocity - (dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif terminal_speed == 'VR': + # Propagate until speed is the rotation speed. + phase.add_calc_expr( + 'v_to_go = velocity - (dVR + dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + dVR={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif terminal_speed == 'VEF': + # Propagate until engine failure. + # Note we expect dVEF to be negative here. + # In balanced field applications, dVEF will be set + # such that it occurs {pilot_reaction_time} seconds + # before V1. + phase.add_calc_expr( + 'v_to_go = velocity - (dVEF + dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + dVEF={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif isinstance(terminal_speed, (float, int)): + # Propagate until velocity is the desired literal value. + phase.add_boundary_balance( + param='t_duration', name='velocity', tgt_val=terminal_speed, loc='final' + ) + phase.options['auto_order'] = True + elif terminal_speed is None: + # Propagate for t_duration + pass + else: + raise ValueError( + f'Unrecognized value for terminal_speed ({terminal_speed}).' + 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' + ) - phase.options['auto_order'] = True - phase.nonlinear_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0E-6, rtol=1.0E-6, debug_print=True) - phase.nonlinear_solver.linesearch = om.BoundsEnforceLS() - phase.linear_solver = om.DirectSolver() + if v_to_go_calc: + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + phase.options['auto_order'] = True + + if user_options['nonlinear_solver'] is not None: + phase.nonlinear_solver = user_options['nonlinear_solver'] + + if user_options['linear_solver'] is not None: + phase.linear_solver = user_options['linear_solver'] return phase @@ -289,6 +617,7 @@ def _extra_ode_init_kwargs(self): InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK) ) + @_init_initial_guess_meta_data class TakeoffBrakeReleaseToEngineFailure(PhaseBuilderBase): """ @@ -346,7 +675,7 @@ class attribute: derived type customization point; the default value default_name = 'takeoff_to_engine_failure' default_ode_class = TakeoffODE - default_options_class = TakeoffBrakeReleaseToDecisionSpeedOptions + default_options_class = DetailedTakeoffPhaseOptions def build_phase(self, aviary_options=None): """ @@ -375,7 +704,7 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=True, - duration_bounds=(1, max_duration), + duration_bounds=(0.1, max_duration), duration_ref=duration_ref, units=units, ) @@ -432,10 +761,9 @@ def build_phase(self, aviary_options=None): phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') phase.add_parameter('v_ef', val=100.0, opt=False, units='kn') - phase.add_calc_expr('v_to_go = v - v_ef', - v={'units': 'kn'}, - v_ef={'units': 'kn'}, - v_to_go={'units': 'kn'}) + phase.add_calc_expr( + 'v_to_go = v - v_ef', v={'units': 'kn'}, v_ef={'units': 'kn'}, v_to_go={'units': 'kn'} + ) phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') phase.add_timeseries_output( @@ -583,8 +911,8 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -914,8 +1242,8 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -1169,8 +1497,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -1481,8 +1809,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -1785,8 +2113,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -2357,8 +2685,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -2657,8 +2985,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -2912,8 +3240,8 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -3004,7 +3332,7 @@ def __init__(self, name=None): self.name = name - self._brake_release_to_decision_speed = None + # self._brake_release_to_decision_speed = None self._decision_speed_to_rotate = None self._rotate_to_liftoff = None self._liftoff_to_obstacle = None @@ -3235,7 +3563,7 @@ def _add_phases(self, aviary_options: AviaryValues): self._add_phase(self._brake_release_to_decision_speed, aviary_options) - # self._add_phase(self._decision_speed_to_rotate, aviary_options) + self._add_phase(self._decision_speed_to_rotate, aviary_options) # self._add_phase(self._rotate_to_liftoff, aviary_options) @@ -3264,14 +3592,13 @@ def _add_phases(self, aviary_options: AviaryValues): def _link_phases(self): traj: dm.Trajectory = self._traj - # brake_release_name = self._brake_release_to_decision_speed.name - # decision_speed_name = self._decision_speed_to_rotate.name - - # basic_vars = ['time', 'distance', 'velocity', 'mass'] + brake_release_name = self._brake_release_to_decision_speed.name + decision_speed_name = self._decision_speed_to_rotate.name + # rotate_name = self._rotate_to_liftoff.name - # traj.link_phases([brake_release_name, decision_speed_name], vars=basic_vars) + basic_vars = ['time', 'distance', 'velocity', 'mass'] - # rotate_name = self._rotate_to_liftoff.name + traj.link_phases([brake_release_name, decision_speed_name], vars=basic_vars, connected=True) # ext_vars = basic_vars + ['angle_of_attack'] diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field.py b/aviary/mission/flops_based/phases/test/test_balanced_field.py index 8c1a4af8d5..4c6d551db5 100644 --- a/aviary/mission/flops_based/phases/test/test_balanced_field.py +++ b/aviary/mission/flops_based/phases/test/test_balanced_field.py @@ -9,15 +9,17 @@ from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import inputs from aviary.utils.preprocessors import preprocess_options + class TestTakeoffToEngineFailureTest(unittest.TestCase): """Test takeoff phase builder.""" def test_case1(self): - aviary_options = inputs.deepcopy() # This builder can be used for both takeoff and landing phases - aero_builder = av.CoreAerodynamicsBuilder(name='low_speed_aero', code_origin=av.LegacyCode.FLOPS) + aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS + ) # fmt: off takeoff_subsystem_options = { @@ -57,53 +59,98 @@ def test_case1(self): preprocess_options(aviary_options, engine_models=engines) prop_builder = av.CorePropulsionBuilder(engine_models=engines) + # BRAKE RELEASE TO DECISION SPEED takeoff_brake_release_user_options = av.AviaryValues() takeoff_brake_release_user_options.set_val('max_duration', val=60.0, units='s') takeoff_brake_release_user_options.set_val('time_duration_ref', val=60.0, units='s') takeoff_brake_release_user_options.set_val('distance_max', val=7500.0, units='ft') takeoff_brake_release_user_options.set_val('max_velocity', val=167.85, units='kn') + takeoff_brake_release_user_options.set_val('terminal_speed', val='V1') + + tobl_nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) + tobl_nl_solver.linesearch = om.BoundsEnforceLS() - takeoff_brake_release_initial_guesses = av.AviaryValues() + takeoff_brake_release_user_options.set_val('nonlinear_solver', val=tobl_nl_solver) + takeoff_brake_release_user_options.set_val('linear_solver', val=om.DirectSolver()) - takeoff_brake_release_initial_guesses.set_val('time', [0.0, 30.0], 's') - takeoff_brake_release_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') - takeoff_brake_release_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') + takeoff_v1_to_vr_initial_guesses = av.AviaryValues() + + takeoff_v1_to_vr_initial_guesses.set_val('time', [0.0, 30.0], 's') + takeoff_v1_to_vr_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') + takeoff_v1_to_vr_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') gross_mass_units = 'lbm' gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) - takeoff_brake_release_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + takeoff_v1_to_vr_initial_guesses.set_val('mass', gross_mass, gross_mass_units) - takeoff_brake_release_initial_guesses.set_val('throttle', 1.0) - takeoff_brake_release_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + takeoff_v1_to_vr_initial_guesses.set_val('throttle', 1.0) + takeoff_v1_to_vr_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') - takeoff_brake_release_builder = av.DetailedTakeoffBrakeReleaseToDecisionSpeedPhaseBuilder( - 'takeoff_brake_release', + takeoff_brake_release_to_decision_speed_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_brake_release_to_decision_speed', core_subsystems=[aero_builder, prop_builder], subsystem_options=takeoff_subsystem_options, user_options=takeoff_brake_release_user_options, - initial_guesses=takeoff_brake_release_initial_guesses, + initial_guesses=takeoff_v1_to_vr_initial_guesses, ) - from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( - takeoff_decision_speed_builder, - takeoff_engine_cutback_builder, - takeoff_engine_cutback_to_mic_p1_builder, - takeoff_liftoff_builder, - takeoff_liftoff_user_options, - takeoff_mic_p1_to_climb_builder, - takeoff_mic_p2_builder, - takeoff_mic_p2_to_engine_cutback_builder, - takeoff_rotate_builder, + # DECISION SPEED TO ROTATION + + takeoff_v1_to_vr_user_options = av.AviaryValues() + + takeoff_v1_to_vr_user_options.set_val('max_duration', val=90.0, units='s') + takeoff_v1_to_vr_user_options.set_val('time_duration_ref', val=60.0, units='s') + takeoff_v1_to_vr_user_options.set_val('distance_max', val=15000.0, units='ft') + takeoff_v1_to_vr_user_options.set_val('max_velocity', val=167.85, units='kn') + takeoff_v1_to_vr_user_options.set_val('terminal_speed', val='VR') + + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) + nl_solver.linesearch = om.BoundsEnforceLS() + + takeoff_v1_to_vr_user_options.set_val('nonlinear_solver', val=nl_solver) + takeoff_v1_to_vr_user_options.set_val('linear_solver', val=om.DirectSolver()) + + takeoff_v1_to_vr_initial_guesses = av.AviaryValues() + + takeoff_v1_to_vr_initial_guesses.set_val('time', [30.0, 1.0], 's') + takeoff_v1_to_vr_initial_guesses.set_val('distance', [4100.0, 4800.0], 'ft') + takeoff_v1_to_vr_initial_guesses.set_val('velocity', [70., 150.0], 'kn') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + takeoff_v1_to_vr_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + takeoff_v1_to_vr_initial_guesses.set_val('throttle', 1.0) + takeoff_v1_to_vr_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + takeoff_decision_speed_to_rotate_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_decision_speed_to_rotate', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=takeoff_v1_to_vr_user_options, + initial_guesses=takeoff_v1_to_vr_initial_guesses, ) + + # from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( + # takeoff_decision_speed_builder, + # takeoff_engine_cutback_builder, + # takeoff_engine_cutback_to_mic_p1_builder, + # takeoff_liftoff_builder, + # takeoff_liftoff_user_options, + # takeoff_mic_p1_to_climb_builder, + # takeoff_mic_p2_builder, + # takeoff_mic_p2_to_engine_cutback_builder, + # takeoff_rotate_builder, + # ) from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems from aviary.variable_info.functions import setup_model_options takeoff_trajectory_builder = av.DetailedTakeoffTrajectoryBuilder('detailed_takeoff') - takeoff_trajectory_builder.set_brake_release_to_decision_speed(takeoff_brake_release_builder) + takeoff_trajectory_builder.set_brake_release_to_decision_speed(takeoff_brake_release_to_decision_speed_builder) - # takeoff_trajectory_builder.set_decision_speed_to_rotate(takeoff_decision_speed_builder) + takeoff_trajectory_builder.set_decision_speed_to_rotate(takeoff_decision_speed_to_rotate_builder) # takeoff_trajectory_builder.set_rotate_to_liftoff(takeoff_rotate_builder) @@ -143,15 +190,15 @@ def test_case1(self): aviary_options=aviary_options, model=takeoff.model, traj=traj ) - distance_max, units = takeoff_liftoff_user_options.get_item('distance_max') + # distance_max, units = takeoff_liftoff_user_options.get_item('distance_max') # liftoff = takeoff_trajectory_builder.get_phase('takeoff_liftoff') # liftoff.add_objective(Dynamic.Mission.DISTANCE, loc='final', ref=distance_max, units=units) # Insert a constraint for a fake decision speed, until abort is added. - takeoff.model.add_constraint( - 'traj.takeoff_brake_release.states:velocity', equals=149.47, units='kn', ref=150.0, indices=[-1] - ) + # takeoff.model.add_constraint( + # 'traj.takeoff_brake_release.states:velocity', equals=149.47, units='kn', ref=150.0, indices=[-1] + # ) # takeoff.model.add_constraint( # 'traj.takeoff_decision_speed.states:velocity', @@ -184,10 +231,11 @@ def test_case1(self): takeoff.run_model() + takeoff.model.list_vars(print_arrays=True) + # takeoff.model.run_apply_nonlinear() # takeoff.model.list_vars(print_arrays=True) - # om.n2(takeoff.model) - + om.n2(takeoff.model) if __name__ == '__main__': diff --git a/aviary/variable_info/variables.py b/aviary/variable_info/variables.py index 5d2d079c69..6bb566ecda 100644 --- a/aviary/variable_info/variables.py +++ b/aviary/variable_info/variables.py @@ -645,6 +645,7 @@ class Vehicle: """Vehicle properties and states in a vehicle-fixed reference frame.""" ANGLE_OF_ATTACK = 'angle_of_attack' + ANGLE_OF_ATTACK_RATE = 'angle_of_attack_rate' BATTERY_STATE_OF_CHARGE = 'battery_state_of_charge' CUMULATIVE_ELECTRIC_ENERGY_USED = 'cumulative_electric_energy_used' DRAG = 'drag' From 01faad46c138c96a9f30f2e98849ba0cfee061e4 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Tue, 5 Aug 2025 15:12:40 -0400 Subject: [PATCH 03/22] Modeled a detailed takeoff and landing up through establishment of climb gradient. --- aviary/mission/flops_based/ode/takeoff_eom.py | 33 ++++- .../phases/detailed_takeoff_phases.py | 119 +++++++++++++----- .../phases/test/test_balanced_field.py | 94 +++++++++++++- 3 files changed, 206 insertions(+), 40 deletions(-) diff --git a/aviary/mission/flops_based/ode/takeoff_eom.py b/aviary/mission/flops_based/ode/takeoff_eom.py index 6a466cc8b7..70b1986d04 100644 --- a/aviary/mission/flops_based/ode/takeoff_eom.py +++ b/aviary/mission/flops_based/ode/takeoff_eom.py @@ -156,7 +156,7 @@ def setup(self): 'sum_forces', SumForces(**kwargs), promotes_inputs=['*'], - promotes_outputs=['forces_horizontal', 'forces_vertical'], + promotes_outputs=['forces_horizontal', 'forces_vertical', 'ground_normal_force'], ) self.add_subsystem( @@ -562,6 +562,13 @@ def setup(self): desc='current sum of forces in the vertical direction', ) + self.add_output( + 'ground_normal_force', + val=np.zeros(nn), + units='N', + desc='runway force pushing up on the landing gear while on the ground and lift < weight.' + ) + def setup_partials(self): options = self.options @@ -589,7 +596,8 @@ def setup_partials(self): Dynamic.Mission.FLIGHT_PATH_ANGLE, ] - self.declare_partials('*', wrt, rows=rows_cols, cols=rows_cols) + self.declare_partials(['forces_vertical', + 'forces_horizontal'], wrt, rows=rows_cols, cols=rows_cols) else: aviary_options: AviaryValues = options['aviary_options'] @@ -640,6 +648,24 @@ def setup_partials(self): self.declare_partials('forces_vertical', ['*'], dependent=False) + self.declare_partials('ground_normal_force', + Dynamic.Vehicle.MASS, + rows=rows_cols, + cols=rows_cols, + val=grav_metric) + + self.declare_partials('ground_normal_force', + Dynamic.Vehicle.LIFT, + rows=rows_cols, + cols=rows_cols, + val=-1.0) + + self.declare_partials('ground_normal_force', + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + rows=rows_cols, + cols=rows_cols, + val=np.sin(t_inc)) + def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): options = self.options @@ -677,7 +703,9 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): f_v = t_v - drag * s_gamma + lift * c_gamma - weight + outputs['forces_vertical'] = f_v + outputs['ground_normal_force'][...] = 0.0 else: # NOTE using FLOPS GRRUN, which neglects angle of attack @@ -690,6 +718,7 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): t_v = thrust * np.sin(t_inc) f_h = t_h - drag - (weight - (lift + t_v)) * mu + outputs['ground_normal_force'][...] = -t_v - lift + weight outputs['forces_horizontal'] = f_h diff --git a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py index 62851765be..01c90665b2 100644 --- a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py +++ b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py @@ -41,6 +41,7 @@ TakeoffTrajectory : a trajectory builder for detailed takeoff """ +from cmath import phase from collections import namedtuple from platform import node @@ -112,11 +113,18 @@ def declare_options(self): ) self.declare( - name='terminal_speed', - types=(float, int, str), + name='terminal_condition', + values=['VEF', 'V1', 'VR', 'LIFTOFF', 'CLIMB_GRADIENT'], allow_none=True, default=None, - desc='true airspeed at which this phase terminates. One of "VEF", "VR", "V1", or a literal value in knots.', + desc='The condition which governs the end of the phase.', + ) + + self.declare( + name='climbing', + types=bool, + default=False, + desc='If False, assume aircraft is operating on the runway.', ) self.declare( @@ -224,10 +232,11 @@ def build_phase(self, aviary_options=None): max_duration, units = user_options['max_duration'] duration_ref = user_options.get_val('time_duration_ref', units) + climbing = user_options['climbing'] phase.set_time_options( fix_initial=True, - duration_bounds=(0.1, max_duration), + duration_bounds=(0.001, max_duration), duration_ref=duration_ref, units=units, ) @@ -270,6 +279,18 @@ def build_phase(self, aviary_options=None): targets=Dynamic.Vehicle.MASS, ) + if climbing: + phase.add_state( + Dynamic.Mission.FLIGHT_PATH_ANGLE, + fix_initial=True, + lower=0, + ref=1.0, + upper=1.5, + defect_ref=1.0, + units='rad', + rate_source=Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, + ) + # TODO: Energy phase places this under an if num_engines > 0. phase.add_control( Dynamic.Vehicle.Propulsion.THROTTLE, @@ -281,12 +302,12 @@ def build_phase(self, aviary_options=None): if user_options['pitch_control'] == 'alpha_fixed': phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') elif user_options['pitch_control'] == 'alpha_rate': + phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, val=2.0, opt=False, units='deg/s') phase.add_state( Dynamic.Vehicle.ANGLE_OF_ATTACK, fix_initial=True, fix_final=False, - lower=-14, - upper=14, + ref=1.0, units='deg', rate_source=Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, @@ -304,8 +325,8 @@ def build_phase(self, aviary_options=None): ) # Define velocity to go based on definition of terminal speed, if applicable. - terminal_speed = user_options['terminal_speed'] - if terminal_speed == 'V1': + terminal_condition = user_options['terminal_condition'] + if terminal_condition == 'V1': # Propagate until speed is the decision speed. # In balanced field applications, dV1 will be # set such that a rejected takeoff and a @@ -317,18 +338,19 @@ def build_phase(self, aviary_options=None): dv1={'units': 'kn'}, v_to_go={'units': 'kn'}, ) - v_to_go_calc = True - elif terminal_speed == 'VR': + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + elif terminal_condition == 'VR': # Propagate until speed is the rotation speed. - phase.add_calc_expr( - 'v_to_go = velocity - (dVR + dV1 + v_stall)', - velocity={'units': 'kn'}, - dv1={'units': 'kn'}, - dVR={'units': 'kn'}, - v_to_go={'units': 'kn'}, - ) - v_to_go_calc = True - elif terminal_speed == 'VEF': + # phase.add_calc_expr( + # 'v_to_go = velocity - (dVR + dV1 + v_stall)', + # velocity={'units': 'kn'}, + # dv1={'units': 'kn'}, + # dVR={'units': 'kn'}, + # v_to_go={'units': 'kn'}, + # ) + pass + phase.add_boundary_balance(param='t_duration', name='v_over_v_stall', tgt_val=1.05, loc='final') + elif terminal_condition == 'VEF': # Propagate until engine failure. # Note we expect dVEF to be negative here. # In balanced field applications, dVEF will be set @@ -341,24 +363,32 @@ def build_phase(self, aviary_options=None): dVEF={'units': 'kn'}, v_to_go={'units': 'kn'}, ) - v_to_go_calc = True - elif isinstance(terminal_speed, (float, int)): + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + elif terminal_condition == 'LIFTOFF': # Propagate until velocity is the desired literal value. phase.add_boundary_balance( - param='t_duration', name='velocity', tgt_val=terminal_speed, loc='final' + param='t_duration', name='takeoff_eom.ground_normal_force', tgt_val=0.0, lower=0.0001, upper=20, eq_units='MN', loc='final') + elif terminal_condition == 'CLIMB_GRADIENT': + phase.add_calc_expr( + 'climb_gradient = tan(flight_path_angle)', + climb_gradient={'units': 'unitless'}, + flight_path_angle={'units': 'rad'}, ) - phase.options['auto_order'] = True - elif terminal_speed is None: + phase.add_boundary_balance( + param='t_duration', name='climb_gradient', tgt_val=0.024, lower=0.0001, upper=20, eq_units='unitless', loc='final') + elif terminal_condition is None: # Propagate for t_duration pass else: raise ValueError( - f'Unrecognized value for terminal_speed ({terminal_speed}).' + f'Unrecognized value for terminal_speed ({terminal_condition}).' 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' ) - if v_to_go_calc: - phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + phase.add_parameter('dV1', opt=False, val=10.0, units='kn', desc='Decision speed delta above stall speed.') + # phase.add_parameter('dVEF', opt=False, val=10.0, desc='Decision speed delta below decision speed.') + + if phase.boundary_balance_options: phase.options['auto_order'] = True if user_options['nonlinear_solver'] is not None: @@ -378,13 +408,21 @@ def make_default_transcription(self): def _extra_ode_init_kwargs(self): """Return extra kwargs required for initializing the ODE.""" - return {'climbing': False, 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT} + return {'climbing': self.user_options['climbing'], 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT} DetailedTakeoffPhaseBuilder._add_initial_guess_meta_data( InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK) ) +DetailedTakeoffPhaseBuilder._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE) +) + +DetailedTakeoffPhaseBuilder._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Mission.FLIGHT_PATH_ANGLE) +) + @_init_initial_guess_meta_data class TakeoffBrakeReleaseToDecisionSpeed(PhaseBuilderBase): @@ -3335,6 +3373,7 @@ def __init__(self, name=None): # self._brake_release_to_decision_speed = None self._decision_speed_to_rotate = None self._rotate_to_liftoff = None + self._liftoff_to_climb_gradient = None self._liftoff_to_obstacle = None self._obstacle_to_mic_p2 = None self._mic_p2_to_engine_cutback = None @@ -3387,6 +3426,15 @@ def set_rotate_to_liftoff(self, phase_builder: PhaseBuilderBase): """ self._rotate_to_liftoff = phase_builder + def set_liftoff_to_climb_gradient(self, phase_builder: PhaseBuilderBase): + """ + Assign the phase builder for the phase from liftoff until the required climb gradient is reached. + + Args: + phase_builder (PhaseBuilderBase): _description_ + """ + self._liftoff_to_climb_gradient = phase_builder + def set_liftoff_to_obstacle(self, phase_builder: PhaseBuilderBase): """ Assign a phase builder for the short period between liftoff and clearing the @@ -3565,9 +3613,9 @@ def _add_phases(self, aviary_options: AviaryValues): self._add_phase(self._decision_speed_to_rotate, aviary_options) - # self._add_phase(self._rotate_to_liftoff, aviary_options) + self._add_phase(self._rotate_to_liftoff, aviary_options) - # self._add_phase(self._liftoff_to_obstacle, aviary_options) + self._add_phase(self._liftoff_to_climb_gradient, aviary_options) # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 @@ -3594,11 +3642,16 @@ def _link_phases(self): brake_release_name = self._brake_release_to_decision_speed.name decision_speed_name = self._decision_speed_to_rotate.name - # rotate_name = self._rotate_to_liftoff.name + rotate_name = self._rotate_to_liftoff.name + liftoff_name = self._liftoff_to_climb_gradient.name - basic_vars = ['time', 'distance', 'velocity', 'mass'] + traj.link_phases([brake_release_name, decision_speed_name, rotate_name, liftoff_name], + vars=['time', 'distance', 'velocity', 'mass'], + connected=True) - traj.link_phases([brake_release_name, decision_speed_name], vars=basic_vars, connected=True) + traj.link_phases([rotate_name, liftoff_name], + vars=['angle_of_attack'], + connected=True) # ext_vars = basic_vars + ['angle_of_attack'] diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field.py b/aviary/mission/flops_based/phases/test/test_balanced_field.py index 4c6d551db5..c174a18812 100644 --- a/aviary/mission/flops_based/phases/test/test_balanced_field.py +++ b/aviary/mission/flops_based/phases/test/test_balanced_field.py @@ -66,7 +66,7 @@ def test_case1(self): takeoff_brake_release_user_options.set_val('time_duration_ref', val=60.0, units='s') takeoff_brake_release_user_options.set_val('distance_max', val=7500.0, units='ft') takeoff_brake_release_user_options.set_val('max_velocity', val=167.85, units='kn') - takeoff_brake_release_user_options.set_val('terminal_speed', val='V1') + takeoff_brake_release_user_options.set_val('terminal_condition', val='V1') tobl_nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) tobl_nl_solver.linesearch = om.BoundsEnforceLS() @@ -103,7 +103,7 @@ def test_case1(self): takeoff_v1_to_vr_user_options.set_val('time_duration_ref', val=60.0, units='s') takeoff_v1_to_vr_user_options.set_val('distance_max', val=15000.0, units='ft') takeoff_v1_to_vr_user_options.set_val('max_velocity', val=167.85, units='kn') - takeoff_v1_to_vr_user_options.set_val('terminal_speed', val='VR') + takeoff_v1_to_vr_user_options.set_val('terminal_condition', val='VR') nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) nl_solver.linesearch = om.BoundsEnforceLS() @@ -132,6 +132,86 @@ def test_case1(self): initial_guesses=takeoff_v1_to_vr_initial_guesses, ) + # ROTATION TO LIFTOFF + + vr_to_liftoff_user_options = av.AviaryValues() + + vr_to_liftoff_user_options.set_val('max_duration', val=90.0, units='s') + vr_to_liftoff_user_options.set_val('time_duration_ref', val=60.0, units='s') + vr_to_liftoff_user_options.set_val('distance_max', val=15000.0, units='ft') + vr_to_liftoff_user_options.set_val('max_velocity', val=167.85, units='kn') + vr_to_liftoff_user_options.set_val('pitch_control', val='alpha_rate', units='unitless') + vr_to_liftoff_user_options.set_val('terminal_condition', val='LIFTOFF') + + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) + nl_solver.linesearch = om.BoundsEnforceLS() + + vr_to_liftoff_user_options.set_val('nonlinear_solver', val=nl_solver) + vr_to_liftoff_user_options.set_val('linear_solver', val=om.DirectSolver()) + + vr_to_liftoff_initial_guesses = av.AviaryValues() + + vr_to_liftoff_initial_guesses.set_val('time', [31.0, 5.0], 's') + vr_to_liftoff_initial_guesses.set_val('distance', [4800.0, 5500.0], 'ft') + vr_to_liftoff_initial_guesses.set_val('velocity', [100., 120.0], 'kn') + vr_to_liftoff_initial_guesses.set_val('angle_of_attack', [0.0, 5.0], 'deg') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + vr_to_liftoff_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + vr_to_liftoff_initial_guesses.set_val('throttle', 1.0) + vr_to_liftoff_initial_guesses.set_val('angle_of_attack_rate', 2.0, units='deg/s') + + vr_to_liftoff_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_rotate_to_liftoff', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=vr_to_liftoff_user_options, + initial_guesses=vr_to_liftoff_initial_guesses, + ) + + # LIFTOFF TO CLIMB GRADIENT + + liftoff_to_climb_gradient_user_options = av.AviaryValues() + + liftoff_to_climb_gradient_user_options.set_val('max_duration', val=90.0, units='s') + liftoff_to_climb_gradient_user_options.set_val('time_duration_ref', val=60.0, units='s') + liftoff_to_climb_gradient_user_options.set_val('distance_max', val=15000.0, units='ft') + liftoff_to_climb_gradient_user_options.set_val('max_velocity', val=167.85, units='kn') + liftoff_to_climb_gradient_user_options.set_val('pitch_control', val='alpha_rate', units='unitless') + liftoff_to_climb_gradient_user_options.set_val('climbing', val=True, units='unitless') + liftoff_to_climb_gradient_user_options.set_val('terminal_condition', val='CLIMB_GRADIENT') + + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) + nl_solver.linesearch = om.BoundsEnforceLS() + + liftoff_to_climb_gradient_user_options.set_val('nonlinear_solver', val=nl_solver) + liftoff_to_climb_gradient_user_options.set_val('linear_solver', val=om.DirectSolver()) + + liftoff_to_climb_gradient_initial_guesses = av.AviaryValues() + + liftoff_to_climb_gradient_initial_guesses.set_val('time', [35.0, 5.0], 's') + liftoff_to_climb_gradient_initial_guesses.set_val('distance', [5000.0, 6000.0], 'ft') + liftoff_to_climb_gradient_initial_guesses.set_val('velocity', [120., 100.0], 'kn') + liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack', [5.0, 10.0], 'deg') + liftoff_to_climb_gradient_initial_guesses.set_val('flight_path_angle', [0.0, 5.0], 'deg') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + liftoff_to_climb_gradient_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + liftoff_to_climb_gradient_initial_guesses.set_val('throttle', 1.0) + liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack_rate', 2.0, units='deg/s') + + liftoff_to_climb_gradient_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_liftoff_to_climb_gradient', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=liftoff_to_climb_gradient_user_options, + initial_guesses=liftoff_to_climb_gradient_initial_guesses, + ) + # from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( # takeoff_decision_speed_builder, # takeoff_engine_cutback_builder, @@ -152,7 +232,9 @@ def test_case1(self): takeoff_trajectory_builder.set_decision_speed_to_rotate(takeoff_decision_speed_to_rotate_builder) - # takeoff_trajectory_builder.set_rotate_to_liftoff(takeoff_rotate_builder) + takeoff_trajectory_builder.set_rotate_to_liftoff(vr_to_liftoff_builder) + + takeoff_trajectory_builder.set_liftoff_to_climb_gradient(liftoff_to_climb_gradient_builder) # takeoff_trajectory_builder.set_liftoff_to_obstacle(takeoff_liftoff_builder) @@ -231,11 +313,13 @@ def test_case1(self): takeoff.run_model() - takeoff.model.list_vars(print_arrays=True) + takeoff.model.list_vars(print_arrays=True, units=True) + + # takeoff.check_partials(compact_print=True) # takeoff.model.run_apply_nonlinear() # takeoff.model.list_vars(print_arrays=True) - om.n2(takeoff.model) + # om.n2(takeoff.model) if __name__ == '__main__': From bb016e92b373f0418b13672ec9d70726afa296d4 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Thu, 7 Aug 2025 16:39:10 -0400 Subject: [PATCH 04/22] takeoff through obstacle working with a newton solver to balance alpha --- aviary/mission/flops_based/ode/takeoff_eom.py | 65 +++++++---- aviary/mission/flops_based/ode/takeoff_ode.py | 17 ++- .../phases/detailed_takeoff_phases.py | 93 ++++++++++++--- .../phases/test/test_balanced_field.py | 107 ++++++++++++++++-- aviary/mission/initial_guess_builders.py | 30 ++--- 5 files changed, 241 insertions(+), 71 deletions(-) diff --git a/aviary/mission/flops_based/ode/takeoff_eom.py b/aviary/mission/flops_based/ode/takeoff_eom.py index 70b1986d04..87283a68f5 100644 --- a/aviary/mission/flops_based/ode/takeoff_eom.py +++ b/aviary/mission/flops_based/ode/takeoff_eom.py @@ -118,6 +118,13 @@ def initialize(self): desc='current friction coefficient key, either rolling friction or braking friction', ) + self.options.declare( + 'pitch_control', + values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], + default='alpha_fixed', + desc='How pitch is controlled.', + ) + options.declare( 'aviary_options', types=AviaryValues, @@ -131,6 +138,7 @@ def setup(self): climbing = options['climbing'] friction_key = options['friction_key'] aviary_options = options['aviary_options'] + pitch_control = options['pitch_control'] mu = aviary_options.get_val(friction_key) kwargs = {'num_nodes': nn, 'climbing': climbing} @@ -186,6 +194,16 @@ def setup(self): promotes=['*'], ) + if self.options['pitch_control'] == 'gamma_fixed': + # If using fixed-flight-path-angle control + alpha_resid_comp = om.InputResidsComp() + self.add_subsystem( + 'alpha_resid_comp', + alpha_resid_comp, + promotes=['*'] + ) + alpha_resid_comp.add_input(Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, shape=(nn,), units='rad/s') + alpha_resid_comp.add_output(Dynamic.Vehicle.ANGLE_OF_ATTACK, shape=(nn,), val=0.0, units='rad') class DistanceRates(om.ExplicitComponent): """ @@ -241,6 +259,7 @@ def setup_partials(self): def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): velocity = inputs[Dynamic.Mission.VELOCITY] + if self.options['climbing']: flight_path_angle = inputs[Dynamic.Mission.FLIGHT_PATH_ANGLE] @@ -255,6 +274,8 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): else: range_rate = velocity + outputs[Dynamic.Mission.ALTITUDE_RATE][...] = 0.0 + outputs[Dynamic.Mission.DISTANCE_RATE] = range_rate def compute_partials(self, inputs, J, discrete_inputs=None): @@ -566,7 +587,7 @@ def setup(self): 'ground_normal_force', val=np.zeros(nn), units='N', - desc='runway force pushing up on the landing gear while on the ground and lift < weight.' + desc='runway force pushing up on the landing gear while on the ground and lift < weight.', ) def setup_partials(self): @@ -596,8 +617,9 @@ def setup_partials(self): Dynamic.Mission.FLIGHT_PATH_ANGLE, ] - self.declare_partials(['forces_vertical', - 'forces_horizontal'], wrt, rows=rows_cols, cols=rows_cols) + self.declare_partials( + ['forces_vertical', 'forces_horizontal'], wrt, rows=rows_cols, cols=rows_cols + ) else: aviary_options: AviaryValues = options['aviary_options'] @@ -648,23 +670,29 @@ def setup_partials(self): self.declare_partials('forces_vertical', ['*'], dependent=False) - self.declare_partials('ground_normal_force', - Dynamic.Vehicle.MASS, - rows=rows_cols, - cols=rows_cols, - val=grav_metric) + self.declare_partials( + 'ground_normal_force', + Dynamic.Vehicle.MASS, + rows=rows_cols, + cols=rows_cols, + val=grav_metric, + ) - self.declare_partials('ground_normal_force', - Dynamic.Vehicle.LIFT, - rows=rows_cols, - cols=rows_cols, - val=-1.0) + self.declare_partials( + 'ground_normal_force', + Dynamic.Vehicle.LIFT, + rows=rows_cols, + cols=rows_cols, + val=-1.0, + ) - self.declare_partials('ground_normal_force', - Dynamic.Vehicle.Propulsion.THRUST_TOTAL, - rows=rows_cols, - cols=rows_cols, - val=np.sin(t_inc)) + self.declare_partials( + 'ground_normal_force', + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + rows=rows_cols, + cols=rows_cols, + val=np.sin(t_inc), + ) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): options = self.options @@ -703,7 +731,6 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): f_v = t_v - drag * s_gamma + lift * c_gamma - weight - outputs['forces_vertical'] = f_v outputs['ground_normal_force'][...] = 0.0 diff --git a/aviary/mission/flops_based/ode/takeoff_ode.py b/aviary/mission/flops_based/ode/takeoff_ode.py index 7fbbb2479d..1ec596748d 100644 --- a/aviary/mission/flops_based/ode/takeoff_ode.py +++ b/aviary/mission/flops_based/ode/takeoff_ode.py @@ -29,6 +29,13 @@ def initialize(self): desc='mode of operation (ground roll or flight)', ) + self.options.declare( + 'pitch_control', + values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], + default='alpha_fixed', + desc='How pitch is controlled.', + ) + def setup(self): options = self.options @@ -64,12 +71,13 @@ def setup(self): 'climbing': options['climbing'], 'friction_key': options['friction_key'], 'aviary_options': options['aviary_options'], + 'pitch_control': options['pitch_control'] } self.add_subsystem( 'takeoff_eom', TakeoffEOM(**kwargs), - promotes_inputs=[ + promotes=[ Dynamic.Mission.FLIGHT_PATH_ANGLE, Dynamic.Mission.VELOCITY, Dynamic.Vehicle.MASS, @@ -77,8 +85,6 @@ def setup(self): Dynamic.Vehicle.Propulsion.THRUST_TOTAL, Dynamic.Vehicle.DRAG, Dynamic.Vehicle.ANGLE_OF_ATTACK, - ], - promotes_outputs=[ Dynamic.Mission.DISTANCE_RATE, Dynamic.Mission.ALTITUDE_RATE, Dynamic.Mission.VELOCITY_RATE, @@ -103,3 +109,8 @@ def setup(self): self.set_input_defaults(Dynamic.Mission.ALTITUDE, np.zeros(nn), 'm') self.set_input_defaults(Dynamic.Mission.VELOCITY, np.zeros(nn), 'm/s') self.set_input_defaults(Aircraft.Wing.AREA, 1.0, 'm**2') + + if self.options['pitch_control'] == 'gamma_fixed': + self.nonlinear_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, atol=1.0E-6, rtol=1.0E-6, iprint=0, debug_print=False) + self.nonlinear_solver.linesearch = om.ArmijoGoldsteinLS() + self.linear_solver = om.DirectSolver(assemble_jac=True) diff --git a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py index 01c90665b2..26b706fcac 100644 --- a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py +++ b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py @@ -114,7 +114,7 @@ def declare_options(self): self.declare( name='terminal_condition', - values=['VEF', 'V1', 'VR', 'LIFTOFF', 'CLIMB_GRADIENT'], + values=['VEF', 'V1', 'VR', 'LIFTOFF', 'CLIMB_GRADIENT', 'OBSTACLE'], allow_none=True, default=None, desc='The condition which governs the end of the phase.', @@ -129,9 +129,10 @@ def declare_options(self): self.declare( name='pitch_control', - values=['alpha_fixed', 'alpha_rate'], + values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], default='alpha_fixed', - desc='Specifies whether alpha is controled as a fixed value or as a state with a fixed rate.', + desc='Specifies how alpha is controlled - Alpha can be a fixed parameter, specified via a fixed rate parameter, ' \ + 'or whether the climb gradient is constant', ) self.declare( @@ -291,6 +292,16 @@ def build_phase(self, aviary_options=None): rate_source=Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, ) + phase.add_state( + Dynamic.Mission.ALTITUDE, + fix_initial=False, + lower=0, + ref=1.0, + defect_ref=1.0, + units='ft', + rate_source=Dynamic.Mission.ALTITUDE_RATE, + ) + # TODO: Energy phase places this under an if num_engines > 0. phase.add_control( Dynamic.Vehicle.Propulsion.THROTTLE, @@ -301,13 +312,14 @@ def build_phase(self, aviary_options=None): if user_options['pitch_control'] == 'alpha_fixed': phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') - elif user_options['pitch_control'] == 'alpha_rate': - phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, val=2.0, opt=False, units='deg/s') + elif user_options['pitch_control'] == 'alpha_rate_fixed': + phase.add_parameter( + Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, val=2.0, opt=False, units='deg/s' + ) phase.add_state( Dynamic.Vehicle.ANGLE_OF_ATTACK, fix_initial=True, fix_final=False, - ref=1.0, units='deg', rate_source=Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, @@ -349,7 +361,9 @@ def build_phase(self, aviary_options=None): # v_to_go={'units': 'kn'}, # ) pass - phase.add_boundary_balance(param='t_duration', name='v_over_v_stall', tgt_val=1.05, loc='final') + phase.add_boundary_balance( + param='t_duration', name='v_over_v_stall', tgt_val=1.05, loc='final' + ) elif terminal_condition == 'VEF': # Propagate until engine failure. # Note we expect dVEF to be negative here. @@ -367,7 +381,14 @@ def build_phase(self, aviary_options=None): elif terminal_condition == 'LIFTOFF': # Propagate until velocity is the desired literal value. phase.add_boundary_balance( - param='t_duration', name='takeoff_eom.ground_normal_force', tgt_val=0.0, lower=0.0001, upper=20, eq_units='MN', loc='final') + param='t_duration', + name='takeoff_eom.ground_normal_force', + tgt_val=0.0, + lower=0.0001, + upper=20, + eq_units='MN', + loc='final', + ) elif terminal_condition == 'CLIMB_GRADIENT': phase.add_calc_expr( 'climb_gradient = tan(flight_path_angle)', @@ -375,7 +396,24 @@ def build_phase(self, aviary_options=None): flight_path_angle={'units': 'rad'}, ) phase.add_boundary_balance( - param='t_duration', name='climb_gradient', tgt_val=0.024, lower=0.0001, upper=20, eq_units='unitless', loc='final') + param='t_duration', + name='climb_gradient', + tgt_val=0.024, + lower=0.0001, + upper=20, + eq_units='unitless', + loc='final', + ) + elif terminal_condition == 'OBSTACLE': + phase.add_boundary_balance( + param='t_duration', + name=Dynamic.Mission.ALTITUDE, + tgt_val=35, + lower=0.0001, + upper=100, + eq_units='ft', + loc='final', + ) elif terminal_condition is None: # Propagate for t_duration pass @@ -385,7 +423,9 @@ def build_phase(self, aviary_options=None): 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' ) - phase.add_parameter('dV1', opt=False, val=10.0, units='kn', desc='Decision speed delta above stall speed.') + phase.add_parameter( + 'dV1', opt=False, val=10.0, units='kn', desc='Decision speed delta above stall speed.' + ) # phase.add_parameter('dVEF', opt=False, val=10.0, desc='Decision speed delta below decision speed.') if phase.boundary_balance_options: @@ -408,7 +448,11 @@ def make_default_transcription(self): def _extra_ode_init_kwargs(self): """Return extra kwargs required for initializing the ODE.""" - return {'climbing': self.user_options['climbing'], 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT} + return { + 'climbing': self.user_options['climbing'], + 'pitch_control': self.user_options['pitch_control'], + 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT, + } DetailedTakeoffPhaseBuilder._add_initial_guess_meta_data( @@ -3374,6 +3418,7 @@ def __init__(self, name=None): self._decision_speed_to_rotate = None self._rotate_to_liftoff = None self._liftoff_to_climb_gradient = None + self._climb_gradient_to_obstacle = None self._liftoff_to_obstacle = None self._obstacle_to_mic_p2 = None self._mic_p2_to_engine_cutback = None @@ -3435,6 +3480,15 @@ def set_liftoff_to_climb_gradient(self, phase_builder: PhaseBuilderBase): """ self._liftoff_to_climb_gradient = phase_builder + def set_climb_gradient_to_obstacle(self, phase_builder: PhaseBuilderBase): + """ + Assign the phase builder for the phase from achievement of climb gradient until obstacle clearance. + + Args: + phase_builder (PhaseBuilderBase): _description_ + """ + self._climb_gradient_to_obstacle = phase_builder + def set_liftoff_to_obstacle(self, phase_builder: PhaseBuilderBase): """ Assign a phase builder for the short period between liftoff and clearing the @@ -3617,6 +3671,8 @@ def _add_phases(self, aviary_options: AviaryValues): self._add_phase(self._liftoff_to_climb_gradient, aviary_options) + self._add_phase(self._climb_gradient_to_obstacle, aviary_options) + # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 # if obstacle_to_mic_p2 is not None: @@ -3644,14 +3700,17 @@ def _link_phases(self): decision_speed_name = self._decision_speed_to_rotate.name rotate_name = self._rotate_to_liftoff.name liftoff_name = self._liftoff_to_climb_gradient.name + climb_gradient_name = self._climb_gradient_to_obstacle.name + + traj.link_phases( + [brake_release_name, decision_speed_name, rotate_name, liftoff_name, climb_gradient_name], + vars=['time', 'distance', 'velocity', 'mass'], + connected=True, + ) - traj.link_phases([brake_release_name, decision_speed_name, rotate_name, liftoff_name], - vars=['time', 'distance', 'velocity', 'mass'], - connected=True) + traj.link_phases([rotate_name, liftoff_name], vars=['angle_of_attack',], connected=True) - traj.link_phases([rotate_name, liftoff_name], - vars=['angle_of_attack'], - connected=True) + traj.link_phases([liftoff_name, climb_gradient_name], vars=['flight_path_angle', 'altitude'], connected=True) # ext_vars = basic_vars + ['angle_of_attack'] diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field.py b/aviary/mission/flops_based/phases/test/test_balanced_field.py index c174a18812..5b68957bd7 100644 --- a/aviary/mission/flops_based/phases/test/test_balanced_field.py +++ b/aviary/mission/flops_based/phases/test/test_balanced_field.py @@ -68,7 +68,7 @@ def test_case1(self): takeoff_brake_release_user_options.set_val('max_velocity', val=167.85, units='kn') takeoff_brake_release_user_options.set_val('terminal_condition', val='V1') - tobl_nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) + tobl_nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) tobl_nl_solver.linesearch = om.BoundsEnforceLS() takeoff_brake_release_user_options.set_val('nonlinear_solver', val=tobl_nl_solver) @@ -105,7 +105,7 @@ def test_case1(self): takeoff_v1_to_vr_user_options.set_val('max_velocity', val=167.85, units='kn') takeoff_v1_to_vr_user_options.set_val('terminal_condition', val='VR') - nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) nl_solver.linesearch = om.BoundsEnforceLS() takeoff_v1_to_vr_user_options.set_val('nonlinear_solver', val=nl_solver) @@ -140,10 +140,10 @@ def test_case1(self): vr_to_liftoff_user_options.set_val('time_duration_ref', val=60.0, units='s') vr_to_liftoff_user_options.set_val('distance_max', val=15000.0, units='ft') vr_to_liftoff_user_options.set_val('max_velocity', val=167.85, units='kn') - vr_to_liftoff_user_options.set_val('pitch_control', val='alpha_rate', units='unitless') + vr_to_liftoff_user_options.set_val('pitch_control', val='alpha_rate_fixed', units='unitless') vr_to_liftoff_user_options.set_val('terminal_condition', val='LIFTOFF') - nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) nl_solver.linesearch = om.BoundsEnforceLS() vr_to_liftoff_user_options.set_val('nonlinear_solver', val=nl_solver) @@ -171,7 +171,7 @@ def test_case1(self): initial_guesses=vr_to_liftoff_initial_guesses, ) - # LIFTOFF TO CLIMB GRADIENT + # LIFTOFF TO CLIMB GRADIENT liftoff_to_climb_gradient_user_options = av.AviaryValues() @@ -179,11 +179,11 @@ def test_case1(self): liftoff_to_climb_gradient_user_options.set_val('time_duration_ref', val=60.0, units='s') liftoff_to_climb_gradient_user_options.set_val('distance_max', val=15000.0, units='ft') liftoff_to_climb_gradient_user_options.set_val('max_velocity', val=167.85, units='kn') - liftoff_to_climb_gradient_user_options.set_val('pitch_control', val='alpha_rate', units='unitless') + liftoff_to_climb_gradient_user_options.set_val('pitch_control', val='alpha_rate_fixed', units='unitless') liftoff_to_climb_gradient_user_options.set_val('climbing', val=True, units='unitless') liftoff_to_climb_gradient_user_options.set_val('terminal_condition', val='CLIMB_GRADIENT') - nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=True) + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) nl_solver.linesearch = om.BoundsEnforceLS() liftoff_to_climb_gradient_user_options.set_val('nonlinear_solver', val=nl_solver) @@ -191,11 +191,11 @@ def test_case1(self): liftoff_to_climb_gradient_initial_guesses = av.AviaryValues() - liftoff_to_climb_gradient_initial_guesses.set_val('time', [35.0, 5.0], 's') + liftoff_to_climb_gradient_initial_guesses.set_val('time', [35.0, 1.0], 's') liftoff_to_climb_gradient_initial_guesses.set_val('distance', [5000.0, 6000.0], 'ft') liftoff_to_climb_gradient_initial_guesses.set_val('velocity', [120., 100.0], 'kn') liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack', [5.0, 10.0], 'deg') - liftoff_to_climb_gradient_initial_guesses.set_val('flight_path_angle', [0.0, 5.0], 'deg') + liftoff_to_climb_gradient_initial_guesses.set_val('flight_path_angle', [0.0, 2.0], 'deg') gross_mass_units = 'lbm' gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) @@ -212,6 +212,45 @@ def test_case1(self): initial_guesses=liftoff_to_climb_gradient_initial_guesses, ) + # CLIMB GRADIENT TO OBSTACLE + + climb_gradient_to_obstacle_user_options = av.AviaryValues() + + climb_gradient_to_obstacle_user_options.set_val('max_duration', val=90.0, units='s') + climb_gradient_to_obstacle_user_options.set_val('time_duration_ref', val=60.0, units='s') + climb_gradient_to_obstacle_user_options.set_val('distance_max', val=15000.0, units='ft') + climb_gradient_to_obstacle_user_options.set_val('max_velocity', val=200., units='kn') + climb_gradient_to_obstacle_user_options.set_val('pitch_control', val='gamma_fixed', units='unitless') + climb_gradient_to_obstacle_user_options.set_val('climbing', val=True, units='unitless') + climb_gradient_to_obstacle_user_options.set_val('terminal_condition', val='OBSTACLE') + + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=False) + nl_solver.linesearch = om.BoundsEnforceLS() + + climb_gradient_to_obstacle_user_options.set_val('nonlinear_solver', val=nl_solver) + climb_gradient_to_obstacle_user_options.set_val('linear_solver', val=om.DirectSolver()) + + climb_gradient_to_obstacle_initial_guesses = av.AviaryValues() + + climb_gradient_to_obstacle_initial_guesses.set_val('time', [35.0, 3.0], 's') + climb_gradient_to_obstacle_initial_guesses.set_val('distance', [5500.0, 5800.0], 'ft') + climb_gradient_to_obstacle_initial_guesses.set_val('velocity', [120., 120.0], 'kn') + climb_gradient_to_obstacle_initial_guesses.set_val('flight_path_angle', [2.0, 2.0], 'deg') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + climb_gradient_to_obstacle_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + climb_gradient_to_obstacle_initial_guesses.set_val('throttle', 1.0) + + climb_gradient_to_obstacle_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_climb_gradient_to_obstacle', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=climb_gradient_to_obstacle_user_options, + initial_guesses=climb_gradient_to_obstacle_initial_guesses, + ) + # from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( # takeoff_decision_speed_builder, # takeoff_engine_cutback_builder, @@ -236,6 +275,8 @@ def test_case1(self): takeoff_trajectory_builder.set_liftoff_to_climb_gradient(liftoff_to_climb_gradient_builder) + takeoff_trajectory_builder.set_climb_gradient_to_obstacle(climb_gradient_to_obstacle_builder) + # takeoff_trajectory_builder.set_liftoff_to_obstacle(takeoff_liftoff_builder) # takeoff_trajectory_builder.set_obstacle_to_mic_p2(takeoff_mic_p2_builder) @@ -303,7 +344,7 @@ def test_case1(self): # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' with warnings.catch_warnings(): warnings.simplefilter('ignore', om.PromotionWarning) - takeoff.setup(check=True) + takeoff.setup(check=False) av.set_aviary_initial_values(takeoff, aviary_options) @@ -311,10 +352,54 @@ def test_case1(self): takeoff.final_setup() + # takeoff.model.run_apply_nonlinear() + + # takeoff.model.list_vars(print_arrays=True) + takeoff.run_model() - takeoff.model.list_vars(print_arrays=True, units=True) + takeoff.model.traj.phases.takeoff_climb_gradient_to_obstacle.ode_iter_group.segment_prop_group.ode_all.takeoff_eom.list_vars(print_arrays=True) + + # vars = takeoff.model.list_vars(print_arrays=True, units=True, prom_name=True, return_format='dict', out_stream=None) + + # vars = {meta['prom_name']: meta for meta in vars.values()} + + # systems = set([path.rsplit('.', 1)[0] for path in vars.keys()]) + + # print('\n'.join(vars.keys())) + + # for sys in takeoff.model.system_iter(include_self=True, recurse=True): + # sys.list_vars(prom_name=True, units=True) + + + # from textual.app import App, ComposeResult + # from textual.widgets import DataTable, Collapsible + + + # class TableApp(App): + + # def __init__(self, systems): + # self._systems = systems + + # def compose(self) -> ComposeResult: + # for sys, vars in self._systems.items(): + # with Collapsible(title=sys.pathname): + # yield DataTable() + + # def on_mount(self) -> None: + # table = self.query_one(DataTable) + # table.add_columns('promoted name', 'units') + # print(table) + # # table.add_rows(ROWS[1:]) + + # systems = {} + + # for sys in takeoff.model.system_iter(include_self=True, recurse=True): + # vars = sys.list_vars(prom_name=True, units=True, out_stream=None) + # systems[sys] = vars + # app = TableApp(systems) + # app.run() # takeoff.check_partials(compact_print=True) # takeoff.model.run_apply_nonlinear() diff --git a/aviary/mission/initial_guess_builders.py b/aviary/mission/initial_guess_builders.py index 33a6118da3..686404e58d 100644 --- a/aviary/mission/initial_guess_builders.py +++ b/aviary/mission/initial_guess_builders.py @@ -45,37 +45,25 @@ def __init__(self, key): def apply_initial_guess( self, prob: om.Problem, traj_name, phase: dm.Phase, phase_name, val, units ): - """Set the initial guess on the problem.""" - complete_key = self._get_complete_key(traj_name, phase_name) - # TODO: this is a short term hack in need of an appropriate long term solution - # - to interpolate, or not to interpolate: that is the question - # - the solution should probably be a value decoration (a wrapper) that is - # both lightweight and easy to check and unpack if self.key in phase.state_options: phase.set_state_val(self.key, val, units=units) elif self.key in phase.control_options: phase.set_control_val(self.key, val, units=units) elif self.key in phase.parameter_options: phase.set_parameter_val(self.key, val, units=units) + elif self.key == phase.time_options['name']: + prob.set_integ_var_val(initial=val[0], duration=val[1], units=units) else: - prob.set_val(complete_key, val, units=units) + raise ValueError(f'{phase.msginfo} Attempting to apply initial guess for {self.key}.\n' + 'Not find in the states, control, parameters, or integration variable of the phase.') - # if isinstance(val, np.ndarray) or (isinstance(val, Sequence) and not isinstance(val, str)): - # val = phase.interp(self.key, val) + # def _get_complete_key(self, traj_name, phase_name): + # """Compose the complete key for setting the initial guess.""" + # _ = traj_name + # _ = phase_name - # try: - # prob.set_val(complete_key, val, units) - # except KeyError: - # complete_key = complete_key.replace('polynomial_controls', 'controls') - # prob.set_val(complete_key, val, units) - - def _get_complete_key(self, traj_name, phase_name): - """Compose the complete key for setting the initial guess.""" - _ = traj_name - _ = phase_name - - return self.key + # return self.key class InitialGuessControl(InitialGuess): From 0fd314d8a9ccb20db9c8d285ff9cc2446d9b6df6 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Wed, 27 Aug 2025 13:15:42 -0400 Subject: [PATCH 05/22] starting to work --- aviary/api.py | 3 + .../phases/balanced_field_trajectory.py | 780 ++++++++++++++++++ .../phases/test/test_balanced_field.py | 2 + .../phases/test/test_balanced_field2.py | 375 +++++++++ 4 files changed, 1160 insertions(+) create mode 100644 aviary/mission/flops_based/phases/balanced_field_trajectory.py create mode 100644 aviary/mission/flops_based/phases/test/test_balanced_field2.py diff --git a/aviary/api.py b/aviary/api.py index cfa26ce244..7ea2a39742 100644 --- a/aviary/api.py +++ b/aviary/api.py @@ -180,6 +180,9 @@ TakeoffTrajectory as DetailedTakeoffTrajectoryBuilder, ) +from aviary.mission.flops_based.phases.balanced_field_trajectory import BalancedFieldPhaseBuilder +from aviary.mission.balanced_field_traj_builder import BalancedFieldTrajectoryBuilder + ############## # Subsystems # ############## diff --git a/aviary/mission/flops_based/phases/balanced_field_trajectory.py b/aviary/mission/flops_based/phases/balanced_field_trajectory.py new file mode 100644 index 0000000000..fef0563a94 --- /dev/null +++ b/aviary/mission/flops_based/phases/balanced_field_trajectory.py @@ -0,0 +1,780 @@ +""" +Define utilities for building detailed takeoff phases and the typical takeoff trajectory. + +Classes +------- +TakeoffBrakeReleaseToDecisionSpeed : a phase builder for the first phase of takeoff, from +brake release to decision speed, the maximum speed at which takeoff can be safely brought +to full stop using zero thrust while braking + +TakeoffDecisionSpeedToRotate : a phase builder for the second phase of takeoff, from +decision speed to rotation + +TakeoffDecisionSpeedBrakeDelay : a phase builder for the second phase of aborted takeoff, +from decision speed to brake application + +TakeoffRotateToLiftoff : a phase builder for the third phase of takeoff, from rotation to +liftoff + +TakeoffLiftoffToObstacle : a phase builder for the fourth phase of takeoff, from liftoff +to clearing the required obstacle + +TakeoffObstacleToMicP2 : a phase builder for the fifth phase of takeoff, from +clearing the required obstacle to the P2 mic lication; this phase is required for +acoustic calculations + +TakeoffMicP2ToEngineCutback : a phase builder for the sixth phase of takeoff, from the +P2 mic location to engine cutback; this phase is required for acoustic calculations + +TakeoffEngineCutback : a phase builder for the seventh phase of takeoff, from +start to finish of engine cutback; this phase is required for acoustic calculations + +TakeoffEngineCutbackToMicP1 : a phase builder for the eighth phase of takeoff, from +engine cutback to the P1 mic lication; this phase is required for acoustic calculations + +TakeoffMicP1ToClimb : a phase builder for the ninth phase of takeoff, from +P1 mic location to climb; this phase is required for acoustic calculations + +TakeoffBrakeToAbort : a phase builder for the last phase of aborted takeoff, from brake +application to full stop + +TakeoffTrajectory : a trajectory builder for detailed takeoff +""" + +from collections import namedtuple + +import dymos as dm +import openmdao.api as om +from openmdao.solvers.solver import NonlinearSolver, LinearSolver + +from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE +from aviary.mission.initial_guess_builders import ( + InitialGuessControl, + InitialGuessIntegrationVariable, + InitialGuessParameter, + InitialGuessPolynomialControl, + InitialGuessState, +) +from aviary.mission.phase_builder_base import PhaseBuilderBase +from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder +from aviary.utils.aviary_options_dict import AviaryOptionsDictionary +from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.enums import LegacyCode +from aviary.variable_info.functions import setup_trajectory_params +from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData +from aviary.variable_info.variables import Dynamic, Mission + + +def _init_initial_guess_meta_data(cls: PhaseBuilderBase): + """Create default initial guess meta data preset with common items.""" + cls._initial_guesses_meta_data_ = {} + + cls._add_initial_guess_meta_data( + InitialGuessIntegrationVariable(), + desc='initial guess for initial time and duration specified as a tuple', + ) + + cls._add_initial_guess_meta_data( + InitialGuessState('distance'), desc='initial guess for horizontal distance traveled' + ) + + cls._add_initial_guess_meta_data(InitialGuessState('velocity'), desc='initial guess for speed') + + cls._add_initial_guess_meta_data(InitialGuessState('mass'), desc='initial guess for mass') + + cls._add_initial_guess_meta_data( + InitialGuessControl('throttle'), desc='initial guess for throttle' + ) + + cls._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK) + ) + + cls._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE) + ) + + cls._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Mission.FLIGHT_PATH_ANGLE) + ) + + return cls + + +class BalancedFieldPhaseOptions(AviaryOptionsDictionary): + def declare_options(self): + self.declare( + name='max_duration', + default=1000.0, + units='s', + desc='Upper bound on duration for this phase.', + ) + + self.declare( + name='time_duration_ref', default=10.0, units='s', desc='Scale factor ref for duration.' + ) + + self.declare( + name='distance_max', default=1000.0, units='ft', desc='Upper bound for distance.' + ) + + self.declare( + name='max_velocity', default=100.0, units='ft/s', desc='Upper bound for velocity.' + ) + + self.declare( + name='terminal_condition', + values=['VEF', 'V1', 'VR', 'LIFTOFF', 'CLIMB_GRADIENT', 'OBSTACLE'], + allow_none=True, + default=None, + desc='The condition which governs the end of the phase.', + ) + + self.declare( + name='climbing', + types=bool, + default=False, + desc='If False, assume aircraft is operating on the runway.', + ) + + self.declare( + name='pitch_control', + values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], + default='alpha_fixed', + desc='Specifies how alpha is controlled - Alpha can be a fixed parameter, specified via a fixed rate parameter, ' \ + 'or whether the climb gradient is constant', + ) + + self.declare( + name='nonlinear_solver', + types=(NonlinearSolver,), + allow_none=True, + default=None, + desc='Nonlinear solver applied to the phase if it needs to solve for the terminal speed condition.', + ) + + self.declare( + name='linear_solver', + types=(LinearSolver,), + allow_none=True, + default=None, + desc='Linear solver applied to the phase if it needs to solve for the terminal speed condition.', + ) + + +@_init_initial_guess_meta_data +class BalancedFieldPhaseBuilder(PhaseBuilderBase): + """ + Define a phase builder for detailed takeoff phases. + + Attributes + ---------- + name : str ('takeoff_brake_release') + object label + + user_options : AviaryValues () + state/path constraint values and flags + + supported options: + - max_duration (1000.0, 's') + - time_duration_ref (10.0, 's') + - distance_max (1000.0, 'ft') + - max_velocity (100.0, 'ft/s') + + initial_guesses : AviaryValues () + state/path beginning values to be set on the problem + + supported options: + - time + - range + - velocity + - mass + - throttle + - angle_of_attack + + ode_class : type (None) + advanced: the type of system defining the ODE + + transcription : "Dymos transcription object" (None) + advanced: an object providing the transcription technique of the + optimal control problem + + default_name : str + class attribute: derived type customization point; the default value + for name + + default_ode_class : type + class attribute: derived type customization point; the default value + for ode_class used by build_phase + + Methods + ------- + build_phase + make_default_transcription + """ + + __slots__ = () + + default_name = 'detailed_takeoff' + default_ode_class = TakeoffODE + default_options_class = BalancedFieldPhaseOptions + + def build_phase(self, aviary_options=None): + """ + Return a new phase object for analysis using these constraints. + + If ode_class is None, default_ode_class is used. + + If transcription is None, the return value from calling + make_default_transcription is used. + + Parameters + ---------- + aviary_options : AviaryValues (empty) + collection of Aircraft/Mission specific options + + Returns + ------- + dymos.Phase + """ + phase: dm.Phase = super().build_phase(aviary_options) + + user_options: AviaryValues = self.user_options + + max_duration, units = user_options['max_duration'] + duration_ref = user_options.get_val('time_duration_ref', units) + climbing = user_options['climbing'] + + phase.set_time_options( + fix_initial=True, + duration_bounds=(0.001, max_duration), + duration_ref=duration_ref, + units=units, + ) + + distance_max, units = user_options['distance_max'] + + phase.add_state( + Dynamic.Mission.DISTANCE, + fix_initial=True, + lower=0, + ref=distance_max, + defect_ref=distance_max, + units=units, + upper=distance_max, + rate_source=Dynamic.Mission.DISTANCE_RATE, + ) + + max_velocity, units = user_options['max_velocity'] + + phase.add_state( + Dynamic.Mission.VELOCITY, + fix_initial=True, + lower=0, + ref=max_velocity, + defect_ref=max_velocity, + units=units, + upper=max_velocity, + rate_source=Dynamic.Mission.VELOCITY_RATE, + ) + + phase.add_state( + Dynamic.Vehicle.MASS, + fix_initial=True, + fix_final=False, + lower=0.0, + upper=1e9, + ref=5e4, + units='kg', + rate_source=Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE_TOTAL, + targets=Dynamic.Vehicle.MASS, + ) + + if climbing: + phase.add_state( + Dynamic.Mission.FLIGHT_PATH_ANGLE, + fix_initial=True, + lower=0, + ref=1.0, + upper=1.5, + defect_ref=1.0, + units='rad', + rate_source=Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, + ) + + phase.add_state( + Dynamic.Mission.ALTITUDE, + fix_initial=False, + lower=0, + ref=1.0, + defect_ref=1.0, + units='ft', + rate_source=Dynamic.Mission.ALTITUDE_RATE, + ) + + # TODO: Energy phase places this under an if num_engines > 0. + phase.add_control( + Dynamic.Vehicle.Propulsion.THROTTLE, + targets=Dynamic.Vehicle.Propulsion.THROTTLE, + units='unitless', + opt=False, + ) + + if user_options['pitch_control'] == 'alpha_fixed': + phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') + elif user_options['pitch_control'] == 'alpha_rate_fixed': + phase.add_parameter( + Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, val=2.0, opt=False, units='deg/s' + ) + phase.add_state( + Dynamic.Vehicle.ANGLE_OF_ATTACK, + fix_initial=True, + fix_final=False, + ref=1.0, + units='deg', + rate_source=Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, + targets=Dynamic.Vehicle.ANGLE_OF_ATTACK, + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + output_name=Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + units='lbf', + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' + ) + + # Define velocity to go based on definition of terminal speed, if applicable. + terminal_condition = user_options['terminal_condition'] + if terminal_condition == 'V1': + # Propagate until speed is the decision speed. + # In balanced field applications, dV1 will be + # set such that a rejected takeoff and a + # takeoff to a 35 ft altitude for obstacle clearance + # require the same range. + phase.add_calc_expr( + 'v_to_go = velocity - (dV1 + v_stall)', + velocity={'units': 'kn'}, + dV1={'units': 'kn'}, + v_to_go={'units': 'kn'}, + v_stall={'units': 'kn'}, + ) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + elif terminal_condition == 'VR': + # Propagate until speed is the rotation speed. + # phase.add_calc_expr( + # 'v_to_go = velocity - (dVR + dV1 + v_stall)', + # velocity={'units': 'kn'}, + # dv1={'units': 'kn'}, + # dVR={'units': 'kn'}, + # v_to_go={'units': 'kn'}, + # ) + pass + phase.add_boundary_balance( + param='t_duration', name='v_over_v_stall', tgt_val=1.05, loc='final' + ) + elif terminal_condition == 'VEF': + # Propagate until engine failure. + # Note we expect dVEF to be negative here. + # In balanced field applications, dVEF will be set + # such that it occurs {pilot_reaction_time} seconds + # before V1. + phase.add_calc_expr( + 'v_to_go = velocity - (dVEF + dV1 + v_stall)', + velocity={'units': 'kn'}, + dV1={'units': 'kn'}, + dVEF={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + elif terminal_condition == 'LIFTOFF': + # Propagate until velocity is the desired literal value. + phase.add_boundary_balance( + param='t_duration', + name='takeoff_eom.ground_normal_force', + tgt_val=0.0, + lower=0.0001, + upper=20, + eq_units='MN', + loc='final', + ) + elif terminal_condition == 'CLIMB_GRADIENT': + phase.add_calc_expr( + 'climb_gradient = tan(flight_path_angle)', + climb_gradient={'units': 'unitless'}, + flight_path_angle={'units': 'rad'}, + ) + phase.add_boundary_balance( + param='t_duration', + name='climb_gradient', + tgt_val=0.024, + lower=0.0001, + upper=20, + eq_units='unitless', + loc='final', + ) + elif terminal_condition == 'OBSTACLE': + phase.add_boundary_balance( + param='t_duration', + name=Dynamic.Mission.ALTITUDE, + tgt_val=35, + lower=0.0001, + upper=100, + eq_units='ft', + loc='final', + ) + elif terminal_condition is None: + # Propagate for t_duration + pass + else: + raise ValueError( + f'Unrecognized value for terminal_speed ({terminal_condition}).' + 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' + ) + + phase.add_parameter( + 'dV1', opt=False, val=10.0, units='kn', desc='Decision speed delta above stall speed.' + ) + # phase.add_parameter('dVEF', opt=False, val=10.0, desc='Decision speed delta below decision speed.') + + if phase.boundary_balance_options: + phase.options['auto_order'] = True + + if user_options['nonlinear_solver'] is not None: + phase.nonlinear_solver = user_options['nonlinear_solver'] + + if user_options['linear_solver'] is not None: + phase.linear_solver = user_options['linear_solver'] + + return phase + + def make_default_transcription(self): + """Return a transcription object to be used by default in build_phase.""" + transcription = dm.PicardShooting(num_segments=1, nodes_per_seg=10) + + return transcription + + def _extra_ode_init_kwargs(self): + """Return extra kwargs required for initializing the ODE.""" + return { + 'climbing': self.user_options['climbing'], + 'pitch_control': self.user_options['pitch_control'], + 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT, + } + + +class BalancedFieldTrajectoryBuilder: + """ + Define a trajectory builder for detailed takeoff. + + Identify, collect, and call the necessary phase builders to create a typical takeoff + trajectory. + """ + MappedPhase = namedtuple('MappedPhase', ('phase', 'phase_builder')) + + default_name = 'detailed_takeoff' + + def __init__(self, name=None): + if name is None: + name = self.default_name + + self.name = name + + self._brake_release_to_decision_speed = None + + self._phases = {} + self._traj = None + + def get_phase_names(self): + """Return a list of base names for available phases.""" + keys = list(self._phases) + + return keys + + def get_phase(self, key) -> dm.Phase: + """ + Return the phase associated with the specified base name. + + Raises + ------ + KeyError + if the specified base name is not found + """ + mapped_phase = self._phases[key] + + return mapped_phase.phase + + def set_brake_release_to_decision_speed(self, phase_builder: PhaseBuilderBase): + """ + Assign a phase builder for the beginning of takeoff to the time when the pilot + must choose either to liftoff or halt the aircraft. + """ + self._brake_release_to_decision_speed = phase_builder + + # def set_decision_speed_to_rotate(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for the short distance between achieving decision speed + # and beginning the rotation phase. + # """ + # self._decision_speed_to_rotate = phase_builder + + # def set_rotate_to_liftoff(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for the short distance required to rotate the aircraft + # to achieve liftoff. + # """ + # self._rotate_to_liftoff = phase_builder + + # def set_liftoff_to_climb_gradient(self, phase_builder: PhaseBuilderBase): + # """ + # Assign the phase builder for the phase from liftoff until the required climb gradient is reached. + + # Args: + # phase_builder (PhaseBuilderBase): _description_ + # """ + # self._liftoff_to_climb_gradient = phase_builder + + # def set_climb_gradient_to_obstacle(self, phase_builder: PhaseBuilderBase): + # """ + # Assign the phase builder for the phase from achievement of climb gradient until obstacle clearance. + + # Args: + # phase_builder (PhaseBuilderBase): _description_ + # """ + # self._climb_gradient_to_obstacle = phase_builder + + # def set_liftoff_to_obstacle(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for the short period between liftoff and clearing the + # required obstacle. + # """ + # self._liftoff_to_obstacle = phase_builder + + # def set_obstacle_to_mic_p2(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for the fifth phase of takeoff, from clearing the required + # obstacle to the p2 mic loation. This phase is required for acoustic calculations. + # """ + # self._obstacle_to_mic_p2 = phase_builder + + # def set_mic_p2_to_engine_cutback(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for the sixth phase of takeoff, from the p2 mic location + # to engine cutback. This phase is required for acoustic calculations. + # """ + # self._mic_p2_to_engine_cutback = phase_builder + + # def set_engine_cutback(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for the seventh phase of takeoff, from start to + # finish of engine cutback. This phase is required for acoustic calculations. + # """ + # self._engine_cutback = phase_builder + + # def set_engine_cutback_to_mic_p1(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for the eighth phase of takeoff, engine cutback + # to the P1 mic location. This phase is required for acoustic calculations. + # """ + # self._engine_cutback_to_mic_p1 = phase_builder + + # def set_mic_p1_to_climb(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for the ninth phase of takeoff, from P1 mic + # location to climb. This phase is required for acoustic calculations. + # """ + # self._mic_p1_to_climb = phase_builder + + # def set_decision_speed_to_brake(self, phase_builder: PhaseBuilderBase): + # """ + # Assign a phase builder for delayed braking when the engine fails. + + # Note, this phase is optional. It is only required if balanced field length + # calculations are required. + # """ + # self._decision_speed_to_brake = phase_builder + + # def set_brake_to_abort(self, phase_builder: PhaseBuilderBase, balanced_field_ref=8_000.0): + # """ + # Assign a phase builder for braking to fullstop after engine failure. + + # Note, this phase is optional. It is only required if balanced field length + # calculations are required. + + # Parameters + # ---------- + # phase_builder : PhaseBuilderBase + + # balanced_field_ref : float (8_000.0) + # the ref value to use for the linkage constraint for the final range + # between the liftoff-to-obstacle and the decision-speed-to-abort phases; + + # Notes + # ----- + # The default value for `balanced_field_ref` is appropriate total takeoff distances + # calculated in 'ft' for larger commercial passenger transports traveling in the + # continental United States. International travel of similar aircraft may require a + # larger value, while a smaller aircraft with a shorter range may require a smaller + # value. + # """ + # self._brake_to_abort = phase_builder + # self._balanced_field_ref = balanced_field_ref + + def build_trajectory( + self, *, aviary_options: AviaryValues, model: om.Group = None, traj: dm.Trajectory = None + ) -> dm.Trajectory: + """ + Return a new trajectory for detailed takeoff analysis. + + Call only after assigning phase builders for required phases. + + Parameters + ---------- + aviary_options : AviaryValues + collection of Aircraft/Mission specific options + + model : openmdao.api.Group (None) + the model handling trajectory parameter setup; if `None`, trajectory + parameter setup will not be handled + + traj : dymos.Trajectory (None) + the trajectory to update; if `None`, a new trajetory will be updated and + returned + + Returns + ------- + the updated trajectory; if the specified trajectory is `None`, a new trajectory + will be updated and returned + + Notes + ----- + Do not modify this object or any of its referenced data between the call to + `build_trajectory()` and the call to `apply_initial_guesses()`, or the behavior + is undefined, no diagnostic required. + """ + if traj is None: + traj = dm.Trajectory() + + self._traj = traj + + self._add_phases(aviary_options) + self._link_phases() + + if model is not None: + phase_names = self.get_phase_names() + + # This is a level 3 method that uses the default subsystems. + # We need to create parameters for just the inputs we have. + # They mostly come from the low-speed aero subsystem. + + aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, LegacyCode('FLOPS')) + + kwargs = {'method': 'low_speed'} + + params = aero.get_parameters(aviary_options, **kwargs) + + # takeoff introduces this one. + params[Mission.Takeoff.LIFT_COEFFICIENT_MAX] = { + 'shape': (1,), + 'static_target': True, + } + + ext_params = {} + for phase in self._phases.keys(): + ext_params[phase] = params + + print(ext_params) + exit(0) + + setup_trajectory_params( + model, traj, aviary_options, phase_names, external_parameters=ext_params + ) + + return traj + + def apply_initial_guesses(self, prob: om.Problem, traj_name): + """ + Call `prob.set_val()` for states/parameters/etc. for each phase in this + trajectory. + + Call only after `build_trajectory()` and `prob.setup()`. + + Returns + ------- + not_applied : dict[str, list[str]] + for any phase with missing initial guesses that cannot be applied, a list of + those missing initial guesses; if a given phase has no missing initial + guesses, the returned mapping will not contain the name of that phase + """ + not_applied = {} + phase_builder: PhaseBuilderBase = None + + for phase, phase_builder in self._phases.values(): + tmp = phase_builder.apply_initial_guesses(prob, traj_name, phase) + + if tmp: + not_applied[phase_builder.name] = tmp + + return not_applied + + def _add_phases(self, aviary_options: AviaryValues): + self._phases = {} + + self._add_phase(self._brake_release_to_decision_speed, aviary_options) + + # self._add_phase(self._decision_speed_to_rotate, aviary_options) + + # self._add_phase(self._rotate_to_liftoff, aviary_options) + + # self._add_phase(self._liftoff_to_climb_gradient, aviary_options) + + # self._add_phase(self._climb_gradient_to_obstacle, aviary_options) + + # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 + + # if obstacle_to_mic_p2 is not None: + # self._add_phase(obstacle_to_mic_p2, aviary_options) + + # self._add_phase(self._mic_p2_to_engine_cutback, aviary_options) + + # self._add_phase(self._engine_cutback, aviary_options) + + # self._add_phase(self._engine_cutback_to_mic_p1, aviary_options) + + # self._add_phase(self._mic_p1_to_climb, aviary_options) + + # decision_speed_to_brake = self._decision_speed_to_brake + + # if decision_speed_to_brake is not None: + # self._add_phase(decision_speed_to_brake, aviary_options) + + # self._add_phase(self._brake_to_abort, aviary_options) + + def _link_phases(self): + traj: dm.Trajectory = self._traj + + # brake_release_name = self._brake_release_to_decision_speed.name + # decision_speed_name = self._decision_speed_to_rotate.name + # rotate_name = self._rotate_to_liftoff.name + # liftoff_name = self._liftoff_to_climb_gradient.name + # climb_gradient_name = self._climb_gradient_to_obstacle.name + + # traj.link_phases( + # [brake_release_name, decision_speed_name, rotate_name, liftoff_name, climb_gradient_name], + # vars=['time', 'distance', 'velocity', 'mass'], + # connected=True, + # ) + + # traj.link_phases([rotate_name, liftoff_name], vars=['angle_of_attack',], connected=True) + + # traj.link_phases([liftoff_name, climb_gradient_name], vars=['flight_path_angle', 'altitude'], connected=True) + + def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): + name = phase_builder.name + phase = phase_builder.build_phase(aviary_options) + + self._traj.add_phase(name, phase) + + self._phases[name] = self.MappedPhase(phase, phase_builder) diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field.py b/aviary/mission/flops_based/phases/test/test_balanced_field.py index 5b68957bd7..c8357ef349 100644 --- a/aviary/mission/flops_based/phases/test/test_balanced_field.py +++ b/aviary/mission/flops_based/phases/test/test_balanced_field.py @@ -360,6 +360,8 @@ def test_case1(self): takeoff.model.traj.phases.takeoff_climb_gradient_to_obstacle.ode_iter_group.segment_prop_group.ode_all.takeoff_eom.list_vars(print_arrays=True) + om.n2(takeoff) + # vars = takeoff.model.list_vars(print_arrays=True, units=True, prom_name=True, return_format='dict', out_stream=None) # vars = {meta['prom_name']: meta for meta in vars.values()} diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field2.py b/aviary/mission/flops_based/phases/test/test_balanced_field2.py new file mode 100644 index 0000000000..fb310f93e4 --- /dev/null +++ b/aviary/mission/flops_based/phases/test/test_balanced_field2.py @@ -0,0 +1,375 @@ +import aviary.api as av +import warnings +import unittest +import dymos as dm +import openmdao.api as om +from aviary.api import Dynamic, Mission +from aviary.core.aviary_group import AviaryGroup +from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData +from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import inputs +from aviary.utils.preprocessors import preprocess_options + + +class TestBalancedField(unittest.TestCase): + """Test takeoff phase builder.""" + + def test_balanced_field_2(self): + aviary_options = inputs.deepcopy() + + # This builder can be used for both takeoff and landing phases + aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS + ) + + # fmt: off + takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } + } + # fmt: off + + # when using spoilers, add a few more options + takeoff_spoiler_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT), + 'spoiler_lift_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT), + } + } + + # We also need propulsion analysis for takeoff and landing. No additional configuration + # is needed for this builder + engines = [av.build_engine_deck(aviary_options)] + preprocess_options(aviary_options, engine_models=engines) + prop_builder = av.CorePropulsionBuilder(engine_models=engines) + + # # BRAKE RELEASE TO DECISION SPEED + balanced_field_user_options = av.AviaryValues() + + # balanced_field_user_options.set_val('max_duration', val=60.0, units='s') + # balanced_field_user_options.set_val('time_duration_ref', val=60.0, units='s') + # balanced_field_user_options.set_val('distance_max', val=7500.0, units='ft') + # balanced_field_user_options.set_val('max_velocity', val=167.85, units='kn') + # balanced_field_user_options.set_val('terminal_condition', val='VEF') + + # tobl_nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) + # tobl_nl_solver.linesearch = om.BoundsEnforceLS() + + # balanced_field_user_options.set_val('nonlinear_solver', val=tobl_nl_solver) + # balanced_field_user_options.set_val('linear_solver', val=om.DirectSolver()) + + # takeoff_v1_to_vr_initial_guesses = av.AviaryValues() + + # takeoff_v1_to_vr_initial_guesses.set_val('time', [0.0, 30.0], 's') + # takeoff_v1_to_vr_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') + # takeoff_v1_to_vr_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') + + # gross_mass_units = 'lbm' + # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + # takeoff_v1_to_vr_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + # takeoff_v1_to_vr_initial_guesses.set_val('throttle', 1.0) + # takeoff_v1_to_vr_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + # takeoff_brake_release_to_decision_speed_builder = av.BalancedFieldPhaseBuilder( + # 'takeoff_brake_release_to_decision_speed', + # core_subsystems=[aero_builder, prop_builder], + # subsystem_options=takeoff_subsystem_options, + # user_options=balanced_field_user_options, + # initial_guesses=takeoff_v1_to_vr_initial_guesses, + # ) + + # # DECISION SPEED TO ROTATION + + # takeoff_v1_to_vr_user_options = av.AviaryValues() + + # takeoff_v1_to_vr_user_options.set_val('max_duration', val=90.0, units='s') + # takeoff_v1_to_vr_user_options.set_val('time_duration_ref', val=60.0, units='s') + # takeoff_v1_to_vr_user_options.set_val('distance_max', val=15000.0, units='ft') + # takeoff_v1_to_vr_user_options.set_val('max_velocity', val=167.85, units='kn') + # takeoff_v1_to_vr_user_options.set_val('terminal_condition', val='VR') + + # nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) + # nl_solver.linesearch = om.BoundsEnforceLS() + + # takeoff_v1_to_vr_user_options.set_val('nonlinear_solver', val=nl_solver) + # takeoff_v1_to_vr_user_options.set_val('linear_solver', val=om.DirectSolver()) + + # takeoff_v1_to_vr_initial_guesses = av.AviaryValues() + + # takeoff_v1_to_vr_initial_guesses.set_val('time', [30.0, 1.0], 's') + # takeoff_v1_to_vr_initial_guesses.set_val('distance', [4100.0, 4800.0], 'ft') + # takeoff_v1_to_vr_initial_guesses.set_val('velocity', [70., 150.0], 'kn') + + # gross_mass_units = 'lbm' + # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + # takeoff_v1_to_vr_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + # takeoff_v1_to_vr_initial_guesses.set_val('throttle', 1.0) + # takeoff_v1_to_vr_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + # takeoff_decision_speed_to_rotate_builder = av.BalancedFieldPhaseBuilder( + # 'takeoff_decision_speed_to_rotate', + # core_subsystems=[aero_builder, prop_builder], + # subsystem_options=takeoff_subsystem_options, + # user_options=takeoff_v1_to_vr_user_options, + # initial_guesses=takeoff_v1_to_vr_initial_guesses, + # ) + + # # ROTATION TO LIFTOFF + + # vr_to_liftoff_user_options = av.AviaryValues() + + # vr_to_liftoff_user_options.set_val('max_duration', val=90.0, units='s') + # vr_to_liftoff_user_options.set_val('time_duration_ref', val=60.0, units='s') + # vr_to_liftoff_user_options.set_val('distance_max', val=15000.0, units='ft') + # vr_to_liftoff_user_options.set_val('max_velocity', val=167.85, units='kn') + # vr_to_liftoff_user_options.set_val('pitch_control', val='alpha_rate_fixed', units='unitless') + # vr_to_liftoff_user_options.set_val('terminal_condition', val='LIFTOFF') + + # nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) + # nl_solver.linesearch = om.BoundsEnforceLS() + + # vr_to_liftoff_user_options.set_val('nonlinear_solver', val=nl_solver) + # vr_to_liftoff_user_options.set_val('linear_solver', val=om.DirectSolver()) + + # vr_to_liftoff_initial_guesses = av.AviaryValues() + + # vr_to_liftoff_initial_guesses.set_val('time', [31.0, 5.0], 's') + # vr_to_liftoff_initial_guesses.set_val('distance', [4800.0, 5500.0], 'ft') + # vr_to_liftoff_initial_guesses.set_val('velocity', [100., 120.0], 'kn') + # vr_to_liftoff_initial_guesses.set_val('angle_of_attack', [0.0, 5.0], 'deg') + + # gross_mass_units = 'lbm' + # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + # vr_to_liftoff_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + # vr_to_liftoff_initial_guesses.set_val('throttle', 1.0) + # vr_to_liftoff_initial_guesses.set_val('angle_of_attack_rate', 2.0, units='deg/s') + + # vr_to_liftoff_builder = av.BalancedFieldPhaseBuilder( + # 'takeoff_rotate_to_liftoff', + # core_subsystems=[aero_builder, prop_builder], + # subsystem_options=takeoff_subsystem_options, + # user_options=vr_to_liftoff_user_options, + # initial_guesses=vr_to_liftoff_initial_guesses, + # ) + + # # LIFTOFF TO CLIMB GRADIENT + + # liftoff_to_climb_gradient_user_options = av.AviaryValues() + + # liftoff_to_climb_gradient_user_options.set_val('max_duration', val=90.0, units='s') + # liftoff_to_climb_gradient_user_options.set_val('time_duration_ref', val=60.0, units='s') + # liftoff_to_climb_gradient_user_options.set_val('distance_max', val=15000.0, units='ft') + # liftoff_to_climb_gradient_user_options.set_val('max_velocity', val=167.85, units='kn') + # liftoff_to_climb_gradient_user_options.set_val('pitch_control', val='alpha_rate_fixed', units='unitless') + # liftoff_to_climb_gradient_user_options.set_val('climbing', val=True, units='unitless') + # liftoff_to_climb_gradient_user_options.set_val('terminal_condition', val='CLIMB_GRADIENT') + + # nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) + # nl_solver.linesearch = om.BoundsEnforceLS() + + # liftoff_to_climb_gradient_user_options.set_val('nonlinear_solver', val=nl_solver) + # liftoff_to_climb_gradient_user_options.set_val('linear_solver', val=om.DirectSolver()) + + # liftoff_to_climb_gradient_initial_guesses = av.AviaryValues() + + # liftoff_to_climb_gradient_initial_guesses.set_val('time', [35.0, 1.0], 's') + # liftoff_to_climb_gradient_initial_guesses.set_val('distance', [5000.0, 6000.0], 'ft') + # liftoff_to_climb_gradient_initial_guesses.set_val('velocity', [120., 100.0], 'kn') + # liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack', [5.0, 10.0], 'deg') + # liftoff_to_climb_gradient_initial_guesses.set_val('flight_path_angle', [0.0, 2.0], 'deg') + + # gross_mass_units = 'lbm' + # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + # liftoff_to_climb_gradient_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + # liftoff_to_climb_gradient_initial_guesses.set_val('throttle', 1.0) + # liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack_rate', 2.0, units='deg/s') + + # liftoff_to_climb_gradient_builder = av.BalancedFieldPhaseBuilder( + # 'takeoff_liftoff_to_climb_gradient', + # core_subsystems=[aero_builder, prop_builder], + # subsystem_options=takeoff_subsystem_options, + # user_options=liftoff_to_climb_gradient_user_options, + # initial_guesses=liftoff_to_climb_gradient_initial_guesses, + # ) + + # # CLIMB GRADIENT TO OBSTACLE + + # climb_gradient_to_obstacle_user_options = av.AviaryValues() + + # climb_gradient_to_obstacle_user_options.set_val('max_duration', val=90.0, units='s') + # climb_gradient_to_obstacle_user_options.set_val('time_duration_ref', val=60.0, units='s') + # climb_gradient_to_obstacle_user_options.set_val('distance_max', val=15000.0, units='ft') + # climb_gradient_to_obstacle_user_options.set_val('max_velocity', val=200., units='kn') + # climb_gradient_to_obstacle_user_options.set_val('pitch_control', val='gamma_fixed', units='unitless') + # climb_gradient_to_obstacle_user_options.set_val('climbing', val=True, units='unitless') + # climb_gradient_to_obstacle_user_options.set_val('terminal_condition', val='OBSTACLE') + + # nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=False) + # nl_solver.linesearch = om.BoundsEnforceLS() + + # climb_gradient_to_obstacle_user_options.set_val('nonlinear_solver', val=nl_solver) + # climb_gradient_to_obstacle_user_options.set_val('linear_solver', val=om.DirectSolver()) + + # climb_gradient_to_obstacle_initial_guesses = av.AviaryValues() + + # climb_gradient_to_obstacle_initial_guesses.set_val('time', [35.0, 3.0], 's') + # climb_gradient_to_obstacle_initial_guesses.set_val('distance', [5500.0, 5800.0], 'ft') + # climb_gradient_to_obstacle_initial_guesses.set_val('velocity', [120., 120.0], 'kn') + # climb_gradient_to_obstacle_initial_guesses.set_val('flight_path_angle', [2.0, 2.0], 'deg') + + # gross_mass_units = 'lbm' + # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + # climb_gradient_to_obstacle_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + # climb_gradient_to_obstacle_initial_guesses.set_val('throttle', 1.0) + + # climb_gradient_to_obstacle_builder = av.BalancedFieldPhaseBuilder( + # 'takeoff_climb_gradient_to_obstacle', + # core_subsystems=[aero_builder, prop_builder], + # subsystem_options=takeoff_subsystem_options, + # user_options=climb_gradient_to_obstacle_user_options, + # initial_guesses=climb_gradient_to_obstacle_initial_guesses, + # ) + + # from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( + # takeoff_decision_speed_builder, + # takeoff_engine_cutback_builder, + # takeoff_engine_cutback_to_mic_p1_builder, + # takeoff_liftoff_builder, + # takeoff_liftoff_user_options, + # takeoff_mic_p1_to_climb_builder, + # takeoff_mic_p2_builder, + # takeoff_mic_p2_to_engine_cutback_builder, + # takeoff_rotate_builder, + # ) + from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems + from aviary.variable_info.functions import setup_model_options + + takeoff_trajectory_builder = av.BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) + + test_problem = om.Problem() # model=AviaryGroup(aviary_options=aviary_options, aviary_metadata=BaseMetaData)) + + # default subsystems + default_premission_subsystems = get_default_premission_subsystems('FLOPS', engines) + + # Upstream pre-mission analysis for aero + test_problem.model.add_subsystem( + 'core_subsystems', + av.CorePreMission( + aviary_options=aviary_options, + subsystems=default_premission_subsystems, + ), + promotes_inputs=['*'], + promotes_outputs=['*'], + ) + + # Instantiate the trajectory and add the phases + traj = takeoff_trajectory_builder.build_trajectory(aviary_options=aviary_options, model=test_problem.model) + + varnames = [ + av.Aircraft.Wing.AREA, + av.Aircraft.Wing.ASPECT_RATIO, + av.Aircraft.Wing.SPAN, + ] + av.set_aviary_input_defaults(test_problem.model, varnames, aviary_options) + + setup_model_options(test_problem, aviary_options) + + # suppress warnings: + # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' + with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + test_problem.setup(check=False) + + av.set_aviary_initial_values(test_problem, aviary_options) + + takeoff_trajectory_builder.apply_initial_guesses(test_problem, 'traj') + + test_problem.final_setup() + + # test_problem.model.list_vars(print_arrays=True) + + # takeoff.model.run_apply_nonlinear() + + # takeoff.model.list_vars(print_arrays=True) + + import dymos + dymos.run_problem(test_problem, run_driver=False, make_plots=True) + + print(test_problem.get_reports_dir()) + # test_problem.model.list_vars() + + # takeoff.model.traj.phases.takeoff_climb_gradient_to_obstacle.ode_iter_group.segment_prop_group.ode_all.takeoff_eom.list_vars(print_arrays=True) + + # vars = takeoff.model.list_vars(print_arrays=True, units=True, prom_name=True, return_format='dict', out_stream=None) + + # vars = {meta['prom_name']: meta for meta in vars.values()} + + # systems = set([path.rsplit('.', 1)[0] for path in vars.keys()]) + + # print('\n'.join(vars.keys())) + + # for sys in takeoff.model.system_iter(include_self=True, recurse=True): + # sys.list_vars(prom_name=True, units=True) + + + # from textual.app import App, ComposeResult + # from textual.widgets import DataTable, Collapsible + + + # class TableApp(App): + + # def __init__(self, systems): + # self._systems = systems + + # def compose(self) -> ComposeResult: + # for sys, vars in self._systems.items(): + # with Collapsible(title=sys.pathname): + # yield DataTable() + + # def on_mount(self) -> None: + # table = self.query_one(DataTable) + # table.add_columns('promoted name', 'units') + # print(table) + # # table.add_rows(ROWS[1:]) + + # systems = {} + + # for sys in takeoff.model.system_iter(include_self=True, recurse=True): + # vars = sys.list_vars(prom_name=True, units=True, out_stream=None) + # systems[sys] = vars + + # app = TableApp(systems) + # app.run() + + # takeoff.check_partials(compact_print=True) + # takeoff.model.run_apply_nonlinear() + # takeoff.model.list_vars(print_arrays=True) + # om.n2(takeoff.model) + + +if __name__ == '__main__': + unittest.main() From 31ce9fb5a110b1681361ee5925c5f0a41c8544c0 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Wed, 27 Aug 2025 13:16:13 -0400 Subject: [PATCH 06/22] starting to work, part2 --- .../phases/balanced_field_trajectory.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/aviary/mission/flops_based/phases/balanced_field_trajectory.py b/aviary/mission/flops_based/phases/balanced_field_trajectory.py index fef0563a94..a75523a384 100644 --- a/aviary/mission/flops_based/phases/balanced_field_trajectory.py +++ b/aviary/mission/flops_based/phases/balanced_field_trajectory.py @@ -86,17 +86,11 @@ def _init_initial_guess_meta_data(cls: PhaseBuilderBase): InitialGuessControl('throttle'), desc='initial guess for throttle' ) - cls._add_initial_guess_meta_data( - InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK) - ) + cls._add_initial_guess_meta_data(InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK)) - cls._add_initial_guess_meta_data( - InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE) - ) + cls._add_initial_guess_meta_data(InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE)) - cls._add_initial_guess_meta_data( - InitialGuessParameter(Dynamic.Mission.FLIGHT_PATH_ANGLE) - ) + cls._add_initial_guess_meta_data(InitialGuessParameter(Dynamic.Mission.FLIGHT_PATH_ANGLE)) return cls @@ -141,7 +135,7 @@ def declare_options(self): name='pitch_control', values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], default='alpha_fixed', - desc='Specifies how alpha is controlled - Alpha can be a fixed parameter, specified via a fixed rate parameter, ' \ + desc='Specifies how alpha is controlled - Alpha can be a fixed parameter, specified via a fixed rate parameter, ' 'or whether the climb gradient is constant', ) @@ -472,6 +466,7 @@ class BalancedFieldTrajectoryBuilder: Identify, collect, and call the necessary phase builders to create a typical takeoff trajectory. """ + MappedPhase = namedtuple('MappedPhase', ('phase', 'phase_builder')) default_name = 'detailed_takeoff' From 9b121d0058bba16f062f89d34d4f631a0ea2a795 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Tue, 2 Sep 2025 14:52:42 -0400 Subject: [PATCH 07/22] balanced field is at least working, but needs to be improved. --- aviary/mission/balanced_field_traj_builder.py | 777 ++++++++++++++++++ .../phases/balanced_field_trajectory.py | 638 +++++++------- .../phases/test/test_balanced_field2.py | 3 + aviary/variable_info/variables.py | 1 + 4 files changed, 1131 insertions(+), 288 deletions(-) create mode 100644 aviary/mission/balanced_field_traj_builder.py diff --git a/aviary/mission/balanced_field_traj_builder.py b/aviary/mission/balanced_field_traj_builder.py new file mode 100644 index 0000000000..29c4257426 --- /dev/null +++ b/aviary/mission/balanced_field_traj_builder.py @@ -0,0 +1,777 @@ +""" +Define utilities for building phases. + +Classes +------- +PhaseBuilderBase : the interface for a phase builder +""" + +from abc import ABC +from collections import namedtuple + +import dymos as dm +import openmdao.api as om +import numpy as np + +from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE +from aviary.mission.flops_based.phases.balanced_field_trajectory import BalancedFieldPhaseBuilder +from aviary.mission.initial_guess_builders import InitialGuess +from aviary.utils.aviary_values import AviaryValues, get_keys +from aviary.variable_info.variable_meta_data import _MetaData +from aviary.variable_info.variables import Dynamic +from aviary.variable_info.functions import setup_trajectory_params +from aviary.api import Aircraft, Dynamic, Mission + +from aviary.mission.phase_builder_base import PhaseBuilderBase + + +_require_new_initial_guesses_meta_data_class_attr_ = namedtuple( + '_require_new_initial_guesses_meta_data_class_attr_', () +) + + +class BalancedFieldTrajectoryBuilder(ABC): + """ + Define the interface for a balanced field trajectory builder. + + Attributes + ---------- + name : str ('_unknown phase_') + object label + + core_subsystems : (None) + list of SubsystemBuilderBase objects that will be added to the phase ODE + + user_options : OptionsDictionary () + state/path constraint values and flags + + initial_guesses : AviaryValues () + state/path beginning values to be set on the problem + + ode_class : type (None) + advanced: the type of system defining the ODE + + transcription : "Dymos transcription object" (None) + advanced: an object providing the transcription technique of the + optimal control problem + + subsystem_options : dict (None) + dictionary of parameters to be passed to the subsystem builders + + default_name : str + class attribute: derived type customization point; the default value + for name + + default_ode_class : type + class attribute: derived type customization point; the default value + for ode_class used by build_phase + + default_options_class : type + class attribute: derived type customization point; the default class + containing the phase options options_dictionary + + is_analytic_phase : bool (False) + class attribute: derived type customization point; if True, build_phase + will return an AnalyticPhase instead of a Phase + + num_nodes : int (5) + class attribute: derived type customization point; the default value + for num_nodes used by build_phase, only for AnalyticPhases + + Methods + ------- + build_phase + make_default_transcription + """ + + __slots__ = ( + 'name', + 'core_subsystems', + 'external_subsystems', + 'subsystem_options', + 'user_options', + 'initial_guesses', + 'ode_class', + 'transcription', + 'is_analytic_phase', + 'num_nodes', + 'meta_data', + '_traj', + '_phases', + ) + + MappedPhase = namedtuple('MappedPhase', ('phase', 'phase_builder')) + + _initial_guesses_meta_data_ = _require_new_initial_guesses_meta_data_class_attr_() + + default_name = '_unknown phase_' + + default_ode_class = TakeoffODE + default_options_class = om.OptionsDictionary + + default_meta_data = _MetaData + # endregion : derived type customization points + + def __init__( + self, + name=None, + core_subsystems=None, + external_subsystems=None, + user_options=None, + initial_guesses=None, + subsystem_options=None, + num_nodes=10, + meta_data=None, + traj=None, + ): + if name is None: + name = self.default_name + + self.name = name + + if core_subsystems is None: + core_subsystems = [] + if external_subsystems is None: + external_subsystems = [] + + self.core_subsystems = core_subsystems + self.external_subsystems = external_subsystems + + if subsystem_options is None: + subsystem_options = {} + + self.subsystem_options = subsystem_options + + self.user_options = self.default_options_class(user_options) + + if initial_guesses is None: + initial_guesses = AviaryValues() + + self.initial_guesses = initial_guesses + self.validate_initial_guesses() + + self.ode_class = self.default_ode_class + self.num_nodes = num_nodes + + if external_subsystems is None: + external_subsystems = [] + + self.external_subsystems = external_subsystems + + if meta_data is None: + meta_data = self.default_meta_data + + self.meta_data = meta_data + + self._traj = traj + self._phases = {} + + def build_trajectory( + self, *, aviary_options: AviaryValues, model: om.Group = None, traj: dm.Trajectory = None + ) -> dm.Trajectory: + """ + Return a new trajectory for detailed takeoff analysis. + + Call only after assigning phase builders for required phases. + + Parameters + ---------- + aviary_options : AviaryValues + collection of Aircraft/Mission specific options + + model : openmdao.api.Group (None) + the model handling trajectory parameter setup; if `None`, trajectory + parameter setup will not be handled + + traj : dymos.Trajectory (None) + the trajectory to update; if `None`, a new trajetory will be updated and + returned + + Returns + ------- + the updated trajectory; if the specified trajectory is `None`, a new trajectory + will be updated and returned + + Notes + ----- + Do not modify this object or any of its referenced data between the call to + `build_trajectory()` and the call to `apply_initial_guesses()`, or the behavior + is undefined, no diagnostic required. + """ + # ode_class = self.ode_class + + # transcription = dm.PicardShooting(nodes_per_seg=10) + + if traj is None: + self._traj = traj = dm.Trajectory(parallel_phases=False) + model.add_subsystem('traj', self._traj) + + if aviary_options is None: + aviary_options = AviaryValues() + + # kwargs = self._extra_ode_init_kwargs() + + # kwargs = {'aviary_options': aviary_options, **kwargs} + + # # if subsystem_options is not None: + # # kwargs['subsystem_options'] = subsystem_options + + # kwargs['core_subsystems'] = self.core_subsystems + # kwargs['external_subsystems'] = self.external_subsystems + + # Add all phase builders here. + + common_user_options = AviaryValues() + common_user_options.set_val('max_duration', val=100.0, units='s') + common_user_options.set_val('time_duration_ref', val=60.0, units='s') + common_user_options.set_val('distance_max', val=10000.0, units='ft') + common_user_options.set_val('max_velocity', val=200.0, units='kn') + + tobl_nl_solver = om.NewtonSolver( + solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6 + ) + tobl_nl_solver.linesearch = om.BoundsEnforceLS() + + common_user_options.set_val('nonlinear_solver', val=tobl_nl_solver) + common_user_options.set_val('linear_solver', val=om.DirectSolver()) + + common_initial_guesses = AviaryValues() + + common_initial_guesses.set_val('time', [0.0, 30.0], 's') + common_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') + common_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') + common_initial_guesses.set_val('throttle', 1.0) + common_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + gross_mass_units = 'lbm' + gross_mass = aviary_options.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + common_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + # + # First phase: Brake release to engine failure + # + # TODO: would like to be able to do this - user_options=balanced_field_user_options | {'terminal_condition': 'VEF'} + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'VEF') + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('dV1', 3.0, 'kn') + initial_guesses.set_val('dVEF', 1.0, 'kn') + takeoff_brake_release_to_engine_failure_builder = BalancedFieldPhaseBuilder( + 'takeoff_brake_release_to_engine_failure', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + self._add_phase(takeoff_brake_release_to_engine_failure_builder, aviary_options) + + # + # Second Phase: Engine Failure to Decision Speed + # + # TODO: dymos PicardShooting shouldn't require initial guesses if connecting an initial state value + # In that case just assume the state value holds through the phase as an initial guess. + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'V1') + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [30.0, 31.0], 's') + initial_guesses.set_val('distance', [4100.0, 4500.0], 'ft') + initial_guesses.set_val('velocity', [150.0, 151], 'kn') + initial_guesses.set_val('throttle', 0.5) # TODO: Don't do this hack, decrement num engines + initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + initial_guesses.set_val('dV1', 3.0, 'kn') + + takeoff_engine_failure_to_v1_builder = BalancedFieldPhaseBuilder( + 'takeoff_engine_failure_to_v1', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_engine_failure_to_v1_builder, aviary_options) + + # + # Third Phase: Decision Speed to Rejected Takeoff Stop + # + # TODO: dymos PicardShooting shouldn't require initial guesses if connecting an initial state value + # In that case just assume the state value holds through the phase as an initial guess. + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'STOP') + user_options.set_val('friction_key', Mission.Takeoff.BRAKING_FRICTION_COEFFICIENT) + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [31.0, 20.0], 's') + initial_guesses.set_val('distance', [4500.0, 6500.0], 'ft') + initial_guesses.set_val('velocity', [151.0, 5.0], 'kn') + initial_guesses.set_val('throttle', 0.0) + initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + takeoff_v1_to_roll_stop = BalancedFieldPhaseBuilder( + 'takeoff_v1_to_roll_stop', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_v1_to_roll_stop, aviary_options) + + # + # Fourth Phase: Decision Speed to Rotation Speed + # + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'VR') + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [36.0, 1.0], 's') + initial_guesses.set_val('distance', [4500.0, 5000.0], 'ft') + initial_guesses.set_val('velocity', [151.0, 155.0], 'kn') + initial_guesses.set_val('throttle', 0.5) + initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + takeoff_v1_to_vr = BalancedFieldPhaseBuilder( + 'takeoff_v1_to_vr', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_v1_to_vr, aviary_options) + + # + # Fifth Phase: Rotate to liftoff + # + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'LIFTOFF') + user_options.set_val('pitch_control', 'alpha_rate_fixed') + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [37.0, 7.0], 's') + initial_guesses.set_val('distance', [5000.0, 5500.0], 'ft') + initial_guesses.set_val('velocity', [155.0, 160.0], 'kn') + initial_guesses.set_val('throttle', 0.5) + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK, [0.0, 14.0], 'deg') + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, 3.0, 'deg/s') + + takeoff_vr_to_liftoff = BalancedFieldPhaseBuilder( + 'takeoff_vr_to_liftoff', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_vr_to_liftoff, aviary_options) + + # + # Sixth Phase: Liftoff to V2 + # + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'OBSTACLE') + user_options.set_val('pitch_control', 'alpha_rate_fixed') + user_options.set_val('climbing', True) + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [37.0, 15.0], 's') + initial_guesses.set_val('distance', [5000.0, 5500.0], 'ft') + initial_guesses.set_val('velocity', [155.0, 160.0], 'kn') + initial_guesses.set_val('throttle', 0.5) + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK, [10.0, 10.0], 'deg') + initial_guesses.set_val(Dynamic.Mission.FLIGHT_PATH_ANGLE, [0.0, 0.5], 'deg') + initial_guesses.set_val(Dynamic.Mission.ALTITUDE, [0.0, 0.5], 'ft') + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, 1.0, 'deg/s') + + takeoff_liftoff_to_v2 = BalancedFieldPhaseBuilder( + 'takeoff_liftoff_to_v2', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_liftoff_to_v2, aviary_options) + + bal_comp = self._traj.add_subsystem('balance_comp', om.BalanceComp()) + + # The first balance sets dVEF such that the elapsed time from engine failure to V1 is one second. + bal_comp.add_balance('dVEF', lhs_name='t_react', rhs_val=1.0, eq_units='s', units='kn') + self._traj.connect( + 'balance_comp.dVEF', 'takeoff_brake_release_to_engine_failure.parameters:dVEF' + ) + self._traj.connect('takeoff_engine_failure_to_v1.t_duration', 'balance_comp.t_react') + + # The second balance sets dV1 such that the distance at the end of the abort roll and the distance at obstacle clearance are the same + bal_comp.add_balance( + 'dV1', + lhs_name='distance_abort', + rhs_name='distance_obstacle', + eq_units='ft', + units='kn', + ) + self._traj.connect( + 'balance_comp.dV1', + [ + 'takeoff_brake_release_to_engine_failure.parameters:dV1', + 'takeoff_engine_failure_to_v1.parameters:dV1', + ], + ) + self._traj.connect( + 'takeoff_liftoff_to_v2.final_states:distance', 'balance_comp.distance_obstacle' + ) + self._traj.connect( + 'takeoff_v1_to_roll_stop.final_states:distance', 'balance_comp.distance_abort' + ) + + self._traj.nonlinear_solver = om.NewtonSolver( + solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6 + ) + self._traj.nonlinear_solver.linesearch = om.BoundsEnforceLS() + self._traj.linear_solver = om.DirectSolver() + + # Press to Takeoff Sequence + self._traj.link_phases( + [ + 'takeoff_brake_release_to_engine_failure', + 'takeoff_engine_failure_to_v1', + 'takeoff_v1_to_vr', + 'takeoff_vr_to_liftoff', + 'takeoff_liftoff_to_v2', + ], + vars=['time', 'distance', 'velocity', 'mass'], + connected=True, + ) + + self._traj.link_phases( + ['takeoff_v1_to_vr', 'takeoff_vr_to_liftoff', 'takeoff_liftoff_to_v2'], + vars=[Dynamic.Vehicle.ANGLE_OF_ATTACK], + connected=True, + ) + + # Abort Sequence + self._traj.link_phases( + ['takeoff_engine_failure_to_v1', 'takeoff_v1_to_roll_stop'], + vars=['time', 'distance', 'velocity', 'mass'], + connected=True, + ) + + if model is not None: + phase_names = list(self._phases.keys()) + + # This is a level 3 method that uses the default subsystems. + # We need to create parameters for just the inputs we have. + # They mostly come from the low-speed aero subsystem. + + # aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, LegacyCode('FLOPS')) + + # for cs in self.core_subsystems: + # print(cs.name) + # print(cs.get_parameters(**kwargs)) + # exit(0) + + kwargs = {'method': 'low_speed'} + + # TODO: Why is get_parameters different for different subsystems? + # Do with without blindly indexing. + aero_builder = self.core_subsystems[0] + + params = aero_builder.get_parameters(aviary_options, **kwargs) + + # takeoff introduces this one. + params[Mission.Takeoff.LIFT_COEFFICIENT_MAX] = { + 'shape': (1,), + 'static_target': True, + } + + ext_params = {} + for phase in self._phases.keys(): + ext_params[phase] = params + + setup_trajectory_params( + model, self._traj, aviary_options, phase_names, external_parameters=ext_params + ) + + return self._traj + + def validate_initial_guesses(self): + """ + Raise TypeError if an unsupported initial guess is found. + + Users can call this method when updating initial guesses after initialization. + """ + initial_guesses = self.initial_guesses + + # if not initial_guesses: + # return # acceptable + + # meta_data = self._initial_guesses_meta_data_ + + # for key in get_keys(initial_guesses): + # if key not in meta_data: + # raise TypeError( + # f'{self.__class__.__name__}: {self.name}: unsupported initial guess: {key}' + # ) + + def apply_initial_guesses(self, prob: om.Problem, traj_name): # , phase: dm.Phase): + """Apply any stored initial guesses; return a list of guesses not applied.""" + not_applied = {} + phase_builder: PhaseBuilderBase = None + + for phase, phase_builder in self._phases.values(): + tmp = phase_builder.apply_initial_guesses(prob, traj_name, phase) + + if tmp: + not_applied[phase_builder.name] = tmp + + return not_applied + + def _extra_ode_init_kwargs(self): + """Return extra kwargs required for initializing the ODE.""" + return {} + + def to_phase_info(self): + """ + Return the stored settings as phase info. + + Returns + ------- + tuple + name : str + object label + phase_info : dict + stored settings + """ + subsystem_options = self.subsystem_options # TODO: aero info? + user_options = self.user_options.to_phase_info() + initial_guesses = dict(self.initial_guesses) + + # TODO some of these may be purely programming API hooks, rather than for use + # with phase info + # - ode_class + # - transcription + # - external_subsystems + # - meta_data + + phase_info = dict( + subsystem_options=subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + return (self.name, phase_info) + + @classmethod + def from_phase_info( + cls, name, phase_info: dict, core_subsystems=None, meta_data=None, transcription=None + ): + """ + Return a new phase builder based on the specified phase info. + + Note, calling code is responsible for matching phase info to the correct phase + builder type, or the behavior is undefined. + + Parameters + ---------- + name : str + object label + phase_info : dict + stored settings + """ + # loop over user_options dict entries + # if the value is not a tuple, wrap it in a tuple with the second entry of 'unitless' + for key, value in phase_info['user_options'].items(): + if not isinstance(value, tuple): + phase_info['user_options'][key] = (value, 'unitless') + + subsystem_options = phase_info.get('subsystem_options', {}) # TODO: aero info? + user_options = phase_info.get('user_options', ()) + initial_guesses = AviaryValues(phase_info.get('initial_guesses', ())) + external_subsystems = phase_info.get('external_subsystems', []) + # TODO core subsystems in phase info? + + # TODO some of these may be purely programming API hooks, rather than for use + # with phase info + # - ode_class + # - transcription + # - external_subsystems + # - meta_data + + phase_builder = cls( + name, + subsystem_options=subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + meta_data=meta_data, + core_subsystems=core_subsystems, + external_subsystems=external_subsystems, + transcription=transcription, + ) + + return phase_builder + + @classmethod + def _add_initial_guess_meta_data(cls, initial_guess: InitialGuess, desc=None): + """ + Update supported initial guesses with a new item. + + Raises + ------ + ValueError + if a repeat initial guess is found + """ + meta_data = cls._initial_guesses_meta_data_ + name = initial_guess.key + + meta_data[name] = dict(apply_initial_guess=initial_guess.apply_initial_guess, desc=desc) + + def _add_user_defined_constraints(self, phase, constraints): + """Add each constraint and its corresponding arguments to the phase.""" + for constraint_name, kwargs in constraints.items(): + if kwargs['type'] == 'boundary': + kwargs.pop('type') + + if 'target' in kwargs: + # Support for constraint aliases. + target = kwargs.pop('target') + kwargs['constraint_name'] = constraint_name + phase.add_boundary_constraint(target, **kwargs) + else: + phase.add_boundary_constraint(constraint_name, **kwargs) + + elif kwargs['type'] == 'path': + kwargs.pop('type') + phase.add_path_constraint(constraint_name, **kwargs) + + def add_state(self, name, target, rate_source): + """ + Add a state to this phase using the options in the phase_info. + + Parameters + ---------- + name : str + The name of this state in the phase_info options. + target : str + State promoted variable path to the ODE. + rate_source : str + Source of the state rate in the ODE. + """ + options = self.user_options + + initial, _ = options[f'{name}_initial'] + final, _ = options[f'{name}_final'] + bounds, units = options[f'{name}_bounds'] + ref, _ = options[f'{name}_ref'] + ref0, _ = options[f'{name}_ref0'] + defect_ref, _ = options[f'{name}_defect_ref'] + solve_segments = options[f'{name}_solve_segments'] + + # If a value is specified for the starting node, then fix_initial is True. + # Otherwise, input_initial is True. + # The problem configurator may change input_initial to False requested or necessary, (e.g., + # for parallel phases in MPI.) + + self.phase.add_state( + target, + fix_initial=initial is not None, + input_initial=False, + lower=bounds[0], + upper=bounds[1], + units=units, + rate_source=rate_source, + ref=ref, + ref0=ref0, + defect_ref=defect_ref, + solve_segments='forward' if solve_segments else None, + ) + + if final is not None: + constraint_ref, _ = options[f'{name}_constraint_ref'] + if constraint_ref is None: + # If unspecified, final is a good value for it. + constraint_ref = final + self.phase.add_boundary_constraint( + target, + loc='final', + equals=final, + units=units, + ref=final, + ) + + def add_control( + self, name, target, rate_targets=None, rate2_targets=None, add_constraints=True + ): + """ + Add a control to this phase using the options in the phase-info. + + Parameters + ---------- + name : str + The name of this control in the phase_info options. + target : str + Control promoted variable path to the ODE. + rate_source : list of str + List of rate targets for this control. + rate2_targets : Sequence of str or None + (Optional) The parameter in the ODE to which the control 2nd derivative is connected. + add_constraints : bool + When True, add constraints on any declared initial and final values if this control is + being optimized. Default is True. + """ + options = self.user_options + phase = self.phase + + initial, _ = options[f'{name}_initial'] + final, _ = options[f'{name}_final'] + bounds, units = options[f'{name}_bounds'] + ref, _ = options[f'{name}_ref'] + ref0, _ = options[f'{name}_ref0'] + polynomial_order = options[f'{name}_polynomial_order'] + opt = options[f'{name}_optimize'] + + if ref == 1.0: + # This has not been moved from default, so find a good value. + candidates = [x for x in (bounds[0], bounds[1], initial, final) if x is not None] + if len(candidates) > 0: + ref = np.max(np.abs(np.array(candidates))) + + extra_options = {} + if polynomial_order is not None: + extra_options['control_type'] = 'polynomial' + extra_options['order'] = polynomial_order + + if opt is True: + extra_options['lower'] = bounds[0] + extra_options['upper'] = bounds[1] + extra_options['ref'] = ref + extra_options['ref0'] = ref0 + + if units not in ['unitless', None]: + extra_options['units'] = units + + if rate_targets is not None: + extra_options['rate_targets'] = rate_targets + + if rate2_targets is not None: + extra_options['rate2_targets'] = rate2_targets + + phase.add_control(target, targets=target, opt=opt, **extra_options) + + # Add timeseries for any control. + phase.add_timeseries_output(target) + + if not add_constraints: + return + + # Add an initial constraint. + if opt and initial is not None: + phase.add_boundary_constraint( + target, loc='initial', equals=initial, units=units, ref=ref + ) + + # Add a final constraint. + if opt and final is not None: + phase.add_boundary_constraint(target, loc='final', equals=final, units=units, ref=ref) + + def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): + name = phase_builder.name + phase = phase_builder.build_phase(aviary_options) + + self._traj.add_phase(name, phase) + + self._phases[name] = self.MappedPhase(phase, phase_builder) diff --git a/aviary/mission/flops_based/phases/balanced_field_trajectory.py b/aviary/mission/flops_based/phases/balanced_field_trajectory.py index a75523a384..b156af9f39 100644 --- a/aviary/mission/flops_based/phases/balanced_field_trajectory.py +++ b/aviary/mission/flops_based/phases/balanced_field_trajectory.py @@ -62,7 +62,10 @@ from aviary.variable_info.enums import LegacyCode from aviary.variable_info.functions import setup_trajectory_params from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData -from aviary.variable_info.variables import Dynamic, Mission +from aviary.variable_info.variables import Aircraft, Dynamic, Mission + + +VR_RATIO = 1.05 def _init_initial_guess_meta_data(cls: PhaseBuilderBase): @@ -81,6 +84,9 @@ def _init_initial_guess_meta_data(cls: PhaseBuilderBase): cls._add_initial_guess_meta_data(InitialGuessState('velocity'), desc='initial guess for speed') cls._add_initial_guess_meta_data(InitialGuessState('mass'), desc='initial guess for mass') + cls._add_initial_guess_meta_data( + InitialGuessState(Dynamic.Mission.ALTITUDE), desc='initial guess for altitude' + ) cls._add_initial_guess_meta_data( InitialGuessControl('throttle'), desc='initial guess for throttle' @@ -91,6 +97,8 @@ def _init_initial_guess_meta_data(cls: PhaseBuilderBase): cls._add_initial_guess_meta_data(InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE)) cls._add_initial_guess_meta_data(InitialGuessParameter(Dynamic.Mission.FLIGHT_PATH_ANGLE)) + cls._add_initial_guess_meta_data(InitialGuessParameter('dV1')) + cls._add_initial_guess_meta_data(InitialGuessParameter('dVEF')) return cls @@ -118,7 +126,7 @@ def declare_options(self): self.declare( name='terminal_condition', - values=['VEF', 'V1', 'VR', 'LIFTOFF', 'CLIMB_GRADIENT', 'OBSTACLE'], + values=['VEF', 'V1', 'VR', 'LIFTOFF', 'V2', 'CLIMB_GRADIENT', 'OBSTACLE', 'STOP'], allow_none=True, default=None, desc='The condition which governs the end of the phase.', @@ -131,6 +139,13 @@ def declare_options(self): desc='If False, assume aircraft is operating on the runway.', ) + self.declare( + name='friction_key', + types=str, + default=Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT, + desc='Friction Coefficient to use in the phase', + ) + self.declare( name='pitch_control', values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], @@ -349,12 +364,19 @@ def build_phase(self, aviary_options=None): # takeoff to a 35 ft altitude for obstacle clearance # require the same range. phase.add_calc_expr( - 'v_to_go = velocity - (dV1 + v_stall)', + f'v_to_go = velocity - ({VR_RATIO} * v_stall - dV1)', velocity={'units': 'kn'}, dV1={'units': 'kn'}, v_to_go={'units': 'kn'}, v_stall={'units': 'kn'}, ) + phase.add_parameter( + 'dV1', + opt=False, + val=1.0, + units='kn', + desc='Decision speed delta below rotation speed.', + ) phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') elif terminal_condition == 'VR': # Propagate until speed is the rotation speed. @@ -365,10 +387,29 @@ def build_phase(self, aviary_options=None): # dVR={'units': 'kn'}, # v_to_go={'units': 'kn'}, # ) - pass phase.add_boundary_balance( - param='t_duration', name='v_over_v_stall', tgt_val=1.05, loc='final' + param='t_duration', + name='v_over_v_stall', + tgt_val=VR_RATIO, + loc='final', + lower=-1000, + upper=1000, + ref=10.0, ) + elif terminal_condition == 'V2': + # Propagate until speed is the rotation speed. + # phase.add_calc_expr( + # 'v_to_go = velocity - (dVR + dV1 + v_stall)', + # velocity={'units': 'kn'}, + # dv1={'units': 'kn'}, + # dVR={'units': 'kn'}, + # v_to_go={'units': 'kn'}, + # ) + # phase.add_boundary_balance( + # param='t_duration', name='v_over_v_stall', tgt_val=1.2, loc='final', + # lower=-1000, upper=1000, ref=10. + # ) + pass elif terminal_condition == 'VEF': # Propagate until engine failure. # Note we expect dVEF to be negative here. @@ -376,13 +417,28 @@ def build_phase(self, aviary_options=None): # such that it occurs {pilot_reaction_time} seconds # before V1. phase.add_calc_expr( - 'v_to_go = velocity - (dVEF + dV1 + v_stall)', + f'v_to_go = velocity - ({VR_RATIO} * v_stall - dV1 - dVEF)', velocity={'units': 'kn'}, dV1={'units': 'kn'}, - dVEF={'units': 'kn'}, v_to_go={'units': 'kn'}, + v_stall={'units': 'kn'}, + dVEF={'units': 'kn'}, ) phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + phase.add_parameter( + 'dVEF', + opt=False, + val=3.0, + units='kn', + desc='Engine failure speed delta below decision speed.', + ) + phase.add_parameter( + 'dV1', + opt=False, + val=1.0, + units='kn', + desc='Decision speed delta below rotation speed.', + ) elif terminal_condition == 'LIFTOFF': # Propagate until velocity is the desired literal value. phase.add_boundary_balance( @@ -390,7 +446,7 @@ def build_phase(self, aviary_options=None): name='takeoff_eom.ground_normal_force', tgt_val=0.0, lower=0.0001, - upper=20, + upper=1000, eq_units='MN', loc='final', ) @@ -405,7 +461,7 @@ def build_phase(self, aviary_options=None): name='climb_gradient', tgt_val=0.024, lower=0.0001, - upper=20, + upper=1000, eq_units='unitless', loc='final', ) @@ -415,10 +471,20 @@ def build_phase(self, aviary_options=None): name=Dynamic.Mission.ALTITUDE, tgt_val=35, lower=0.0001, - upper=100, + upper=1000, eq_units='ft', loc='final', ) + elif terminal_condition == 'STOP': + phase.add_boundary_balance( + param='t_duration', + name=Dynamic.Mission.VELOCITY, + tgt_val=5.0, + lower=0.0001, + upper=1000, + eq_units='kn', + loc='final', + ) elif terminal_condition is None: # Propagate for t_duration pass @@ -428,11 +494,6 @@ def build_phase(self, aviary_options=None): 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' ) - phase.add_parameter( - 'dV1', opt=False, val=10.0, units='kn', desc='Decision speed delta above stall speed.' - ) - # phase.add_parameter('dVEF', opt=False, val=10.0, desc='Decision speed delta below decision speed.') - if phase.boundary_balance_options: phase.options['auto_order'] = True @@ -442,6 +503,10 @@ def build_phase(self, aviary_options=None): if user_options['linear_solver'] is not None: phase.linear_solver = user_options['linear_solver'] + phase.add_timeseries_output( + 'v_over_v_stall', output_name='v_over_v_stall', units='unitless' + ) + return phase def make_default_transcription(self): @@ -455,321 +520,318 @@ def _extra_ode_init_kwargs(self): return { 'climbing': self.user_options['climbing'], 'pitch_control': self.user_options['pitch_control'], - 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT, + 'friction_key': self.user_options['friction_key'], } -class BalancedFieldTrajectoryBuilder: - """ - Define a trajectory builder for detailed takeoff. +# class BalancedFieldTrajectoryBuilder: +# """ +# Define a trajectory builder for detailed takeoff. + +# Identify, collect, and call the necessary phase builders to create a typical takeoff +# trajectory. +# """ + +# MappedPhase = namedtuple('MappedPhase', ('phase', 'phase_builder')) + +# default_name = 'detailed_takeoff' + +# def __init__(self, name=None): +# if name is None: +# name = self.default_name + +# self.name = name + +# self._brake_release_to_decision_speed = None + +# self._phases = {} +# self._traj = None + +# def get_phase_names(self): +# """Return a list of base names for available phases.""" +# keys = list(self._phases) + +# return keys + +# def get_phase(self, key) -> dm.Phase: +# """ +# Return the phase associated with the specified base name. + +# Raises +# ------ +# KeyError +# if the specified base name is not found +# """ +# mapped_phase = self._phases[key] + +# return mapped_phase.phase + +# def set_brake_release_to_decision_speed(self, phase_builder: PhaseBuilderBase): +# """ +# Assign a phase builder for the beginning of takeoff to the time when the pilot +# must choose either to liftoff or halt the aircraft. +# """ +# self._brake_release_to_decision_speed = phase_builder + +# # def set_decision_speed_to_rotate(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for the short distance between achieving decision speed +# # and beginning the rotation phase. +# # """ +# # self._decision_speed_to_rotate = phase_builder + +# # def set_rotate_to_liftoff(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for the short distance required to rotate the aircraft +# # to achieve liftoff. +# # """ +# # self._rotate_to_liftoff = phase_builder + +# # def set_liftoff_to_climb_gradient(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign the phase builder for the phase from liftoff until the required climb gradient is reached. + +# # Args: +# # phase_builder (PhaseBuilderBase): _description_ +# # """ +# # self._liftoff_to_climb_gradient = phase_builder + +# # def set_climb_gradient_to_obstacle(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign the phase builder for the phase from achievement of climb gradient until obstacle clearance. + +# # Args: +# # phase_builder (PhaseBuilderBase): _description_ +# # """ +# # self._climb_gradient_to_obstacle = phase_builder + +# # def set_liftoff_to_obstacle(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for the short period between liftoff and clearing the +# # required obstacle. +# # """ +# # self._liftoff_to_obstacle = phase_builder + +# # def set_obstacle_to_mic_p2(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for the fifth phase of takeoff, from clearing the required +# # obstacle to the p2 mic loation. This phase is required for acoustic calculations. +# # """ +# # self._obstacle_to_mic_p2 = phase_builder + +# # def set_mic_p2_to_engine_cutback(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for the sixth phase of takeoff, from the p2 mic location +# # to engine cutback. This phase is required for acoustic calculations. +# # """ +# # self._mic_p2_to_engine_cutback = phase_builder + +# # def set_engine_cutback(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for the seventh phase of takeoff, from start to +# # finish of engine cutback. This phase is required for acoustic calculations. +# # """ +# # self._engine_cutback = phase_builder + +# # def set_engine_cutback_to_mic_p1(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for the eighth phase of takeoff, engine cutback +# # to the P1 mic location. This phase is required for acoustic calculations. +# # """ +# # self._engine_cutback_to_mic_p1 = phase_builder + +# # def set_mic_p1_to_climb(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for the ninth phase of takeoff, from P1 mic +# # location to climb. This phase is required for acoustic calculations. +# # """ +# # self._mic_p1_to_climb = phase_builder + +# # def set_decision_speed_to_brake(self, phase_builder: PhaseBuilderBase): +# # """ +# # Assign a phase builder for delayed braking when the engine fails. + +# # Note, this phase is optional. It is only required if balanced field length +# # calculations are required. +# # """ +# # self._decision_speed_to_brake = phase_builder + +# # def set_brake_to_abort(self, phase_builder: PhaseBuilderBase, balanced_field_ref=8_000.0): +# # """ +# # Assign a phase builder for braking to fullstop after engine failure. + +# # Note, this phase is optional. It is only required if balanced field length +# # calculations are required. + +# # Parameters +# # ---------- +# # phase_builder : PhaseBuilderBase + +# # balanced_field_ref : float (8_000.0) +# # the ref value to use for the linkage constraint for the final range +# # between the liftoff-to-obstacle and the decision-speed-to-abort phases; + +# # Notes +# # ----- +# # The default value for `balanced_field_ref` is appropriate total takeoff distances +# # calculated in 'ft' for larger commercial passenger transports traveling in the +# # continental United States. International travel of similar aircraft may require a +# # larger value, while a smaller aircraft with a shorter range may require a smaller +# # value. +# # """ +# # self._brake_to_abort = phase_builder +# # self._balanced_field_ref = balanced_field_ref + +# def build_trajectory( +# self, *, aviary_options: AviaryValues, model: om.Group = None, traj: dm.Trajectory = None +# ) -> dm.Trajectory: +# """ +# Return a new trajectory for detailed takeoff analysis. + +# Call only after assigning phase builders for required phases. + +# Parameters +# ---------- +# aviary_options : AviaryValues +# collection of Aircraft/Mission specific options + +# model : openmdao.api.Group (None) +# the model handling trajectory parameter setup; if `None`, trajectory +# parameter setup will not be handled + +# traj : dymos.Trajectory (None) +# the trajectory to update; if `None`, a new trajetory will be updated and +# returned - Identify, collect, and call the necessary phase builders to create a typical takeoff - trajectory. - """ +# Returns +# ------- +# the updated trajectory; if the specified trajectory is `None`, a new trajectory +# will be updated and returned - MappedPhase = namedtuple('MappedPhase', ('phase', 'phase_builder')) - - default_name = 'detailed_takeoff' +# Notes +# ----- +# Do not modify this object or any of its referenced data between the call to +# `build_trajectory()` and the call to `apply_initial_guesses()`, or the behavior +# is undefined, no diagnostic required. +# """ +# if traj is None: +# traj = dm.Trajectory() - def __init__(self, name=None): - if name is None: - name = self.default_name +# self._traj = traj - self.name = name +# self._add_phases(aviary_options) +# self._link_phases() - self._brake_release_to_decision_speed = None +# if model is not None: +# phase_names = self.get_phase_names() - self._phases = {} - self._traj = None +# # This is a level 3 method that uses the default subsystems. +# # We need to create parameters for just the inputs we have. +# # They mostly come from the low-speed aero subsystem. - def get_phase_names(self): - """Return a list of base names for available phases.""" - keys = list(self._phases) +# aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, LegacyCode('FLOPS')) - return keys +# kwargs = {'method': 'low_speed'} - def get_phase(self, key) -> dm.Phase: - """ - Return the phase associated with the specified base name. +# params = aero.get_parameters(aviary_options, **kwargs) - Raises - ------ - KeyError - if the specified base name is not found - """ - mapped_phase = self._phases[key] +# # takeoff introduces this one. +# params[Mission.Takeoff.LIFT_COEFFICIENT_MAX] = { +# 'shape': (1,), +# 'static_target': True, +# } - return mapped_phase.phase +# ext_params = {} +# for phase in self._phases.keys(): +# ext_params[phase] = params - def set_brake_release_to_decision_speed(self, phase_builder: PhaseBuilderBase): - """ - Assign a phase builder for the beginning of takeoff to the time when the pilot - must choose either to liftoff or halt the aircraft. - """ - self._brake_release_to_decision_speed = phase_builder - - # def set_decision_speed_to_rotate(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for the short distance between achieving decision speed - # and beginning the rotation phase. - # """ - # self._decision_speed_to_rotate = phase_builder - - # def set_rotate_to_liftoff(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for the short distance required to rotate the aircraft - # to achieve liftoff. - # """ - # self._rotate_to_liftoff = phase_builder - - # def set_liftoff_to_climb_gradient(self, phase_builder: PhaseBuilderBase): - # """ - # Assign the phase builder for the phase from liftoff until the required climb gradient is reached. - - # Args: - # phase_builder (PhaseBuilderBase): _description_ - # """ - # self._liftoff_to_climb_gradient = phase_builder - - # def set_climb_gradient_to_obstacle(self, phase_builder: PhaseBuilderBase): - # """ - # Assign the phase builder for the phase from achievement of climb gradient until obstacle clearance. - - # Args: - # phase_builder (PhaseBuilderBase): _description_ - # """ - # self._climb_gradient_to_obstacle = phase_builder - - # def set_liftoff_to_obstacle(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for the short period between liftoff and clearing the - # required obstacle. - # """ - # self._liftoff_to_obstacle = phase_builder - - # def set_obstacle_to_mic_p2(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for the fifth phase of takeoff, from clearing the required - # obstacle to the p2 mic loation. This phase is required for acoustic calculations. - # """ - # self._obstacle_to_mic_p2 = phase_builder - - # def set_mic_p2_to_engine_cutback(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for the sixth phase of takeoff, from the p2 mic location - # to engine cutback. This phase is required for acoustic calculations. - # """ - # self._mic_p2_to_engine_cutback = phase_builder - - # def set_engine_cutback(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for the seventh phase of takeoff, from start to - # finish of engine cutback. This phase is required for acoustic calculations. - # """ - # self._engine_cutback = phase_builder - - # def set_engine_cutback_to_mic_p1(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for the eighth phase of takeoff, engine cutback - # to the P1 mic location. This phase is required for acoustic calculations. - # """ - # self._engine_cutback_to_mic_p1 = phase_builder - - # def set_mic_p1_to_climb(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for the ninth phase of takeoff, from P1 mic - # location to climb. This phase is required for acoustic calculations. - # """ - # self._mic_p1_to_climb = phase_builder - - # def set_decision_speed_to_brake(self, phase_builder: PhaseBuilderBase): - # """ - # Assign a phase builder for delayed braking when the engine fails. - - # Note, this phase is optional. It is only required if balanced field length - # calculations are required. - # """ - # self._decision_speed_to_brake = phase_builder - - # def set_brake_to_abort(self, phase_builder: PhaseBuilderBase, balanced_field_ref=8_000.0): - # """ - # Assign a phase builder for braking to fullstop after engine failure. - - # Note, this phase is optional. It is only required if balanced field length - # calculations are required. - - # Parameters - # ---------- - # phase_builder : PhaseBuilderBase - - # balanced_field_ref : float (8_000.0) - # the ref value to use for the linkage constraint for the final range - # between the liftoff-to-obstacle and the decision-speed-to-abort phases; - - # Notes - # ----- - # The default value for `balanced_field_ref` is appropriate total takeoff distances - # calculated in 'ft' for larger commercial passenger transports traveling in the - # continental United States. International travel of similar aircraft may require a - # larger value, while a smaller aircraft with a shorter range may require a smaller - # value. - # """ - # self._brake_to_abort = phase_builder - # self._balanced_field_ref = balanced_field_ref - - def build_trajectory( - self, *, aviary_options: AviaryValues, model: om.Group = None, traj: dm.Trajectory = None - ) -> dm.Trajectory: - """ - Return a new trajectory for detailed takeoff analysis. +# setup_trajectory_params( +# model, traj, aviary_options, phase_names, external_parameters=ext_params +# ) - Call only after assigning phase builders for required phases. +# return traj - Parameters - ---------- - aviary_options : AviaryValues - collection of Aircraft/Mission specific options - - model : openmdao.api.Group (None) - the model handling trajectory parameter setup; if `None`, trajectory - parameter setup will not be handled - - traj : dymos.Trajectory (None) - the trajectory to update; if `None`, a new trajetory will be updated and - returned - - Returns - ------- - the updated trajectory; if the specified trajectory is `None`, a new trajectory - will be updated and returned - - Notes - ----- - Do not modify this object or any of its referenced data between the call to - `build_trajectory()` and the call to `apply_initial_guesses()`, or the behavior - is undefined, no diagnostic required. - """ - if traj is None: - traj = dm.Trajectory() +# def apply_initial_guesses(self, prob: om.Problem, traj_name): +# """ +# Call `prob.set_val()` for states/parameters/etc. for each phase in this +# trajectory. - self._traj = traj +# Call only after `build_trajectory()` and `prob.setup()`. - self._add_phases(aviary_options) - self._link_phases() - - if model is not None: - phase_names = self.get_phase_names() - - # This is a level 3 method that uses the default subsystems. - # We need to create parameters for just the inputs we have. - # They mostly come from the low-speed aero subsystem. - - aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, LegacyCode('FLOPS')) - - kwargs = {'method': 'low_speed'} - - params = aero.get_parameters(aviary_options, **kwargs) - - # takeoff introduces this one. - params[Mission.Takeoff.LIFT_COEFFICIENT_MAX] = { - 'shape': (1,), - 'static_target': True, - } - - ext_params = {} - for phase in self._phases.keys(): - ext_params[phase] = params - - print(ext_params) - exit(0) - - setup_trajectory_params( - model, traj, aviary_options, phase_names, external_parameters=ext_params - ) - - return traj - - def apply_initial_guesses(self, prob: om.Problem, traj_name): - """ - Call `prob.set_val()` for states/parameters/etc. for each phase in this - trajectory. - - Call only after `build_trajectory()` and `prob.setup()`. - - Returns - ------- - not_applied : dict[str, list[str]] - for any phase with missing initial guesses that cannot be applied, a list of - those missing initial guesses; if a given phase has no missing initial - guesses, the returned mapping will not contain the name of that phase - """ - not_applied = {} - phase_builder: PhaseBuilderBase = None +# Returns +# ------- +# not_applied : dict[str, list[str]] +# for any phase with missing initial guesses that cannot be applied, a list of +# those missing initial guesses; if a given phase has no missing initial +# guesses, the returned mapping will not contain the name of that phase +# """ +# not_applied = {} +# phase_builder: PhaseBuilderBase = None - for phase, phase_builder in self._phases.values(): - tmp = phase_builder.apply_initial_guesses(prob, traj_name, phase) +# for phase, phase_builder in self._phases.values(): +# tmp = phase_builder.apply_initial_guesses(prob, traj_name, phase) - if tmp: - not_applied[phase_builder.name] = tmp +# if tmp: +# not_applied[phase_builder.name] = tmp - return not_applied +# return not_applied - def _add_phases(self, aviary_options: AviaryValues): - self._phases = {} +# def _add_phases(self, aviary_options: AviaryValues): +# self._phases = {} - self._add_phase(self._brake_release_to_decision_speed, aviary_options) +# self._add_phase(self._brake_release_to_decision_speed, aviary_options) - # self._add_phase(self._decision_speed_to_rotate, aviary_options) +# # self._add_phase(self._decision_speed_to_rotate, aviary_options) - # self._add_phase(self._rotate_to_liftoff, aviary_options) +# # self._add_phase(self._rotate_to_liftoff, aviary_options) - # self._add_phase(self._liftoff_to_climb_gradient, aviary_options) +# # self._add_phase(self._liftoff_to_climb_gradient, aviary_options) - # self._add_phase(self._climb_gradient_to_obstacle, aviary_options) +# # self._add_phase(self._climb_gradient_to_obstacle, aviary_options) - # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 +# # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 - # if obstacle_to_mic_p2 is not None: - # self._add_phase(obstacle_to_mic_p2, aviary_options) +# # if obstacle_to_mic_p2 is not None: +# # self._add_phase(obstacle_to_mic_p2, aviary_options) - # self._add_phase(self._mic_p2_to_engine_cutback, aviary_options) +# # self._add_phase(self._mic_p2_to_engine_cutback, aviary_options) - # self._add_phase(self._engine_cutback, aviary_options) +# # self._add_phase(self._engine_cutback, aviary_options) - # self._add_phase(self._engine_cutback_to_mic_p1, aviary_options) +# # self._add_phase(self._engine_cutback_to_mic_p1, aviary_options) - # self._add_phase(self._mic_p1_to_climb, aviary_options) +# # self._add_phase(self._mic_p1_to_climb, aviary_options) - # decision_speed_to_brake = self._decision_speed_to_brake +# # decision_speed_to_brake = self._decision_speed_to_brake - # if decision_speed_to_brake is not None: - # self._add_phase(decision_speed_to_brake, aviary_options) +# # if decision_speed_to_brake is not None: +# # self._add_phase(decision_speed_to_brake, aviary_options) - # self._add_phase(self._brake_to_abort, aviary_options) +# # self._add_phase(self._brake_to_abort, aviary_options) - def _link_phases(self): - traj: dm.Trajectory = self._traj +# def _link_phases(self): +# traj: dm.Trajectory = self._traj - # brake_release_name = self._brake_release_to_decision_speed.name - # decision_speed_name = self._decision_speed_to_rotate.name - # rotate_name = self._rotate_to_liftoff.name - # liftoff_name = self._liftoff_to_climb_gradient.name - # climb_gradient_name = self._climb_gradient_to_obstacle.name +# # brake_release_name = self._brake_release_to_decision_speed.name +# # decision_speed_name = self._decision_speed_to_rotate.name +# # rotate_name = self._rotate_to_liftoff.name +# # liftoff_name = self._liftoff_to_climb_gradient.name +# # climb_gradient_name = self._climb_gradient_to_obstacle.name - # traj.link_phases( - # [brake_release_name, decision_speed_name, rotate_name, liftoff_name, climb_gradient_name], - # vars=['time', 'distance', 'velocity', 'mass'], - # connected=True, - # ) +# # traj.link_phases( +# # [brake_release_name, decision_speed_name, rotate_name, liftoff_name, climb_gradient_name], +# # vars=['time', 'distance', 'velocity', 'mass'], +# # connected=True, +# # ) - # traj.link_phases([rotate_name, liftoff_name], vars=['angle_of_attack',], connected=True) +# # traj.link_phases([rotate_name, liftoff_name], vars=['angle_of_attack',], connected=True) - # traj.link_phases([liftoff_name, climb_gradient_name], vars=['flight_path_angle', 'altitude'], connected=True) +# # traj.link_phases([liftoff_name, climb_gradient_name], vars=['flight_path_angle', 'altitude'], connected=True) - def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): - name = phase_builder.name - phase = phase_builder.build_phase(aviary_options) +# def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): +# name = phase_builder.name +# phase = phase_builder.build_phase(aviary_options) - self._traj.add_phase(name, phase) +# self._traj.add_phase(name, phase) - self._phases[name] = self.MappedPhase(phase, phase_builder) +# self._phases[name] = self.MappedPhase(phase, phase_builder) diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field2.py b/aviary/mission/flops_based/phases/test/test_balanced_field2.py index fb310f93e4..d6bed6057f 100644 --- a/aviary/mission/flops_based/phases/test/test_balanced_field2.py +++ b/aviary/mission/flops_based/phases/test/test_balanced_field2.py @@ -14,6 +14,7 @@ class TestBalancedField(unittest.TestCase): """Test takeoff phase builder.""" def test_balanced_field_2(self): + # TODO: Why doesn't aviary options respect keys() and values() if its dict-like? aviary_options = inputs.deepcopy() # This builder can be used for both takeoff and landing phases @@ -308,6 +309,8 @@ def test_balanced_field_2(self): takeoff_trajectory_builder.apply_initial_guesses(test_problem, 'traj') + # test_problem.set_val(Mission.Takeoff.DECISION_SPEED_INCREMENT, 20, units='kn') + test_problem.final_setup() # test_problem.model.list_vars(print_arrays=True) diff --git a/aviary/variable_info/variables.py b/aviary/variable_info/variables.py index a0efc55355..d2f900a49f 100644 --- a/aviary/variable_info/variables.py +++ b/aviary/variable_info/variables.py @@ -773,6 +773,7 @@ class Takeoff: DRAG_COEFFICIENT_FLAP_INCREMENT = 'mission:takeoff:drag_coefficient_flap_increment' DRAG_COEFFICIENT_MIN = 'mission:takeoff:drag_coefficient_min' + ENGINE_FAILURE_SPEED_INCREMENT = 'mission:takeoff:engine_failure_speed_increment' FIELD_LENGTH = 'mission:takeoff:field_length' FINAL_ALTITUDE = 'mission:takeoff:final_altitude' FINAL_MACH = 'mission:takeoff:final_mach' From cec5c3fac02efb3125b4cb215425dea5e7070d79 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 15 Sep 2025 15:10:55 -0400 Subject: [PATCH 08/22] working --- aviary/mission/balanced_field_traj_builder.py | 97 ++++- aviary/mission/flops_based/ode/takeoff_eom.py | 20 +- aviary/mission/flops_based/ode/takeoff_ode.py | 15 +- .../phases/balanced_field_trajectory.py | 387 ++---------------- .../phases/test/test_balanced_field2.py | 257 ------------ aviary/mission/initial_guess_builders.py | 6 +- 6 files changed, 150 insertions(+), 632 deletions(-) diff --git a/aviary/mission/balanced_field_traj_builder.py b/aviary/mission/balanced_field_traj_builder.py index 29c4257426..6a2678f2f6 100644 --- a/aviary/mission/balanced_field_traj_builder.py +++ b/aviary/mission/balanced_field_traj_builder.py @@ -206,6 +206,10 @@ def build_trajectory( self._traj = traj = dm.Trajectory(parallel_phases=False) model.add_subsystem('traj', self._traj) + # We're adding a balance comp, so use auto-ordering to put + # systems in best order. + self._traj.options['auto_order'] = True + if aviary_options is None: aviary_options = AviaryValues() @@ -299,7 +303,7 @@ def build_trajectory( user_options.set_val('terminal_condition', 'STOP') user_options.set_val('friction_key', Mission.Takeoff.BRAKING_FRICTION_COEFFICIENT) initial_guesses = common_initial_guesses.deepcopy() - initial_guesses.set_val('time', [31.0, 20.0], 's') + initial_guesses.set_val('time', [31.0, 10.0], 's') initial_guesses.set_val('distance', [4500.0, 6500.0], 'ft') initial_guesses.set_val('velocity', [151.0, 5.0], 'kn') initial_guesses.set_val('throttle', 0.0) @@ -342,14 +346,14 @@ def build_trajectory( # user_options = common_user_options.deepcopy() user_options.set_val('terminal_condition', 'LIFTOFF') - user_options.set_val('pitch_control', 'alpha_rate_fixed') + user_options.set_val('pitch_control', 'ALPHA_RATE_FIXED') initial_guesses = common_initial_guesses.deepcopy() initial_guesses.set_val('time', [37.0, 7.0], 's') initial_guesses.set_val('distance', [5000.0, 5500.0], 'ft') initial_guesses.set_val('velocity', [155.0, 160.0], 'kn') initial_guesses.set_val('throttle', 0.5) initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK, [0.0, 14.0], 'deg') - initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, 3.0, 'deg/s') + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, 2.5, 'deg/s') takeoff_vr_to_liftoff = BalancedFieldPhaseBuilder( 'takeoff_vr_to_liftoff', @@ -362,31 +366,60 @@ def build_trajectory( self._add_phase(takeoff_vr_to_liftoff, aviary_options) # - # Sixth Phase: Liftoff to V2 + # Sixth Phase: Liftoff to Climb Gradient # user_options = common_user_options.deepcopy() - user_options.set_val('terminal_condition', 'OBSTACLE') - user_options.set_val('pitch_control', 'alpha_rate_fixed') + user_options.set_val('terminal_condition', 'CLIMB_GRADIENT') + user_options.set_val('pitch_control', 'ALPHA_RATE_FIXED') user_options.set_val('climbing', True) initial_guesses = common_initial_guesses.deepcopy() - initial_guesses.set_val('time', [37.0, 15.0], 's') + initial_guesses.set_val('time', [37.0, 1.0], 's') initial_guesses.set_val('distance', [5000.0, 5500.0], 'ft') initial_guesses.set_val('velocity', [155.0, 160.0], 'kn') initial_guesses.set_val('throttle', 0.5) initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK, [10.0, 10.0], 'deg') initial_guesses.set_val(Dynamic.Mission.FLIGHT_PATH_ANGLE, [0.0, 0.5], 'deg') initial_guesses.set_val(Dynamic.Mission.ALTITUDE, [0.0, 0.5], 'ft') - initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, 1.0, 'deg/s') + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, 2.0, 'deg/s') + + takeoff_liftoff_to_climb_gradient = BalancedFieldPhaseBuilder( + 'takeoff_liftoff_to_climb_gradient', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_liftoff_to_climb_gradient, aviary_options) + + # + # Seventh Phase: Climb Gradient to Obstacle + # + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'OBSTACLE') + user_options.set_val('pitch_control', 'GAMMA_FIXED') + user_options.set_val('climbing', True) + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [37.0, 5.0], 's') + initial_guesses.set_val('distance', [5000.0, 5500.0], 'ft') + initial_guesses.set_val('velocity', [155.0, 160.0], 'kn') + initial_guesses.set_val('throttle', 0.5) + initial_guesses.set_val(Dynamic.Mission.FLIGHT_PATH_ANGLE, [0.1, 0.1], 'deg') + initial_guesses.set_val(Dynamic.Mission.ALTITUDE, [0.5, 35.0], 'ft') - takeoff_liftoff_to_v2 = BalancedFieldPhaseBuilder( - 'takeoff_liftoff_to_v2', + takeoff_climb_gradient_to_obstacle = BalancedFieldPhaseBuilder( + 'takeoff_climb_gradient_to_obstacle', core_subsystems=self.core_subsystems, subsystem_options=self.subsystem_options, user_options=user_options, initial_guesses=initial_guesses, ) - self._add_phase(takeoff_liftoff_to_v2, aviary_options) + self._add_phase(takeoff_climb_gradient_to_obstacle, aviary_options) + + # + # Add a trajectory-level balance to satisfy inter-phase relationships + # bal_comp = self._traj.add_subsystem('balance_comp', om.BalanceComp()) @@ -413,12 +446,41 @@ def build_trajectory( ], ) self._traj.connect( - 'takeoff_liftoff_to_v2.final_states:distance', 'balance_comp.distance_obstacle' + 'takeoff_climb_gradient_to_obstacle.final_states:distance', + 'balance_comp.distance_obstacle', ) self._traj.connect( 'takeoff_v1_to_roll_stop.final_states:distance', 'balance_comp.distance_abort' ) + # The third balance sets the trigger ratio of V/V_stall for rotation such that + # the climb gradient is achieved with V/V_stall = 1.2 (V2) + + bal_comp.add_balance( + 'VR_trigger_ratio', + lhs_name='v_over_v_stall_at_climb_gradient', + rhs_val=1.2, + eq_units='unitless', + units='unitless', + ) + + self._traj.connect( + 'balance_comp.VR_trigger_ratio', + [ + 'takeoff_brake_release_to_engine_failure.parameters:VR_ratio', + 'takeoff_engine_failure_to_v1.parameters:VR_ratio', + 'takeoff_v1_to_vr.parameters:VR_ratio', + ], + ) + + self._traj.connect( + 'takeoff_liftoff_to_climb_gradient.timeseries.v_over_v_stall', + 'balance_comp.v_over_v_stall_at_climb_gradient', + src_indices=om.slicer[-1, ...], + ) + + # Add a newton solver to the balanced field trajectory + self._traj.nonlinear_solver = om.NewtonSolver( solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6 ) @@ -432,18 +494,25 @@ def build_trajectory( 'takeoff_engine_failure_to_v1', 'takeoff_v1_to_vr', 'takeoff_vr_to_liftoff', - 'takeoff_liftoff_to_v2', + 'takeoff_liftoff_to_climb_gradient', + 'takeoff_climb_gradient_to_obstacle', ], vars=['time', 'distance', 'velocity', 'mass'], connected=True, ) self._traj.link_phases( - ['takeoff_v1_to_vr', 'takeoff_vr_to_liftoff', 'takeoff_liftoff_to_v2'], + ['takeoff_v1_to_vr', 'takeoff_vr_to_liftoff', 'takeoff_liftoff_to_climb_gradient'], vars=[Dynamic.Vehicle.ANGLE_OF_ATTACK], connected=True, ) + self._traj.link_phases( + ['takeoff_liftoff_to_climb_gradient', 'takeoff_climb_gradient_to_obstacle'], + vars=[Dynamic.Mission.ALTITUDE, Dynamic.Mission.FLIGHT_PATH_ANGLE], + connected=True, + ) + # Abort Sequence self._traj.link_phases( ['takeoff_engine_failure_to_v1', 'takeoff_v1_to_roll_stop'], diff --git a/aviary/mission/flops_based/ode/takeoff_eom.py b/aviary/mission/flops_based/ode/takeoff_eom.py index 87283a68f5..bfeea0b5cb 100644 --- a/aviary/mission/flops_based/ode/takeoff_eom.py +++ b/aviary/mission/flops_based/ode/takeoff_eom.py @@ -120,8 +120,8 @@ def initialize(self): self.options.declare( 'pitch_control', - values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], - default='alpha_fixed', + values=['ALPHA_FIXED', 'ALPHA_RATE_FIXED', 'GAMMA_FIXED'], + default='ALPHA_FIXED', desc='How pitch is controlled.', ) @@ -194,16 +194,17 @@ def setup(self): promotes=['*'], ) - if self.options['pitch_control'] == 'gamma_fixed': + if self.options['pitch_control'] == 'GAMMA_FIXED': # If using fixed-flight-path-angle control alpha_resid_comp = om.InputResidsComp() - self.add_subsystem( - 'alpha_resid_comp', - alpha_resid_comp, - promotes=['*'] + self.add_subsystem('alpha_resid_comp', alpha_resid_comp, promotes=['*']) + alpha_resid_comp.add_input( + Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, shape=(nn,), units='rad/s' ) - alpha_resid_comp.add_input(Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, shape=(nn,), units='rad/s') - alpha_resid_comp.add_output(Dynamic.Vehicle.ANGLE_OF_ATTACK, shape=(nn,), val=0.0, units='rad') + alpha_resid_comp.add_output( + Dynamic.Vehicle.ANGLE_OF_ATTACK, shape=(nn,), val=0.0, units='rad' + ) + class DistanceRates(om.ExplicitComponent): """ @@ -259,7 +260,6 @@ def setup_partials(self): def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): velocity = inputs[Dynamic.Mission.VELOCITY] - if self.options['climbing']: flight_path_angle = inputs[Dynamic.Mission.FLIGHT_PATH_ANGLE] diff --git a/aviary/mission/flops_based/ode/takeoff_ode.py b/aviary/mission/flops_based/ode/takeoff_ode.py index 1ec596748d..9b25938b37 100644 --- a/aviary/mission/flops_based/ode/takeoff_ode.py +++ b/aviary/mission/flops_based/ode/takeoff_ode.py @@ -31,8 +31,8 @@ def initialize(self): self.options.declare( 'pitch_control', - values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], - default='alpha_fixed', + values=['ALPHA_FIXED', 'ALPHA_RATE_FIXED', 'GAMMA_FIXED'], + default='ALPHA_FIXED', desc='How pitch is controlled.', ) @@ -71,7 +71,7 @@ def setup(self): 'climbing': options['climbing'], 'friction_key': options['friction_key'], 'aviary_options': options['aviary_options'], - 'pitch_control': options['pitch_control'] + 'pitch_control': options['pitch_control'], } self.add_subsystem( @@ -111,6 +111,13 @@ def setup(self): self.set_input_defaults(Aircraft.Wing.AREA, 1.0, 'm**2') if self.options['pitch_control'] == 'gamma_fixed': - self.nonlinear_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, atol=1.0E-6, rtol=1.0E-6, iprint=0, debug_print=False) + self.nonlinear_solver = om.NewtonSolver( + solve_subsystems=True, + maxiter=100, + atol=1.0e-6, + rtol=1.0e-6, + iprint=0, + debug_print=False, + ) self.nonlinear_solver.linesearch = om.ArmijoGoldsteinLS() self.linear_solver = om.DirectSolver(assemble_jac=True) diff --git a/aviary/mission/flops_based/phases/balanced_field_trajectory.py b/aviary/mission/flops_based/phases/balanced_field_trajectory.py index b156af9f39..1b25938eaf 100644 --- a/aviary/mission/flops_based/phases/balanced_field_trajectory.py +++ b/aviary/mission/flops_based/phases/balanced_field_trajectory.py @@ -65,7 +65,7 @@ from aviary.variable_info.variables import Aircraft, Dynamic, Mission -VR_RATIO = 1.05 +# VR_RATIO = 1.18 def _init_initial_guess_meta_data(cls: PhaseBuilderBase): @@ -126,7 +126,16 @@ def declare_options(self): self.declare( name='terminal_condition', - values=['VEF', 'V1', 'VR', 'LIFTOFF', 'V2', 'CLIMB_GRADIENT', 'OBSTACLE', 'STOP'], + values=[ + 'VEF', + 'V1', + 'VR', + 'LIFTOFF', + 'CLIMB_GRADIENT', + 'MAX_ALPHA', + 'OBSTACLE', + 'STOP', + ], allow_none=True, default=None, desc='The condition which governs the end of the phase.', @@ -148,8 +157,8 @@ def declare_options(self): self.declare( name='pitch_control', - values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], - default='alpha_fixed', + values=['ALPHA_FIXED', 'ALPHA_RATE_FIXED', 'GAMMA_FIXED'], + default='ALPHA_FIXED', desc='Specifies how alpha is controlled - Alpha can be a fixed parameter, specified via a fixed rate parameter, ' 'or whether the climb gradient is constant', ) @@ -329,9 +338,11 @@ def build_phase(self, aviary_options=None): opt=False, ) - if user_options['pitch_control'] == 'alpha_fixed': + phase.add_parameter('VR_ratio', val=1.05, units='unitless', opt=False) + + if user_options['pitch_control'] == 'ALPHA_FIXED': phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') - elif user_options['pitch_control'] == 'alpha_rate_fixed': + elif user_options['pitch_control'] == 'ALPHA_RATE_FIXED': phase.add_parameter( Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, val=2.0, opt=False, units='deg/s' ) @@ -344,6 +355,8 @@ def build_phase(self, aviary_options=None): rate_source=Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, targets=Dynamic.Vehicle.ANGLE_OF_ATTACK, ) + else: + phase.add_timeseries_output(Dynamic.Vehicle.ANGLE_OF_ATTACK, units='deg') phase.add_timeseries_output( Dynamic.Vehicle.Propulsion.THRUST_TOTAL, @@ -364,11 +377,12 @@ def build_phase(self, aviary_options=None): # takeoff to a 35 ft altitude for obstacle clearance # require the same range. phase.add_calc_expr( - f'v_to_go = velocity - ({VR_RATIO} * v_stall - dV1)', + f'v_to_go = velocity - (VR_ratio * v_stall - dV1)', velocity={'units': 'kn'}, dV1={'units': 'kn'}, v_to_go={'units': 'kn'}, v_stall={'units': 'kn'}, + VR_ratio={'units': 'unitless'}, ) phase.add_parameter( 'dV1', @@ -380,36 +394,22 @@ def build_phase(self, aviary_options=None): phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') elif terminal_condition == 'VR': # Propagate until speed is the rotation speed. - # phase.add_calc_expr( - # 'v_to_go = velocity - (dVR + dV1 + v_stall)', - # velocity={'units': 'kn'}, - # dv1={'units': 'kn'}, - # dVR={'units': 'kn'}, - # v_to_go={'units': 'kn'}, - # ) + phase.add_calc_expr( + 'v_to_go = velocity - (VR_ratio * v_stall)', + velocity={'units': 'kn'}, + VR_ratio={'units': 'unitless'}, + v_stall={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) phase.add_boundary_balance( param='t_duration', - name='v_over_v_stall', - tgt_val=VR_RATIO, + name='v_to_go', + tgt_val=0.0, loc='final', lower=-1000, upper=1000, ref=10.0, ) - elif terminal_condition == 'V2': - # Propagate until speed is the rotation speed. - # phase.add_calc_expr( - # 'v_to_go = velocity - (dVR + dV1 + v_stall)', - # velocity={'units': 'kn'}, - # dv1={'units': 'kn'}, - # dVR={'units': 'kn'}, - # v_to_go={'units': 'kn'}, - # ) - # phase.add_boundary_balance( - # param='t_duration', name='v_over_v_stall', tgt_val=1.2, loc='final', - # lower=-1000, upper=1000, ref=10. - # ) - pass elif terminal_condition == 'VEF': # Propagate until engine failure. # Note we expect dVEF to be negative here. @@ -417,8 +417,9 @@ def build_phase(self, aviary_options=None): # such that it occurs {pilot_reaction_time} seconds # before V1. phase.add_calc_expr( - f'v_to_go = velocity - ({VR_RATIO} * v_stall - dV1 - dVEF)', + 'v_to_go = velocity - (VR_ratio * v_stall - dV1 - dVEF)', velocity={'units': 'kn'}, + VR_ratio={'units': 'unitless'}, dV1={'units': 'kn'}, v_to_go={'units': 'kn'}, v_stall={'units': 'kn'}, @@ -439,6 +440,17 @@ def build_phase(self, aviary_options=None): units='kn', desc='Decision speed delta below rotation speed.', ) + elif terminal_condition == 'MAX_ALPHA': + # Propagate until velocity is the desired literal value. + phase.add_boundary_balance( + param='t_duration', + name=Dynamic.Vehicle.ANGLE_OF_ATTACK, + tgt_val=12.0, + lower=0.0001, + upper=1000, + eq_units='deg', + loc='final', + ) elif terminal_condition == 'LIFTOFF': # Propagate until velocity is the desired literal value. phase.add_boundary_balance( @@ -522,316 +534,3 @@ def _extra_ode_init_kwargs(self): 'pitch_control': self.user_options['pitch_control'], 'friction_key': self.user_options['friction_key'], } - - -# class BalancedFieldTrajectoryBuilder: -# """ -# Define a trajectory builder for detailed takeoff. - -# Identify, collect, and call the necessary phase builders to create a typical takeoff -# trajectory. -# """ - -# MappedPhase = namedtuple('MappedPhase', ('phase', 'phase_builder')) - -# default_name = 'detailed_takeoff' - -# def __init__(self, name=None): -# if name is None: -# name = self.default_name - -# self.name = name - -# self._brake_release_to_decision_speed = None - -# self._phases = {} -# self._traj = None - -# def get_phase_names(self): -# """Return a list of base names for available phases.""" -# keys = list(self._phases) - -# return keys - -# def get_phase(self, key) -> dm.Phase: -# """ -# Return the phase associated with the specified base name. - -# Raises -# ------ -# KeyError -# if the specified base name is not found -# """ -# mapped_phase = self._phases[key] - -# return mapped_phase.phase - -# def set_brake_release_to_decision_speed(self, phase_builder: PhaseBuilderBase): -# """ -# Assign a phase builder for the beginning of takeoff to the time when the pilot -# must choose either to liftoff or halt the aircraft. -# """ -# self._brake_release_to_decision_speed = phase_builder - -# # def set_decision_speed_to_rotate(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for the short distance between achieving decision speed -# # and beginning the rotation phase. -# # """ -# # self._decision_speed_to_rotate = phase_builder - -# # def set_rotate_to_liftoff(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for the short distance required to rotate the aircraft -# # to achieve liftoff. -# # """ -# # self._rotate_to_liftoff = phase_builder - -# # def set_liftoff_to_climb_gradient(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign the phase builder for the phase from liftoff until the required climb gradient is reached. - -# # Args: -# # phase_builder (PhaseBuilderBase): _description_ -# # """ -# # self._liftoff_to_climb_gradient = phase_builder - -# # def set_climb_gradient_to_obstacle(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign the phase builder for the phase from achievement of climb gradient until obstacle clearance. - -# # Args: -# # phase_builder (PhaseBuilderBase): _description_ -# # """ -# # self._climb_gradient_to_obstacle = phase_builder - -# # def set_liftoff_to_obstacle(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for the short period between liftoff and clearing the -# # required obstacle. -# # """ -# # self._liftoff_to_obstacle = phase_builder - -# # def set_obstacle_to_mic_p2(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for the fifth phase of takeoff, from clearing the required -# # obstacle to the p2 mic loation. This phase is required for acoustic calculations. -# # """ -# # self._obstacle_to_mic_p2 = phase_builder - -# # def set_mic_p2_to_engine_cutback(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for the sixth phase of takeoff, from the p2 mic location -# # to engine cutback. This phase is required for acoustic calculations. -# # """ -# # self._mic_p2_to_engine_cutback = phase_builder - -# # def set_engine_cutback(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for the seventh phase of takeoff, from start to -# # finish of engine cutback. This phase is required for acoustic calculations. -# # """ -# # self._engine_cutback = phase_builder - -# # def set_engine_cutback_to_mic_p1(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for the eighth phase of takeoff, engine cutback -# # to the P1 mic location. This phase is required for acoustic calculations. -# # """ -# # self._engine_cutback_to_mic_p1 = phase_builder - -# # def set_mic_p1_to_climb(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for the ninth phase of takeoff, from P1 mic -# # location to climb. This phase is required for acoustic calculations. -# # """ -# # self._mic_p1_to_climb = phase_builder - -# # def set_decision_speed_to_brake(self, phase_builder: PhaseBuilderBase): -# # """ -# # Assign a phase builder for delayed braking when the engine fails. - -# # Note, this phase is optional. It is only required if balanced field length -# # calculations are required. -# # """ -# # self._decision_speed_to_brake = phase_builder - -# # def set_brake_to_abort(self, phase_builder: PhaseBuilderBase, balanced_field_ref=8_000.0): -# # """ -# # Assign a phase builder for braking to fullstop after engine failure. - -# # Note, this phase is optional. It is only required if balanced field length -# # calculations are required. - -# # Parameters -# # ---------- -# # phase_builder : PhaseBuilderBase - -# # balanced_field_ref : float (8_000.0) -# # the ref value to use for the linkage constraint for the final range -# # between the liftoff-to-obstacle and the decision-speed-to-abort phases; - -# # Notes -# # ----- -# # The default value for `balanced_field_ref` is appropriate total takeoff distances -# # calculated in 'ft' for larger commercial passenger transports traveling in the -# # continental United States. International travel of similar aircraft may require a -# # larger value, while a smaller aircraft with a shorter range may require a smaller -# # value. -# # """ -# # self._brake_to_abort = phase_builder -# # self._balanced_field_ref = balanced_field_ref - -# def build_trajectory( -# self, *, aviary_options: AviaryValues, model: om.Group = None, traj: dm.Trajectory = None -# ) -> dm.Trajectory: -# """ -# Return a new trajectory for detailed takeoff analysis. - -# Call only after assigning phase builders for required phases. - -# Parameters -# ---------- -# aviary_options : AviaryValues -# collection of Aircraft/Mission specific options - -# model : openmdao.api.Group (None) -# the model handling trajectory parameter setup; if `None`, trajectory -# parameter setup will not be handled - -# traj : dymos.Trajectory (None) -# the trajectory to update; if `None`, a new trajetory will be updated and -# returned - -# Returns -# ------- -# the updated trajectory; if the specified trajectory is `None`, a new trajectory -# will be updated and returned - -# Notes -# ----- -# Do not modify this object or any of its referenced data between the call to -# `build_trajectory()` and the call to `apply_initial_guesses()`, or the behavior -# is undefined, no diagnostic required. -# """ -# if traj is None: -# traj = dm.Trajectory() - -# self._traj = traj - -# self._add_phases(aviary_options) -# self._link_phases() - -# if model is not None: -# phase_names = self.get_phase_names() - -# # This is a level 3 method that uses the default subsystems. -# # We need to create parameters for just the inputs we have. -# # They mostly come from the low-speed aero subsystem. - -# aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, LegacyCode('FLOPS')) - -# kwargs = {'method': 'low_speed'} - -# params = aero.get_parameters(aviary_options, **kwargs) - -# # takeoff introduces this one. -# params[Mission.Takeoff.LIFT_COEFFICIENT_MAX] = { -# 'shape': (1,), -# 'static_target': True, -# } - -# ext_params = {} -# for phase in self._phases.keys(): -# ext_params[phase] = params - -# setup_trajectory_params( -# model, traj, aviary_options, phase_names, external_parameters=ext_params -# ) - -# return traj - -# def apply_initial_guesses(self, prob: om.Problem, traj_name): -# """ -# Call `prob.set_val()` for states/parameters/etc. for each phase in this -# trajectory. - -# Call only after `build_trajectory()` and `prob.setup()`. - -# Returns -# ------- -# not_applied : dict[str, list[str]] -# for any phase with missing initial guesses that cannot be applied, a list of -# those missing initial guesses; if a given phase has no missing initial -# guesses, the returned mapping will not contain the name of that phase -# """ -# not_applied = {} -# phase_builder: PhaseBuilderBase = None - -# for phase, phase_builder in self._phases.values(): -# tmp = phase_builder.apply_initial_guesses(prob, traj_name, phase) - -# if tmp: -# not_applied[phase_builder.name] = tmp - -# return not_applied - -# def _add_phases(self, aviary_options: AviaryValues): -# self._phases = {} - -# self._add_phase(self._brake_release_to_decision_speed, aviary_options) - -# # self._add_phase(self._decision_speed_to_rotate, aviary_options) - -# # self._add_phase(self._rotate_to_liftoff, aviary_options) - -# # self._add_phase(self._liftoff_to_climb_gradient, aviary_options) - -# # self._add_phase(self._climb_gradient_to_obstacle, aviary_options) - -# # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 - -# # if obstacle_to_mic_p2 is not None: -# # self._add_phase(obstacle_to_mic_p2, aviary_options) - -# # self._add_phase(self._mic_p2_to_engine_cutback, aviary_options) - -# # self._add_phase(self._engine_cutback, aviary_options) - -# # self._add_phase(self._engine_cutback_to_mic_p1, aviary_options) - -# # self._add_phase(self._mic_p1_to_climb, aviary_options) - -# # decision_speed_to_brake = self._decision_speed_to_brake - -# # if decision_speed_to_brake is not None: -# # self._add_phase(decision_speed_to_brake, aviary_options) - -# # self._add_phase(self._brake_to_abort, aviary_options) - -# def _link_phases(self): -# traj: dm.Trajectory = self._traj - -# # brake_release_name = self._brake_release_to_decision_speed.name -# # decision_speed_name = self._decision_speed_to_rotate.name -# # rotate_name = self._rotate_to_liftoff.name -# # liftoff_name = self._liftoff_to_climb_gradient.name -# # climb_gradient_name = self._climb_gradient_to_obstacle.name - -# # traj.link_phases( -# # [brake_release_name, decision_speed_name, rotate_name, liftoff_name, climb_gradient_name], -# # vars=['time', 'distance', 'velocity', 'mass'], -# # connected=True, -# # ) - -# # traj.link_phases([rotate_name, liftoff_name], vars=['angle_of_attack',], connected=True) - -# # traj.link_phases([liftoff_name, climb_gradient_name], vars=['flight_path_angle', 'altitude'], connected=True) - -# def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): -# name = phase_builder.name -# phase = phase_builder.build_phase(aviary_options) - -# self._traj.add_phase(name, phase) - -# self._phases[name] = self.MappedPhase(phase, phase_builder) diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field2.py b/aviary/mission/flops_based/phases/test/test_balanced_field2.py index d6bed6057f..448e520033 100644 --- a/aviary/mission/flops_based/phases/test/test_balanced_field2.py +++ b/aviary/mission/flops_based/phases/test/test_balanced_field2.py @@ -60,209 +60,8 @@ def test_balanced_field_2(self): preprocess_options(aviary_options, engine_models=engines) prop_builder = av.CorePropulsionBuilder(engine_models=engines) - # # BRAKE RELEASE TO DECISION SPEED balanced_field_user_options = av.AviaryValues() - # balanced_field_user_options.set_val('max_duration', val=60.0, units='s') - # balanced_field_user_options.set_val('time_duration_ref', val=60.0, units='s') - # balanced_field_user_options.set_val('distance_max', val=7500.0, units='ft') - # balanced_field_user_options.set_val('max_velocity', val=167.85, units='kn') - # balanced_field_user_options.set_val('terminal_condition', val='VEF') - - # tobl_nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) - # tobl_nl_solver.linesearch = om.BoundsEnforceLS() - - # balanced_field_user_options.set_val('nonlinear_solver', val=tobl_nl_solver) - # balanced_field_user_options.set_val('linear_solver', val=om.DirectSolver()) - - # takeoff_v1_to_vr_initial_guesses = av.AviaryValues() - - # takeoff_v1_to_vr_initial_guesses.set_val('time', [0.0, 30.0], 's') - # takeoff_v1_to_vr_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') - # takeoff_v1_to_vr_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') - - # gross_mass_units = 'lbm' - # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) - # takeoff_v1_to_vr_initial_guesses.set_val('mass', gross_mass, gross_mass_units) - - # takeoff_v1_to_vr_initial_guesses.set_val('throttle', 1.0) - # takeoff_v1_to_vr_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') - - # takeoff_brake_release_to_decision_speed_builder = av.BalancedFieldPhaseBuilder( - # 'takeoff_brake_release_to_decision_speed', - # core_subsystems=[aero_builder, prop_builder], - # subsystem_options=takeoff_subsystem_options, - # user_options=balanced_field_user_options, - # initial_guesses=takeoff_v1_to_vr_initial_guesses, - # ) - - # # DECISION SPEED TO ROTATION - - # takeoff_v1_to_vr_user_options = av.AviaryValues() - - # takeoff_v1_to_vr_user_options.set_val('max_duration', val=90.0, units='s') - # takeoff_v1_to_vr_user_options.set_val('time_duration_ref', val=60.0, units='s') - # takeoff_v1_to_vr_user_options.set_val('distance_max', val=15000.0, units='ft') - # takeoff_v1_to_vr_user_options.set_val('max_velocity', val=167.85, units='kn') - # takeoff_v1_to_vr_user_options.set_val('terminal_condition', val='VR') - - # nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) - # nl_solver.linesearch = om.BoundsEnforceLS() - - # takeoff_v1_to_vr_user_options.set_val('nonlinear_solver', val=nl_solver) - # takeoff_v1_to_vr_user_options.set_val('linear_solver', val=om.DirectSolver()) - - # takeoff_v1_to_vr_initial_guesses = av.AviaryValues() - - # takeoff_v1_to_vr_initial_guesses.set_val('time', [30.0, 1.0], 's') - # takeoff_v1_to_vr_initial_guesses.set_val('distance', [4100.0, 4800.0], 'ft') - # takeoff_v1_to_vr_initial_guesses.set_val('velocity', [70., 150.0], 'kn') - - # gross_mass_units = 'lbm' - # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) - # takeoff_v1_to_vr_initial_guesses.set_val('mass', gross_mass, gross_mass_units) - - # takeoff_v1_to_vr_initial_guesses.set_val('throttle', 1.0) - # takeoff_v1_to_vr_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') - - # takeoff_decision_speed_to_rotate_builder = av.BalancedFieldPhaseBuilder( - # 'takeoff_decision_speed_to_rotate', - # core_subsystems=[aero_builder, prop_builder], - # subsystem_options=takeoff_subsystem_options, - # user_options=takeoff_v1_to_vr_user_options, - # initial_guesses=takeoff_v1_to_vr_initial_guesses, - # ) - - # # ROTATION TO LIFTOFF - - # vr_to_liftoff_user_options = av.AviaryValues() - - # vr_to_liftoff_user_options.set_val('max_duration', val=90.0, units='s') - # vr_to_liftoff_user_options.set_val('time_duration_ref', val=60.0, units='s') - # vr_to_liftoff_user_options.set_val('distance_max', val=15000.0, units='ft') - # vr_to_liftoff_user_options.set_val('max_velocity', val=167.85, units='kn') - # vr_to_liftoff_user_options.set_val('pitch_control', val='alpha_rate_fixed', units='unitless') - # vr_to_liftoff_user_options.set_val('terminal_condition', val='LIFTOFF') - - # nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) - # nl_solver.linesearch = om.BoundsEnforceLS() - - # vr_to_liftoff_user_options.set_val('nonlinear_solver', val=nl_solver) - # vr_to_liftoff_user_options.set_val('linear_solver', val=om.DirectSolver()) - - # vr_to_liftoff_initial_guesses = av.AviaryValues() - - # vr_to_liftoff_initial_guesses.set_val('time', [31.0, 5.0], 's') - # vr_to_liftoff_initial_guesses.set_val('distance', [4800.0, 5500.0], 'ft') - # vr_to_liftoff_initial_guesses.set_val('velocity', [100., 120.0], 'kn') - # vr_to_liftoff_initial_guesses.set_val('angle_of_attack', [0.0, 5.0], 'deg') - - # gross_mass_units = 'lbm' - # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) - # vr_to_liftoff_initial_guesses.set_val('mass', gross_mass, gross_mass_units) - - # vr_to_liftoff_initial_guesses.set_val('throttle', 1.0) - # vr_to_liftoff_initial_guesses.set_val('angle_of_attack_rate', 2.0, units='deg/s') - - # vr_to_liftoff_builder = av.BalancedFieldPhaseBuilder( - # 'takeoff_rotate_to_liftoff', - # core_subsystems=[aero_builder, prop_builder], - # subsystem_options=takeoff_subsystem_options, - # user_options=vr_to_liftoff_user_options, - # initial_guesses=vr_to_liftoff_initial_guesses, - # ) - - # # LIFTOFF TO CLIMB GRADIENT - - # liftoff_to_climb_gradient_user_options = av.AviaryValues() - - # liftoff_to_climb_gradient_user_options.set_val('max_duration', val=90.0, units='s') - # liftoff_to_climb_gradient_user_options.set_val('time_duration_ref', val=60.0, units='s') - # liftoff_to_climb_gradient_user_options.set_val('distance_max', val=15000.0, units='ft') - # liftoff_to_climb_gradient_user_options.set_val('max_velocity', val=167.85, units='kn') - # liftoff_to_climb_gradient_user_options.set_val('pitch_control', val='alpha_rate_fixed', units='unitless') - # liftoff_to_climb_gradient_user_options.set_val('climbing', val=True, units='unitless') - # liftoff_to_climb_gradient_user_options.set_val('terminal_condition', val='CLIMB_GRADIENT') - - # nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) - # nl_solver.linesearch = om.BoundsEnforceLS() - - # liftoff_to_climb_gradient_user_options.set_val('nonlinear_solver', val=nl_solver) - # liftoff_to_climb_gradient_user_options.set_val('linear_solver', val=om.DirectSolver()) - - # liftoff_to_climb_gradient_initial_guesses = av.AviaryValues() - - # liftoff_to_climb_gradient_initial_guesses.set_val('time', [35.0, 1.0], 's') - # liftoff_to_climb_gradient_initial_guesses.set_val('distance', [5000.0, 6000.0], 'ft') - # liftoff_to_climb_gradient_initial_guesses.set_val('velocity', [120., 100.0], 'kn') - # liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack', [5.0, 10.0], 'deg') - # liftoff_to_climb_gradient_initial_guesses.set_val('flight_path_angle', [0.0, 2.0], 'deg') - - # gross_mass_units = 'lbm' - # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) - # liftoff_to_climb_gradient_initial_guesses.set_val('mass', gross_mass, gross_mass_units) - - # liftoff_to_climb_gradient_initial_guesses.set_val('throttle', 1.0) - # liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack_rate', 2.0, units='deg/s') - - # liftoff_to_climb_gradient_builder = av.BalancedFieldPhaseBuilder( - # 'takeoff_liftoff_to_climb_gradient', - # core_subsystems=[aero_builder, prop_builder], - # subsystem_options=takeoff_subsystem_options, - # user_options=liftoff_to_climb_gradient_user_options, - # initial_guesses=liftoff_to_climb_gradient_initial_guesses, - # ) - - # # CLIMB GRADIENT TO OBSTACLE - - # climb_gradient_to_obstacle_user_options = av.AviaryValues() - - # climb_gradient_to_obstacle_user_options.set_val('max_duration', val=90.0, units='s') - # climb_gradient_to_obstacle_user_options.set_val('time_duration_ref', val=60.0, units='s') - # climb_gradient_to_obstacle_user_options.set_val('distance_max', val=15000.0, units='ft') - # climb_gradient_to_obstacle_user_options.set_val('max_velocity', val=200., units='kn') - # climb_gradient_to_obstacle_user_options.set_val('pitch_control', val='gamma_fixed', units='unitless') - # climb_gradient_to_obstacle_user_options.set_val('climbing', val=True, units='unitless') - # climb_gradient_to_obstacle_user_options.set_val('terminal_condition', val='OBSTACLE') - - # nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=False) - # nl_solver.linesearch = om.BoundsEnforceLS() - - # climb_gradient_to_obstacle_user_options.set_val('nonlinear_solver', val=nl_solver) - # climb_gradient_to_obstacle_user_options.set_val('linear_solver', val=om.DirectSolver()) - - # climb_gradient_to_obstacle_initial_guesses = av.AviaryValues() - - # climb_gradient_to_obstacle_initial_guesses.set_val('time', [35.0, 3.0], 's') - # climb_gradient_to_obstacle_initial_guesses.set_val('distance', [5500.0, 5800.0], 'ft') - # climb_gradient_to_obstacle_initial_guesses.set_val('velocity', [120., 120.0], 'kn') - # climb_gradient_to_obstacle_initial_guesses.set_val('flight_path_angle', [2.0, 2.0], 'deg') - - # gross_mass_units = 'lbm' - # gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) - # climb_gradient_to_obstacle_initial_guesses.set_val('mass', gross_mass, gross_mass_units) - - # climb_gradient_to_obstacle_initial_guesses.set_val('throttle', 1.0) - - # climb_gradient_to_obstacle_builder = av.BalancedFieldPhaseBuilder( - # 'takeoff_climb_gradient_to_obstacle', - # core_subsystems=[aero_builder, prop_builder], - # subsystem_options=takeoff_subsystem_options, - # user_options=climb_gradient_to_obstacle_user_options, - # initial_guesses=climb_gradient_to_obstacle_initial_guesses, - # ) - - # from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( - # takeoff_decision_speed_builder, - # takeoff_engine_cutback_builder, - # takeoff_engine_cutback_to_mic_p1_builder, - # takeoff_liftoff_builder, - # takeoff_liftoff_user_options, - # takeoff_mic_p1_to_climb_builder, - # takeoff_mic_p2_builder, - # takeoff_mic_p2_to_engine_cutback_builder, - # takeoff_rotate_builder, - # ) from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems from aviary.variable_info.functions import setup_model_options @@ -309,69 +108,13 @@ def test_balanced_field_2(self): takeoff_trajectory_builder.apply_initial_guesses(test_problem, 'traj') - # test_problem.set_val(Mission.Takeoff.DECISION_SPEED_INCREMENT, 20, units='kn') - test_problem.final_setup() - # test_problem.model.list_vars(print_arrays=True) - - # takeoff.model.run_apply_nonlinear() - - # takeoff.model.list_vars(print_arrays=True) import dymos dymos.run_problem(test_problem, run_driver=False, make_plots=True) print(test_problem.get_reports_dir()) - # test_problem.model.list_vars() - - # takeoff.model.traj.phases.takeoff_climb_gradient_to_obstacle.ode_iter_group.segment_prop_group.ode_all.takeoff_eom.list_vars(print_arrays=True) - - # vars = takeoff.model.list_vars(print_arrays=True, units=True, prom_name=True, return_format='dict', out_stream=None) - - # vars = {meta['prom_name']: meta for meta in vars.values()} - - # systems = set([path.rsplit('.', 1)[0] for path in vars.keys()]) - - # print('\n'.join(vars.keys())) - - # for sys in takeoff.model.system_iter(include_self=True, recurse=True): - # sys.list_vars(prom_name=True, units=True) - - - # from textual.app import App, ComposeResult - # from textual.widgets import DataTable, Collapsible - - - # class TableApp(App): - - # def __init__(self, systems): - # self._systems = systems - - # def compose(self) -> ComposeResult: - # for sys, vars in self._systems.items(): - # with Collapsible(title=sys.pathname): - # yield DataTable() - - # def on_mount(self) -> None: - # table = self.query_one(DataTable) - # table.add_columns('promoted name', 'units') - # print(table) - # # table.add_rows(ROWS[1:]) - - # systems = {} - - # for sys in takeoff.model.system_iter(include_self=True, recurse=True): - # vars = sys.list_vars(prom_name=True, units=True, out_stream=None) - # systems[sys] = vars - - # app = TableApp(systems) - # app.run() - - # takeoff.check_partials(compact_print=True) - # takeoff.model.run_apply_nonlinear() - # takeoff.model.list_vars(print_arrays=True) - # om.n2(takeoff.model) if __name__ == '__main__': diff --git a/aviary/mission/initial_guess_builders.py b/aviary/mission/initial_guess_builders.py index 686404e58d..e52dc0a684 100644 --- a/aviary/mission/initial_guess_builders.py +++ b/aviary/mission/initial_guess_builders.py @@ -45,7 +45,6 @@ def __init__(self, key): def apply_initial_guess( self, prob: om.Problem, traj_name, phase: dm.Phase, phase_name, val, units ): - if self.key in phase.state_options: phase.set_state_val(self.key, val, units=units) elif self.key in phase.control_options: @@ -55,8 +54,9 @@ def apply_initial_guess( elif self.key == phase.time_options['name']: prob.set_integ_var_val(initial=val[0], duration=val[1], units=units) else: - raise ValueError(f'{phase.msginfo} Attempting to apply initial guess for {self.key}.\n' - 'Not find in the states, control, parameters, or integration variable of the phase.') + pass + # raise ValueError(f'{phase.msginfo} Attempting to apply initial guess for {self.key}.\n' + # 'Not find in the states, control, parameters, or integration variable of the phase.') # def _get_complete_key(self, traj_name, phase_name): # """Compose the complete key for setting the initial guess.""" From 2a856d24c62adac5fafce9eefdb3396c0643a495 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 19 Sep 2025 18:01:59 -0400 Subject: [PATCH 09/22] first pass at putting bal field in a subsys --- .../balanced_field/__init__.py | 0 .../balanced_field/balanced_field_builder.py | 20 +++ .../balanced_field/balanced_field_submodel.py | 137 ++++++++++++++++++ .../balanced_field/run_balanced_field.py | 46 ++++++ .../phases/test/test_balanced_field2.py | 12 +- .../height_energy_problem_configurator.py | 5 +- 6 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 aviary/examples/external_subsystems/balanced_field/__init__.py create mode 100644 aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py create mode 100644 aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py create mode 100644 aviary/examples/external_subsystems/balanced_field/run_balanced_field.py diff --git a/aviary/examples/external_subsystems/balanced_field/__init__.py b/aviary/examples/external_subsystems/balanced_field/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py new file mode 100644 index 0000000000..ca041d5764 --- /dev/null +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py @@ -0,0 +1,20 @@ +import openmdao.api as om + +import aviary.api as av +from aviary.api import Mission +from aviary.examples.external_subsystems.balanced_field.balanced_field_submodel import ( + create_balance_field_subprob, +) +from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase + + +class BalancedFieldBuilder(SubsystemBuilderBase): + + def build_post_mission( + self, aviary_inputs, phase_info=None, phase_mission_bus_lengths=None, **kwargs + ): + return create_balance_field_subprob(aviary_inputs) + + +if __name__ == '__main__': + unittest.main() diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py new file mode 100644 index 0000000000..ddd3b926a3 --- /dev/null +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py @@ -0,0 +1,137 @@ +""" +Group containing a submodel component with detailed landing. +""" +from copy import copy +import warnings + +import openmdao.api as om + +import aviary.api as av +from aviary.utils.preprocessors import preprocess_options +from aviary.variable_info.enums import EquationsOfMotion +from aviary.variable_info.variables import Aircraft, Mission, Settings + + +def create_balance_field_subprob(aviary_inputs, use_spoiler=False): + + subprob = create_prob(aviary_inputs, use_spoiler) + + comp = AviarySubmodelComp( + problem=subprob, + inputs=[ + Aircraft.Wing.AREA, + Aircraft.Wing.ASPECT_RATIO, + Aircraft.Wing.HEIGHT, + Aircraft.Wing.SPAN, + Mission.Takeoff.DRAG_COEFFICIENT_MIN, + Mission.Takeoff.LIFT_COEFFICIENT_MAX, + #('traj.takeoff_brake_release_to_engine_failure.states:mass', Mission.Summary.GROSS_MASS), + ], + outputs=[ + ('traj.takeoff_climb_gradient_to_obstacle.final_states:distance', 'distance_obstacle'), + ] + ) + + return comp + + +def create_prob(aviary_inputs, use_spoiler=False): + """ + Return a problem + """ + aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS + ) + + # fmt: off + takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } + } + # fmt: off + + # when using spoilers, add a few more options + if use_spoiler: + + spoiler_drag = aviary_inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT) + spoiler_lift = aviary_inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT) + + takeoff_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': spoiler_drag, + 'spoiler_lift_coefficient': spoiler_lift, + } + } + + # We also need propulsion analysis for takeoff and landing. No additional configuration + # is needed for this builder + engines = [av.build_engine_deck(aviary_inputs)] + # Note that the aviary_inputs is already in a pre-processed state. + prop_builder = av.CorePropulsionBuilder(engine_models=engines) + + balanced_field_user_options = av.AviaryValues() + + from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems + from aviary.variable_info.functions import setup_model_options + + dto_build = av.BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) + + subprob = om.Problem() + + #ivc = om.IndepVarComp(Mission.Summary.GROSS_MASS, val=1.0, units='lbm') + #subprob.model.add_subsystem('takeoff_mass_ivc', ivc, promotes=['*']) + #subprob.model.connect( + # Mission.Summary.GROSS_MASS, + # "traj.takeoff_brake_release_to_engine_failure.states:mass", + # flat_src_indices=[0], + #) + + # Instantiate the trajectory and add the phases + traj = dto_build.build_trajectory(aviary_options=aviary_inputs, model=subprob.model) + + setup_model_options(subprob, aviary_inputs) + + # This is kind of janky, but we need these after the subproblem sets it up. + subprob.aviary_inputs = aviary_inputs + subprob.dto_build = dto_build + + return subprob + + +class AviarySubmodelComp(om.SubmodelComp): + """ + We need to subclass so that we can set the initial conditions. + """ + def setup(self): + # suppress warnings: + # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' + with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + super().setup() + + + sub = self._subprob + av.set_aviary_initial_values(sub, sub.aviary_inputs) + sub.dto_build.apply_initial_guesses(sub, 'traj') + + sub.final_setup() diff --git a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py new file mode 100644 index 0000000000..4a4f0076b5 --- /dev/null +++ b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py @@ -0,0 +1,46 @@ +""" +Balanced Field problem as an external subsystem in post-mission. +""" +from copy import deepcopy + +import numpy as np + +import openmdao.api as om + +from aviary.examples.external_subsystems.balanced_field.balanced_field_builder import BalancedFieldBuilder +from aviary.interface.methods_for_level2 import AviaryProblem +from aviary.models.aircraft.advanced_single_aisle.phase_info import phase_info +from aviary.variable_info.variables import Mission + +local_phase_info = deepcopy(phase_info) + +local_phase_info['post_mission']['external_subsystems'] = [BalancedFieldBuilder()] + +prob = AviaryProblem() + +prob.load_inputs( + 'models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv', + local_phase_info, +) + +# A few values that aren't in the csv file. +prob.aviary_inputs.set_val(Mission.Takeoff.FUEL_SIMPLE, 577.0, units='lbm') +prob.aviary_inputs.set_val(Mission.Takeoff.LIFT_OVER_DRAG, 17.35, units='unitless') +prob.aviary_inputs.set_val(Mission.Design.THRUST_TAKEOFF_PER_ENG, 24555.5, units='lbf') + +# initial guess for mass +prob.aviary_inputs.set_val(Mission.Design.GROSS_MASS, 135000.0, units='lbm') + +prob.check_and_preprocess_inputs() + +prob.build_model() +prob.add_driver('SNOPT', max_iter=50, verbosity=1) + +prob.add_design_variables() + +prob.add_objective() + +prob.setup() +prob.run_aviary_problem('dymos_solution.db') + + diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field2.py b/aviary/mission/flops_based/phases/test/test_balanced_field2.py index 448e520033..3899fd2b67 100644 --- a/aviary/mission/flops_based/phases/test/test_balanced_field2.py +++ b/aviary/mission/flops_based/phases/test/test_balanced_field2.py @@ -1,11 +1,11 @@ -import aviary.api as av -import warnings import unittest +import warnings + import dymos as dm import openmdao.api as om -from aviary.api import Dynamic, Mission -from aviary.core.aviary_group import AviaryGroup -from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData + +import aviary.api as av +from aviary.api import Mission from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import inputs from aviary.utils.preprocessors import preprocess_options @@ -70,7 +70,7 @@ def test_balanced_field_2(self): subsystem_options=takeoff_subsystem_options, user_options=balanced_field_user_options) - test_problem = om.Problem() # model=AviaryGroup(aviary_options=aviary_options, aviary_metadata=BaseMetaData)) + test_problem = om.Problem() # default subsystems default_premission_subsystems = get_default_premission_subsystems('FLOPS', engines) diff --git a/aviary/mission/height_energy_problem_configurator.py b/aviary/mission/height_energy_problem_configurator.py index 7599f12bb5..acf3ecd04e 100644 --- a/aviary/mission/height_energy_problem_configurator.py +++ b/aviary/mission/height_energy_problem_configurator.py @@ -451,7 +451,10 @@ def _add_post_mission_takeoff_systems(self, aviary_group): if phase_options.get('mach_optimize', False): # Create an ExecComp to compute the difference in mach mach_diff_comp = om.ExecComp( - 'mach_resid_for_connecting_takeoff = final_mach - initial_mach' + 'mach_resid_for_connecting_takeoff = final_mach - initial_mach', + mach_resid_for_connecting_takeoff={'units': 'unitless'}, + final_mach={'units': 'unitless'}, + initial_mach={'units': 'unitless'}, ) aviary_group.add_subsystem('mach_diff_comp', mach_diff_comp) From ce58c5dc5f1c6305f8d2c9fcb0ab02001fde2c9e Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Thu, 25 Sep 2025 09:16:10 -0400 Subject: [PATCH 10/22] Successful connection to mass --- .../balanced_field/balanced_field_submodel.py | 2 +- .../external_subsystems/balanced_field/run_balanced_field.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py index ddd3b926a3..5c8ec5c0a8 100644 --- a/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py @@ -25,7 +25,7 @@ def create_balance_field_subprob(aviary_inputs, use_spoiler=False): Aircraft.Wing.SPAN, Mission.Takeoff.DRAG_COEFFICIENT_MIN, Mission.Takeoff.LIFT_COEFFICIENT_MAX, - #('traj.takeoff_brake_release_to_engine_failure.states:mass', Mission.Summary.GROSS_MASS), + ('traj.takeoff_brake_release_to_engine_failure.initial_states:mass', Mission.Summary.GROSS_MASS), ], outputs=[ ('traj.takeoff_climb_gradient_to_obstacle.final_states:distance', 'distance_obstacle'), diff --git a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py index 4a4f0076b5..cb840d4f89 100644 --- a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py +++ b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py @@ -41,6 +41,6 @@ prob.add_objective() prob.setup() -prob.run_aviary_problem('dymos_solution.db') - +# TODO: N3CC optimization does not return success. +prob.run_aviary_problem('dymos_solution.db') From 4cf92e34cf502c0c6d4f9c007eb5124dd850a1eb Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Thu, 25 Sep 2025 12:56:47 -0400 Subject: [PATCH 11/22] a little cleanup --- .../balanced_field/balanced_field_submodel.py | 3 --- aviary/mission/balanced_field_traj_builder.py | 2 +- .../mission/flops_based/phases/balanced_field_trajectory.py | 6 ------ 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py index 5c8ec5c0a8..6448ea7f4f 100644 --- a/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py @@ -1,14 +1,11 @@ """ Group containing a submodel component with detailed landing. """ -from copy import copy import warnings import openmdao.api as om import aviary.api as av -from aviary.utils.preprocessors import preprocess_options -from aviary.variable_info.enums import EquationsOfMotion from aviary.variable_info.variables import Aircraft, Mission, Settings diff --git a/aviary/mission/balanced_field_traj_builder.py b/aviary/mission/balanced_field_traj_builder.py index 6a2678f2f6..94cc69058b 100644 --- a/aviary/mission/balanced_field_traj_builder.py +++ b/aviary/mission/balanced_field_traj_builder.py @@ -16,7 +16,7 @@ from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE from aviary.mission.flops_based.phases.balanced_field_trajectory import BalancedFieldPhaseBuilder from aviary.mission.initial_guess_builders import InitialGuess -from aviary.utils.aviary_values import AviaryValues, get_keys +from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.variable_meta_data import _MetaData from aviary.variable_info.variables import Dynamic from aviary.variable_info.functions import setup_trajectory_params diff --git a/aviary/mission/flops_based/phases/balanced_field_trajectory.py b/aviary/mission/flops_based/phases/balanced_field_trajectory.py index 1b25938eaf..0c9a90a40f 100644 --- a/aviary/mission/flops_based/phases/balanced_field_trajectory.py +++ b/aviary/mission/flops_based/phases/balanced_field_trajectory.py @@ -44,7 +44,6 @@ from collections import namedtuple import dymos as dm -import openmdao.api as om from openmdao.solvers.solver import NonlinearSolver, LinearSolver from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE @@ -52,16 +51,11 @@ InitialGuessControl, InitialGuessIntegrationVariable, InitialGuessParameter, - InitialGuessPolynomialControl, InitialGuessState, ) from aviary.mission.phase_builder_base import PhaseBuilderBase -from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.utils.aviary_options_dict import AviaryOptionsDictionary from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.enums import LegacyCode -from aviary.variable_info.functions import setup_trajectory_params -from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData from aviary.variable_info.variables import Aircraft, Dynamic, Mission From 10e1babaafef5f64190898a18fe1862db3f78b89 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Thu, 25 Sep 2025 16:02:56 -0400 Subject: [PATCH 12/22] leanup --- aviary/mission/balanced_field_traj_builder.py | 288 +----------------- 1 file changed, 1 insertion(+), 287 deletions(-) diff --git a/aviary/mission/balanced_field_traj_builder.py b/aviary/mission/balanced_field_traj_builder.py index 94cc69058b..6db0f312d5 100644 --- a/aviary/mission/balanced_field_traj_builder.py +++ b/aviary/mission/balanced_field_traj_builder.py @@ -148,7 +148,6 @@ def __init__( initial_guesses = AviaryValues() self.initial_guesses = initial_guesses - self.validate_initial_guesses() self.ode_class = self.default_ode_class self.num_nodes = num_nodes @@ -198,9 +197,6 @@ def build_trajectory( `build_trajectory()` and the call to `apply_initial_guesses()`, or the behavior is undefined, no diagnostic required. """ - # ode_class = self.ode_class - - # transcription = dm.PicardShooting(nodes_per_seg=10) if traj is None: self._traj = traj = dm.Trajectory(parallel_phases=False) @@ -213,18 +209,6 @@ def build_trajectory( if aviary_options is None: aviary_options = AviaryValues() - # kwargs = self._extra_ode_init_kwargs() - - # kwargs = {'aviary_options': aviary_options, **kwargs} - - # # if subsystem_options is not None: - # # kwargs['subsystem_options'] = subsystem_options - - # kwargs['core_subsystems'] = self.core_subsystems - # kwargs['external_subsystems'] = self.external_subsystems - - # Add all phase builders here. - common_user_options = AviaryValues() common_user_options.set_val('max_duration', val=100.0, units='s') common_user_options.set_val('time_duration_ref', val=60.0, units='s') @@ -240,7 +224,6 @@ def build_trajectory( common_user_options.set_val('linear_solver', val=om.DirectSolver()) common_initial_guesses = AviaryValues() - common_initial_guesses.set_val('time', [0.0, 30.0], 's') common_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') common_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') @@ -527,17 +510,10 @@ def build_trajectory( # We need to create parameters for just the inputs we have. # They mostly come from the low-speed aero subsystem. - # aero = CoreAerodynamicsBuilder('core_aerodynamics', BaseMetaData, LegacyCode('FLOPS')) - - # for cs in self.core_subsystems: - # print(cs.name) - # print(cs.get_parameters(**kwargs)) - # exit(0) - kwargs = {'method': 'low_speed'} # TODO: Why is get_parameters different for different subsystems? - # Do with without blindly indexing. + # Do without blindly indexing. aero_builder = self.core_subsystems[0] params = aero_builder.get_parameters(aviary_options, **kwargs) @@ -558,25 +534,6 @@ def build_trajectory( return self._traj - def validate_initial_guesses(self): - """ - Raise TypeError if an unsupported initial guess is found. - - Users can call this method when updating initial guesses after initialization. - """ - initial_guesses = self.initial_guesses - - # if not initial_guesses: - # return # acceptable - - # meta_data = self._initial_guesses_meta_data_ - - # for key in get_keys(initial_guesses): - # if key not in meta_data: - # raise TypeError( - # f'{self.__class__.__name__}: {self.name}: unsupported initial guess: {key}' - # ) - def apply_initial_guesses(self, prob: om.Problem, traj_name): # , phase: dm.Phase): """Apply any stored initial guesses; return a list of guesses not applied.""" not_applied = {} @@ -594,249 +551,6 @@ def _extra_ode_init_kwargs(self): """Return extra kwargs required for initializing the ODE.""" return {} - def to_phase_info(self): - """ - Return the stored settings as phase info. - - Returns - ------- - tuple - name : str - object label - phase_info : dict - stored settings - """ - subsystem_options = self.subsystem_options # TODO: aero info? - user_options = self.user_options.to_phase_info() - initial_guesses = dict(self.initial_guesses) - - # TODO some of these may be purely programming API hooks, rather than for use - # with phase info - # - ode_class - # - transcription - # - external_subsystems - # - meta_data - - phase_info = dict( - subsystem_options=subsystem_options, - user_options=user_options, - initial_guesses=initial_guesses, - ) - - return (self.name, phase_info) - - @classmethod - def from_phase_info( - cls, name, phase_info: dict, core_subsystems=None, meta_data=None, transcription=None - ): - """ - Return a new phase builder based on the specified phase info. - - Note, calling code is responsible for matching phase info to the correct phase - builder type, or the behavior is undefined. - - Parameters - ---------- - name : str - object label - phase_info : dict - stored settings - """ - # loop over user_options dict entries - # if the value is not a tuple, wrap it in a tuple with the second entry of 'unitless' - for key, value in phase_info['user_options'].items(): - if not isinstance(value, tuple): - phase_info['user_options'][key] = (value, 'unitless') - - subsystem_options = phase_info.get('subsystem_options', {}) # TODO: aero info? - user_options = phase_info.get('user_options', ()) - initial_guesses = AviaryValues(phase_info.get('initial_guesses', ())) - external_subsystems = phase_info.get('external_subsystems', []) - # TODO core subsystems in phase info? - - # TODO some of these may be purely programming API hooks, rather than for use - # with phase info - # - ode_class - # - transcription - # - external_subsystems - # - meta_data - - phase_builder = cls( - name, - subsystem_options=subsystem_options, - user_options=user_options, - initial_guesses=initial_guesses, - meta_data=meta_data, - core_subsystems=core_subsystems, - external_subsystems=external_subsystems, - transcription=transcription, - ) - - return phase_builder - - @classmethod - def _add_initial_guess_meta_data(cls, initial_guess: InitialGuess, desc=None): - """ - Update supported initial guesses with a new item. - - Raises - ------ - ValueError - if a repeat initial guess is found - """ - meta_data = cls._initial_guesses_meta_data_ - name = initial_guess.key - - meta_data[name] = dict(apply_initial_guess=initial_guess.apply_initial_guess, desc=desc) - - def _add_user_defined_constraints(self, phase, constraints): - """Add each constraint and its corresponding arguments to the phase.""" - for constraint_name, kwargs in constraints.items(): - if kwargs['type'] == 'boundary': - kwargs.pop('type') - - if 'target' in kwargs: - # Support for constraint aliases. - target = kwargs.pop('target') - kwargs['constraint_name'] = constraint_name - phase.add_boundary_constraint(target, **kwargs) - else: - phase.add_boundary_constraint(constraint_name, **kwargs) - - elif kwargs['type'] == 'path': - kwargs.pop('type') - phase.add_path_constraint(constraint_name, **kwargs) - - def add_state(self, name, target, rate_source): - """ - Add a state to this phase using the options in the phase_info. - - Parameters - ---------- - name : str - The name of this state in the phase_info options. - target : str - State promoted variable path to the ODE. - rate_source : str - Source of the state rate in the ODE. - """ - options = self.user_options - - initial, _ = options[f'{name}_initial'] - final, _ = options[f'{name}_final'] - bounds, units = options[f'{name}_bounds'] - ref, _ = options[f'{name}_ref'] - ref0, _ = options[f'{name}_ref0'] - defect_ref, _ = options[f'{name}_defect_ref'] - solve_segments = options[f'{name}_solve_segments'] - - # If a value is specified for the starting node, then fix_initial is True. - # Otherwise, input_initial is True. - # The problem configurator may change input_initial to False requested or necessary, (e.g., - # for parallel phases in MPI.) - - self.phase.add_state( - target, - fix_initial=initial is not None, - input_initial=False, - lower=bounds[0], - upper=bounds[1], - units=units, - rate_source=rate_source, - ref=ref, - ref0=ref0, - defect_ref=defect_ref, - solve_segments='forward' if solve_segments else None, - ) - - if final is not None: - constraint_ref, _ = options[f'{name}_constraint_ref'] - if constraint_ref is None: - # If unspecified, final is a good value for it. - constraint_ref = final - self.phase.add_boundary_constraint( - target, - loc='final', - equals=final, - units=units, - ref=final, - ) - - def add_control( - self, name, target, rate_targets=None, rate2_targets=None, add_constraints=True - ): - """ - Add a control to this phase using the options in the phase-info. - - Parameters - ---------- - name : str - The name of this control in the phase_info options. - target : str - Control promoted variable path to the ODE. - rate_source : list of str - List of rate targets for this control. - rate2_targets : Sequence of str or None - (Optional) The parameter in the ODE to which the control 2nd derivative is connected. - add_constraints : bool - When True, add constraints on any declared initial and final values if this control is - being optimized. Default is True. - """ - options = self.user_options - phase = self.phase - - initial, _ = options[f'{name}_initial'] - final, _ = options[f'{name}_final'] - bounds, units = options[f'{name}_bounds'] - ref, _ = options[f'{name}_ref'] - ref0, _ = options[f'{name}_ref0'] - polynomial_order = options[f'{name}_polynomial_order'] - opt = options[f'{name}_optimize'] - - if ref == 1.0: - # This has not been moved from default, so find a good value. - candidates = [x for x in (bounds[0], bounds[1], initial, final) if x is not None] - if len(candidates) > 0: - ref = np.max(np.abs(np.array(candidates))) - - extra_options = {} - if polynomial_order is not None: - extra_options['control_type'] = 'polynomial' - extra_options['order'] = polynomial_order - - if opt is True: - extra_options['lower'] = bounds[0] - extra_options['upper'] = bounds[1] - extra_options['ref'] = ref - extra_options['ref0'] = ref0 - - if units not in ['unitless', None]: - extra_options['units'] = units - - if rate_targets is not None: - extra_options['rate_targets'] = rate_targets - - if rate2_targets is not None: - extra_options['rate2_targets'] = rate2_targets - - phase.add_control(target, targets=target, opt=opt, **extra_options) - - # Add timeseries for any control. - phase.add_timeseries_output(target) - - if not add_constraints: - return - - # Add an initial constraint. - if opt and initial is not None: - phase.add_boundary_constraint( - target, loc='initial', equals=initial, units=units, ref=ref - ) - - # Add a final constraint. - if opt and final is not None: - phase.add_boundary_constraint(target, loc='final', equals=final, units=units, ref=ref) - def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): name = phase_builder.name phase = phase_builder.build_phase(aviary_options) From 60e05ff9cfc1723e174002378c0e42631520a325 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Mon, 6 Oct 2025 12:41:15 -0400 Subject: [PATCH 13/22] Working on supporting post-mission core subsystems. --- aviary/api.py | 2 +- aviary/core/aviary_group.py | 2 +- aviary/core/post_mission_group.py | 11 +++++++++++ aviary/core/pre_mission_group.py | 11 +++++++++++ .../flops_based/test/test_computed_aero_group.py | 16 ++++++++-------- .../flops_based/test/test_tabular_aero_group.py | 2 +- .../{premission.py => core_premission.py} | 14 ++++++-------- .../geometry/gasp_based/test/test_override.py | 2 +- .../performance/performance_builder.py | 3 +++ .../test/test_flops_based_premission.py | 2 +- .../test/test_gasp_based_premission.py | 2 +- aviary/subsystems/test/test_premission.py | 2 +- .../test_FLOPS_balanced_field_length.py | 2 +- .../test_FLOPS_detailed_landing.py | 2 +- .../test_FLOPS_detailed_takeoff.py | 2 +- 15 files changed, 49 insertions(+), 26 deletions(-) rename aviary/subsystems/{premission.py => core_premission.py} (86%) diff --git a/aviary/api.py b/aviary/api.py index 7ea2a39742..2d0394ee46 100644 --- a/aviary/api.py +++ b/aviary/api.py @@ -88,7 +88,7 @@ ################### # Miscellaneous -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase from aviary.utils.preprocessors import ( preprocess_crewpayload, diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 0cc2c1eceb..cf712ea4f5 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -17,9 +17,9 @@ from aviary.mission.two_dof_problem_configurator import TwoDOFProblemConfigurator from aviary.mission.utils import get_phase_mission_bus_lengths, process_guess_var from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder from aviary.subsystems.mass.mass_builder import CoreMassBuilder -from aviary.subsystems.premission import CorePreMission from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder from aviary.subsystems.performance.performance_builder import CorePerformanceBuilder from aviary.utils.functions import get_path diff --git a/aviary/core/post_mission_group.py b/aviary/core/post_mission_group.py index 33e8e10fa6..e52392f639 100644 --- a/aviary/core/post_mission_group.py +++ b/aviary/core/post_mission_group.py @@ -1,11 +1,22 @@ import openmdao.api as om +from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import promote_aircraft_and_mission_vars +from aviary.variable_info.variable_meta_data import _MetaData class PostMissionGroup(om.Group): """OpenMDAO group that holds all post-mission systems.""" + def initialize(self): + self.options.declare( + 'aviary_options', + types=AviaryValues, + desc='collection of Aircraft/Mission specific options', + ) + self.options.declare('subsystems', desc='list of core subsystem builders') + self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + def configure(self): """ Configure this group for post-mission. diff --git a/aviary/core/pre_mission_group.py b/aviary/core/pre_mission_group.py index 0122d37b95..1f32206d68 100644 --- a/aviary/core/pre_mission_group.py +++ b/aviary/core/pre_mission_group.py @@ -1,12 +1,23 @@ import openmdao.api as om +from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import promote_aircraft_and_mission_vars from aviary.variable_info.functions import override_aviary_vars +from aviary.variable_info.variable_meta_data import _MetaData class PreMissionGroup(om.Group): """OpenMDAO group that holds all pre-mission systems.""" + def initialize(self): + self.options.declare( + 'aviary_options', + types=AviaryValues, + desc='collection of Aircraft/Mission specific options', + ) + self.options.declare('subsystems', desc='list of core subsystem builders') + self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + def configure(self): """ Configure this group for pre-mission. diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py index 1af28385be..236c14fc31 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py @@ -4,7 +4,7 @@ import openmdao.api as om from openmdao.utils.assert_utils import assert_near_equal -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values from aviary.utils.preprocessors import preprocess_options @@ -277,7 +277,7 @@ def test_n3cc_drag(self): 0.03291, 0.03596, 0.04024, 0.04509, 0.05057, 0.05668, 0.06342 ], [ - 0.02274, 0.02293, 0.02366, 0.02494, 0.02659, 0.02820, 0.03004, 0.03244, + 0.02274, 0.02293, 0.02366, 0.02494, 0.02659, 0.02820, 0.03004, 0.03244, 0.03596, 0.04066, 0.04560, 0.05224, 0.06038, 0.07001, 0.08114 ], [ @@ -402,32 +402,32 @@ def test_large_single_aisle_2_drag(self): 0.03007, 0.03246, 0.03509, 0.03804, 0.04134, 0.04521, 0.04987 ], [ - + 0.02279, 0.02269, 0.02293, 0.02350, 0.02441, 0.02555, 0.02685, 0.02829, 0.03002, 0.03235, 0.03499, 0.03794, 0.04124, 0.04511, 0.04977 ], [ - + 0.02297, 0.02282, 0.02303, 0.02360, 0.02454, 0.02571, 0.02699, 0.02843, 0.03016, 0.03241, 0.03502, 0.03803, 0.04140, 0.04537, 0.05014 ], [ - + 0.02328, 0.02306, 0.02323, 0.02380, 0.02478, 0.02598, 0.02724, 0.02868, 0.03042, 0.03260, 0.03512, 0.03860, 0.04232, 0.04671, 0.05204 ], [ - + 0.02399, 0.02360, 0.02370, 0.02428, 0.02533, 0.02662, 0.02789, 0.02933, 0.03129, 0.03403, 0.03737, 0.04141, 0.04587, 0.05069, 0.05587 ], [ - + 0.02567, 0.02496, 0.02490, 0.02548, 0.02670, 0.02821, 0.02970, 0.03157, 0.03426, 0.03794, 0.04203, 0.04630, 0.05183, 0.05791, 0.06470 ], [ - + 0.04212, 0.04044, 0.03992, 0.04052, 0.04226, 0.04476, 0.04771, 0.05158, 0.05638, 0.06166, 0.06690, 0.07238, 0.07763, 0.08296, 0.08858 ] diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py index 936d59ab21..c62171fbc3 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py @@ -11,7 +11,7 @@ from aviary.interface.methods_for_level2 import AviaryProblem from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.subsystems.atmosphere.atmosphere import Atmosphere -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import set_aviary_initial_values diff --git a/aviary/subsystems/premission.py b/aviary/subsystems/core_premission.py similarity index 86% rename from aviary/subsystems/premission.py rename to aviary/subsystems/core_premission.py index b55a49c3d8..63930a61fc 100644 --- a/aviary/subsystems/premission.py +++ b/aviary/subsystems/core_premission.py @@ -1,16 +1,14 @@ -import openmdao import openmdao.api as om -from packaging import version from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.functions import override_aviary_vars from aviary.variable_info.variable_meta_data import _MetaData -use_new_openmdao_syntax = version.parse(openmdao.__version__) >= version.parse('3.28') - class CorePreMission(om.Group): - """Group that contains all pre-mission groups of core Aviary subsystems (geometry, mass, propulsion, aerodynamics).""" + """Group that contains all pre-mission groups of core Aviary subsystems: + (geometry, mass, propulsion, aerodynamics, performance). + """ def initialize(self): self.options.declare( @@ -20,6 +18,7 @@ def initialize(self): ) self.options.declare('subsystems', desc='list of core subsystem builders') self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + # NOTE this flag is only needed for tests - in AviaryProblem it should always be False self.options.declare( 'process_overrides', @@ -30,9 +29,8 @@ def initialize(self): ) def setup(self, **kwargs): - if use_new_openmdao_syntax: - # rely on openMDAO's auto-ordering for this group - self.options['auto_order'] = True + # rely on openMDAO's auto-ordering for this group + self.options['auto_order'] = True aviary_options = self.options['aviary_options'] core_subsystems = self.options['subsystems'] diff --git a/aviary/subsystems/geometry/gasp_based/test/test_override.py b/aviary/subsystems/geometry/gasp_based/test/test_override.py index ef87489989..468bcc745d 100644 --- a/aviary/subsystems/geometry/gasp_based/test/test_override.py +++ b/aviary/subsystems/geometry/gasp_based/test/test_override.py @@ -7,7 +7,7 @@ from aviary.interface.methods_for_level2 import AviaryGroup from aviary.subsystems.aerodynamics.gasp_based.gaspaero import AeroGeom -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.process_input_decks import create_vehicle diff --git a/aviary/subsystems/performance/performance_builder.py b/aviary/subsystems/performance/performance_builder.py index 5a18acadb7..a9c447c47e 100644 --- a/aviary/subsystems/performance/performance_builder.py +++ b/aviary/subsystems/performance/performance_builder.py @@ -13,3 +13,6 @@ def __init__(self, name=None, meta_data=None): def build_pre_mission(self, aviary_inputs, **kwargs): return PerformancePremission() + + def build_post_mission(self, aviary_inputs, phase_info, phase_mission_bus_lengths): + pass #return group \ No newline at end of file diff --git a/aviary/subsystems/test/test_flops_based_premission.py b/aviary/subsystems/test/test_flops_based_premission.py index 82e90846dd..56995cb739 100644 --- a/aviary/subsystems/test/test_flops_based_premission.py +++ b/aviary/subsystems/test/test_flops_based_premission.py @@ -4,7 +4,7 @@ from openmdao.utils.testing_utils import use_tempdirs from parameterized import parameterized -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/subsystems/test/test_gasp_based_premission.py b/aviary/subsystems/test/test_gasp_based_premission.py index d3f3ce4fd9..47c22fdaad 100644 --- a/aviary/subsystems/test/test_gasp_based_premission.py +++ b/aviary/subsystems/test/test_gasp_based_premission.py @@ -4,7 +4,7 @@ from openmdao.utils.testing_utils import use_tempdirs from aviary.interface.methods_for_level2 import AviaryProblem -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/subsystems/test/test_premission.py b/aviary/subsystems/test/test_premission.py index 16335bfb52..87c7c9e722 100644 --- a/aviary/subsystems/test/test_premission.py +++ b/aviary/subsystems/test/test_premission.py @@ -10,7 +10,7 @@ from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder from aviary.subsystems.mass.mass_builder import CoreMassBuilder -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.aviary_values import get_items, get_keys diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py index 4475f8adc6..f47f12a07c 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py @@ -19,7 +19,7 @@ from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( inputs as _inputs, ) -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values, set_aviary_input_defaults from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py index bdb1b511e9..0e66eaf455 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py @@ -17,7 +17,7 @@ from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( landing_trajectory_builder as _landing_trajectory_builder, ) -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values, set_aviary_input_defaults from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py index d4bdb72166..f1b01d7d62 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py @@ -17,7 +17,7 @@ from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( takeoff_trajectory_builder as _takeoff_trajectory_builder, ) -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values, set_aviary_input_defaults from aviary.utils.preprocessors import preprocess_options From c9d2f726d711c7ac3a447678eb0488735da2e3fb Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Thu, 9 Oct 2025 12:54:12 -0400 Subject: [PATCH 14/22] progress --- aviary/core/aviary_group.py | 18 +++ aviary/core/post_mission_group.py | 4 + .../balanced_field/run_balanced_field.py | 5 +- aviary/mission/initial_guess_builders.py | 7 - .../performance/balanced_field_builder.py | 19 +++ .../performance/balanced_field_submodel.py | 134 ++++++++++++++++++ .../performance/performance_builder.py | 11 +- 7 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 aviary/subsystems/performance/balanced_field_builder.py create mode 100644 aviary/subsystems/performance/balanced_field_submodel.py diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index cf712ea4f5..032f5d7651 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -17,6 +17,7 @@ from aviary.mission.two_dof_problem_configurator import TwoDOFProblemConfigurator from aviary.mission.utils import get_phase_mission_bus_lengths, process_guess_var from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder +from aviary.subsystems.core_postmission import CorePostMission from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder from aviary.subsystems.mass.mass_builder import CoreMassBuilder @@ -1032,6 +1033,23 @@ def add_post_mission_systems(self, verbosity=None): 'The aircraft may not have enough space for fuel, so check the value of Mission.Constraints.EXCESS_FUEL_CAPACITY for details.' ) + default_subsystems = [ + self.core_subsystems['performance'], + ] + + post_mission.add_subsystem( + 'core_subsystems', + CorePostMission( + aviary_options=self.aviary_inputs, + subsystems=default_subsystems, + phase_info=self.phase_info, + phase_mission_bus_lengths=phase_mission_bus_lengths, + post_mission_info=self.post_mission_info, + ), + promotes_inputs=['*'], + promotes_outputs=['*'], + ) + def link_phases(self, verbosity=None, comm=None): """ Link phases together after they've been added. diff --git a/aviary/core/post_mission_group.py b/aviary/core/post_mission_group.py index e52392f639..e69698d501 100644 --- a/aviary/core/post_mission_group.py +++ b/aviary/core/post_mission_group.py @@ -17,6 +17,10 @@ def initialize(self): self.options.declare('subsystems', desc='list of core subsystem builders') self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + def setup(self): + # rely on openMDAO's auto-ordering for this group + self.options['auto_order'] = True + def configure(self): """ Configure this group for post-mission. diff --git a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py index cb840d4f89..f5ce12a28b 100644 --- a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py +++ b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py @@ -14,7 +14,8 @@ local_phase_info = deepcopy(phase_info) -local_phase_info['post_mission']['external_subsystems'] = [BalancedFieldBuilder()] +#local_phase_info['post_mission']['external_subsystems'] = [BalancedFieldBuilder()] +local_phase_info['post_mission']['balanced_field'] = True prob = AviaryProblem() @@ -43,4 +44,4 @@ prob.setup() # TODO: N3CC optimization does not return success. -prob.run_aviary_problem('dymos_solution.db') +prob.run_aviary_problem() diff --git a/aviary/mission/initial_guess_builders.py b/aviary/mission/initial_guess_builders.py index e52dc0a684..9c1c083cd5 100644 --- a/aviary/mission/initial_guess_builders.py +++ b/aviary/mission/initial_guess_builders.py @@ -58,13 +58,6 @@ def apply_initial_guess( # raise ValueError(f'{phase.msginfo} Attempting to apply initial guess for {self.key}.\n' # 'Not find in the states, control, parameters, or integration variable of the phase.') - # def _get_complete_key(self, traj_name, phase_name): - # """Compose the complete key for setting the initial guess.""" - # _ = traj_name - # _ = phase_name - - # return self.key - class InitialGuessControl(InitialGuess): """ diff --git a/aviary/subsystems/performance/balanced_field_builder.py b/aviary/subsystems/performance/balanced_field_builder.py new file mode 100644 index 0000000000..6441631d4d --- /dev/null +++ b/aviary/subsystems/performance/balanced_field_builder.py @@ -0,0 +1,19 @@ +import openmdao.api as om + +import aviary.api as av +from aviary.subsystems.performance.balanced_field_submodel import ( + create_balance_field_subprob, +) +from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase + + +class BalancedFieldBuilder(SubsystemBuilderBase): + + def build_post_mission( + self, aviary_inputs, phase_info=None, phase_mission_bus_lengths=None, **kwargs + ): + return create_balance_field_subprob(aviary_inputs) + + +if __name__ == '__main__': + unittest.main() diff --git a/aviary/subsystems/performance/balanced_field_submodel.py b/aviary/subsystems/performance/balanced_field_submodel.py new file mode 100644 index 0000000000..6448ea7f4f --- /dev/null +++ b/aviary/subsystems/performance/balanced_field_submodel.py @@ -0,0 +1,134 @@ +""" +Group containing a submodel component with detailed landing. +""" +import warnings + +import openmdao.api as om + +import aviary.api as av +from aviary.variable_info.variables import Aircraft, Mission, Settings + + +def create_balance_field_subprob(aviary_inputs, use_spoiler=False): + + subprob = create_prob(aviary_inputs, use_spoiler) + + comp = AviarySubmodelComp( + problem=subprob, + inputs=[ + Aircraft.Wing.AREA, + Aircraft.Wing.ASPECT_RATIO, + Aircraft.Wing.HEIGHT, + Aircraft.Wing.SPAN, + Mission.Takeoff.DRAG_COEFFICIENT_MIN, + Mission.Takeoff.LIFT_COEFFICIENT_MAX, + ('traj.takeoff_brake_release_to_engine_failure.initial_states:mass', Mission.Summary.GROSS_MASS), + ], + outputs=[ + ('traj.takeoff_climb_gradient_to_obstacle.final_states:distance', 'distance_obstacle'), + ] + ) + + return comp + + +def create_prob(aviary_inputs, use_spoiler=False): + """ + Return a problem + """ + aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS + ) + + # fmt: off + takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } + } + # fmt: off + + # when using spoilers, add a few more options + if use_spoiler: + + spoiler_drag = aviary_inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT) + spoiler_lift = aviary_inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT) + + takeoff_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': spoiler_drag, + 'spoiler_lift_coefficient': spoiler_lift, + } + } + + # We also need propulsion analysis for takeoff and landing. No additional configuration + # is needed for this builder + engines = [av.build_engine_deck(aviary_inputs)] + # Note that the aviary_inputs is already in a pre-processed state. + prop_builder = av.CorePropulsionBuilder(engine_models=engines) + + balanced_field_user_options = av.AviaryValues() + + from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems + from aviary.variable_info.functions import setup_model_options + + dto_build = av.BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) + + subprob = om.Problem() + + #ivc = om.IndepVarComp(Mission.Summary.GROSS_MASS, val=1.0, units='lbm') + #subprob.model.add_subsystem('takeoff_mass_ivc', ivc, promotes=['*']) + #subprob.model.connect( + # Mission.Summary.GROSS_MASS, + # "traj.takeoff_brake_release_to_engine_failure.states:mass", + # flat_src_indices=[0], + #) + + # Instantiate the trajectory and add the phases + traj = dto_build.build_trajectory(aviary_options=aviary_inputs, model=subprob.model) + + setup_model_options(subprob, aviary_inputs) + + # This is kind of janky, but we need these after the subproblem sets it up. + subprob.aviary_inputs = aviary_inputs + subprob.dto_build = dto_build + + return subprob + + +class AviarySubmodelComp(om.SubmodelComp): + """ + We need to subclass so that we can set the initial conditions. + """ + def setup(self): + # suppress warnings: + # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' + with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + super().setup() + + + sub = self._subprob + av.set_aviary_initial_values(sub, sub.aviary_inputs) + sub.dto_build.apply_initial_guesses(sub, 'traj') + + sub.final_setup() diff --git a/aviary/subsystems/performance/performance_builder.py b/aviary/subsystems/performance/performance_builder.py index a9c447c47e..1b248b43f5 100644 --- a/aviary/subsystems/performance/performance_builder.py +++ b/aviary/subsystems/performance/performance_builder.py @@ -1,3 +1,6 @@ +from aviary.subsystems.performance.balanced_field_submodel import ( + create_balance_field_subprob, +) from aviary.subsystems.performance.performance_premission import PerformancePremission from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase @@ -14,5 +17,11 @@ def __init__(self, name=None, meta_data=None): def build_pre_mission(self, aviary_inputs, **kwargs): return PerformancePremission() - def build_post_mission(self, aviary_inputs, phase_info, phase_mission_bus_lengths): + def build_post_mission(self, aviary_inputs, phase_info, phase_mission_bus_lengths, **kwargs): + + if 'post_mission_info' in kwargs: + post = kwargs['post_mission_info'] + if post.get('balanced_field', False): + return create_balance_field_subprob(aviary_inputs) + pass #return group \ No newline at end of file From 90c43e21b9e224ef2d197b9975c36d1fac2be3d2 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Thu, 9 Oct 2025 12:56:36 -0400 Subject: [PATCH 15/22] Balanced field works as a performance post-mission system --- .../performance/balanced_field_builder.py | 19 ------------------- .../performance/performance_builder.py | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 aviary/subsystems/performance/balanced_field_builder.py diff --git a/aviary/subsystems/performance/balanced_field_builder.py b/aviary/subsystems/performance/balanced_field_builder.py deleted file mode 100644 index 6441631d4d..0000000000 --- a/aviary/subsystems/performance/balanced_field_builder.py +++ /dev/null @@ -1,19 +0,0 @@ -import openmdao.api as om - -import aviary.api as av -from aviary.subsystems.performance.balanced_field_submodel import ( - create_balance_field_subprob, -) -from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase - - -class BalancedFieldBuilder(SubsystemBuilderBase): - - def build_post_mission( - self, aviary_inputs, phase_info=None, phase_mission_bus_lengths=None, **kwargs - ): - return create_balance_field_subprob(aviary_inputs) - - -if __name__ == '__main__': - unittest.main() diff --git a/aviary/subsystems/performance/performance_builder.py b/aviary/subsystems/performance/performance_builder.py index 1b248b43f5..03caea575c 100644 --- a/aviary/subsystems/performance/performance_builder.py +++ b/aviary/subsystems/performance/performance_builder.py @@ -24,4 +24,4 @@ def build_post_mission(self, aviary_inputs, phase_info, phase_mission_bus_length if post.get('balanced_field', False): return create_balance_field_subprob(aviary_inputs) - pass #return group \ No newline at end of file + return \ No newline at end of file From b33482d794fb8341325ae65759c557612e813d6f Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 10 Oct 2025 19:58:31 -0400 Subject: [PATCH 16/22] latest --- .../external_subsystems/balanced_field/run_balanced_field.py | 2 ++ .../advanced_single_aisle/advanced_single_aisle_FLOPS.csv | 2 +- aviary/subsystems/performance/balanced_field_submodel.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py index f5ce12a28b..ce2a475eff 100644 --- a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py +++ b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py @@ -45,3 +45,5 @@ # TODO: N3CC optimization does not return success. prob.run_aviary_problem() + +prob.model.list_vars(units=True, print_arrays=True) diff --git a/aviary/models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv b/aviary/models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv index a411a73d63..e8e4a1d34b 100644 --- a/aviary/models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv +++ b/aviary/models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv @@ -182,7 +182,7 @@ mission:summary:fuel_flow_scaler,1,unitless mission:takeoff:airport_altitude,0,ft mission:takeoff:angle_of_attack_runway,0,deg mission:takeoff:braking_friction_coefficient,0.35,unitless -mission:takeoff:drag_coefficient_min,0.045,unitless +mission:takeoff:drag_coefficient_min,0.05,unitless mission:takeoff:final_altitude,35,ft mission:takeoff:fuel_simple,577,lbm mission:takeoff:lift_coefficient_max,2,unitless diff --git a/aviary/subsystems/performance/balanced_field_submodel.py b/aviary/subsystems/performance/balanced_field_submodel.py index 6448ea7f4f..adc5e248eb 100644 --- a/aviary/subsystems/performance/balanced_field_submodel.py +++ b/aviary/subsystems/performance/balanced_field_submodel.py @@ -107,6 +107,7 @@ def create_prob(aviary_inputs, use_spoiler=False): traj = dto_build.build_trajectory(aviary_options=aviary_inputs, model=subprob.model) setup_model_options(subprob, aviary_inputs) + subprob.set_solver_print(2) # This is kind of janky, but we need these after the subproblem sets it up. subprob.aviary_inputs = aviary_inputs From 0a0586dce364c35844ed61b8545d4b62126bdbeb Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 24 Oct 2025 13:01:08 -0400 Subject: [PATCH 17/22] Fixed some circular dependencies --- aviary/mission/balanced_field_traj_builder.py | 7 ++-- .../performance/balanced_field_submodel.py | 32 +++++++++++-------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/aviary/mission/balanced_field_traj_builder.py b/aviary/mission/balanced_field_traj_builder.py index 6db0f312d5..c6e07ac30a 100644 --- a/aviary/mission/balanced_field_traj_builder.py +++ b/aviary/mission/balanced_field_traj_builder.py @@ -9,18 +9,17 @@ from abc import ABC from collections import namedtuple +import numpy as np + import dymos as dm import openmdao.api as om -import numpy as np from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE from aviary.mission.flops_based.phases.balanced_field_trajectory import BalancedFieldPhaseBuilder -from aviary.mission.initial_guess_builders import InitialGuess from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.variable_meta_data import _MetaData -from aviary.variable_info.variables import Dynamic +from aviary.variable_info.variables import Dynamic, Mission from aviary.variable_info.functions import setup_trajectory_params -from aviary.api import Aircraft, Dynamic, Mission from aviary.mission.phase_builder_base import PhaseBuilderBase diff --git a/aviary/subsystems/performance/balanced_field_submodel.py b/aviary/subsystems/performance/balanced_field_submodel.py index adc5e248eb..62480ad884 100644 --- a/aviary/subsystems/performance/balanced_field_submodel.py +++ b/aviary/subsystems/performance/balanced_field_submodel.py @@ -5,7 +5,14 @@ import openmdao.api as om -import aviary.api as av +from aviary.mission.balanced_field_traj_builder import BalancedFieldTrajectoryBuilder +from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder +from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.aviary_values import AviaryValues +from aviary.utils.functions import set_aviary_initial_values +from aviary.variable_info.enums import LegacyCode +from aviary.variable_info.functions import setup_model_options from aviary.variable_info.variables import Aircraft, Mission, Settings @@ -36,8 +43,8 @@ def create_prob(aviary_inputs, use_spoiler=False): """ Return a problem """ - aero_builder = av.CoreAerodynamicsBuilder( - name='low_speed_aero', code_origin=av.LegacyCode.FLOPS + aero_builder = CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=LegacyCode.FLOPS ) # fmt: off @@ -79,19 +86,16 @@ def create_prob(aviary_inputs, use_spoiler=False): # We also need propulsion analysis for takeoff and landing. No additional configuration # is needed for this builder - engines = [av.build_engine_deck(aviary_inputs)] + engines = [build_engine_deck(aviary_inputs)] # Note that the aviary_inputs is already in a pre-processed state. - prop_builder = av.CorePropulsionBuilder(engine_models=engines) + prop_builder = CorePropulsionBuilder(engine_models=engines) - balanced_field_user_options = av.AviaryValues() + balanced_field_user_options = AviaryValues() - from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems - from aviary.variable_info.functions import setup_model_options - - dto_build = av.BalancedFieldTrajectoryBuilder('balanced_field_traj', - core_subsystems=[aero_builder, prop_builder], - subsystem_options=takeoff_subsystem_options, - user_options=balanced_field_user_options) + dto_build = BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) subprob = om.Problem() @@ -129,7 +133,7 @@ def setup(self): sub = self._subprob - av.set_aviary_initial_values(sub, sub.aviary_inputs) + set_aviary_initial_values(sub, sub.aviary_inputs) sub.dto_build.apply_initial_guesses(sub, 'traj') sub.final_setup() From c59fa86cbae9a2321514afd6e4df1c9fa8ac6164 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 24 Oct 2025 15:24:19 -0400 Subject: [PATCH 18/22] mostly a few small changes --- .../balanced_field/balanced_field_builder.py | 6 ++++++ .../balanced_field/run_balanced_field.py | 2 +- aviary/subsystems/performance/balanced_field_submodel.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py index ca041d5764..29068b9156 100644 --- a/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py @@ -10,6 +10,12 @@ class BalancedFieldBuilder(SubsystemBuilderBase): + def __init__(self, name=None, meta_data=None): + if name is None: + name = 'balanced_field_length' + + super().__init__(name=name, meta_data=meta_data) + def build_post_mission( self, aviary_inputs, phase_info=None, phase_mission_bus_lengths=None, **kwargs ): diff --git a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py index ce2a475eff..f247c7c07f 100644 --- a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py +++ b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py @@ -46,4 +46,4 @@ # TODO: N3CC optimization does not return success. prob.run_aviary_problem() -prob.model.list_vars(units=True, print_arrays=True) +#prob.model.list_vars(units=True, print_arrays=True) diff --git a/aviary/subsystems/performance/balanced_field_submodel.py b/aviary/subsystems/performance/balanced_field_submodel.py index 62480ad884..08052d16fd 100644 --- a/aviary/subsystems/performance/balanced_field_submodel.py +++ b/aviary/subsystems/performance/balanced_field_submodel.py @@ -111,7 +111,7 @@ def create_prob(aviary_inputs, use_spoiler=False): traj = dto_build.build_trajectory(aviary_options=aviary_inputs, model=subprob.model) setup_model_options(subprob, aviary_inputs) - subprob.set_solver_print(2) + #subprob.set_solver_print(2) # This is kind of janky, but we need these after the subproblem sets it up. subprob.aviary_inputs = aviary_inputs From 5095d6f394a0c4692a5b690db3f3413ebb7103f3 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 24 Oct 2025 15:28:09 -0400 Subject: [PATCH 19/22] mostly a few small changes --- .../external_subsystems/balanced_field/run_balanced_field.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py index f247c7c07f..0baeec878e 100644 --- a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py +++ b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py @@ -14,6 +14,10 @@ local_phase_info = deepcopy(phase_info) +# First way is the manual way using the files in this directory. +# Second is automated. You can turn balanced-field on with a single switch. +# Note: Aero is still hardcoded in this directory. Initial guesses are also hardcoded, +# but it is unclear which ones need to be tuned by the user. #local_phase_info['post_mission']['external_subsystems'] = [BalancedFieldBuilder()] local_phase_info['post_mission']['balanced_field'] = True From a8b64bcf9ad58bc4a79618134d8b0aeaea33c69c Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 12 Nov 2025 11:50:46 -0500 Subject: [PATCH 20/22] Added missing file --- aviary/subsystems/core_postmission.py | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 aviary/subsystems/core_postmission.py diff --git a/aviary/subsystems/core_postmission.py b/aviary/subsystems/core_postmission.py new file mode 100644 index 0000000000..76a92b869e --- /dev/null +++ b/aviary/subsystems/core_postmission.py @@ -0,0 +1,62 @@ +import openmdao.api as om + +from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.functions import override_aviary_vars +from aviary.variable_info.variable_meta_data import _MetaData + + +class CorePostMission(om.Group): + """Group that contains all post-mission groups of core Aviary subsystems: (performance). + """ + + def initialize(self): + self.options.declare( + 'aviary_options', + types=AviaryValues, + desc='collection of Aircraft/Mission specific options', + ) + self.options.declare('subsystems', desc='list of core subsystem builders') + self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + + self.options.declare( + 'phase_info', + desc='The phase_info dict for all phases', + types=dict, + ) + + self.options.declare( + 'phase_mission_bus_lengths', + desc='Mapping from phase names to the lengths of the mission_bus_variables timeseries', + types=dict, + ) + + self.options.declare( + 'post_mission_info', + desc='The post_mission portion of the phase_info.', + types=dict, + ) + + def setup(self, **kwargs): + # rely on openMDAO's auto-ordering for this group + self.options['auto_order'] = True + + aviary_options = self.options['aviary_options'] + phase_info = self.options['phase_info'] + phase_mission_bus_lengths = self.options['phase_mission_bus_lengths'] + post_mission_info = self.options['post_mission_info'] + core_subsystems = self.options['subsystems'] + + for subsystem in core_subsystems: + pre_mission_system = subsystem.build_post_mission( + aviary_options, + phase_info, + phase_mission_bus_lengths, + post_mission_info=post_mission_info, + ) + if pre_mission_system is not None: + self.add_subsystem( + subsystem.name, + pre_mission_system, + promotes_inputs=['*'], + promotes_outputs=['*'], + ) From 7e5a3a64fe8699ed1524f040bf2bc5296129efdf Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 19 Nov 2025 15:35:57 -0500 Subject: [PATCH 21/22] Added file for rob to investigate --- .../balanced_field/balanced_field_large.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 aviary/examples/external_subsystems/balanced_field/balanced_field_large.py diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py new file mode 100644 index 0000000000..29813d3251 --- /dev/null +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py @@ -0,0 +1,129 @@ +import warnings + +import dymos as dm +import openmdao.api as om + +import aviary.api as av +from aviary.api import Mission +from aviary.interface.methods_for_level2 import AviaryProblem +from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import inputs +from aviary.utils.preprocessors import preprocess_options + + +# Note, only creating this aviary problem so that it can read the aircraft csv for us. +prob = AviaryProblem() +prob.load_inputs("models/aircraft/test_aircraft/aircraft_for_bench_FwFm.csv", verbosity=2) +aviary_options = prob.aviary_inputs.deepcopy() + +# These inputs aren't in the aircraft file yet. +aviary_options.set_val(Mission.Takeoff.AIRPORT_ALTITUDE, 0.0, 'ft') +aviary_options.set_val(Mission.Takeoff.DRAG_COEFFICIENT_MIN, 0.05, 'unitless') +aviary_options.set_val(Mission.Takeoff.LIFT_COEFFICIENT_MAX, 3.0, 'unitless') +aviary_options.set_val(Mission.Takeoff.OBSTACLE_HEIGHT, 35.0, 'ft') +aviary_options.set_val(Mission.Takeoff.ANGLE_OF_ATTACK_RUNWAY, 0.0, 'deg') +aviary_options.set_val(Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT, 0.0175) +aviary_options.set_val(Mission.Takeoff.BRAKING_FRICTION_COEFFICIENT, 0.35) +aviary_options.set_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT, 0.085000) +aviary_options.set_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT, -0.810000) +aviary_options.set_val(Mission.Takeoff.THRUST_INCIDENCE, 0.0, 'deg') +aviary_options.set_val(Mission.Takeoff.FUEL_SIMPLE, 577.0, 'lbm') + +# This builder can be used for both takeoff and landing phases +aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS +) + +# fmt: off +takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } +} +# fmt: off + +# when using spoilers, add a few more options +takeoff_spoiler_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT), + 'spoiler_lift_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT), + } +} + +# We also need propulsion analysis for takeoff and landing. No additional configuration +# is needed for this builder +engines = [av.build_engine_deck(aviary_options)] +preprocess_options(aviary_options, engine_models=engines) +prop_builder = av.CorePropulsionBuilder(engine_models=engines) + +balanced_field_user_options = av.AviaryValues() + +from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems +from aviary.variable_info.functions import setup_model_options + +takeoff_trajectory_builder = av.BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) + +test_problem = om.Problem() + +# default subsystems +default_premission_subsystems = get_default_premission_subsystems('FLOPS', engines) + +# Upstream pre-mission analysis for aero +test_problem.model.add_subsystem( + 'core_subsystems', + av.CorePreMission( + aviary_options=aviary_options, + subsystems=default_premission_subsystems, + ), + promotes_inputs=['*'], + promotes_outputs=['*'], +) + +# Instantiate the trajectory and add the phases +traj = takeoff_trajectory_builder.build_trajectory(aviary_options=aviary_options, model=test_problem.model) + +varnames = [ + av.Aircraft.Wing.AREA, + av.Aircraft.Wing.ASPECT_RATIO, + av.Aircraft.Wing.SPAN, +] +av.set_aviary_input_defaults(test_problem.model, varnames, aviary_options) + +setup_model_options(test_problem, aviary_options) + +# suppress warnings: +# "input variable '...' promoted using '*' was already promoted using 'aircraft:*' +with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + test_problem.setup(check=False) + +av.set_aviary_initial_values(test_problem, aviary_options) + +takeoff_trajectory_builder.apply_initial_guesses(test_problem, 'traj') + +test_problem.final_setup() + + +import dymos +dymos.run_problem(test_problem, run_driver=False, make_plots=True) + +print(test_problem.get_reports_dir()) + From 6dfff9d251064ee0c490a7701ad375bd7d217a8e Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Thu, 20 Nov 2025 11:24:20 -0500 Subject: [PATCH 22/22] Adjust mass guess --- .../external_subsystems/balanced_field/balanced_field_large.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py index 29813d3251..791fca1b34 100644 --- a/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py @@ -12,7 +12,7 @@ # Note, only creating this aviary problem so that it can read the aircraft csv for us. prob = AviaryProblem() -prob.load_inputs("models/aircraft/test_aircraft/aircraft_for_bench_FwFm.csv", verbosity=2) +prob.load_inputs("models/aircraft/test_aircraft/aircraft_for_bench_FwFm.csv", verbosity=0) aviary_options = prob.aviary_inputs.deepcopy() # These inputs aren't in the aircraft file yet. @@ -27,6 +27,7 @@ aviary_options.set_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT, -0.810000) aviary_options.set_val(Mission.Takeoff.THRUST_INCIDENCE, 0.0, 'deg') aviary_options.set_val(Mission.Takeoff.FUEL_SIMPLE, 577.0, 'lbm') +aviary_options.set_val(Mission.Design.GROSS_MASS, 160000, units='lbm') # This builder can be used for both takeoff and landing phases aero_builder = av.CoreAerodynamicsBuilder(