Multi-Mission Example#

The Multi-mission Example demonstrates the capability to optimize a single aircraft design considering two missions that the aircraft will perform. For a background on this example see Multi-Mission Overview. It is expected that the users is familiar with methods from level 1 and level 2 before exploring this content.

Theory#

Each of the two missions in the example are instantiated as a single aviary problems with two AviaryGroups inside of it. Each AviaryGroup has it’s own pre-missions, mission, and post-mission elements. Two get the pre-missions to have the same aircraft design, Mission.Design.GROSS_MASS, Mission.Design.RANGE, Aircraft.Wing.SWEEP, are promoted out of the pre-missions to a single values. This ensures that the aircrafts in both pre-missions have the same design even though their passenger count and fuel mass are different. There is no post-mission for the example, but if one was required for calculating cost or acoustic constraints, there would need to be two post-mission systems as well.

Graphs for each mission are created and stored in the run_multimission_example folder. A custom print functions were added to display some important information. The user can see detailed info of each mission result using the prob.model.mission1.list_vars() commands listed in the comments at the bottom of the example.

A number of checks exist in add_aviary_group which under the hood calls check_and_preprocess_inputs to help the user in the case that incomplete as-flow or design passenger information is provided. This was done to provide backward compatability for older aircraft models which only specify design passenger information.

In this example we wanted to compare the same aircraft flying a mission with full passengers vs. a mission with no passengers. However, there are limitations in Aviary’s ability to detect user input vs. default values. So we set the passenger count to one instead. Right now, the only way to set an aircraft to exactly zero passengers is by setting Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS to zero plus any Aircraft.CrewPayload.CARGO_MASS being carried. This zeros out passenger and baggage mass regardless of what value is input to Aircraft.CrewPayload.NUM_PASSENGERS, Aircraft.CrewPayload.NUM_TOURIST_CLASS, Aircraft.CrewPayload.NUM_BUSINESS_CLASS, and Aircraft.CrewPayload.NUM_FIRST_CLASS. Once issue #610 is resolved the user should be able to set passenger and bags mass to exactly zero by setting Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS to zero.

Best Pratices#

The user should be cognizant of the implications of having two pre-mission systems, one for each mission. When trying to mirror these to create a single aircraft that flies different missions, both of the pre-mission systems should be nearly identical in setup, except for fuel-mass, passenger, and payload calculations. There are numerous opportunities for the user to get this wrong, and accidentally create two different aircraft as a result. For example, in a previous iteration of this example, Aircraft.Design.LANDING_TO_TAKEOFF_MASS_RATIO was not specified, which resulted in two different landing gears being designed, one for mission1, one for mission2.

Note: If you are having trouble getting your {glue:md}`Aircraft.Design.EMPTY_MASS` (the final drymass mass summation from pre-mission) to be equal for both pre-missions, use the following OpenMDAO commends at the end of the example to list out and compare the mass from each subsystem.
prob.model.mission1.list_vars(val=True, units=True, print_arrays=False)
prob.model.mission2.list_vars(val=True, units=True, print_arrays=False)

Phase Info#

The same mission distance and profile (takeoff, climb, cruise, descent, landing) is being flown for both missions. To enable this, a single phase_info is imported and then deepcopied. The user could modify the mission2 to be different from mission1 by changing the target_range to a different value (i.e phase_info_mission2['post_mission']['target_range'] = [1500, "nmi"] ).

import copy as copy

import aviary.api as av
from aviary.models.missions.height_energy_default import phase_info
from aviary.validation_cases.validation_tests import get_flops_inputs
from aviary.variable_info.enums import ProblemType
from aviary.variable_info.variables import Aircraft, Mission, Settings

phase_info_mission1 = copy.deepcopy(phase_info)
phase_info_mission2 = copy.deepcopy(phase_info)

Aircraft Configuration#

