Externally Computed Polars

Externally Computed Polars#

This example shows how to build, using the level-2 interface, an aviary model that includes an external susbsystem that computes a lift and drag polar and passes them into the mission aerodynamics for a 3-phase mission (climb, cruise, descent). During the mission, Aviary will interpolate on the computed polars to compute actual lift and drag for a given flight condition.

We start with the assumption that we have an external component called ExternalAero that can compute the lift and drag at any given altitude, mach number, and angle of attack. The details of such a component may be highly complicated and not important for the purposes of this example. We will be using a structured grid, which assumes the data table is regularly spaced in all dimensions. We want to compute lift and drag over a grid of altitudes (in ‘ft’), mach numbers, and angles of attack given by:

Altitude (ft) [    0.  3000.  6000.  9000. 12000. 15000. 18000. 21000. 24000. 27000.
 30000. 33000. 36000. 38000. 42000.]
Mach [0.   0.2  0.4  0.5  0.6  0.7  0.75 0.8  0.85 0.9 ]
Angle of Attack (deg) [-2.  0.  2.  4.  6.  8. 10.]
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/pyoptsparse/pyOpt_MPI.py:68: UserWarning: mpi4py could not be imported. mpi4py is required to use the parallel gradient analysis and parallel objective analysis for non-gradient based optimizers. Continuing using a dummy MPI module from pyOptSparse.
  warnings.warn(warn)

In a structured grid, interpolation data must be present for every combination of inputs. In other words, our ExternalAero component must run a full factorial of points spanning those 3 variables. The Aviary variable hierarchy includes two variables for the polars: Aircraft.Design.LIFT_POLAR, and Aircraft.Design.DRAG_POLAR. The data in each of these polars should be a n x m x k numpy array, where n is the number of altitudes, m is the number of mach numbers, and k is the number of angles of attack. The ExternalAero will need to compute these values and place them into an array of this shape.

If use of a structured grid is not desirable, then the data does not need to meet these formatting requirements. In that case, the data table does not have to be regularly spaced, and each variable (Altitude, Mach, angle_of_attack, LIFT_POLAR, and DRAG_POLAR) must be 1-dimensional numpy arrays of equal length.

Using the level-2 interface, we create a builder for our external ExternalAero subsystem. In this example, the component produces outputs drag_table and lift_table, but we can specify an alias to Aircraft.Design.DRAG_POLAR and Aircraft.Design.LIFT_POLAR respectively. It is important that we inherit from the AerodynamicsBuilderBase to let Aviary know this is builder produces aerodynamics components. Some mission analysis methods require special handling of aerodynamics components that will not occur if we skip this step.

class ExternalAeroBuilder(av.AerodynamicsBuilderBase):
    """
    An example subsystem builder that adds an external aerodynamics component

    Parameters
    ----------
    aero_data : NamedValues
        Altitude, Mach number, and angle of attack data, all in ascending order.
    """

    def __init__(self, name='aero', altitude=None, mach=None,
                 angle_of_attack=None):
        super().__init__(name)
        self.altitude = altitude
        self.mach = mach
        self.angle_of_attack = angle_of_attack
        
    def build_pre_mission(self, aviary_inputs):
        """
        Build an OpenMDAO system for the pre-mission computations of the subsystem.

        Returns
        -------
        pre_mission_sys : openmdao.core.Group
            An OpenMDAO group containing all computations that need to happen in
            the pre-mission part of the Aviary problem. This includes sizing, design,
            and other non-mission parameters.
        """
        aero_group = om.Group()
        aero = ExternalAero(altitude=self.altitude, mach=self.mach, angle_of_attack=self.angle_of_attack)
        aero_group.add_subsystem(
            'premission_aero', 
            aero,
            promotes_inputs=['*'],
            promotes_outputs=[
                ('drag_table', Aircraft.Design.DRAG_POLAR),
                ('lift_table', Aircraft.Design.LIFT_POLAR)
            ]
        )
        return aero_group

Notice that we have passed the altitude, Mach, and angle of attack arrays into the builder so that the ExternalAero component can use them as instantiation arguments.

