Models with External Subsystems#

In level 2, we have given simple examples of defining external subsystems in phase_info. The subsystems that we gave were all dummy subsystems and are not really used in simulation. Assume you have an external subsystem and you want to use it. Let us show you how to achieve this goal.

Installation of Aviary examples#

The Aviary team has provided a few external subsystems for you to use. These are included in the aviary/examples/external_subsystems directory. We’ll now discuss them here and show you how to use them.

Adding simple_weight subsystems#

Currently, there are a couple of examples: battery and simple_weight. Let us take a look at simple_weight first. As shown in this example, this is a simplified example of a component that computes a weight for the wing and horizontal tail. It does not provide realistic computations but rough estimates to Aircraft.Wing.MASS and Aircraft.HorizontalTail.MASS. When this external subsystem is added to your pre-mission phase, Aviary will compute these weights in its core subsystem as usual, but then the wing mass and tail mass values will be overridden by this external subsytem.

In level 2, we have briefly covered how to add external subsystems in phase_info. Alternatively, external subsystems (and any other new keys) can be added after a phase_info is loaded. Let us see how it works using the aircraft_for_bench_FwFm.csv model. First, we import this particular external subsystem.

Then add this external subsystem to pre_mission. That is all you need to do in addition to our traditional level 2 examples. Here is the complete run script,

from copy import deepcopy
from aviary.api import Aircraft
import aviary.api as av

from aviary.examples.external_subsystems.simple_weight.simple_weight_builder import WingWeightBuilder

# Max iterations set to 1 to reduce runtime of example
max_iter = 1
phase_info = deepcopy(av.default_height_energy_phase_info)
# Here we just add the simple weight system to only the pre-mission
phase_info['pre_mission']['external_subsystems'] = [WingWeightBuilder(name="wing_external")]

prob = av.AviaryProblem()

# Load aircraft and options data from user
# Allow for user overrides here
prob.load_inputs('models/test_aircraft/aircraft_for_bench_FwFm.csv', phase_info)

# Have checks for clashing user inputs
# Raise warnings or errors depending on how clashing the issues are
prob.check_and_preprocess_inputs()

prob.add_pre_mission_systems()

prob.add_phases()

prob.add_post_mission_systems()

# Link phases and variables
prob.link_phases()

prob.add_driver("SLSQP", max_iter)

prob.add_design_variables()

prob.add_objective()

prob.setup()

prob.set_initial_guesses()

prob.run_aviary_problem(suppress_solver_print=True)

print('Engine Mass', prob.get_val(av.Aircraft.Engine.MASS))
print('Wing Mass', prob.get_val(av.Aircraft.Wing.MASS))
print('Horizontal Tail Mass', prob.get_val(av.Aircraft.HorizontalTail.MASS))
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:897: OMDeprecationWarning:None: The method `add_polynomial_control` is deprecated and will be removed in Dymos 2.1. Please use `add_control` with the appropriate options to define a polynomial control.
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'climb': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'climb': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'cruise': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'cruise': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'descent': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'descent': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
The following variables have been overridden:
  'aircraft:design:touchdown_mass  152800  lbm
  'aircraft:engine:mass  [7400.]  lbm
  'aircraft:fins:mass  0  lbm
  'aircraft:fuel:auxiliary_fuel_capacity  0  lbm
  'aircraft:fuel:fuselage_fuel_capacity  0  lbm
  'aircraft:fuel:total_capacity  45694  lbm
  'aircraft:fuselage:planform_area  1578.24  ft**2
  'aircraft:fuselage:wetted_area  4158.62  ft**2
  'aircraft:horizontal_tail:wetted_area  592.65  ft**2
  'aircraft:landing_gear:main_gear_oleo_length  102  inch
  'aircraft:landing_gear:nose_gear_oleo_length  67  inch
  'aircraft:vertical_tail:wetted_area  581.13  ft**2
  'aircraft:wing:aspect_ratio  11.22091  unitless
  'aircraft:wing:control_surface_area  137  ft**2
  'aircraft:wing:wetted_area  2396.56  ft**2

The following variables have been overridden by an external subsystem:
  'aircraft:horizontal_tail:mass
  'aircraft:wing:mass

--- Constraint Report [traj] ---
    --- climb ---
        [path]    0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
    --- cruise ---
        [initial] 0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
        [final]   0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
    --- descent ---
        [path]    0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/solvers/linear/linear_rhs_checker.py:178: SolverWarning:DirectSolver in '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:178: SolverWarning:DirectSolver in 'traj.phases.descent.indep_states' <class StateIndependentsComp>: 'rhs_checking' is active but no redundant adjoint dependencies were found, so caching has been disabled.
Model viewer data has already been recorded for Driver.
Full total jacobian for problem 'problem' was computed 3 times, taking 0.5670585510000592 seconds.
Total jacobian shape: (137, 96) 


Jacobian shape: (137, 96)  (8.76% nonzero)
FWD solves: 13   REV solves: 0
Total colors vs. total size: 13 vs 96  (86.46% improvement)

Sparsity computed using tolerance: 1e-25
Time to compute sparsity:   0.5671 sec
Time to compute coloring:   0.0535 sec
Memory to compute coloring:   0.0000 MB
Coloring created on: 2024-11-04 17:19:37
Optimization terminated successfully    (Exit mode 0)
            Current function value: 2.5232658250996027
            Iterations: 7
            Function evaluations: 7
            Gradient evaluations: 7
Optimization Complete
-----------------------------------
Engine Mass [7400.]
Wing Mass [11100.]
Horizontal Tail Mass [5180.]
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/core/driver.py:143: OMDeprecationWarning:boolean evaluation of DriverResult is temporarily implemented to mimick the previous `failed` return behavior of run_driver.
Use the `success` attribute of the returned DriverResult object to test for successful driver completion.

Ignore the intermediate warning messages and you see the outputs at the end. Since this is a FLOPS mission and no objective is provided, we know that the objective is fuel_burned.

To see the outputs without external subsystem add-on, let us comment out the lines that add the wing weight builder and run the modified script:

