2DOF Detailed Takeoff and Landing#
Here we discuss how to optimize the takeoff and landing sequences for aircraft using the two-degrees-of-freedom (2DOF) equations of motion. When we say “detailed takeoff and landing,” this simply means that we model the aircraft trajectory in more detail than other simplified mission representations. This means two main things:
We model the takeoff portion of flight using a series of phases, such as ground roll, rotation, and multiple climb phases. Similarly, we model the landing portion of flight using a series of phases, such as approach, flare, and touchdown.
Instead of using the energy-state approximation for the aircraft equations of motion, we use the full two-degrees-of-freedom (2DOF) equations of motion. This means that there is a notion of angle of attack and aircraft pitch within the flight dynamics equations. These mission methods are both detailed in the Mission Analysis doc page.
These considerations allow us to model specific parts of the aircraft trajectory in more detail, which is especially useful for certain performance-based disciplinary analyses, such as acoustics and controls.
How we define the trajectories#
We use the phase_info object to define the trajectories.
We generally use polynomial controls of order 1 to simplify the optimization problem.
What this means is that the control variables (Mach and altitude) are linear within one phase.
You can increase the order of the polynomial controls by setting altitude_polynomial_order or mach_polynomial_order to a higher value. You can also set them to None to have the optimizer control the values at every node.
We add any constraints needed for the trajectory in the constraints argument passed inside of user_options.
Any arbitrary variable present in the phase ODE can be constrained.
You can use boundary or path constraints by setting the type argument in the constraint dict.
We optimize mach and altitude using the mach_optimize and altitude_optimize flags.
You can choose to disable optimization of these variables by setting them to False for any phase.
Aviary internally handles the connections for Mach and altitude between the phases to ensure continuity in the trajectory.
You can choose how to enforce that the throttle value is between 0 and 1; solver bounded, with boundary constraints, or path constraints.
Initial guesses are important to help the optimizer converge well. These guesses are much more important for the 2DOF model than the energy-state model.
In these examples we only model the takeoff and landing trajectories individually, not as part of a full mission.
This is because acoustic certification is typically done for these phases separately.
However, you can easily combine these phases into a comprehensive mission by adding the takeoff, flight, and landing phases to the mission in that order.
The 2DOF mission method (not the solved_2DOF mission method used here) models the entire mission including takeoff and landing phases.
Note
The integration variable we use in the solved_2DOF model is distance, not time for all of the phases except groundroll.
The groundroll phase’s integration variable is velocity.
This is different than the approach used for energy-state models where the integration variable is time.
Defining the takeoff trajectory#
We follow the diagram below to model the takeoff trajectory, which includes the ground roll, rotation, liftoff, and climb phases. We add constraints at specific points in the flight to ensure we hit certain altitudes and distances needed for acoustic certification. P1 and P2 correspond to microphone locations for acoustic certification.
Note
Each of the phases modeled in the takeoff trajectory use the solved 2DOF model except for phase AB which uses a specific ground roll model.