Next, we add the builder to our phase_info as usual. We are using a single-aisle commercial transport aircraft and mission.

phase_info = av.default_height_energy_phase_info.copy()

external_aero = ExternalAeroBuilder(name='external_aero',
                                    altitude=altitude, mach=mach, angle_of_attack=angle_of_attack)

phase_info['pre_mission']['external_subsystems'] = [external_aero]

Next, the existing mission phases need to be given the information they need to set up our aerodynamics analysis using phase_info. We use the solved_alpha method of Aviary’s included aerodynamics for this, which can accept the input passed from our external subsystem. Since we are using Aviary’s built-in aerodynamics methods, we use the default name “core_aerodynamics”. Don’t forget to update the subsystem_options for each phase. We must specify the method, the aero_data that contains our altitude, Mach, and angle of attack data, as well as the connect_training_data flag to denote we are passing our drag polars via openMDAO connections.

subsystem_options = {'method': 'solved_alpha',
                     'aero_data': aero_data,
                     'connect_training_data': True}

phase_info['climb']['subsystem_options'] = {'core_aerodynamics': subsystem_options}
phase_info['cruise']['subsystem_options'] = {'core_aerodynamics': subsystem_options}
phase_info['descent']['subsystem_options'] = {'core_aerodynamics': subsystem_options}

Finally, we can instantiate the AviaryProblem like normal. However, we need to tell Aviary the size of our lift and drag polars so that it can allocate the right shape for the connection.

from aviary.utils.functions import get_aviary_resource_path

input_file = get_aviary_resource_path("models/test_aircraft/aircraft_for_bench_GwFm.csv")

prob = av.AviaryProblem()
prob.load_inputs(input_file, phase_info)

# Preprocess inputs
prob.check_and_preprocess_inputs()

# Add correctly-sized polar to aviary_inputs so that input components are sized correctly.
shape = (altitude.size, mach.size, angle_of_attack.size)

prob.aviary_inputs.set_val(Aircraft.Design.LIFT_POLAR, np.zeros(shape), units='unitless')
prob.aviary_inputs.set_val(Aircraft.Design.DRAG_POLAR, np.zeros(shape), units='unitless')
/home/runner/work/Aviary/Aviary/aviary/utils/process_input_decks.py:175: UserWarning: Variable 'aircraft:engine:engine_mass_specific' is not in meta_data nor in 'guess_names'. It will be ignored.
  warnings.warn(
/usr/share/miniconda/envs/test/lib/python3.12/site-packages/dymos/phase/phase.py:902: 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:2328: 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:2328: 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:2328: 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:2328: 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:2328: 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:2328: 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:lift_curve_slope
  'aircraft:fuel:auxiliary_fuel_capacity
  'aircraft:fuel:total_capacity
  'aircraft:fuel:wing_volume_geometric_max
  'aircraft:fuselage:avg_diameter
  'aircraft:fuselage:length
  'aircraft:fuselage:mass
  'aircraft:fuselage:planform_area
  'aircraft:fuselage:wetted_area
  'aircraft:horizontal_tail:wetted_area
  'aircraft:nacelle:avg_diameter
  'aircraft:nacelle:avg_length
  'aircraft:nacelle:fineness
  'aircraft:nacelle:surface_area
  'aircraft:vertical_tail:wetted_area
  'aircraft:wing:area
  'aircraft:wing:aspect_ratio
  'aircraft:wing:average_chord
  'aircraft:wing:span
  'aircraft:wing:thickness_to_chord_unweighted
  'aircraft:wing:ultimate_load_factor
  'aircraft:wing:wetted_area

--- 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]
=============================================================
pre_mission.core_subsystems.core_mass.wing_mass.isolated_mass
=============================================================
NL: Newton 0 ; 1709.43762 1
NL: Newton 1 ; 0.0712506333 4.1680745e-05
NL: Newton 2 ; 1.23691279e-10 7.23578786e-14
NL: Newton Converged