# # Here we just add the simple weight system to only the pre-mission
# phase_info['pre_mission']['external_subsystems'] = [WingWeightBuilder(name="wing_external")]

# Max iterations set to 1 to reduce runtime of example
max_iter = 1
prob = av.AviaryProblem()

# Load aircraft and options data from user
# Allow for user overrides here
prob.load_inputs('models/test_aircraft/aircraft_for_bench_FwFm.csv', av.default_height_energy_phase_info)

# Have checks for clashing user inputs
# Raise warnings or errors depending on how clashing the issues are
prob.check_and_preprocess_inputs()

prob.add_pre_mission_systems()

prob.add_phases()

prob.add_post_mission_systems()

# Link phases and variables
prob.link_phases()

prob.add_driver("SLSQP", max_iter)

prob.add_design_variables()

prob.add_objective()

prob.setup()

prob.set_initial_guesses()

prob.run_aviary_problem(suppress_solver_print=True)

print('Engine Mass', prob.get_val(Aircraft.Engine.MASS))
print('Wing Mass', prob.get_val(Aircraft.Wing.MASS))
print('Horizontal Tail Mass', prob.get_val(Aircraft.HorizontalTail.MASS))
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:897: OMDeprecationWarning:None: The method `add_polynomial_control` is deprecated and will be removed in Dymos 2.1. Please use `add_control` with the appropriate options to define a polynomial control.
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'climb': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'climb': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'cruise': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'cruise': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'descent': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'descent': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
The following variables have been overridden:
  'aircraft:design:touchdown_mass  152800  lbm
  'aircraft:engine:mass  [7400.]  lbm
  'aircraft:fins:mass  0  lbm
  'aircraft:fuel:auxiliary_fuel_capacity  0  lbm
  'aircraft:fuel:fuselage_fuel_capacity  0  lbm
  'aircraft:fuel:total_capacity  45694  lbm
  'aircraft:fuselage:planform_area  1578.24  ft**2
  'aircraft:fuselage:wetted_area  4158.62  ft**2
  'aircraft:horizontal_tail:wetted_area  592.65  ft**2
  'aircraft:landing_gear:main_gear_oleo_length  102  inch
  'aircraft:landing_gear:nose_gear_oleo_length  67  inch
  'aircraft:vertical_tail:wetted_area  581.13  ft**2
  'aircraft:wing:aspect_ratio  11.22091  unitless
  'aircraft:wing:control_surface_area  137  ft**2
  'aircraft:wing:wetted_area  2396.56  ft**2

--- Constraint Report [traj] ---
    --- climb ---
        [path]    0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
    --- cruise ---
        [initial] 0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
        [final]   0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
    --- descent ---
        [path]    0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/solvers/linear/linear_rhs_checker.py:178: SolverWarning:DirectSolver in '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:178: SolverWarning:DirectSolver in '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/recorders/sqlite_recorder.py:226: UserWarning:The existing case recorder file, problem_history.db, is being overwritten.
Model viewer data has already been recorded for Driver.
Full total jacobian for problem 'problem2' was computed 3 times, taking 0.6510332589999734 seconds.
Total jacobian shape: (137, 96) 


Jacobian shape: (137, 96)  (7.79% nonzero)
FWD solves: 14   REV solves: 0
Total colors vs. total size: 14 vs 96  (85.42% improvement)

Sparsity computed using tolerance: 1e-25
Time to compute sparsity:   0.6510 sec
Time to compute coloring:   0.0522 sec
Memory to compute coloring:   0.0000 MB
Coloring created on: 2024-11-04 17:19:45
Optimization terminated successfully    (Exit mode 0)
            Current function value: 2.545387536225937
            Iterations: 7
            Function evaluations: 7
            Gradient evaluations: 7
Optimization Complete
-----------------------------------
Engine Mass [7400.]
Wing Mass [16514.72745221]
Horizontal Tail Mass [1783.98445016]
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/core/driver.py:143: OMDeprecationWarning:boolean evaluation of DriverResult is temporarily implemented to mimick the previous `failed` return behavior of run_driver.
Use the `success` attribute of the returned DriverResult object to test for successful driver completion.

As we see, the engine mass is not altered but wing mass and tail mass are changed dramatically. This is not surprising because our simple_weight subsystem is quite simple. Later on, we will show you a more realistic wing weight external subsystem.

Adding battery subsystem#

In the above example, there is no new Aviary variable added to Aviary and the external subsystem is added to pre-mission only. So, the subsystem is not very involved. We will see a more complicated example now. Before we move on, let us recall the steps in Aviary model building:

  • init

  • load_inputs

  • check_and_preprocess_inputs

  • add_pre_mission_systems

  • add_phases

  • add_post_mission_systems

  • link_phases

  • add_driver

  • add_design_variables

  • add_objective

  • setup

  • set_initial_guesses

  • run_aviary_problem

The steps in bold are related specifically to subsystems. So, almost all of the steps involve subsystems. As long as your external subsystem is built based on the guidelines, Aviary will take care of your subsystem.

The next example is the battery subsystem. The battery subsystem provides methods to define the battery subsystem’s states, design variables, fixed values, initial guesses, and mass names. It also provides methods to build OpenMDAO systems for the pre-mission and mission computations of the subsystem, to get the constraints for the subsystem, and to preprocess the inputs for the subsystem. This subsystem has its own set of variables. We will build an Aviary model with full phases (namely, climb, cruise and descent) and maximize the final total mass: Dynamic.Mission.MASS.

We also need BatteryBuilder along with battery related aircraft variables and build a new battery object. Now, add our new battery subsystem into each phase including pre-mission:

from aviary.examples.external_subsystems.battery.battery_builder import BatteryBuilder
from aviary.examples.external_subsystems.battery.battery_variables import Aircraft
from aviary.examples.external_subsystems.battery.battery_variable_meta_data import ExtendedMetaData

battery_builder = BatteryBuilder(include_constraints=False)