In the example, we import a single aircraft configuration (LargeSingleAisle2FLOPS) and then modify it to create a mission1 which carries 162 passengers and mission2, a mission which carries only a single passengers on the same aircraft. The number of seats for passengers in the aircraft, as well as some other systems like passenger airconditioning mass, is set by values of Aircraft.CrewPayload.Design.NUM_PASSENGERS, Aircraft.CrewPayload.Design.NUM_TOURIST_CLASS, Aircraft.CrewPayload.Design.NUM_BUSINESS_CLASS, and Aircraft.CrewPayload.Design.NUM_BUSINESS_CLASS. Whereas the actual number of passengers on the flight is specified by variables of Aircraft.CrewPayload.NUM_PASSENGERS, Aircraft.CrewPayload.NUM_TOURIST_CLASS, Aircraft.CrewPayload.NUM_BUSINESS_CLASS, and Aircraft.CrewPayload.NUM_BUSINESS_CLASS.

aviary_inputs_mission1 = get_flops_inputs('LargeSingleAisle2FLOPS')

aviary_inputs_mission2 = copy.deepcopy(aviary_inputs_mission1)
aviary_inputs_mission2.set_val(Aircraft.CrewPayload.NUM_PASSENGERS, 1, 'unitless')
aviary_inputs_mission2.set_val(Aircraft.CrewPayload.NUM_TOURIST_CLASS, 1, 'unitless')
aviary_inputs_mission2.set_val(Aircraft.CrewPayload.NUM_BUSINESS_CLASS, 0, 'unitless')
aviary_inputs_mission2.set_val(Aircraft.CrewPayload.NUM_FIRST_CLASS, 0, 'unitless')

Adding Aviary Groups#

Now we must set the problem type to multimission, this will allow us to use our other commandes like add_aviary_group to combine the aviary_values and phase_info to create two Aviary Groups which are added to the problem. add_aviary_group can accept engine_builders as well if you wanted to specify a custom engine builder for the models. We give each of the groups a name: mission1 and mission 2 which we will use to reference them later. Lastly we build the model, which adds pre-mission, phases, post-mission, and links phases sequentially. These elements could be called individually if the user desired more control over each one.

prob = av.AviaryProblem(problem_type=ProblemType.MULTI_MISSION)

prob.add_aviary_group('mission1', aircraft=aviary_inputs_mission1, phase_info=phase_info_mission1)

prob.add_aviary_group('mission2', aircraft=aviary_inputs_mission2, phase_info=phase_info_mission2)

prob.build_model()

User Input#

Now we have the opportunity for the user to tell us how these aircraft are the same. Up to this point, each call to add_aviary_group has setup two completely separate aircraft but in the example we are trying to design a single aircraft. To do this we must explicitly tell Aviary which key parameters are mirrored on both aircraft. These key parameters are used to size different subsystems.

promote_inputs tells aviary that between mission1 and mission2, the following 3 values are the same and should be promoted to the top level of the problem: Mission.Design.GROSS_MASS, Mission.Design.RANGE, and Aircraft.Wing.SWEEP. If we did not promote these values, they would still only be accesible inside of their individual groups (i.e. mission1.mission:design:gross_mass).

add_design_var_default is used to tell Aviary that we are setting Aircraft1:GROSS_MASS as a design variable controlled by the optimizer, and to set it’s initial value and upper and lower constraints. Normally this is done as part of a two-step process using prob.model.add_design_var to set the range, and then after setup calling prob.set_val to set the initial value. However, it is more convenient to set them both in one place. The value can still be over-written later using prob.set_val after setup.

add_composite_objective specifies that the output of Mission.Summary.FUEL_BURNED from mission1 and mission2 should be summed together based on a 2:1 weighting. Alternatively, the user could have specified the same thing using add_composite_objective_adv which provides a slightly more streamlined interface if users know their mission frequency and ouput weights separetly. This typically happens if you know how often you will fly a particular mission from historical data, and then you would like to weight the fuel-burn on each mission vs. another metric, such as maintinance costs.

prob.promote_inputs(
    ['mission1', 'mission2'],
    [
        (Mission.Design.GROSS_MASS, 'Aircraft1:GROSS_MASS'),
        (Mission.Design.RANGE, 'Aircraft1:RANGE'),
        (Aircraft.Wing.SWEEP, 'Aircraft1:SWEEP'),
    ],
)