import openmdao.api as om
import aviary.api as av
# fmt: off
subsystem_options = {
'aerodynamics': {
'method': 'low_speed',
'ground_altitude': 0.0, # units='ft'
'angles_of_attack': [
-5.0, -4.0, -3.0, -2.0, -1.0, 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.01, 0.1, 0.2, 0.3, 0.4, 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.04, 0.02, 0.01, 0.02, 0.04, 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: on
mach_optimize = True
altitude_optimize = True
optimizer = 'SLSQP'
num_segments = 3
phase_info = {
'pre_mission': {'include_takeoff': False, 'optimize_mass': False},
'AB': {
'user_options': {
'num_segments': num_segments,
'order': 3,
'ground_roll': True,
'time_duration_ref': (100.0, 'kn'),
'time_duration_bounds': ((100.0, 500.0), 'kn'),
'time_initial': (0.0, 'kn'),
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(0.0, 2.0e3), 'ft'],
'time': [(0.0, 20.0), 's'],
'velocity': [(1.0, 120.0), 'kn'],
'mass': [(175.0e3, 174.85e3), 'lbm'],
},
},
'rotate': {
'user_options': {
'num_segments': num_segments,
'order': 3,
'ground_roll': True,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((1.0e3, 3.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((200.0, 2.0e3), 'ft'),
'throttle_enforcement': 'boundary_constraint',
'rotation': True,
'mass_ref': (170000, 'lbm'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 1,
'mach_bounds': ((0.18, 0.2), 'unitless'),
'altitude_optimize': False,
'altitude_polynomial_order': 1,
'altitude_initial': (0.0, 'ft'),
'altitude_final': (0.0, 'ft'),
'constraints': {
'normal_force': {
'equals': 0.0,
'loc': 'final',
'units': 'lbf',
'type': 'boundary',
'ref': 1.0e5,
},
},
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(2.0e3, 1.0e3), 'ft'],
'time': [(20.0, 25.0), 's'],
'mach': [(0.18, 0.2), 'unitless'],
'mass': [(174.85e3, 174.84e3), 'lbm'],
'angle_of_attack': [(0.0, 12.0), 'deg'],
},
},
'BC': {
'user_options': {
'num_segments': num_segments,
'order': 3,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((1.0, 16.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((500.0, 1500.0), 'ft'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 1,
'mach_bounds': ((0.2, 0.22), 'unitless'),
'altitude_optimize': altitude_optimize,
'altitude_polynomial_order': 1,
'altitude_bounds': ((0.0, 150.0), 'ft'),
'altitude_final': (50.0, 'ft'),
'mass_ref': (170000, 'lbm'),
'throttle_enforcement': 'boundary_constraint',
'rotation': False,
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(3.0e3, 1.0e3), 'ft'],
'time': [(25.0, 35.0), 's'],
'mach': [(0.2, 0.22), 'unitless'],
'altitude': [(0.0, 50.0), 'ft'],
'mass': [(174.84e3, 174.82e3), 'lbm'],
},
},
'CD_to_P2': {
'user_options': {
'num_segments': num_segments,
'order': 3,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((1.0e3, 20.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((3.0e3, 20.0e3), 'ft'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 1,
'mach_bounds': ((0.22, 0.3), 'unitless'),
'altitude_optimize': altitude_optimize,
'altitude_polynomial_order': 1,
'altitude_initial': (50.0, 'ft'),
'altitude_final': (985.0, 'ft'),
'altitude_bounds': ((0.0, 985.0), 'ft'),
'mass_ref': (170000, 'lbm'),
'throttle_enforcement': 'boundary_constraint',
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(4.0e3, 10.0e3), 'ft'],
'time': [(35.0, 60.0), 's'],
'mach': [(0.22, 0.3), 'unitless'],
'mass': [(174.82e3, 174.8e3), 'lbm'],
},
},
'P2_to_DE': {
'user_options': {
'num_segments': num_segments,
'order': 3,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((1.0e3, 20.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((3.0e3, 20.0e3), 'ft'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 1,
'mach_bounds': ((0.22, 0.3), 'unitless'),
'altitude_optimize': altitude_optimize,
'altitude_polynomial_order': 1,
'altitude_bounds': ((985.0, 1100.0), 'ft'),
'mass_ref': (170000, 'lbm'),
'throttle_enforcement': 'path_constraint',
'constraints': {
'distance': {
'upper': 19.0e3,
'ref': 20.0e3,
'loc': 'final',
'units': 'ft',
'type': 'boundary',
},
},
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(14.0e3, 4.0e3), 'ft'],
'time': [(60.0, 80.0), 's'],
'mach': [(0.22, 0.3), 'unitless'],
'altitude': [(985.0, 1100.0), 'ft'],
'mass': [(174.8e3, 174.5e3), 'lbm'],
},
},
'DE': {
'user_options': {
'num_segments': num_segments,
'order': 3,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((500.0, 30.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((50.0, 5000.0), 'ft'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 2,
'mach_bounds': ((0.24, 0.32), 'unitless'),
'altitude_optimize': altitude_optimize,
'altitude_polynomial_order': 2,
'altitude_bounds': ((985.0, 1.5e3), 'ft'),
'mass_ref': (170000, 'lbm'),
'throttle_enforcement': 'path_constraint',
'constraints': {
'flight_path_angle': {
'equals': 4.0,
'loc': 'final',
'units': 'deg',
'type': 'boundary',
},
},
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(18.0e3, 2.0e3), 'ft'],
'mass': [(174.5e3, 174.4e3), 'lbm'],
'mach': [(0.3, 0.3), 'unitless'],
'altitude': [(1100.0, 1200.0), 'ft'],
'time': [(80.0, 85.0), 's'],
},
},
'EF_to_P1': {
'user_options': {
'num_segments': num_segments,
'order': 3,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((500.0, 50.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((1.0e3, 20.0e3), 'ft'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 1,
'mach_bounds': ((0.24, 0.32), 'unitless'),
'altitude_optimize': altitude_optimize,
'altitude_polynomial_order': 1,
'altitude_bounds': ((1.1e3, 1.2e3), 'ft'),
'mass_ref': (170000, 'lbm'),
'throttle_enforcement': 'path_constraint',
'constraints': {
'distance': {
'equals': 21325.0,
'units': 'ft',
'type': 'boundary',
'loc': 'final',
'ref': 30.0e3,
},
'flight_path_angle': {
'equals': 4.0,
'loc': 'final',
'units': 'deg',
'type': 'boundary',
},
},
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(20.0e3, 1325.0), 'ft'],
'mass': [(174.4e3, 174.3e3), 'lbm'],
'mach': [(0.3, 0.3), 'unitless'],
'altitude': [(1100.0, 1200.0), 'ft'],
'time': [(85.0, 90.0), 's'],
},
},
'EF_past_P1': {
'user_options': {
'num_segments': num_segments,
'order': 3,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((20.0e3, 50.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((100.0, 50.0e3), 'ft'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 1,
'mach_bounds': ((0.24, 0.32), 'unitless'),
'altitude_optimize': altitude_optimize,
'altitude_polynomial_order': 1,
'altitude_bounds': ((1.0e3, 3.0e3), 'ft'),
'mass_ref': (170000, 'lbm'),
'throttle_enforcement': 'boundary_constraint',
'constraints': {
'flight_path_angle': {
'equals': 4.0,
'loc': 'final',
'units': 'deg',
'type': 'boundary',
},
'distance': {
'equals': 30.0e3,
'units': 'ft',
'type': 'boundary',
'loc': 'final',
'ref': 30.0e3,
},
},
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(21325.0, 50.0e3), 'ft'],
'mass': [(174.3e3, 174.2e3), 'lbm'],
'mach': [(0.3, 0.3), 'unitless'],
'altitude': [(1200.0, 2000.0), 'ft'],
'time': [(90.0, 180.0), 's'],
},
},
'post_mission': {
'include_landing': False,
'constrain_range': False,
},
}
prob = av.AviaryProblem()
# Load aircraft and options data from user
# Allow for user overrides here
prob.load_inputs('models/aircraft/test_aircraft/aircraft_for_bench_solved2dof.csv', phase_info)
prob.check_and_preprocess_inputs()
prob.build_model()
prob.add_driver(optimizer, max_iter=60)
if optimizer == 'IPOPT':
# custom optimizer seettings
prob.driver.opt_settings['mu_init'] = 1.0
prob.driver.opt_settings['nlp_scaling_method'] = 'none'
prob.driver.opt_settings['limited_memory_max_history'] = 50
prob.add_design_variables()
# Load optimization problem formulation
# Detail which variables the optimizer can control
prob.add_objective('mass') # maximize final mass (i.e. minimize fuel burn)
prob.setup()
# set the start-of-takeoff mass to mission:summary:gross_mass
prob.set_val('mission:summary:gross_mass', 175000, units='lbm')
prob.run_aviary_problem(suppress_solver_print=True)
/usr/share/miniconda/envs/test/lib/python3.13/site-packages/openmdao/utils/relevance.py:1296: OpenMDAOWarning:The following groups have a nonlinear solver that computes gradients and will be treated as atomic for the purposes of determining which systems are included in the optimization iteration:
traj.phases.AB
traj.phases.BC
traj.phases.CD_to_P2
traj.phases.DE
traj.phases.EF_past_P1
traj.phases.EF_to_P1
traj.phases.P2_to_DE
traj.phases.rotate
Singular matrix C in LSQ subproblem (Exit mode 6)
Current function value: -3.4840000000000004
Iterations: 1
Function evaluations: 1
Gradient evaluations: 1
Optimization FAILED.
Singular matrix C in LSQ subproblem
-----------------------------------
Warning:
Aviary run failed. See the dashboard for more details.
{'P1': {'thrust_fraction': np.float64(0.5497622895522126), 'true_airspeed': np.float64(197.60424851977004), 'angle_of_attack': np.float64(4.329524882610447), 'flight_path_angle': np.float64(4.316027519865624), 'altitude': np.float64(1200.0), 'distance': np.float64(21325.0)}, 'P2': {'thrust_fraction': np.float64(0.9511487212021398), 'true_airspeed': np.float64(197.75144760195008), 'angle_of_attack': np.float64(4.190052012537404), 'flight_path_angle': np.float64(5.341625565563308), 'altitude': np.float64(985.0), 'distance': np.float64(14000.0)}}
Defining the landing trajectory#
For the landing trajectory, we also follow a diagram that outlines the approach and touchdown phases. This trajectory is simpler than the takeoff trajectory. P3 corresponds to the microphone location used for acoustic certification.

import openmdao.api as om
import aviary.api as av
# fmt: off
subsystem_options = {
'aerodynamics': {
'method': 'low_speed',
'ground_altitude': 0.0, # units='ft'
'angles_of_attack': [
-5.0, -4.0, -3.0, -2.0, -1.0, 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.01, 0.1, 0.2, 0.3, 0.4, 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.04, 0.02, 0.01, 0.02, 0.04, 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': 2.0,
'drag_coefficient_factor': 2.0,
}
}
# fmt: on
subsystem_options_landing = subsystem_options.copy()
subsystem_options_landing['aerodynamics']['drag_coefficient_factor'] = 3.0
mach_optimize = False
altitude_optimize = False
phase_info = {
'pre_mission': {'include_takeoff': False, 'optimize_mass': False},
'GH': {
'user_options': {
'num_segments': 5,
'order': 3,
'ground_roll': False,
'clean': False,
'time_initial': (0.0, 'ft'),
'time_duration_ref': (2750.0, 'ft'),
'time_duration_bounds': ((500.0, 5.0e3), 'ft'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 1,
'mach_bounds': ((0.1, 0.5), 'unitless'),
'mach_initial': (0.15, 'unitless'),
'mach_final': (0.15, 'unitless'),
'altitude_optimize': altitude_optimize,
'altitude_polynomial_order': 1,
'altitude_initial': (500.0, 'ft'),
'altitude_final': (394.0, 'ft'),
'altitude_bounds': ((0.0, 1000.0), 'ft'),
'mass_initial': (120.0e3, 'lbm'),
'throttle_enforcement': 'bounded',
'rotation': False,
'constraints': {
'flight_path_angle': {
'equals': -3.0,
'loc': 'initial',
'units': 'deg',
'type': 'boundary',
},
},
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(0.0e3, 2.0e3), 'ft'],
'time': [(0.0, 12.0), 's'],
'mass': [(120.0e3, 119.8e3), 'lbm'],
},
},
'HI': {
'user_options': {
'num_segments': 5,
'order': 3,
'ground_roll': False,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((0.0, 16.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((500.0, 15.0e3), 'ft'),
'mach_optimize': mach_optimize,
'mach_polynomial_order': 1,
'mach_initial': (0.15, 'unitless'),
'mach_final': (0.15, 'unitless'),
'mach_bounds': ((0.1, 0.5), 'unitless'),
'altitude_optimize': altitude_optimize,
'altitude_polynomial_order': 1,
'altitude_initial': (394.0, 'ft'),
'altitude_final': (50.0, 'ft'),
'altitude_bounds': ((0.0, 1000.0), 'ft'),
'throttle_enforcement': 'bounded',
'rotation': False,
'constraints': {
'flight_path_angle': {
'equals': -3.0,
'loc': 'final',
'units': 'deg',
'type': 'boundary',
},
},
},
'subsystem_options': subsystem_options,
'initial_guesses': {
'distance': [(2.0e3, 6.5e3), 'ft'],
'time': [(12.0, 50.0), 's'],
'mass': [(119.8e3, 119.7e3), 'lbm'],
},
},
'IJ': {
'user_options': {
'num_segments': 5,
'order': 3,
'ground_roll': False,
'clean': False,
'time_initial_ref': (1.0e3, 'ft'),
'time_initial_bounds': ((0.0, 30.0e3), 'ft'),
'time_duration_ref': (1.0e3, 'ft'),
'time_duration_bounds': ((500.0, 15.0e3), 'ft'),
'mach_optimize': False,
'mach_polynomial_order': 2,
'mach_bounds': ((0.1, 0.5), 'unitless'),
'mach_initial': (0.15, 'unitless'),
'mach_final': (0.15, 'unitless'),
'altitude_optimize': True,
'altitude_polynomial_order': 2,
#'altitude_initial': (50.0, 'ft'),
#'altitude_final': (0.0, 'ft'),
'altitude_bounds': ((0.0, 1000.0), 'ft'),
'throttle_enforcement': 'path_constraint',
'rotation': False,
'constraints': {},
},
'subsystem_options': subsystem_options_landing,
'initial_guesses': {
'altitude': [(50.0, 0.0), 'ft'],
'distance': [(8.5e3, 2.0e3), 'ft'],
'time': [(50.0, 60.0), 's'],
'mass': [(119.7e3, 119.67e3), 'lbm'],
},
},
'post_mission': {
'include_landing': False,
'constrain_range': False,
},
}
prob = av.AviaryProblem()
# Load aircraft and options data from user
# Allow for user overrides here
prob.load_inputs('models/aircraft/test_aircraft/aircraft_for_bench_solved2dof.csv', phase_info)
prob.check_and_preprocess_inputs()
prob.build_model()
prob.add_driver('SLSQP', max_iter=100)
prob.add_design_variables()
# Load optimization problem formulation
# Detail which variables the optimizer can control
prob.add_objective('mass')
prob.setup()
# set the start-of-landing mass to mission:summary:gross_mass
prob.set_val('mission:summary:gross_mass', 175000, units='lbm')
prob.run_aviary_problem()
/usr/share/miniconda/envs/test/lib/python3.13/site-packages/openmdao/utils/relevance.py:1296: OpenMDAOWarning:The following groups have a nonlinear solver that computes gradients and will be treated as atomic for the purposes of determining which systems are included in the optimization iteration:
traj.phases.GH.rhs_all.control_iter_group
traj.phases.GH.rhs_all.throttle_balance_group
traj.phases.HI
traj.phases.IJ
Optimization terminated successfully (Exit mode 0)
Current function value: -3.4984054242291345
Iterations: 19
Function evaluations: 22
Gradient evaluations: 19
Optimization Complete
-----------------------------------
{'P3': {'thrust_fraction': np.float64(0.28557130416198245), 'true_airspeed': np.float64(99.07776305645102), 'angle_of_attack': np.float64(13.987059031256777), 'flight_path_angle': np.float64(-3.0), 'altitude': np.float64(394.0), 'distance': np.float64(2022.6004888991906)}}