phase_info['pre_mission']['external_subsystems'] = [battery_builder]
phase_info['climb']['external_subsystems'] = [battery_builder]
phase_info['cruise']['external_subsystems'] = [battery_builder]
phase_info['descent']['external_subsystems'] = [battery_builder]

Start an Aviary problem and load in an aircraft input deck:

prob = av.AviaryProblem()

prob.load_inputs('models/test_aircraft/aircraft_for_bench_FwFm.csv',
                 phase_info, meta_data=ExtendedMetaData)
{'aircraft:blended_wing_body_design:num_bays': (0, 'unitless'), 'aircraft:crew_and_payload:mass_per_passenger': (180, 'lbm'), 'aircraft:crew_and_payload:num_business_class': (0, 'unitless'), 'aircraft:crew_and_payload:num_first_class': (11, 'unitless'), 'aircraft:crew_and_payload:num_passengers': (169, 'unitless'), 'aircraft:crew_and_payload:num_tourist_class': (158, 'unitless'), 'aircraft:crew_and_payload:passenger_mass_with_bags': (200, 'lbm'), 'aircraft:design:compute_htail_volume_coeff': (False, 'unitless'), 'aircraft:design:compute_vtail_volume_coeff': (False, 'unitless'), 'aircraft:design:part25_structural_category': (3, 'unitless'), 'aircraft:design:reserve_fuel_additional': (3000, 'lbm'), 'aircraft:design:reserve_fuel_fraction': (0, 'unitless'), 'aircraft:design:smooth_mass_discontinuities': (False, 'unitless'), 'aircraft:design:ulf_calculated_from_maneuver': (False, 'unitless'), 'aircraft:design:use_alt_mass': (False, 'unitless'), 'aircraft:electrical:has_hybrid_system': (False, 'unitless'), 'aircraft:engine:compute_propeller_installation_loss': (True, 'unitless'), 'aircraft:engine:constant_fuel_consumption': (0, 'lbm/h'), 'aircraft:engine:flight_idle_max_fraction': (1, 'unitless'), 'aircraft:engine:flight_idle_min_fraction': (0.08, 'unitless'), 'aircraft:engine:flight_idle_thrust_fraction': (0, 'unitless'), 'aircraft:engine:fuel_flow_scaler_constant_term': (0, 'unitless'), 'aircraft:engine:fuel_flow_scaler_linear_term': (0, 'unitless'), 'aircraft:engine:generate_flight_idle': (True, 'unitless'), 'aircraft:engine:geopotential_alt': (False, 'unitless'), 'aircraft:engine:has_propellers': (False, 'unitless'), 'aircraft:engine:ignore_negative_thrust': (False, 'unitless'), 'aircraft:engine:interpolation_method': ('slinear', 'unitless'), 'aircraft:engine:num_engines': (2, 'unitless'), 'aircraft:engine:num_fuselage_engines': (0, 'unitless'), 'aircraft:engine:num_propeller_blades': (0, 'unitless'), 'aircraft:engine:num_wing_engines': (2, 'unitless'), 'aircraft:engine:scale_mass': (True, 'unitless'), 'aircraft:engine:scale_performance': (True, 'unitless'), 'aircraft:engine:subsonic_fuel_flow_scaler': (1, 'unitless'), 'aircraft:engine:supersonic_fuel_flow_scaler': (1, 'unitless'), 'aircraft:engine:type': (<GASPEngineType.TURBOJET: 7>, 'unitless'), 'aircraft:engine:use_propeller_map': (False, 'unitless'), 'aircraft:engine:shaft_power_design': (1.0, 'kW'), 'aircraft:fins:num_fins': (0, 'unitless'), 'aircraft:fuel:num_tanks': (7, 'unitless'), 'aircraft:fuselage:aisle_width': (24, 'inch'), 'aircraft:fuselage:military_cargo_floor': (False, 'unitless'), 'aircraft:fuselage:num_aisles': (1, 'unitless'), 'aircraft:fuselage:num_fuselages': (1, 'unitless'), 'aircraft:fuselage:num_seats_abreast': (6, 'unitless'), 'aircraft:fuselage:seat_pitch': (29, 'inch'), 'aircraft:fuselage:seat_width': (20, 'inch'), 'aircraft:landing_gear:carrier_based': (False, 'unitless'), 'aircraft:landing_gear:drag_coefficient': (0.0, 'unitless'), 'aircraft:landing_gear:fixed_gear': (True, 'unitless'), 'aircraft:strut:dimensional_location_specified': (True, 'unitless'), 'aircraft:vertical_tail:num_tails': (1, 'unitless'), 'aircraft:wing:airfoil_technology': (1.92669766647637, 'unitless'), 'aircraft:wing:choose_fold_location': (True, 'unitless'), 'aircraft:wing:detailed_wing': (False, 'unitless'), 'aircraft:wing:flap_type': (<FlapType.DOUBLE_SLOTTED: 4>, 'unitless'), 'aircraft:wing:fold_dimensional_location_specified': (False, 'unitless'), 'aircraft:wing:has_fold': (False, 'unitless'), 'aircraft:wing:has_strut': (False, 'unitless'), 'aircraft:wing:load_distribution_control': (2, 'unitless'), 'aircraft:wing:loading_above_20': (True, 'unitless'), 'aircraft:wing:num_flap_segments': (2, 'unitless'), 'aircraft:wing:num_integration_stations': (50, 'unitless'), 'aircraft:wing:span_efficiency_reduction': (False, 'unitless'), 'mission:design:cruise_altitude': (35000, 'ft'), 'mission:design:rate_of_climb_at_top_of_climb': (0.0, 'ft/min'), 'mission:summary:fuel_flow_scaler': (1, 'unitless'), 'mission:takeoff:angle_of_attack_runway': (0.0, 'deg'), 'mission:takeoff:obstacle_height': (35.0, 'ft'), 'mission:takeoff:thrust_incidence': (0.0, 'deg'), 'mission:taxi:duration': (0.167, 'h'), 'mission:taxi:mach': (0.0001, 'unitless'), 'settings:verbosity': (<Verbosity.BRIEF: 1>, 'unitless'), 'INGASP.JENGSZ': (4, 'unitless'), 'test_mode': (False, 'unitless'), 'use_surrogates': (True, 'unitless'), 'mass_defect': (10000, 'lbm'), 'settings:problem_type': (<ProblemType.SIZING: 'sizing'>, 'unitless'), 'aircraft:air_conditioning:mass_scaler': (1, 'unitless'), 'aircraft:anti_icing:mass_scaler': (1, 'unitless'), 'aircraft:apu:mass_scaler': (1.1, 'unitless'), 'aircraft:avionics:mass_scaler': (1.2, 'unitless'), 'aircraft:canard:area': (0, 'ft**2'), 'aircraft:canard:aspect_ratio': (0, 'unitless'), 'aircraft:canard:thickness_to_chord': (0, 'unitless'), 'aircraft:crew_and_payload:baggage_mass_per_passenger': (45, 'lbm'), 'aircraft:crew_and_payload:cargo_container_mass_scaler': (1, 'unitless'), 'aircraft:crew_and_payload:flight_crew_mass_scaler': (1, 'unitless'), 'aircraft:crew_and_payload:misc_cargo': (0, 'lbm'), 'aircraft:crew_and_payload:non_flight_crew_mass_scaler': (1, 'unitless'), 'aircraft:crew_and_payload:num_flight_attendants': (3, 'unitless'), 'aircraft:crew_and_payload:num_flight_crew': (2, 'unitless'), 'aircraft:crew_and_payload:num_galley_crew': (0, 'unitless'), 'aircraft:crew_and_payload:passenger_service_mass_scaler': (1, 'unitless'), 'aircraft:crew_and_payload:wing_cargo': (0, 'lbm'), 'aircraft:design:base_area': (0, 'ft**2'), 'aircraft:design:empty_mass_margin_scaler': (0, 'unitless'), 'aircraft:design:lift_dependent_drag_coeff_factor': (0.909839381134961, 'unitless'), 'aircraft:design:touchdown_mass': (152800, 'lbm'), 'aircraft:design:subsonic_drag_coeff_factor': (1, 'unitless'), 'aircraft:design:supersonic_drag_coeff_factor': (1, 'unitless'), 'aircraft:design:zero_lift_drag_coeff_factor': (0.930890028006548, 'unitless'), 'aircraft:electrical:mass_scaler': (1.25, 'unitless'), 'aircraft:engine:additional_mass_fraction': (0, 'unitless'), 'aircraft:engine:data_file': ('models/engines/turbofan_28k.deck', 'unitless'), 'aircraft:engine:mass_scaler': (1.15, 'unitless'), 'aircraft:engine:mass': (7400, 'lbm'), 'aircraft:engine:reference_mass': (7400, 'lbm'), 'aircraft:engine:reference_sls_thrust': (28928.1, 'lbf'), 'aircraft:engine:scaled_sls_thrust': (28928.1, 'lbf'), 'aircraft:engine:thrust_reversers_mass_scaler': (0, 'unitless'), 'aircraft:engine:wing_locations': (array([0.26869218]), 'unitless'), 'aircraft:fins:area': (0, 'ft**2'), 'aircraft:fins:mass_scaler': (1, 'unitless'), 'aircraft:fins:mass': (0, 'lbm'), 'aircraft:fins:taper_ratio': (10, 'unitless'), 'aircraft:fuel:auxiliary_fuel_capacity': (0, 'lbm'), 'aircraft:fuel:density_ratio': (1, 'unitless'), 'aircraft:fuel:fuel_system_mass_scaler': (1, 'unitless'), 'aircraft:fuel:fuselage_fuel_capacity': (0, 'lbm'), 'aircraft:fuel:total_capacity': (45694, 'lbm'), 'aircraft:fuel:unusable_fuel_mass_scaler': (1, 'unitless'), 'aircraft:furnishings:mass_scaler': (1.1, 'unitless'), 'aircraft:fuselage:length': (128, 'ft'), 'aircraft:fuselage:mass_scaler': (1.05, 'unitless'), 'aircraft:fuselage:max_height': (13.17, 'ft'), 'aircraft:fuselage:max_width': (12.33, 'ft'), 'aircraft:fuselage:passenger_compartment_length': (85.5, 'ft'), 'aircraft:fuselage:planform_area': (1578.24, 'ft**2'), 'aircraft:fuselage:wetted_area_scaler': (1, 'unitless'), 'aircraft:fuselage:wetted_area': (4158.62, 'ft**2'), 'aircraft:horizontal_tail:area': (355, 'ft**2'), 'aircraft:horizontal_tail:aspect_ratio': (6, 'unitless'), 'aircraft:horizontal_tail:mass_scaler': (1.2, 'unitless'), 'aircraft:horizontal_tail:taper_ratio': (0.22, 'unitless'), 'aircraft:horizontal_tail:thickness_to_chord': (0.125, 'unitless'), 'aircraft:horizontal_tail:vertical_tail_fraction': (0, 'unitless'), 'aircraft:horizontal_tail:wetted_area_scaler': (1, 'unitless'), 'aircraft:horizontal_tail:wetted_area': (592.65, 'ft**2'), 'aircraft:hydraulics:mass_scaler': (1, 'unitless'), 'aircraft:hydraulics:system_pressure': (3000, 'psi'), 'aircraft:instruments:mass_scaler': (1.25, 'unitless'), 'aircraft:landing_gear:main_gear_mass_scaler': (1.1, 'unitless'), 'aircraft:landing_gear:main_gear_oleo_length': (102, 'inch'), 'aircraft:landing_gear:nose_gear_mass_scaler': (1, 'unitless'), 'aircraft:landing_gear:nose_gear_oleo_length': (67, 'inch'), 'aircraft:nacelle:avg_diameter': (7.94, 'ft'), 'aircraft:nacelle:avg_length': (12.3, 'ft'), 'aircraft:nacelle:mass_scaler': (1, 'unitless'), 'aircraft:nacelle:wetted_area_scaler': (1, 'unitless'), 'aircraft:paint:mass_per_unit_area': (0.037, 'lbm/ft**2'), 'aircraft:propulsion:engine_oil_mass_scaler': (1, 'unitless'), 'aircraft:propulsion:misc_mass_scaler': (1, 'unitless'), 'aircraft:vertical_tail:area': (284, 'ft**2'), 'aircraft:vertical_tail:aspect_ratio': (1.75, 'unitless'), 'aircraft:vertical_tail:mass_scaler': (1, 'unitless'), 'aircraft:vertical_tail:taper_ratio': (0.33, 'unitless'), 'aircraft:vertical_tail:thickness_to_chord': (0.1195, 'unitless'), 'aircraft:vertical_tail:wetted_area_scaler': (1, 'unitless'), 'aircraft:vertical_tail:wetted_area': (581.13, 'ft**2'), 'aircraft:wing:aeroelastic_tailoring_factor': (0, 'unitless'), 'aircraft:wing:area': (1370, 'ft**2'), 'aircraft:wing:aspect_ratio': (11.22091, 'unitless'), 'aircraft:wing:bending_mass_scaler': (1, 'unitless'), 'aircraft:wing:chord_per_semispan': ([0.31, 0.23, 0.084], 'unitless'), 'aircraft:wing:composite_fraction': (0.2, 'unitless'), 'aircraft:wing:control_surface_area': (137, 'ft**2'), 'aircraft:wing:control_surface_area_ratio': (0.1, 'unitless'), 'aircraft:wing:glove_and_bat': (134, 'ft**2'), 'aircraft:wing:input_station_dist': ([0, 0.2759, 0.9367], 'unitless'), 'aircraft:wing:load_fraction': (1, 'unitless'), 'aircraft:wing:load_path_sweep_dist': ([0, 22], 'deg'), 'aircraft:wing:mass_scaler': (1.23, 'unitless'), 'aircraft:wing:max_camber_at_70_semispan': (0, 'unitless'), 'aircraft:wing:misc_mass_scaler': (1, 'unitless'), 'aircraft:wing:shear_control_mass_scaler': (1, 'unitless'), 'aircraft:wing:span': (117.83, 'ft'), 'aircraft:wing:strut_bracing_factor': (0, 'unitless'), 'aircraft:wing:surface_ctrl_mass_scaler': (1, 'unitless'), 'aircraft:wing:sweep': (25, 'deg'), 'aircraft:wing:taper_ratio': (0.278, 'unitless'), 'aircraft:wing:thickness_to_chord_dist': ([0.145, 0.115, 0.104], 'unitless'), 'aircraft:wing:thickness_to_chord': (0.13, 'unitless'), 'aircraft:wing:ultimate_load_factor': (3.75, 'unitless'), 'aircraft:wing:var_sweep_mass_penalty': (0, 'unitless'), 'aircraft:wing:wetted_area_scaler': (1, 'unitless'), 'aircraft:wing:wetted_area': (2396.56, 'ft**2'), 'mission:constraints:max_mach': (0.785, 'unitless'), 'mission:design:gross_mass': (175400, 'lbm'), 'mission:design:range': (1906.0, 'NM'), 'mission:design:thrust_takeoff_per_eng': (28928.1, 'lbf'), 'mission:landing:lift_coefficient_max': (2, 'unitless'), 'mission:summary:cruise_mach': (0.785, 'unitless'), 'mission:takeoff:fuel_simple': (577, 'lbm'), 'mission:takeoff:lift_coefficient_max': (3, 'unitless'), 'mission:takeoff:lift_over_drag': (17.354, 'unitless'), 'settings:equations_of_motion': (<EquationsOfMotion.HEIGHT_ENERGY: 'height_energy'>, 'unitless'), 'settings:mass_method': (<LegacyCode.FLOPS: 'FLOPS'>, 'unitless'), 'mission:summary:gross_mass': (175400, 'lbm')}