prob.add_design_var_default(
    'Aircraft1:GROSS_MASS',
    lower=10.0,
    upper=900e3,
    units='lbm',
    default_val=100000,
)
prob.add_design_var_default(
    'Aircraft1:SWEEP',
    lower=23.0,
    upper=27.0,
    units='deg',
    default_val=25,
)

prob.add_composite_objective(
    ('mission1', Mission.Summary.FUEL_BURNED, 2),
    ('mission2', Mission.Summary.FUEL_BURNED, 1),
    ref=1,
)
# prob.add_composite_objective_adv(missions=['mission1', 'mission2'], mission_weights=[2,1], outputs=[Mission.Summary.FUEL_BURNED], outputoutput_weights=[1] ref=1)

Setting up the model#

Now with the basics complete we can add a driver, set the basic design variables, and setup the aviary model.

prob.add_driver('IPOPT', max_iter=50)
prob.add_design_variables()

prob.setup()

Setting Values#

The Mission.Design.RANGE value must be set to size some of Aviary’s subsystems. These subsystems, such as avionics, have increasing mass as Mission.Design.RANGE increases. These are first order approximations that come with aviary. But because of these, we must ensure that both pre-missions have the same Mission.Design.RANGE, even if the actual range flown buy each mission (target_rage) is different. Without this, the avoinics mass calculated in pre-mission would be different for the two missions, resulting in a different aircraft design, which is counter to what is intended with the multi-mission feature.

set_design_range method will access the phase_info for any missions supplied, find the largest target_rage, and set Aircraft1:Range to that value. This will ensure the avionics are designed similarly for both aircraft.

The total number of passengers (Aircraft.CrewPayload.Design.NUM_PASSENGERS) and the design number of passengers of each type (business, tourist, first class), help to define the passenger air conditioning subsystems and the passenger support mass (seats) respectively. Thus when these values are set equal in mission1 and mission2, we ensure the aircraft will be designed similarly. These settings were already set in the LargeSingleAisle2FLOPS model which we are using.

