From 7fbd0f39e1d18c5f90beb14a39243feb54481f60 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 14 Jan 2025 16:36:14 -0500 Subject: [PATCH 1/7] Added example for working in-mission drag calculations. --- .../custom_aero/__init__.py | 0 .../custom_aero/custom_aero_builder.py | 107 ++++++++++++++++++ .../custom_aero/run_simple_aero.py | 59 ++++++++++ .../custom_aero/simple_drag.py | 104 +++++++++++++++++ aviary/mission/flops_based/ode/mission_ODE.py | 5 +- .../aerodynamics/aerodynamics_builder.py | 4 + .../subsystems/aerodynamics/test/__init__.py | 0 .../test/test_external_mission_aero.py | 65 +++++++++++ 8 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 aviary/examples/external_subsystems/custom_aero/__init__.py create mode 100644 aviary/examples/external_subsystems/custom_aero/custom_aero_builder.py create mode 100644 aviary/examples/external_subsystems/custom_aero/run_simple_aero.py create mode 100644 aviary/examples/external_subsystems/custom_aero/simple_drag.py create mode 100644 aviary/subsystems/aerodynamics/test/__init__.py create mode 100644 aviary/subsystems/aerodynamics/test/test_external_mission_aero.py diff --git a/aviary/examples/external_subsystems/custom_aero/__init__.py b/aviary/examples/external_subsystems/custom_aero/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/aviary/examples/external_subsystems/custom_aero/custom_aero_builder.py b/aviary/examples/external_subsystems/custom_aero/custom_aero_builder.py new file mode 100644 index 000000000..482bcf39b --- /dev/null +++ b/aviary/examples/external_subsystems/custom_aero/custom_aero_builder.py @@ -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 diff --git a/aviary/examples/external_subsystems/custom_aero/run_simple_aero.py b/aviary/examples/external_subsystems/custom_aero/run_simple_aero.py new file mode 100644 index 000000000..056335d3b --- /dev/null +++ b/aviary/examples/external_subsystems/custom_aero/run_simple_aero.py @@ -0,0 +1,59 @@ +""" +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() + + prob.add_driver("IPOPT") + + prob.add_design_variables() + + prob.add_objective() + + prob.setup() + + prob.set_initial_guesses() + + prob.run_aviary_problem(suppress_solver_print=True) + + print('done') diff --git a/aviary/examples/external_subsystems/custom_aero/simple_drag.py b/aviary/examples/external_subsystems/custom_aero/simple_drag.py new file mode 100644 index 000000000..da515d147 --- /dev/null +++ b/aviary/examples/external_subsystems/custom_aero/simple_drag.py @@ -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], + ) \ No newline at end of file diff --git a/aviary/mission/flops_based/ode/mission_ODE.py b/aviary/mission/flops_based/ode/mission_ODE.py index 389ce80a5..f78b08f40 100644 --- a/aviary/mission/flops_based/ode/mission_ODE.py +++ b/aviary/mission/flops_based/ode/mission_ODE.py @@ -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. diff --git a/aviary/subsystems/aerodynamics/aerodynamics_builder.py b/aviary/subsystems/aerodynamics/aerodynamics_builder.py index 0dd98dfd8..52a82d710 100644 --- a/aviary/subsystems/aerodynamics/aerodynamics_builder.py +++ b/aviary/subsystems/aerodynamics/aerodynamics_builder.py @@ -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)') diff --git a/aviary/subsystems/aerodynamics/test/__init__.py b/aviary/subsystems/aerodynamics/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py b/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py new file mode 100644 index 000000000..44a7bdaff --- /dev/null +++ b/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py @@ -0,0 +1,65 @@ +from copy import deepcopy +import unittest + +from openmdao.utils.assert_utils import assert_near_equal + +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) + + +class TestBattery(av.TestSubsystemBuilderBase): + """ + Test replacing internal drag calculation with an external subsystem. + + Mainly, this shows that the "external" method works, and that the external + subsystems in mission are correctly promoting inputs/outputs. + """ + + def test_external_drag(self): + + # 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', + } + + prob = av.AviaryProblem() + + # Load aircraft and options data from user + prob.load_inputs('models/test_aircraft/aircraft_for_bench_FwFm.csv', phase_info) + + prob.check_and_preprocess_inputs() + prob.add_pre_mission_systems() + prob.add_phases() + prob.add_post_mission_systems() + + prob.link_phases() + + # SLSQP didn't work so well here. + prob.add_driver("IPOPT") + + prob.add_design_variables() + prob.add_objective() + + prob.setup() + + prob.set_initial_guesses() + + prob.run_aviary_problem(suppress_solver_print=True) + + drag = prob.get_val("traj.cruise.rhs_all.drag", units='lbf') + assert_near_equal(drag[0], 7272.0265, tolerance=1e-3) + + +if __name__ == '__main__': + unittest.main() From 22f2651ede487146de6fe38a8f93c747bb7205a7 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 14 Jan 2025 18:03:10 -0500 Subject: [PATCH 2/7] Reworked a bit, since I forgot the configure method was being used to specify promtoes. --- aviary/subsystems/subsystem_builder_base.py | 14 ++++++++++++++ aviary/utils/functions.py | 1 + 2 files changed, 15 insertions(+) diff --git a/aviary/subsystems/subsystem_builder_base.py b/aviary/subsystems/subsystem_builder_base.py index 9d4fa20fb..b651529a8 100644 --- a/aviary/subsystems/subsystem_builder_base.py +++ b/aviary/subsystems/subsystem_builder_base.py @@ -219,6 +219,20 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs): """ return None + def mission_inputs(self, **kwargs): + """ + Returns list of mission inputs to be promoted out of the external subsystem. By + default, all aircraft:* and mission:* inputs are promoted. + """ + return [] + + def mission_outputs(self, **kwargs): + """ + Returns list of mission outputs to be promoted out of the external subsystem. By + default, all aircraft:* and mission:* outputs are promoted. + """ + return [] + def define_order(self): """ Return a list of subsystem names that must be defined before this one. E.g., must go before or after aero or prop. diff --git a/aviary/utils/functions.py b/aviary/utils/functions.py index 0f34cb662..54161d89c 100644 --- a/aviary/utils/functions.py +++ b/aviary/utils/functions.py @@ -361,6 +361,7 @@ def promote_aircraft_and_mission_vars(group): break group.promotes(comp.name, outputs=promote_out, inputs=promote_in) + print(comp.name, promote_in, promote_out) return external_outputs From 69c4a078e4b19a474e8fa00de51fd569da4cc9db Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 Jan 2025 11:50:32 -0500 Subject: [PATCH 3/7] cleanup --- aviary/utils/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aviary/utils/functions.py b/aviary/utils/functions.py index 54161d89c..0f34cb662 100644 --- a/aviary/utils/functions.py +++ b/aviary/utils/functions.py @@ -361,7 +361,6 @@ def promote_aircraft_and_mission_vars(group): break group.promotes(comp.name, outputs=promote_out, inputs=promote_in) - print(comp.name, promote_in, promote_out) return external_outputs From 8238f82dbeb8194f17951e9f974ac65653450359 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 Jan 2025 12:19:10 -0500 Subject: [PATCH 4/7] cleanup and merge out --- aviary/examples/external_subsystems/custom_aero/simple_drag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/examples/external_subsystems/custom_aero/simple_drag.py b/aviary/examples/external_subsystems/custom_aero/simple_drag.py index da515d147..23a146bf8 100644 --- a/aviary/examples/external_subsystems/custom_aero/simple_drag.py +++ b/aviary/examples/external_subsystems/custom_aero/simple_drag.py @@ -101,4 +101,4 @@ def setup(self): Aircraft.Wing.AREA, ], promotes_outputs=[Dynamic.Vehicle.DRAG], - ) \ No newline at end of file + ) From 39a1c465872c7fe2cbf9cae0cbc01318c63b8dc5 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 Jan 2025 12:42:38 -0500 Subject: [PATCH 5/7] Fix for testing on our minimal machine --- .../external_subsystems/custom_aero/run_simple_aero.py | 3 ++- .../subsystems/aerodynamics/test/test_external_mission_aero.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/aviary/examples/external_subsystems/custom_aero/run_simple_aero.py b/aviary/examples/external_subsystems/custom_aero/run_simple_aero.py index 056335d3b..47fa42488 100644 --- a/aviary/examples/external_subsystems/custom_aero/run_simple_aero.py +++ b/aviary/examples/external_subsystems/custom_aero/run_simple_aero.py @@ -44,7 +44,8 @@ # Link phases and variables prob.link_phases() - prob.add_driver("IPOPT") + # Note, SLSQP might have troubles here. + prob.add_driver("SLSQP") prob.add_design_variables() diff --git a/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py b/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py index 44a7bdaff..7e7ce190b 100644 --- a/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py +++ b/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py @@ -2,6 +2,7 @@ import unittest from openmdao.utils.assert_utils import assert_near_equal +from openmdao.utils.testing_utils import require_pyoptsparse import aviary.api as av from aviary.examples.external_subsystems.custom_aero.custom_aero_builder import CustomAeroBuilder @@ -17,6 +18,7 @@ class TestBattery(av.TestSubsystemBuilderBase): subsystems in mission are correctly promoting inputs/outputs. """ + @require_pyoptsparse(optimizer="SNOPT") def test_external_drag(self): # Just do cruise in this example. From d3b603c2fc9f602c43733da03ca2133ebb3d54e9 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 Jan 2025 12:42:57 -0500 Subject: [PATCH 6/7] Fix for testing on our minimal machine --- .../subsystems/aerodynamics/test/test_external_mission_aero.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py b/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py index 7e7ce190b..e613c03fe 100644 --- a/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py +++ b/aviary/subsystems/aerodynamics/test/test_external_mission_aero.py @@ -18,7 +18,7 @@ class TestBattery(av.TestSubsystemBuilderBase): subsystems in mission are correctly promoting inputs/outputs. """ - @require_pyoptsparse(optimizer="SNOPT") + @require_pyoptsparse(optimizer="IPOPT") def test_external_drag(self): # Just do cruise in this example. From 4eaebaacbea6d562d39d4447af9790b99ca9b424 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 17 Jan 2025 18:05:34 -0500 Subject: [PATCH 7/7] Added documentation of external aerodynamics methods --- aviary/docs/user_guide/aerodynamics.ipynb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/aviary/docs/user_guide/aerodynamics.ipynb b/aviary/docs/user_guide/aerodynamics.ipynb index 023e8883a..e8504ef81 100644 --- a/aviary/docs/user_guide/aerodynamics.ipynb +++ b/aviary/docs/user_guide/aerodynamics.ipynb @@ -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", @@ -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." ] }, { @@ -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", @@ -191,7 +195,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.1.-1" + "version": "3.12.3" } }, "nbformat": 4,