Since this example contains new variables in the aircraft hierarchy, the metadata for those variables was added to an extended metadata dictionary. We need to pass that into load_inputs so that it can load susbsystem-specific inputs from the csv file.

In the battery subsystem, the type of battery cell we use is 18650. The option is not set in input_file, instead it is controlled by importing the correct battery map here.

We can then check our inputs:

# Have checks for clashing user inputs
# Raise warnings or errors depending on severity of the issues
prob.check_and_preprocess_inputs()

Checking in the setup function call#

Function setup can have an argument check with default value False. If we set it to True, it will cause a default set of checks to be run. So, instead of a simple prob.setup() call, let us do the following:

The following are a few check points printed on the command line:

INFO: checking system
INFO: checking solvers
INFO: checking dup_inputs
INFO: checking missing_recorders
prob.add_pre_mission_systems()

traj = prob.add_phases()

prob.add_post_mission_systems()

# Link phases and variables
prob.link_phases()

max_iter = 1
prob.add_driver("SLSQP", max_iter)

prob.add_design_variables()

prob.add_objective('mass')

prob.setup(check=True)

prob.set_initial_guesses()

prob.run_aviary_problem()

# user defined outputs
print('Battery MASS', prob.get_val(Aircraft.Battery.MASS))
print('Cell Max', prob.get_val(Aircraft.Battery.Cell.MASS))
masses_descent = prob.get_val('traj.descent.timeseries.mass', units='kg')
print(f"Final Descent Mass: {masses_descent[-1]}")