Note: It is good practice, but not required, to set `Aircraft.Design.LANDING_TO_TAKEOFF_MASS_RATIO` in Aviary Values to ensure consistent design of the landing gear for both missions. This combined with `Design.GROSS_MASS` helps to ensure that  `Aircraft.LandingGear.MAIN_GEAR_MASS` and `Aircraft.LandingGear.NOSE_GEAR_MASS` are the same for both missions. If `Aircraft.Design.LANDING_TO_TAKEOFF_MASS_RATIO` is not set, Landing Gear Masses will be caluclated based on`Mission.Summary.CRUISE_MACH` and Mission.Design.RANGE`. This is potentially problematic because `Mission.Summary.CRUISE_MACH` may not be set, and instead cruse mach may be optimized. In that case, `Mission.Summary.CRUISE_MACH` could vary between mission1 and mission2, which would then cascade into differeing `Aircraft.LandingGear.MAIN_GEAR_MASS` which causes the aircraft designs to diverge.
prob.set_design_range(('mission1', 'mission2'), range='Aircraft1:RANGE')

prob.run_aviary_problem()
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/utils/relevance.py:1295: 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: 
mission1.traj.phases.climb.rhs_all.solver_sub
mission1.traj.phases.cruise.rhs_all.solver_sub
mission1.traj.phases.descent.rhs_all.solver_sub
mission2.traj.phases.climb.rhs_all.solver_sub
mission2.traj.phases.cruise.rhs_all.solver_sub
mission2.traj.phases.descent.rhs_all.solver_sub

/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/solvers/linear/linear_rhs_checker.py:175: SolverWarning:DirectSolver in 'mission1.traj.phases.climb.indep_states' <class StateIndependentsComp>: 'rhs_checking' is active but no redundant adjoint dependencies were found, so caching has been disabled.
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/solvers/linear/linear_rhs_checker.py:175: SolverWarning:DirectSolver in 'mission1.traj.phases.cruise.indep_states' <class StateIndependentsComp>: 'rhs_checking' is active but no redundant adjoint dependencies were found, so caching has been disabled.
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/solvers/linear/linear_rhs_checker.py:175: SolverWarning:DirectSolver in 'mission1.traj.phases.descent.indep_states' <class StateIndependentsComp>: 'rhs_checking' is active but no redundant adjoint dependencies were found, so caching has been disabled.
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/solvers/linear/linear_rhs_checker.py:175: SolverWarning:DirectSolver in 'mission2.traj.phases.climb.indep_states' <class StateIndependentsComp>: 'rhs_checking' is active but no redundant adjoint dependencies were found, so caching has been disabled.
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/solvers/linear/linear_rhs_checker.py:175: SolverWarning:DirectSolver in 'mission2.traj.phases.cruise.indep_states' <class StateIndependentsComp>: 'rhs_checking' is active but no redundant adjoint dependencies were found, so caching has been disabled.
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/solvers/linear/linear_rhs_checker.py:175: SolverWarning:DirectSolver in 'mission2.traj.phases.descent.indep_states' <class StateIndependentsComp>: 'rhs_checking' is active but no redundant adjoint dependencies were found, so caching has been disabled.
Total number of variables............................:      192
                     variables with only lower bounds:        0
                variables with lower and upper bounds:      192
                     variables with only upper bounds:        0
Total number of equality constraints.................:      186
Total number of inequality constraints...............:      124
        inequality constraints with only lower bounds:        4
   inequality constraints with lower and upper bounds:      120
        inequality constraints with only upper bounds:        0
Number of Iterations....: 48

                                   (scaled)                 (unscaled)
Objective...............:   1.6910093869045730e+02    2.4853583654189668e+04
Dual infeasibility......:   1.5659192035133453e-07    1.4498296030885190e-05
Constraint violation....:   3.4504359108981673e-14    3.4504359108981673e-14
Variable bound violation:   3.0280702389973158e-08    3.0280702389973158e-08
Complementarity.........:   6.9331150702604885e-08    1.0189934882517956e-05
Overall NLP error.......:   1.5659192035133453e-07    1.4498296030885190e-05


Number of objective function evaluations             = 86
Number of objective gradient evaluations             = 41
Number of equality constraint evaluations            = 86
Number of inequality constraint evaluations          = 86
Number of equality constraint Jacobian evaluations   = 55
Number of inequality constraint Jacobian evaluations = 55
Number of Lagrangian Hessian evaluations             = 0
Total seconds in IPOPT       

Optimization Problem -- Optimization using pyOpt_sparse
================================================================================
    Objective Function: _objfunc


   Objectives
      Index  Name                           Value
          0  composite_objective     2.485358E+04

   Variables (c - continuous, i - integer, d - discrete)
      Index  Name                                       Type      Lower Bound            Value      Upper Bound     Status
          1  Aircraft1:SWEEP_0                             c     2.300000E+01     2.300000E+01     2.700000E+01          l
          3  mission1.traj.climb.t_duration_0              c     5.000000E-01     5.000000E-01     1.500000E+00          l
         66  mission1.traj.descent.t_duration_0            c     5.000000E-01     5.000000E-01     1.500000E+00          l
         98  mission2.traj.climb.t_duration_0              c     5.000000E-01     5.000000E-01     1.500000E+00          l
        161  mission2.traj.descent.t_duration_0            c     5.000000E-01     5.000000E-01     1.500000E+00          l

   Constraints (i - inequality, e - equality)
      Index  Name                                                          Type          Lower           Value           Upper    Status  Lagrange Multiplier (N/A)

                        = 48.091

EXIT: Optimal Solution Found.

Results#

The results of the Multi-mission Example are included in the data table and plots below.

From the table results we can see that the mission1 and mission2 have the same Mission.Design.GROSS_MASS, which has been promoted to Aircraft1:GROSS_MASS. However, the Mission.Summary.GROSS_MASS varies as expected because these represent “as-flown” values. The full passengers mission (mission1) has the higher Mission.Summary.GROSS_MASS. Consequently, the Mission.Summary.FUEL_BURNED for each mission is different, higher for mission1, as expected because this mission is carrying more mass for the same mission. Aircraft.Wing.SWEEP is the same for both missions, indicating that the aircraft wings have been designed similarly in both cases. We do not want to see different values for the wing design because it would mean that the two pre-mission systems are not mirroring eachother. If they were not the same it would mean we are designing two different aircraft.

The Aircraft.LandingGear.MAIN_GEAR_MASS and Aircraft.LandingGear.NOSE_GEAR_MASS masses were also displayed because they are sensitive to Aircraft.Design.LANDING_TO_TAKEOFF_MASS_RATIO. We expect these landing gear masses to be the same and they are which is good news for us and indicates that both pre-mission designs are mirroring eachother.

The Aircraft.Furnishings.MASS and Aircraft.CrewPayload.PASSENGER_SERVICE_MASS are displayed. These values represent the weight of the seats and the air conditioning system for the passengers. They are both the same which is what we expect to see.

The Aircraft.Avionics.MASS also match, indicating that the Mission.Design.RANGE for both aircraft has been properly set to the same value.

mission:design:gross_mass was unavailable. Perhapse it has been promoted to the problem level? mission:design:gross_mass was unavailable. Perhapse it has been promoted to the problem level?

mission1.aircraft:design:empty_mass (lbm), [87415.60555673] mission2.aircraft:design:empty_mass (lbm), [87415.60555673]

mission1.aircraft:landing_gear:main_gear_mass (lbm), [5767.0394946] mission2.aircraft:landing_gear:main_gear_mass (lbm), [5767.0394946]

mission1.aircraft:landing_gear:nose_gear_mass (lbm), [747.15266738] mission2.aircraft:landing_gear:nose_gear_mass (lbm), [747.15266738]

mission1.aircraft:design:landing_to_takeoff_mass_ratio (unitless), [0.84] mission2.aircraft:design:landing_to_takeoff_mass_ratio (unitless), [0.84]

mission1.aircraft:avionics:mass (lbm), [1281.74589387] mission2.aircraft:avionics:mass (lbm), [1281.74589387]

mission1.aircraft:furnishings:mass (lbm), [14690.33988] mission2.aircraft:furnishings:mass (lbm), [14690.33988]

mission1.aircraft:crew_and_payload:passenger_service_mass (lbm), [2524.47559296] mission2.aircraft:crew_and_payload:passenger_service_mass (lbm), [2524.47559296]

mission1.mission:summary:gross_mass (lbm), [157440.88610181] mission2.mission:summary:gross_mass (lbm), [120056.71107322]

mission1.mission:summary:fuel_burned (lbm), [27050.67106054] mission2.mission:summary:fuel_burned (lbm), [22916.49603196]

mission1.aircraft:crew_and_payload:passenger_mass (lbm), [26730.] mission2.aircraft:crew_and_payload:passenger_mass (lbm), [165.]

mission1.aircraft:crew_and_payload:passenger_payload_mass (lbm), [32400.] mission2.aircraft:crew_and_payload:passenger_payload_mass (lbm), [200.]

mission1.aircraft:crew_and_payload:cargo_mass (lbm), [4077.] mission2.aircraft:crew_and_payload:cargo_mass (lbm), [4077.]

mission1.aircraft:crew_and_payload:total_payload_mass (lbm), [36477.] mission2.aircraft:crew_and_payload:total_payload_mass (lbm), [4277.]

Objective Value (unitless): [25672.61271768] Aircraft1:GROSS_MASS (lbm) [157440.88610181] Aircraft1:SWEEP (deg) [23.]

In the graph below The Altitude, Drag force, Throttle command, and Mass of the mission1 (full passenger load) and mission2 (one passenger) are displayed. The mission2 shows a characteristic smaller mass throughout the flight as expected since we have just one passenger, and a slightly lower throttle profile to match, indicating the engine is not being pushed as hard to meet the demands of a lighter plane. Otherwise the missions themselves match, showing Mach, Distance, and Altitude all identical for every part of the mission. We did not allow the mach or altitude to be optimized for this mission so these results are not surprising.

Note: When comparing the graphs of the two missions, not all the graphs have the same scale.

Results