Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for external aero in mission. #645

Merged
merged 9 commits into from
Jan 22, 2025
10 changes: 7 additions & 3 deletions aviary/docs/user_guide/aerodynamics.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"- `computed`: uses regression-based techniques to estimate lift and drag\n",
"- `low_speed`: for use in detailed takeoff analysis, and includes high-lift devices and considers angle-of-attack\n",
"- `tabular`: allows the user to substitute the lift and drag coefficient calculations in `computed` with data tables\n",
"- `external`: disables Aviary's core aerodynamics computation, intended for use with external subsystems to replace all aerodynamic calculations.\n",
"\n",
"### Computed Aerodynamics\n",
"The FLOPS based aerodynamics subsystem uses a modified version of algorithms from the EDET (Empirical Drag Estimation Technique) program [^edet] to internally compute drag polars. FLOPS improvements to EDET as implemented in Aviary include smoothing of drag polars, more accurate Reynolds number calculations, and use of the Sommer and Short T' method [^tprime] for skin friction calculations.\n",
Expand All @@ -124,7 +125,10 @@
"- The lift-dependent drag coefficient table must include Mach number and lift coefficient as independent variables.\n",
"- The zero-lift drag coefficient table must include altitude and Mach number as independent variables.\n",
"\n",
"Tabular aerodynamics uses Aviary's [data_interpolator_builder](../_srcdocs/packages/utils/data_interpolator_builder) interface. This component is unique as it requires two data tables to be provided. All configuration options, such as the choice to use a structured metamodel or training data, are applied to both tables."
"Tabular aerodynamics uses Aviary's [data_interpolator_builder](../_srcdocs/packages/utils/data_interpolator_builder) interface. This component is unique as it requires two data tables to be provided. All configuration options, such as the choice to use a structured metamodel or training data, are applied to both tables.\n",
"\n",
"### External Aerodynamics\n",
"Selecting the `external` aerodynamics method disables Aviary's core aerodynamics group. This allows for external subsystems to completely replace these calculations."
]
},
{
Expand All @@ -142,7 +146,7 @@
"cab = CoreAerodynamicsBuilder(code_origin=LegacyCode.FLOPS)\n",
"# here we are only checking that the CoreAerodynamicsBuilder has a build_mission for a given method\n",
"# we know this will fail when it attempts to build the aero groups\n",
"for method in (None,'computed','low_speed','tabular','solved_alpha'):\n",
"for method in (None,'computed','low_speed','tabular','solved_alpha','external'):\n",
" try:\n",
" cab.build_mission(1,AviaryValues(),method=method)\n",
" except ValueError as e:\n",
Expand Down Expand Up @@ -191,7 +195,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.1.-1"
"version": "3.12.3"
}
},
"nbformat": 4,
Expand Down
Empty file.
107 changes: 107 additions & 0 deletions aviary/examples/external_subsystems/custom_aero/custom_aero_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Builder for a simple drag calculation that replaces Aviary's calculation.
"""
import openmdao.api as om

from aviary.examples.external_subsystems.custom_aero.simple_drag import SimpleAeroGroup
from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase
from aviary.variable_info.variables import Aircraft, Dynamic


class CustomAeroBuilder(SubsystemBuilderBase):
"""
Prototype of a subsystem that overrides an aviary internally computed var.

It also provides a method to build OpenMDAO systems for the pre-mission and mission computations of the subsystem.

Attributes
----------
name : str ('simple_aero')
object label
"""

def __init__(self, name='simple_aero'):
super().__init__(name)

def build_mission(self, num_nodes, aviary_inputs, **kwargs):
"""
Build an OpenMDAO system for the mission computations of the subsystem.

Returns
-------
Returns
-------
mission_sys : openmdao.core.System
An OpenMDAO system containing all computations that need to happen
during the mission. This includes time-dependent states that are
being integrated as well as any other variables that vary during
the mission.
"""
aero_group = SimpleAeroGroup(
num_nodes=num_nodes,
)
return aero_group

def mission_inputs(self, **kwargs):
promotes = [
Dynamic.Atmosphere.STATIC_PRESSURE,
Dynamic.Atmosphere.MACH,
Dynamic.Vehicle.MASS,
'aircraft:*',
]
return promotes

def mission_outputs(self, **kwargs):
promotes = [
Dynamic.Vehicle.DRAG,
Dynamic.Vehicle.LIFT,
]
return promotes

def get_parameters(self, aviary_inputs=None, phase_info=None):
"""
Return a dictionary of fixed values for the subsystem.

Optional, used if subsystems have fixed values.

Used in the phase builders (e.g. cruise_phase.py) when other parameters are added to the phase.

This is distinct from `get_design_vars` in a nuanced way. Design variables
are variables that are optimized by the problem that are not at the phase level.
An example would be something that occurs in the pre-mission level of the problem.
Parameters are fixed values that are held constant throughout a phase, but if
`opt=True`, they are able to change during the optimization.

Parameters
----------
phase_info : dict
The phase_info subdict for this phase.

Returns
-------
fixed_values : dict
A dictionary where the keys are the names of the fixed variables
and the values are dictionaries with the following keys:

- 'value': float or array
The fixed value for the variable.
- 'units': str
The units for the fixed value (optional).
- any additional keyword arguments required by OpenMDAO for the fixed
variable.
"""
params = {}
params[Aircraft.Wing.AREA] = {
'shape': (1, ),
'static_target': True,
'units': 'ft**2',
}
return params

def needs_mission_solver(self, aviary_inputs):
"""
Return True if the mission subsystem needs to be in the solver loop in mission, otherwise
return False. Aviary will only place it in the solver loop when True. The default is
True.
"""
return False
60 changes: 60 additions & 0 deletions aviary/examples/external_subsystems/custom_aero/run_simple_aero.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Run the a mission with a simple external component that computes the wing
and horizontal tail mass.
"""
from copy import deepcopy

import aviary.api as av
from aviary.examples.external_subsystems.custom_aero.custom_aero_builder import CustomAeroBuilder


phase_info = deepcopy(av.default_height_energy_phase_info)

# Just do cruise in this example.
phase_info.pop('climb')
phase_info.pop('descent')

# Add custom aero.
# TODO: This API for replacing aero will be changed an upcoming release.
phase_info['cruise']['external_subsystems'] = [CustomAeroBuilder()]

# Disable internal aero
# TODO: This API for replacing aero will be changed an upcoming release.
phase_info['cruise']['subsystem_options']['core_aerodynamics'] = {
'method': 'external',
}


if __name__ == '__main__':
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)

# Preprocess inputs
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()

# Note, SLSQP might have troubles here.
prob.add_driver("SLSQP")

prob.add_design_variables()

prob.add_objective()

prob.setup()

prob.set_initial_guesses()

prob.run_aviary_problem(suppress_solver_print=True)

print('done')
104 changes: 104 additions & 0 deletions aviary/examples/external_subsystems/custom_aero/simple_drag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import numpy as np

import openmdao.api as om

from aviary.subsystems.aerodynamics.aero_common import DynamicPressure
from aviary.subsystems.aerodynamics.flops_based.lift import LiftEqualsWeight
from aviary.subsystems.aerodynamics.flops_based.drag import SimpleDrag
from aviary.variable_info.variables import Aircraft, Dynamic, Mission


class SimplestDragCoeff(om.ExplicitComponent):
"""
Simple representation of aircraft drag as CD = CD_zero + k * CL**2

Values are fictional. Typically, some higher fidelity method will go here instead.
"""

def initialize(self):
self.options.declare(
"num_nodes", default=1, types=int,
desc="Number of nodes along mission segment"
)

self.options.declare("CD_zero", default=0.01)
self.options.declare("k", default=0.065)

def setup(self):
nn = self.options["num_nodes"]

self.add_input('cl', val=np.zeros(nn))

self.add_output('CD', val=np.zeros(nn))

def setup_partials(self):
nn = self.options["num_nodes"]
arange = np.arange(nn)

self.declare_partials('CD', 'cl', rows=arange, cols=arange)

def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
CD_zero = self.options["CD_zero"]
k = self.options["k"]

cl = inputs['cl']

outputs['CD'] = CD_zero + k * cl**2

def compute_partials(self, inputs, partials, discrete_inputs=None):
k = self.options["k"]

cl = inputs['cl']

partials['CD', 'cl'] = 2.0 * k * cl


class SimpleAeroGroup(om.Group):

def initialize(self):
self.options.declare(
"num_nodes", default=1, types=int,
desc="Number of nodes along mission segment"
)

def setup(self):
nn = self.options["num_nodes"]

self.add_subsystem(
'DynamicPressure',
DynamicPressure(num_nodes=nn),
promotes_inputs=[
Dynamic.Atmosphere.MACH,
Dynamic.Atmosphere.STATIC_PRESSURE,
],
promotes_outputs=[Dynamic.Atmosphere.DYNAMIC_PRESSURE],
)

self.add_subsystem(
"Lift",
LiftEqualsWeight(num_nodes=nn),
promotes_inputs=[
Aircraft.Wing.AREA,
Dynamic.Vehicle.MASS,
Dynamic.Atmosphere.DYNAMIC_PRESSURE,
],
promotes_outputs=['cl', Dynamic.Vehicle.LIFT],
)

self.add_subsystem(
"SimpleDragCoeff",
SimplestDragCoeff(num_nodes=nn),
promotes_inputs=['cl'],
promotes_outputs=['CD'],
)

self.add_subsystem(
"SimpleDrag",
SimpleDrag(num_nodes=nn),
promotes_inputs=[
'CD',
Dynamic.Atmosphere.DYNAMIC_PRESSURE,
Aircraft.Wing.AREA,
],
promotes_outputs=[Dynamic.Vehicle.DRAG],
)
5 changes: 4 additions & 1 deletion aviary/mission/flops_based/ode/mission_ODE.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ def setup(self):
target = external_subsystem_group

target.add_subsystem(
subsystem.name, subsystem_mission
subsystem.name,
subsystem_mission,
promotes_inputs=subsystem.mission_inputs(**kwargs),
promotes_outputs=subsystem.mission_outputs(**kwargs),
)

# Only add the external subsystem group if it has at least one subsystem.
Expand Down
4 changes: 4 additions & 0 deletions aviary/subsystems/aerodynamics/aerodynamics_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs):
CDI_data=kwargs.pop('CDI_data'),
**kwargs)

elif method == 'external':
# Aero completely replaced by external group.
aero_group = None

else:
raise ValueError('FLOPS-based aero method is not one of the following: '
'(computed, low_speed, solved_alpha, tabular)')
Expand Down
Empty file.
Loading
Loading