print('done')
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:897: OMDeprecationWarning:None: The method `add_polynomial_control` is deprecated and will be removed in Dymos 2.1. Please use `add_control` with the appropriate options to define a polynomial control.
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'climb': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'climb': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'cruise': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'cruise': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'descent': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'descent': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
The following variables have been overridden:
  'aircraft:design:touchdown_mass  152800  lbm
  'aircraft:engine:mass  [7400.]  lbm
  'aircraft:fins:mass  0  lbm
  'aircraft:fuel:auxiliary_fuel_capacity  0  lbm
  'aircraft:fuel:fuselage_fuel_capacity  0  lbm
  'aircraft:fuel:total_capacity  45694  lbm
  'aircraft:fuselage:planform_area  1578.24  ft**2
  'aircraft:fuselage:wetted_area  4158.62  ft**2
  'aircraft:horizontal_tail:wetted_area  592.65  ft**2
  'aircraft:landing_gear:main_gear_oleo_length  102  inch
  'aircraft:landing_gear:nose_gear_oleo_length  67  inch
  'aircraft:vertical_tail:wetted_area  581.13  ft**2
  'aircraft:wing:aspect_ratio  11.22091  unitless
  'aircraft:wing:control_surface_area  137  ft**2
  'aircraft:wing:wetted_area  2396.56  ft**2