===============================================
pre_mission.core_subsystems.core_mass.fuel_mass
===============================================
Warning: req_fuel_mass > max_wingfuel_mass, adding a body tank
Warning: req_fuel_mass > max_wingfuel_mass, adding a body tank
NL: Newton 0 ; 45744.3839 1
Warning: req_fuel_mass > max_wingfuel_mass, adding a body tank
Warning: req_fuel_mass > max_wingfuel_mass, adding a body tank
NL: Newton 1 ; 10.8955797 0.000238183985
Warning: req_fuel_mass > max_wingfuel_mass, adding a body tank
Warning: req_fuel_mass > max_wingfuel_mass, adding a body tank
NL: Newton 2 ; 0 0
NL: Newton Converged

=========================
traj.phases.climb.rhs_all
=========================
+  
+  ===========================================
+  traj.phases.climb.rhs_all.core_aerodynamics
+  ===========================================
+  NL: Newton 0 ; 2.59001986 1
+  NL: Newton 1 ; 0.000434550464 0.000167778816
+  NL: Newton 2 ; 9.81087013e-11 3.7879517e-11
+  NL: Newton Converged
NL: Newton 0 ; 0.0742154392 1
+  
+  ===========================================
+  traj.phases.climb.rhs_all.core_aerodynamics
+  ===========================================
+  NL: Newton 0 ; 3.46518961e-15 1
+  NL: Newton Converged
NL: Newton 1 ; 1.44208449e-05 0.000194310578
+  
+  ===========================================
+  traj.phases.climb.rhs_all.core_aerodynamics
+  ===========================================
+  NL: Newton 0 ; 4.12741161e-15 1
+  NL: Newton Converged
NL: Newton 2 ; 4.1275495e-15 5.56157794e-14
NL: Newton Converged

===============================
traj.phases.cruise.indep_states
===============================
NL: Newton Converged in 0 iterations

==========================
traj.phases.cruise.rhs_all
==========================
+  
+  ============================================
+  traj.phases.cruise.rhs_all.core_aerodynamics
+  ============================================
+  NL: Newton 0 ; 2.18733565 1
+  NL: Newton 1 ; 0.000368185841 0.000168326174
+  NL: Newton 2 ; 1.26385647e-11 5.7780637e-12
+  NL: Newton Converged
NL: Newton 0 ; 0.0114704127 1
+  
+  ============================================
+  traj.phases.cruise.rhs_all.core_aerodynamics
+  ============================================
+  NL: Newton 0 ; 1.0215403e-15 1
+  NL: Newton Converged
NL: Newton 1 ; 5.59006288e-07 4.87346274e-05
+  
+  ============================================
+  traj.phases.cruise.rhs_all.core_aerodynamics
+  ============================================
+  NL: Newton 0 ; 7.63386316e-16 1
+  NL: Newton Converged
NL: Newton 2 ; 7.63522833e-16 6.65645478e-14
NL: Newton Converged

================================
traj.phases.descent.indep_states
================================
NL: Newton Converged in 0 iterations

===========================
traj.phases.descent.rhs_all
===========================
+  
+  =============================================
+  traj.phases.descent.rhs_all.core_aerodynamics
+  =============================================
+  NL: Newton 0 ; 2.25049651 1
+  NL: Newton 1 ; 0.000216424917 9.61676306e-05
+  NL: Newton 2 ; 1.97108295e-11 8.75843595e-12
+  NL: Newton Converged
NL: Newton 0 ; 0.123935965 1
+  
+  =============================================
+  traj.phases.descent.rhs_all.core_aerodynamics
+  =============================================
+  NL: Newton 0 ; 5.4603626e-16 1
+  NL: Newton Converged
NL: Newton 1 ; 0.00368270888 0.0297146101
+  
+  =============================================
+  traj.phases.descent.rhs_all.core_aerodynamics
+  =============================================
+  NL: Newton 0 ; 5.70316273e-16 1
+  NL: Newton Converged
NL: Newton 2 ; 1.80675877e-05 0.000145781635
+  
+  =============================================
+  traj.phases.descent.rhs_all.core_aerodynamics
+  =============================================
+  NL: Newton 0 ; 3.0800599e-16 1
+  NL: Newton Converged
NL: Newton 3 ; 3.08104732e-16 2.48599937e-15
NL: Newton Converged