--- Constraint Report [traj] ---
    --- climb ---
        [path]    0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
    --- cruise ---
        [initial] 0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
        [final]   0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
    --- descent ---
        [path]    0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/recorders/sqlite_recorder.py:226: UserWarning:The existing case recorder file, problem_history.db, is being overwritten.
INFO: checking out_of_order
INFO: checking system
INFO: checking solvers
INFO: checking dup_inputs
INFO: checking missing_recorders
WARNING: The Problem has no recorder of any kind attached
INFO: checking unserializable_options
INFO: checking comp_has_no_outputs
INFO: checking auto_ivc_warnings
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
Model viewer data has already been recorded for Driver.
INFO: checking out_of_order
INFO: checking system
INFO: checking solvers
INFO: checking dup_inputs
INFO: checking missing_recorders
WARNING: The Problem has no recorder of any kind attached
INFO: checking unserializable_options
INFO: checking comp_has_no_outputs
INFO: checking auto_ivc_warnings
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/error_checking/check_config.py:118: SetupWarning:Need to attach NonlinearBlockJac, NewtonSolver, or BroydenSolver to 'phases' when connecting components inside parallel groups
Full total jacobian for problem 'problem3' was computed 3 times, taking 1.317478637000022 seconds.
Total jacobian shape: (227, 188) 


Jacobian shape: (227, 188)  (3.55% nonzero)
FWD solves: 12   REV solves: 0
Total colors vs. total size: 12 vs 188  (93.62% improvement)

Sparsity computed using tolerance: 1e-25
Time to compute sparsity:   1.3175 sec
Time to compute coloring:   0.0977 sec
Memory to compute coloring:   0.0000 MB
Coloring created on: 2024-11-04 17:19:54
Optimization terminated successfully    (Exit mode 0)
            Current function value: -1.242978162586334
            Iterations: 15
            Function evaluations: 15
            Gradient evaluations: 15
Optimization Complete
-----------------------------------
Battery MASS [0.10100856]
Cell Max [0.045]
Final Descent Mass: [62148.90812932]
done
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/openmdao/core/driver.py:143: OMDeprecationWarning:boolean evaluation of DriverResult is temporarily implemented to mimick the previous `failed` return behavior of run_driver.
Use the `success` attribute of the returned DriverResult object to test for successful driver completion.

More on outputs#

We are done with our model. For our current example, let us add a few more lines after the aviary run:

print('Battery MASS', prob.get_val(Aircraft.Battery.MASS))
print('Cell Max', prob.get_val(Aircraft.Battery.Cell.MASS))
Battery MASS [0.10100856]
Cell Max [0.045]

Since our objective is mass, we want to print the value of Dynamic.Mission.Mass. Remember, we have imported Dynamic from aviary.variable_info.variables for this purpose.

So, we have to print the final mass in a different way. Keep in mind that we have three phases in the mission and that final mass is our objective. So, we can get the final mass of the descent phase instead. Let us try this approach. Let us comment out the print statement of final mass (and the import of Dynamic), then add the following lines:

masses_descent = prob.get_val('traj.descent.timeseries.mass', units='kg')
print(f"Final Descent Mass: {masses_descent[-1]}")
Final Descent Mass: [62148.90812932]

More on objectives#

Now, let us change our objective to battery state of charge after the climb phase. So, comment out prob.add_objective('mass') and add the following line right after:

prob.model.add_objective(
    f'traj.climb.states:{Mission.Battery.STATE_OF_CHARGE}', index=-1, ref=-1)

In the above, index=-1 means the end of climb phase and ref=-1 means that we want to maximize the state of charge at the end of climb phase. Once again, we are unable to print battery state of charge as we did with battery mass and battery cell mass. We will use the same approach to get mass. In fact, we have prepared for this purpose by setting up time series of climb and cruise phases as well. All we need to do is to add the following lines:

soc_cruise = prob.get_val(
    'traj.climb.timeseries.mission:battery:state_of_charge')
print(f"State of Charge: {soc_cruise[-1]}")

Now you get a new output:

State of Charge: [0.97458496]

The check_partials function#

In order to make sure that your model computes all the derivatives correctly, OpenMDAO provides a method called check_partials which checks partial derivatives comprehensively for all Components in your model. You should check your partial derivatives before integrating your external subsystem. This is a good practice to ensure that your model is working correctly and can be used in an optimization context.

Adding an OpenAeroStruct wingbox external subsystem#

OpenAeroStruct (OAS) is a lightweight tool that performs aerostructural optimization using OpenMDAO. This is an example that shows you how to use an existing external package with Aviary.

Installation of OpenAeroStruct#

We would like to have easy access to the examples and source code. So we install OpenAeroStruct by cloning the OpenAeroStruct repository. We show you how to do the installation on Linux. Assume you want to install it at ~/$USER/workspace. Do the following:

cd ~/$USER/workspace
git clone https://github.com/mdolab/OpenAeroStruct.git
~/$USER/workspace/OpenAeroStruct
pip install -e .

If everything runs smoothly, you should see something like:

Successfully installed openaerostruct

Most of the packages that OpenAeroStruct depends on are installed already (see here). For our example, we need ambiance and an optional package: OpenVSP.

To install ambiance, do the following:

pip install ambiance

You should see something like:

Installing collected packages: ambiance
Successfully installed ambiance-1.3.1

Note

You must ensure that the Python version in your environment matches the Python used to compile OpenVSP. You must install OpenVSP on your Linux box yourself.

To check your installation of OpenVSP is successful, please run

(av1)$ python openaerostruct/tests/test_vsp_aero_analysis.py

Windows users should visit OpenVSP and follow the instruction there.

Understanding the OpenAeroStruct Example#

The OpenAeroStruct example is explained in detail in Using Aviary and OpenAeroStruct Together.

Running the OpenAeroStruct Example#

We are ready to run this example. First, we create an OASWingWeightBuilder instance.

import numpy as np
import openmdao.api as om
import aviary.api as av
from aviary.examples.external_subsystems.OAS_weight.OAS_wing_weight_builder import OASWingWeightBuilder

wing_weight_builder = OASWingWeightBuilder()

Let’s add a few phases in the mission. In particular, let’s add the object we just created as an external subsystem to pre_mission. We are only adding to pre_mission here as the OpenAeroStruct design is only done in the sizing portion of pre-mission and doesn’t need to be called during the mission.

# Load the phase_info and other common setup tasks
phase_info = {
    'climb_1': {
        'subsystem_options': {'core_aerodynamics': {'method': 'computed'}},
        'user_options': {
            'optimize_mach': False,
            'optimize_altitude': False,
            'polynomial_control_order': 1,
            'num_segments': 5,
            'order': 3,
            'solve_for_distance': False,
            'initial_mach': (0.2, 'unitless'),
            'final_mach': (0.72, 'unitless'),
            'mach_bounds': ((0.18, 0.74), 'unitless'),
            'initial_altitude': (0.0, 'ft'),
            'final_altitude': (32000.0, 'ft'),
            'altitude_bounds': ((0.0, 34000.0), 'ft'),
            'throttle_enforcement': 'path_constraint',
            'fix_initial': True,
            'constrain_final': False,
            'fix_duration': False,
            'initial_bounds': ((0.0, 0.0), 'min'),
            'duration_bounds': ((64.0, 192.0), 'min'),
        },
        'initial_guesses': {'time': ([0, 128], 'min')},
    },
    'climb_2': {
        'subsystem_options': {'core_aerodynamics': {'method': 'computed'}},
        'user_options': {
            'optimize_mach': False,
            'optimize_altitude': False,
            'polynomial_control_order': 1,
            'num_segments': 5,
            'order': 3,
            'solve_for_distance': False,
            'initial_mach': (0.72, 'unitless'),
            'final_mach': (0.72, 'unitless'),
            'mach_bounds': ((0.7, 0.74), 'unitless'),
            'initial_altitude': (32000.0, 'ft'),
            'final_altitude': (34000.0, 'ft'),
            'altitude_bounds': ((23000.0, 38000.0), 'ft'),
            'throttle_enforcement': 'boundary_constraint',
            'fix_initial': False,
            'constrain_final': False,
            'fix_duration': False,
            'initial_bounds': ((64.0, 192.0), 'min'),
            'duration_bounds': ((56.5, 169.5), 'min'),
        },
        'initial_guesses': {'time': ([128, 113], 'min')},
    },
    'descent_1': {
        'subsystem_options': {'core_aerodynamics': {'method': 'computed'}},
        'user_options': {
            'optimize_mach': False,
            'optimize_altitude': False,
            'polynomial_control_order': 1,
            'num_segments': 5,
            'order': 3,
            'solve_for_distance': False,
            'initial_mach': (0.72, 'unitless'),
            'final_mach': (0.36, 'unitless'),
            'mach_bounds': ((0.34, 0.74), 'unitless'),
            'initial_altitude': (34000.0, 'ft'),
            'final_altitude': (500.0, 'ft'),
            'altitude_bounds': ((0.0, 38000.0), 'ft'),
            'throttle_enforcement': 'path_constraint',
            'fix_initial': False,
            'constrain_final': True,
            'fix_duration': False,
            'initial_bounds': ((120.5, 361.5), 'min'),
            'duration_bounds': ((29.0, 87.0), 'min'),
        },
        'initial_guesses': {'time': ([241, 58], 'min')},
    },
    'post_mission': {
        'include_landing': False,
        'constrain_range': True,
        'target_range': (1800., 'nmi'),
    },
}

phase_info['pre_mission'] = {'include_takeoff': False, 'optimize_mass': True}
phase_info['pre_mission']['external_subsystems'] = [wing_weight_builder]

We can now create an Aviary problem, load in an aircraft input deck, and do routine input checks:

aircraft_definition_file = 'models/test_aircraft/aircraft_for_bench_FwFm.csv'
make_plots = False
max_iter = 0
optimizer = 'SNOPT'

prob = av.AviaryProblem()

prob.load_inputs(aircraft_definition_file, phase_info)
prob.check_and_preprocess_inputs()
prob.add_pre_mission_systems()
prob.add_phases()
prob.add_post_mission_systems()
prob.link_phases()
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:897: OMDeprecationWarning:None: The method `add_polynomial_control` is deprecated and will be removed in Dymos 2.1. Please use `add_control` with the appropriate options to define a polynomial control.

Next we select the driver and call setup on the problem:

driver = prob.driver = om.pyOptSparseDriver()
driver.options["optimizer"] = optimizer
driver.declare_coloring()
driver.opt_settings["Major iterations limit"] = max_iter
driver.opt_settings["Major optimality tolerance"] = 1e-4
driver.opt_settings["Major feasibility tolerance"] = 1e-5
driver.opt_settings["iSumm"] = 6

prob.add_design_variables()
prob.add_objective()
prob.setup()
prob.set_initial_guesses()
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'climb_1': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'climb_1': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'climb_2': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'climb_2': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'mach' in phase 'descent_1': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:2323: RuntimeWarning: Invalid options for non-optimal control 'altitude' in phase 'descent_1': lower, upper, ref
  warnings.warn(f"Invalid options for non-optimal control '{name}' in phase "
The following variables have been overridden:
  'aircraft:design:touchdown_mass  152800  lbm
  'aircraft:engine:mass  [7400.]  lbm
  'aircraft:fins:mass  0  lbm
  'aircraft:fuel:auxiliary_fuel_capacity  0  lbm
  'aircraft:fuel:fuselage_fuel_capacity  0  lbm
  'aircraft:fuel:total_capacity  45694  lbm
  'aircraft:fuselage:planform_area  1578.24  ft**2
  'aircraft:fuselage:wetted_area  4158.62  ft**2
  'aircraft:horizontal_tail:wetted_area  592.65  ft**2
  'aircraft:landing_gear:main_gear_oleo_length  102  inch
  'aircraft:landing_gear:nose_gear_oleo_length  67  inch
  'aircraft:vertical_tail:wetted_area  581.13  ft**2
  'aircraft:wing:aspect_ratio  11.22091  unitless
  'aircraft:wing:control_surface_area  137  ft**2
  'aircraft:wing:wetted_area  2396.56  ft**2

The following variables have been overridden by an external subsystem:
  'aircraft:wing:mass

--- Constraint Report [traj] ---
    --- climb_1 ---
        [path]    0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
    --- climb_2 ---
        [initial] 0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
        [final]   0.0000e+00 <= throttle <= 1.0000e+00  [unitless]
    --- descent_1 ---
        [path]    0.0000e+00 <= throttle <= 1.0000e+00  [unitless]

Now we need to set some OpenAeroStruct-specific parameters before running Aviary:

OAS_sys = 'pre_mission.wing_weight.aerostructures.'
prob.set_val(OAS_sys + 'box_upper_x', np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34, 0.35, 0.36, 0.37, 0.38, 0.39, 0.4, 0.41, 0.42, 0.43, 0.44, 0.45, 0.46, 0.47, 0.48, 0.49, 0.5, 0.51, 0.52, 0.53, 0.54, 0.55, 0.56, 0.57, 0.58, 0.59, 0.6]), units='unitless')
prob.set_val(OAS_sys + 'box_lower_x', np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34, 0.35, 0.36, 0.37, 0.38, 0.39, 0.4, 0.41, 0.42, 0.43, 0.44, 0.45, 0.46, 0.47, 0.48, 0.49, 0.5, 0.51, 0.52, 0.53, 0.54, 0.55, 0.56, 0.57, 0.58, 0.59, 0.6]), units='unitless')
prob.set_val(OAS_sys + 'box_upper_y', np.array([ 0.0447,  0.046,  0.0472,  0.0484,  0.0495,  0.0505,  0.0514,  0.0523,  0.0531,  0.0538, 0.0545,  0.0551,  0.0557, 0.0563,  0.0568, 0.0573,  0.0577,  0.0581,  0.0585,  0.0588,  0.0591,  0.0593,  0.0595,  0.0597,  0.0599,  0.06,    0.0601,  0.0602,  0.0602,  0.0602,  0.0602,  0.0602,  0.0601,  0.06,    0.0599,  0.0598,  0.0596,  0.0594,  0.0592,  0.0589,  0.0586,  0.0583,  0.058,   0.0576,  0.0572,  0.0568,  0.0563,  0.0558,  0.0553,  0.0547,  0.0541]), units='unitless')
prob.set_val(OAS_sys + 'box_lower_y', np.array([-0.0447, -0.046, -0.0473, -0.0485, -0.0496, -0.0506, -0.0515, -0.0524, -0.0532, -0.054, -0.0547, -0.0554, -0.056, -0.0565, -0.057, -0.0575, -0.0579, -0.0583, -0.0586, -0.0589, -0.0592, -0.0594, -0.0595, -0.0596, -0.0597, -0.0598, -0.0598, -0.0598, -0.0598, -0.0597, -0.0596, -0.0594, -0.0592, -0.0589, -0.0586, -0.0582, -0.0578, -0.0573, -0.0567, -0.0561, -0.0554, -0.0546, -0.0538, -0.0529, -0.0519, -0.0509, -0.0497, -0.0485, -0.0472, -0.0458, -0.0444]), units='unitless')
prob.set_val(OAS_sys + 'twist_cp', np.array([-6., -6., -4., 0.]), units='deg')
prob.set_val(OAS_sys + 'spar_thickness_cp', np.array([0.004, 0.005, 0.008, 0.01]), units='m')
prob.set_val(OAS_sys + 'skin_thickness_cp', np.array([0.005, 0.01, 0.015, 0.025]), units='m')
prob.set_val(OAS_sys + 't_over_c_cp', np.array([0.08, 0.08, 0.10, 0.08]), units='unitless')
prob.set_val(OAS_sys + 'airfoil_t_over_c', 0.12, units='unitless')
prob.set_val(OAS_sys + 'fuel', 40044.0, units='lbm')
prob.set_val(OAS_sys + 'fuel_reserve', 3000.0, units='lbm')
prob.set_val(OAS_sys + 'CD0', 0.0078, units='unitless')
prob.set_val(OAS_sys + 'cruise_Mach', 0.785, units='unitless')
prob.set_val(OAS_sys + 'cruise_altitude', 11303.682962301647, units='m')
prob.set_val(OAS_sys + 'cruise_range', 3500, units='nmi')
prob.set_val(OAS_sys + 'cruise_SFC', 0.53 / 3600, units='1/s')
prob.set_val(OAS_sys + 'engine_mass', 7400, units='lbm')
prob.set_val(OAS_sys + 'engine_location', np.array([25, -10.0, 0.0]), units='m')

We are now ready to run Aviary on this model. Note that there are multiple numbers of optimization loops that are output from this run even though we have set max_iter to 0. This is because OpenAeroStruct has an optimization process internally. In order to shorten the runtime, we have set run_driver = False. This means that we will not run optimization but run model.

Finally, we print the newly computed wing mass:

print('wing mass = ',prob.model.get_val(av.Aircraft.Wing.MASS, units='lbm'))
wing mass =  [2.20462262]

The result is comparable to the output without OpenAeroStruct external subsystem.