diff --git a/aviary/api.py b/aviary/api.py index 28f543f05..2d0394ee4 100644 --- a/aviary/api.py +++ b/aviary/api.py @@ -88,7 +88,7 @@ ################### # Miscellaneous -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase from aviary.utils.preprocessors import ( preprocess_crewpayload, @@ -148,6 +148,7 @@ LandingNoseDownToStop as DetailedLandingNoseDownToStopPhaseBuilder, ) from aviary.mission.flops_based.phases.detailed_takeoff_phases import ( + DetailedTakeoffPhaseBuilder, TakeoffBrakeReleaseToDecisionSpeed as DetailedTakeoffBrakeReleaseToDecisionSpeedPhaseBuilder, TakeoffDecisionSpeedToRotate as DetailedTakeoffDecisionSpeedToRotatePhaseBuilder, TakeoffDecisionSpeedBrakeDelay as DetailedTakeoffDecisionSpeedBrakeDelayPhaseBuilder, @@ -179,6 +180,9 @@ TakeoffTrajectory as DetailedTakeoffTrajectoryBuilder, ) +from aviary.mission.flops_based.phases.balanced_field_trajectory import BalancedFieldPhaseBuilder +from aviary.mission.balanced_field_traj_builder import BalancedFieldTrajectoryBuilder + ############## # Subsystems # ############## diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index e5f7e4166..7b31e8858 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -17,9 +17,10 @@ from aviary.mission.two_dof_problem_configurator import TwoDOFProblemConfigurator from aviary.mission.utils import get_phase_mission_bus_lengths, process_guess_var from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder +from aviary.subsystems.core_postmission import CorePostMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder from aviary.subsystems.mass.mass_builder import CoreMassBuilder -from aviary.subsystems.premission import CorePreMission from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder from aviary.subsystems.performance.performance_builder import CorePerformanceBuilder from aviary.utils.functions import get_path @@ -1032,6 +1033,23 @@ def add_post_mission_systems(self, verbosity=None): 'The aircraft may not have enough space for fuel, so check the value of Mission.Constraints.EXCESS_FUEL_CAPACITY for details.' ) + default_subsystems = [ + self.core_subsystems['performance'], + ] + + post_mission.add_subsystem( + 'core_subsystems', + CorePostMission( + aviary_options=self.aviary_inputs, + subsystems=default_subsystems, + phase_info=self.phase_info, + phase_mission_bus_lengths=phase_mission_bus_lengths, + post_mission_info=self.post_mission_info, + ), + promotes_inputs=['*'], + promotes_outputs=['*'], + ) + def link_phases(self, verbosity=None, comm=None): """ Link phases together after they've been added. diff --git a/aviary/core/post_mission_group.py b/aviary/core/post_mission_group.py index 48d5895f4..2614cc365 100644 --- a/aviary/core/post_mission_group.py +++ b/aviary/core/post_mission_group.py @@ -2,7 +2,9 @@ import openmdao.api as om from packaging import version +from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import promote_aircraft_and_mission_vars +from aviary.variable_info.variable_meta_data import _MetaData use_new_openmdao_syntax = version.parse(openmdao.__version__) >= version.parse('3.28') @@ -10,6 +12,15 @@ class PostMissionGroup(om.Group): """OpenMDAO group that holds all post-mission systems.""" + def initialize(self): + self.options.declare( + 'aviary_options', + types=AviaryValues, + desc='collection of Aircraft/Mission specific options', + ) + self.options.declare('subsystems', desc='list of core subsystem builders') + self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + def setup(self, **kwargs): if use_new_openmdao_syntax: # rely on openMDAO's auto-ordering for this group diff --git a/aviary/core/pre_mission_group.py b/aviary/core/pre_mission_group.py index 0122d37b9..1f32206d6 100644 --- a/aviary/core/pre_mission_group.py +++ b/aviary/core/pre_mission_group.py @@ -1,12 +1,23 @@ import openmdao.api as om +from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import promote_aircraft_and_mission_vars from aviary.variable_info.functions import override_aviary_vars +from aviary.variable_info.variable_meta_data import _MetaData class PreMissionGroup(om.Group): """OpenMDAO group that holds all pre-mission systems.""" + def initialize(self): + self.options.declare( + 'aviary_options', + types=AviaryValues, + desc='collection of Aircraft/Mission specific options', + ) + self.options.declare('subsystems', desc='list of core subsystem builders') + self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + def configure(self): """ Configure this group for pre-mission. diff --git a/aviary/examples/external_subsystems/balanced_field/__init__.py b/aviary/examples/external_subsystems/balanced_field/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py new file mode 100644 index 000000000..29068b915 --- /dev/null +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_builder.py @@ -0,0 +1,26 @@ +import openmdao.api as om + +import aviary.api as av +from aviary.api import Mission +from aviary.examples.external_subsystems.balanced_field.balanced_field_submodel import ( + create_balance_field_subprob, +) +from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase + + +class BalancedFieldBuilder(SubsystemBuilderBase): + + def __init__(self, name=None, meta_data=None): + if name is None: + name = 'balanced_field_length' + + super().__init__(name=name, meta_data=meta_data) + + def build_post_mission( + self, aviary_inputs, phase_info=None, phase_mission_bus_lengths=None, **kwargs + ): + return create_balance_field_subprob(aviary_inputs) + + +if __name__ == '__main__': + unittest.main() diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py new file mode 100644 index 000000000..791fca1b3 --- /dev/null +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_large.py @@ -0,0 +1,130 @@ +import warnings + +import dymos as dm +import openmdao.api as om + +import aviary.api as av +from aviary.api import Mission +from aviary.interface.methods_for_level2 import AviaryProblem +from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import inputs +from aviary.utils.preprocessors import preprocess_options + + +# Note, only creating this aviary problem so that it can read the aircraft csv for us. +prob = AviaryProblem() +prob.load_inputs("models/aircraft/test_aircraft/aircraft_for_bench_FwFm.csv", verbosity=0) +aviary_options = prob.aviary_inputs.deepcopy() + +# These inputs aren't in the aircraft file yet. +aviary_options.set_val(Mission.Takeoff.AIRPORT_ALTITUDE, 0.0, 'ft') +aviary_options.set_val(Mission.Takeoff.DRAG_COEFFICIENT_MIN, 0.05, 'unitless') +aviary_options.set_val(Mission.Takeoff.LIFT_COEFFICIENT_MAX, 3.0, 'unitless') +aviary_options.set_val(Mission.Takeoff.OBSTACLE_HEIGHT, 35.0, 'ft') +aviary_options.set_val(Mission.Takeoff.ANGLE_OF_ATTACK_RUNWAY, 0.0, 'deg') +aviary_options.set_val(Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT, 0.0175) +aviary_options.set_val(Mission.Takeoff.BRAKING_FRICTION_COEFFICIENT, 0.35) +aviary_options.set_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT, 0.085000) +aviary_options.set_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT, -0.810000) +aviary_options.set_val(Mission.Takeoff.THRUST_INCIDENCE, 0.0, 'deg') +aviary_options.set_val(Mission.Takeoff.FUEL_SIMPLE, 577.0, 'lbm') +aviary_options.set_val(Mission.Design.GROSS_MASS, 160000, units='lbm') + +# This builder can be used for both takeoff and landing phases +aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS +) + +# fmt: off +takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } +} +# fmt: off + +# when using spoilers, add a few more options +takeoff_spoiler_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT), + 'spoiler_lift_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT), + } +} + +# We also need propulsion analysis for takeoff and landing. No additional configuration +# is needed for this builder +engines = [av.build_engine_deck(aviary_options)] +preprocess_options(aviary_options, engine_models=engines) +prop_builder = av.CorePropulsionBuilder(engine_models=engines) + +balanced_field_user_options = av.AviaryValues() + +from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems +from aviary.variable_info.functions import setup_model_options + +takeoff_trajectory_builder = av.BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) + +test_problem = om.Problem() + +# default subsystems +default_premission_subsystems = get_default_premission_subsystems('FLOPS', engines) + +# Upstream pre-mission analysis for aero +test_problem.model.add_subsystem( + 'core_subsystems', + av.CorePreMission( + aviary_options=aviary_options, + subsystems=default_premission_subsystems, + ), + promotes_inputs=['*'], + promotes_outputs=['*'], +) + +# Instantiate the trajectory and add the phases +traj = takeoff_trajectory_builder.build_trajectory(aviary_options=aviary_options, model=test_problem.model) + +varnames = [ + av.Aircraft.Wing.AREA, + av.Aircraft.Wing.ASPECT_RATIO, + av.Aircraft.Wing.SPAN, +] +av.set_aviary_input_defaults(test_problem.model, varnames, aviary_options) + +setup_model_options(test_problem, aviary_options) + +# suppress warnings: +# "input variable '...' promoted using '*' was already promoted using 'aircraft:*' +with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + test_problem.setup(check=False) + +av.set_aviary_initial_values(test_problem, aviary_options) + +takeoff_trajectory_builder.apply_initial_guesses(test_problem, 'traj') + +test_problem.final_setup() + + +import dymos +dymos.run_problem(test_problem, run_driver=False, make_plots=True) + +print(test_problem.get_reports_dir()) + diff --git a/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py b/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py new file mode 100644 index 000000000..6448ea7f4 --- /dev/null +++ b/aviary/examples/external_subsystems/balanced_field/balanced_field_submodel.py @@ -0,0 +1,134 @@ +""" +Group containing a submodel component with detailed landing. +""" +import warnings + +import openmdao.api as om + +import aviary.api as av +from aviary.variable_info.variables import Aircraft, Mission, Settings + + +def create_balance_field_subprob(aviary_inputs, use_spoiler=False): + + subprob = create_prob(aviary_inputs, use_spoiler) + + comp = AviarySubmodelComp( + problem=subprob, + inputs=[ + Aircraft.Wing.AREA, + Aircraft.Wing.ASPECT_RATIO, + Aircraft.Wing.HEIGHT, + Aircraft.Wing.SPAN, + Mission.Takeoff.DRAG_COEFFICIENT_MIN, + Mission.Takeoff.LIFT_COEFFICIENT_MAX, + ('traj.takeoff_brake_release_to_engine_failure.initial_states:mass', Mission.Summary.GROSS_MASS), + ], + outputs=[ + ('traj.takeoff_climb_gradient_to_obstacle.final_states:distance', 'distance_obstacle'), + ] + ) + + return comp + + +def create_prob(aviary_inputs, use_spoiler=False): + """ + Return a problem + """ + aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS + ) + + # fmt: off + takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } + } + # fmt: off + + # when using spoilers, add a few more options + if use_spoiler: + + spoiler_drag = aviary_inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT) + spoiler_lift = aviary_inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT) + + takeoff_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': spoiler_drag, + 'spoiler_lift_coefficient': spoiler_lift, + } + } + + # We also need propulsion analysis for takeoff and landing. No additional configuration + # is needed for this builder + engines = [av.build_engine_deck(aviary_inputs)] + # Note that the aviary_inputs is already in a pre-processed state. + prop_builder = av.CorePropulsionBuilder(engine_models=engines) + + balanced_field_user_options = av.AviaryValues() + + from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems + from aviary.variable_info.functions import setup_model_options + + dto_build = av.BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) + + subprob = om.Problem() + + #ivc = om.IndepVarComp(Mission.Summary.GROSS_MASS, val=1.0, units='lbm') + #subprob.model.add_subsystem('takeoff_mass_ivc', ivc, promotes=['*']) + #subprob.model.connect( + # Mission.Summary.GROSS_MASS, + # "traj.takeoff_brake_release_to_engine_failure.states:mass", + # flat_src_indices=[0], + #) + + # Instantiate the trajectory and add the phases + traj = dto_build.build_trajectory(aviary_options=aviary_inputs, model=subprob.model) + + setup_model_options(subprob, aviary_inputs) + + # This is kind of janky, but we need these after the subproblem sets it up. + subprob.aviary_inputs = aviary_inputs + subprob.dto_build = dto_build + + return subprob + + +class AviarySubmodelComp(om.SubmodelComp): + """ + We need to subclass so that we can set the initial conditions. + """ + def setup(self): + # suppress warnings: + # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' + with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + super().setup() + + + sub = self._subprob + av.set_aviary_initial_values(sub, sub.aviary_inputs) + sub.dto_build.apply_initial_guesses(sub, 'traj') + + sub.final_setup() diff --git a/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py new file mode 100644 index 000000000..0baeec878 --- /dev/null +++ b/aviary/examples/external_subsystems/balanced_field/run_balanced_field.py @@ -0,0 +1,53 @@ +""" +Balanced Field problem as an external subsystem in post-mission. +""" +from copy import deepcopy + +import numpy as np + +import openmdao.api as om + +from aviary.examples.external_subsystems.balanced_field.balanced_field_builder import BalancedFieldBuilder +from aviary.interface.methods_for_level2 import AviaryProblem +from aviary.models.aircraft.advanced_single_aisle.phase_info import phase_info +from aviary.variable_info.variables import Mission + +local_phase_info = deepcopy(phase_info) + +# First way is the manual way using the files in this directory. +# Second is automated. You can turn balanced-field on with a single switch. +# Note: Aero is still hardcoded in this directory. Initial guesses are also hardcoded, +# but it is unclear which ones need to be tuned by the user. +#local_phase_info['post_mission']['external_subsystems'] = [BalancedFieldBuilder()] +local_phase_info['post_mission']['balanced_field'] = True + +prob = AviaryProblem() + +prob.load_inputs( + 'models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv', + local_phase_info, +) + +# A few values that aren't in the csv file. +prob.aviary_inputs.set_val(Mission.Takeoff.FUEL_SIMPLE, 577.0, units='lbm') +prob.aviary_inputs.set_val(Mission.Takeoff.LIFT_OVER_DRAG, 17.35, units='unitless') +prob.aviary_inputs.set_val(Mission.Design.THRUST_TAKEOFF_PER_ENG, 24555.5, units='lbf') + +# initial guess for mass +prob.aviary_inputs.set_val(Mission.Design.GROSS_MASS, 135000.0, units='lbm') + +prob.check_and_preprocess_inputs() + +prob.build_model() +prob.add_driver('SNOPT', max_iter=50, verbosity=1) + +prob.add_design_variables() + +prob.add_objective() + +prob.setup() + +# TODO: N3CC optimization does not return success. +prob.run_aviary_problem() + +#prob.model.list_vars(units=True, print_arrays=True) diff --git a/aviary/mission/balanced_field_traj_builder.py b/aviary/mission/balanced_field_traj_builder.py new file mode 100644 index 000000000..c6e07ac30 --- /dev/null +++ b/aviary/mission/balanced_field_traj_builder.py @@ -0,0 +1,559 @@ +""" +Define utilities for building phases. + +Classes +------- +PhaseBuilderBase : the interface for a phase builder +""" + +from abc import ABC +from collections import namedtuple + +import numpy as np + +import dymos as dm +import openmdao.api as om + +from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE +from aviary.mission.flops_based.phases.balanced_field_trajectory import BalancedFieldPhaseBuilder +from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.variable_meta_data import _MetaData +from aviary.variable_info.variables import Dynamic, Mission +from aviary.variable_info.functions import setup_trajectory_params + +from aviary.mission.phase_builder_base import PhaseBuilderBase + + +_require_new_initial_guesses_meta_data_class_attr_ = namedtuple( + '_require_new_initial_guesses_meta_data_class_attr_', () +) + + +class BalancedFieldTrajectoryBuilder(ABC): + """ + Define the interface for a balanced field trajectory builder. + + Attributes + ---------- + name : str ('_unknown phase_') + object label + + core_subsystems : (None) + list of SubsystemBuilderBase objects that will be added to the phase ODE + + user_options : OptionsDictionary () + state/path constraint values and flags + + initial_guesses : AviaryValues () + state/path beginning values to be set on the problem + + ode_class : type (None) + advanced: the type of system defining the ODE + + transcription : "Dymos transcription object" (None) + advanced: an object providing the transcription technique of the + optimal control problem + + subsystem_options : dict (None) + dictionary of parameters to be passed to the subsystem builders + + default_name : str + class attribute: derived type customization point; the default value + for name + + default_ode_class : type + class attribute: derived type customization point; the default value + for ode_class used by build_phase + + default_options_class : type + class attribute: derived type customization point; the default class + containing the phase options options_dictionary + + is_analytic_phase : bool (False) + class attribute: derived type customization point; if True, build_phase + will return an AnalyticPhase instead of a Phase + + num_nodes : int (5) + class attribute: derived type customization point; the default value + for num_nodes used by build_phase, only for AnalyticPhases + + Methods + ------- + build_phase + make_default_transcription + """ + + __slots__ = ( + 'name', + 'core_subsystems', + 'external_subsystems', + 'subsystem_options', + 'user_options', + 'initial_guesses', + 'ode_class', + 'transcription', + 'is_analytic_phase', + 'num_nodes', + 'meta_data', + '_traj', + '_phases', + ) + + MappedPhase = namedtuple('MappedPhase', ('phase', 'phase_builder')) + + _initial_guesses_meta_data_ = _require_new_initial_guesses_meta_data_class_attr_() + + default_name = '_unknown phase_' + + default_ode_class = TakeoffODE + default_options_class = om.OptionsDictionary + + default_meta_data = _MetaData + # endregion : derived type customization points + + def __init__( + self, + name=None, + core_subsystems=None, + external_subsystems=None, + user_options=None, + initial_guesses=None, + subsystem_options=None, + num_nodes=10, + meta_data=None, + traj=None, + ): + if name is None: + name = self.default_name + + self.name = name + + if core_subsystems is None: + core_subsystems = [] + if external_subsystems is None: + external_subsystems = [] + + self.core_subsystems = core_subsystems + self.external_subsystems = external_subsystems + + if subsystem_options is None: + subsystem_options = {} + + self.subsystem_options = subsystem_options + + self.user_options = self.default_options_class(user_options) + + if initial_guesses is None: + initial_guesses = AviaryValues() + + self.initial_guesses = initial_guesses + + self.ode_class = self.default_ode_class + self.num_nodes = num_nodes + + if external_subsystems is None: + external_subsystems = [] + + self.external_subsystems = external_subsystems + + if meta_data is None: + meta_data = self.default_meta_data + + self.meta_data = meta_data + + self._traj = traj + self._phases = {} + + def build_trajectory( + self, *, aviary_options: AviaryValues, model: om.Group = None, traj: dm.Trajectory = None + ) -> dm.Trajectory: + """ + Return a new trajectory for detailed takeoff analysis. + + Call only after assigning phase builders for required phases. + + Parameters + ---------- + aviary_options : AviaryValues + collection of Aircraft/Mission specific options + + model : openmdao.api.Group (None) + the model handling trajectory parameter setup; if `None`, trajectory + parameter setup will not be handled + + traj : dymos.Trajectory (None) + the trajectory to update; if `None`, a new trajetory will be updated and + returned + + Returns + ------- + the updated trajectory; if the specified trajectory is `None`, a new trajectory + will be updated and returned + + Notes + ----- + Do not modify this object or any of its referenced data between the call to + `build_trajectory()` and the call to `apply_initial_guesses()`, or the behavior + is undefined, no diagnostic required. + """ + + if traj is None: + self._traj = traj = dm.Trajectory(parallel_phases=False) + model.add_subsystem('traj', self._traj) + + # We're adding a balance comp, so use auto-ordering to put + # systems in best order. + self._traj.options['auto_order'] = True + + if aviary_options is None: + aviary_options = AviaryValues() + + common_user_options = AviaryValues() + common_user_options.set_val('max_duration', val=100.0, units='s') + common_user_options.set_val('time_duration_ref', val=60.0, units='s') + common_user_options.set_val('distance_max', val=10000.0, units='ft') + common_user_options.set_val('max_velocity', val=200.0, units='kn') + + tobl_nl_solver = om.NewtonSolver( + solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6 + ) + tobl_nl_solver.linesearch = om.BoundsEnforceLS() + + common_user_options.set_val('nonlinear_solver', val=tobl_nl_solver) + common_user_options.set_val('linear_solver', val=om.DirectSolver()) + + common_initial_guesses = AviaryValues() + common_initial_guesses.set_val('time', [0.0, 30.0], 's') + common_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') + common_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') + common_initial_guesses.set_val('throttle', 1.0) + common_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + gross_mass_units = 'lbm' + gross_mass = aviary_options.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + common_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + # + # First phase: Brake release to engine failure + # + # TODO: would like to be able to do this - user_options=balanced_field_user_options | {'terminal_condition': 'VEF'} + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'VEF') + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('dV1', 3.0, 'kn') + initial_guesses.set_val('dVEF', 1.0, 'kn') + takeoff_brake_release_to_engine_failure_builder = BalancedFieldPhaseBuilder( + 'takeoff_brake_release_to_engine_failure', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + self._add_phase(takeoff_brake_release_to_engine_failure_builder, aviary_options) + + # + # Second Phase: Engine Failure to Decision Speed + # + # TODO: dymos PicardShooting shouldn't require initial guesses if connecting an initial state value + # In that case just assume the state value holds through the phase as an initial guess. + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'V1') + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [30.0, 31.0], 's') + initial_guesses.set_val('distance', [4100.0, 4500.0], 'ft') + initial_guesses.set_val('velocity', [150.0, 151], 'kn') + initial_guesses.set_val('throttle', 0.5) # TODO: Don't do this hack, decrement num engines + initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + initial_guesses.set_val('dV1', 3.0, 'kn') + + takeoff_engine_failure_to_v1_builder = BalancedFieldPhaseBuilder( + 'takeoff_engine_failure_to_v1', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_engine_failure_to_v1_builder, aviary_options) + + # + # Third Phase: Decision Speed to Rejected Takeoff Stop + # + # TODO: dymos PicardShooting shouldn't require initial guesses if connecting an initial state value + # In that case just assume the state value holds through the phase as an initial guess. + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'STOP') + user_options.set_val('friction_key', Mission.Takeoff.BRAKING_FRICTION_COEFFICIENT) + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [31.0, 10.0], 's') + initial_guesses.set_val('distance', [4500.0, 6500.0], 'ft') + initial_guesses.set_val('velocity', [151.0, 5.0], 'kn') + initial_guesses.set_val('throttle', 0.0) + initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + takeoff_v1_to_roll_stop = BalancedFieldPhaseBuilder( + 'takeoff_v1_to_roll_stop', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_v1_to_roll_stop, aviary_options) + + # + # Fourth Phase: Decision Speed to Rotation Speed + # + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'VR') + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [36.0, 1.0], 's') + initial_guesses.set_val('distance', [4500.0, 5000.0], 'ft') + initial_guesses.set_val('velocity', [151.0, 155.0], 'kn') + initial_guesses.set_val('throttle', 0.5) + initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + takeoff_v1_to_vr = BalancedFieldPhaseBuilder( + 'takeoff_v1_to_vr', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_v1_to_vr, aviary_options) + + # + # Fifth Phase: Rotate to liftoff + # + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'LIFTOFF') + user_options.set_val('pitch_control', 'ALPHA_RATE_FIXED') + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [37.0, 7.0], 's') + initial_guesses.set_val('distance', [5000.0, 5500.0], 'ft') + initial_guesses.set_val('velocity', [155.0, 160.0], 'kn') + initial_guesses.set_val('throttle', 0.5) + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK, [0.0, 14.0], 'deg') + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, 2.5, 'deg/s') + + takeoff_vr_to_liftoff = BalancedFieldPhaseBuilder( + 'takeoff_vr_to_liftoff', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_vr_to_liftoff, aviary_options) + + # + # Sixth Phase: Liftoff to Climb Gradient + # + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'CLIMB_GRADIENT') + user_options.set_val('pitch_control', 'ALPHA_RATE_FIXED') + user_options.set_val('climbing', True) + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [37.0, 1.0], 's') + initial_guesses.set_val('distance', [5000.0, 5500.0], 'ft') + initial_guesses.set_val('velocity', [155.0, 160.0], 'kn') + initial_guesses.set_val('throttle', 0.5) + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK, [10.0, 10.0], 'deg') + initial_guesses.set_val(Dynamic.Mission.FLIGHT_PATH_ANGLE, [0.0, 0.5], 'deg') + initial_guesses.set_val(Dynamic.Mission.ALTITUDE, [0.0, 0.5], 'ft') + initial_guesses.set_val(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, 2.0, 'deg/s') + + takeoff_liftoff_to_climb_gradient = BalancedFieldPhaseBuilder( + 'takeoff_liftoff_to_climb_gradient', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_liftoff_to_climb_gradient, aviary_options) + + # + # Seventh Phase: Climb Gradient to Obstacle + # + user_options = common_user_options.deepcopy() + user_options.set_val('terminal_condition', 'OBSTACLE') + user_options.set_val('pitch_control', 'GAMMA_FIXED') + user_options.set_val('climbing', True) + initial_guesses = common_initial_guesses.deepcopy() + initial_guesses.set_val('time', [37.0, 5.0], 's') + initial_guesses.set_val('distance', [5000.0, 5500.0], 'ft') + initial_guesses.set_val('velocity', [155.0, 160.0], 'kn') + initial_guesses.set_val('throttle', 0.5) + initial_guesses.set_val(Dynamic.Mission.FLIGHT_PATH_ANGLE, [0.1, 0.1], 'deg') + initial_guesses.set_val(Dynamic.Mission.ALTITUDE, [0.5, 35.0], 'ft') + + takeoff_climb_gradient_to_obstacle = BalancedFieldPhaseBuilder( + 'takeoff_climb_gradient_to_obstacle', + core_subsystems=self.core_subsystems, + subsystem_options=self.subsystem_options, + user_options=user_options, + initial_guesses=initial_guesses, + ) + + self._add_phase(takeoff_climb_gradient_to_obstacle, aviary_options) + + # + # Add a trajectory-level balance to satisfy inter-phase relationships + # + + bal_comp = self._traj.add_subsystem('balance_comp', om.BalanceComp()) + + # The first balance sets dVEF such that the elapsed time from engine failure to V1 is one second. + bal_comp.add_balance('dVEF', lhs_name='t_react', rhs_val=1.0, eq_units='s', units='kn') + self._traj.connect( + 'balance_comp.dVEF', 'takeoff_brake_release_to_engine_failure.parameters:dVEF' + ) + self._traj.connect('takeoff_engine_failure_to_v1.t_duration', 'balance_comp.t_react') + + # The second balance sets dV1 such that the distance at the end of the abort roll and the distance at obstacle clearance are the same + bal_comp.add_balance( + 'dV1', + lhs_name='distance_abort', + rhs_name='distance_obstacle', + eq_units='ft', + units='kn', + ) + self._traj.connect( + 'balance_comp.dV1', + [ + 'takeoff_brake_release_to_engine_failure.parameters:dV1', + 'takeoff_engine_failure_to_v1.parameters:dV1', + ], + ) + self._traj.connect( + 'takeoff_climb_gradient_to_obstacle.final_states:distance', + 'balance_comp.distance_obstacle', + ) + self._traj.connect( + 'takeoff_v1_to_roll_stop.final_states:distance', 'balance_comp.distance_abort' + ) + + # The third balance sets the trigger ratio of V/V_stall for rotation such that + # the climb gradient is achieved with V/V_stall = 1.2 (V2) + + bal_comp.add_balance( + 'VR_trigger_ratio', + lhs_name='v_over_v_stall_at_climb_gradient', + rhs_val=1.2, + eq_units='unitless', + units='unitless', + ) + + self._traj.connect( + 'balance_comp.VR_trigger_ratio', + [ + 'takeoff_brake_release_to_engine_failure.parameters:VR_ratio', + 'takeoff_engine_failure_to_v1.parameters:VR_ratio', + 'takeoff_v1_to_vr.parameters:VR_ratio', + ], + ) + + self._traj.connect( + 'takeoff_liftoff_to_climb_gradient.timeseries.v_over_v_stall', + 'balance_comp.v_over_v_stall_at_climb_gradient', + src_indices=om.slicer[-1, ...], + ) + + # Add a newton solver to the balanced field trajectory + + self._traj.nonlinear_solver = om.NewtonSolver( + solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6 + ) + self._traj.nonlinear_solver.linesearch = om.BoundsEnforceLS() + self._traj.linear_solver = om.DirectSolver() + + # Press to Takeoff Sequence + self._traj.link_phases( + [ + 'takeoff_brake_release_to_engine_failure', + 'takeoff_engine_failure_to_v1', + 'takeoff_v1_to_vr', + 'takeoff_vr_to_liftoff', + 'takeoff_liftoff_to_climb_gradient', + 'takeoff_climb_gradient_to_obstacle', + ], + vars=['time', 'distance', 'velocity', 'mass'], + connected=True, + ) + + self._traj.link_phases( + ['takeoff_v1_to_vr', 'takeoff_vr_to_liftoff', 'takeoff_liftoff_to_climb_gradient'], + vars=[Dynamic.Vehicle.ANGLE_OF_ATTACK], + connected=True, + ) + + self._traj.link_phases( + ['takeoff_liftoff_to_climb_gradient', 'takeoff_climb_gradient_to_obstacle'], + vars=[Dynamic.Mission.ALTITUDE, Dynamic.Mission.FLIGHT_PATH_ANGLE], + connected=True, + ) + + # Abort Sequence + self._traj.link_phases( + ['takeoff_engine_failure_to_v1', 'takeoff_v1_to_roll_stop'], + vars=['time', 'distance', 'velocity', 'mass'], + connected=True, + ) + + if model is not None: + phase_names = list(self._phases.keys()) + + # This is a level 3 method that uses the default subsystems. + # We need to create parameters for just the inputs we have. + # They mostly come from the low-speed aero subsystem. + + kwargs = {'method': 'low_speed'} + + # TODO: Why is get_parameters different for different subsystems? + # Do without blindly indexing. + aero_builder = self.core_subsystems[0] + + params = aero_builder.get_parameters(aviary_options, **kwargs) + + # takeoff introduces this one. + params[Mission.Takeoff.LIFT_COEFFICIENT_MAX] = { + 'shape': (1,), + 'static_target': True, + } + + ext_params = {} + for phase in self._phases.keys(): + ext_params[phase] = params + + setup_trajectory_params( + model, self._traj, aviary_options, phase_names, external_parameters=ext_params + ) + + return self._traj + + def apply_initial_guesses(self, prob: om.Problem, traj_name): # , phase: dm.Phase): + """Apply any stored initial guesses; return a list of guesses not applied.""" + not_applied = {} + phase_builder: PhaseBuilderBase = None + + for phase, phase_builder in self._phases.values(): + tmp = phase_builder.apply_initial_guesses(prob, traj_name, phase) + + if tmp: + not_applied[phase_builder.name] = tmp + + return not_applied + + def _extra_ode_init_kwargs(self): + """Return extra kwargs required for initializing the ODE.""" + return {} + + def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): + name = phase_builder.name + phase = phase_builder.build_phase(aviary_options) + + self._traj.add_phase(name, phase) + + self._phases[name] = self.MappedPhase(phase, phase_builder) diff --git a/aviary/mission/flops_based/ode/takeoff_eom.py b/aviary/mission/flops_based/ode/takeoff_eom.py index 6a466cc8b..bfeea0b5c 100644 --- a/aviary/mission/flops_based/ode/takeoff_eom.py +++ b/aviary/mission/flops_based/ode/takeoff_eom.py @@ -118,6 +118,13 @@ def initialize(self): desc='current friction coefficient key, either rolling friction or braking friction', ) + self.options.declare( + 'pitch_control', + values=['ALPHA_FIXED', 'ALPHA_RATE_FIXED', 'GAMMA_FIXED'], + default='ALPHA_FIXED', + desc='How pitch is controlled.', + ) + options.declare( 'aviary_options', types=AviaryValues, @@ -131,6 +138,7 @@ def setup(self): climbing = options['climbing'] friction_key = options['friction_key'] aviary_options = options['aviary_options'] + pitch_control = options['pitch_control'] mu = aviary_options.get_val(friction_key) kwargs = {'num_nodes': nn, 'climbing': climbing} @@ -156,7 +164,7 @@ def setup(self): 'sum_forces', SumForces(**kwargs), promotes_inputs=['*'], - promotes_outputs=['forces_horizontal', 'forces_vertical'], + promotes_outputs=['forces_horizontal', 'forces_vertical', 'ground_normal_force'], ) self.add_subsystem( @@ -186,6 +194,17 @@ def setup(self): promotes=['*'], ) + if self.options['pitch_control'] == 'GAMMA_FIXED': + # If using fixed-flight-path-angle control + alpha_resid_comp = om.InputResidsComp() + self.add_subsystem('alpha_resid_comp', alpha_resid_comp, promotes=['*']) + alpha_resid_comp.add_input( + Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, shape=(nn,), units='rad/s' + ) + alpha_resid_comp.add_output( + Dynamic.Vehicle.ANGLE_OF_ATTACK, shape=(nn,), val=0.0, units='rad' + ) + class DistanceRates(om.ExplicitComponent): """ @@ -255,6 +274,8 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): else: range_rate = velocity + outputs[Dynamic.Mission.ALTITUDE_RATE][...] = 0.0 + outputs[Dynamic.Mission.DISTANCE_RATE] = range_rate def compute_partials(self, inputs, J, discrete_inputs=None): @@ -562,6 +583,13 @@ def setup(self): desc='current sum of forces in the vertical direction', ) + self.add_output( + 'ground_normal_force', + val=np.zeros(nn), + units='N', + desc='runway force pushing up on the landing gear while on the ground and lift < weight.', + ) + def setup_partials(self): options = self.options @@ -589,7 +617,9 @@ def setup_partials(self): Dynamic.Mission.FLIGHT_PATH_ANGLE, ] - self.declare_partials('*', wrt, rows=rows_cols, cols=rows_cols) + self.declare_partials( + ['forces_vertical', 'forces_horizontal'], wrt, rows=rows_cols, cols=rows_cols + ) else: aviary_options: AviaryValues = options['aviary_options'] @@ -640,6 +670,30 @@ def setup_partials(self): self.declare_partials('forces_vertical', ['*'], dependent=False) + self.declare_partials( + 'ground_normal_force', + Dynamic.Vehicle.MASS, + rows=rows_cols, + cols=rows_cols, + val=grav_metric, + ) + + self.declare_partials( + 'ground_normal_force', + Dynamic.Vehicle.LIFT, + rows=rows_cols, + cols=rows_cols, + val=-1.0, + ) + + self.declare_partials( + 'ground_normal_force', + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + rows=rows_cols, + cols=rows_cols, + val=np.sin(t_inc), + ) + def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): options = self.options @@ -678,6 +732,7 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): f_v = t_v - drag * s_gamma + lift * c_gamma - weight outputs['forces_vertical'] = f_v + outputs['ground_normal_force'][...] = 0.0 else: # NOTE using FLOPS GRRUN, which neglects angle of attack @@ -690,6 +745,7 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): t_v = thrust * np.sin(t_inc) f_h = t_h - drag - (weight - (lift + t_v)) * mu + outputs['ground_normal_force'][...] = -t_v - lift + weight outputs['forces_horizontal'] = f_h diff --git a/aviary/mission/flops_based/ode/takeoff_ode.py b/aviary/mission/flops_based/ode/takeoff_ode.py index 7fbbb2479..9b25938b3 100644 --- a/aviary/mission/flops_based/ode/takeoff_ode.py +++ b/aviary/mission/flops_based/ode/takeoff_ode.py @@ -29,6 +29,13 @@ def initialize(self): desc='mode of operation (ground roll or flight)', ) + self.options.declare( + 'pitch_control', + values=['ALPHA_FIXED', 'ALPHA_RATE_FIXED', 'GAMMA_FIXED'], + default='ALPHA_FIXED', + desc='How pitch is controlled.', + ) + def setup(self): options = self.options @@ -64,12 +71,13 @@ def setup(self): 'climbing': options['climbing'], 'friction_key': options['friction_key'], 'aviary_options': options['aviary_options'], + 'pitch_control': options['pitch_control'], } self.add_subsystem( 'takeoff_eom', TakeoffEOM(**kwargs), - promotes_inputs=[ + promotes=[ Dynamic.Mission.FLIGHT_PATH_ANGLE, Dynamic.Mission.VELOCITY, Dynamic.Vehicle.MASS, @@ -77,8 +85,6 @@ def setup(self): Dynamic.Vehicle.Propulsion.THRUST_TOTAL, Dynamic.Vehicle.DRAG, Dynamic.Vehicle.ANGLE_OF_ATTACK, - ], - promotes_outputs=[ Dynamic.Mission.DISTANCE_RATE, Dynamic.Mission.ALTITUDE_RATE, Dynamic.Mission.VELOCITY_RATE, @@ -103,3 +109,15 @@ def setup(self): self.set_input_defaults(Dynamic.Mission.ALTITUDE, np.zeros(nn), 'm') self.set_input_defaults(Dynamic.Mission.VELOCITY, np.zeros(nn), 'm/s') self.set_input_defaults(Aircraft.Wing.AREA, 1.0, 'm**2') + + if self.options['pitch_control'] == 'gamma_fixed': + self.nonlinear_solver = om.NewtonSolver( + solve_subsystems=True, + maxiter=100, + atol=1.0e-6, + rtol=1.0e-6, + iprint=0, + debug_print=False, + ) + self.nonlinear_solver.linesearch = om.ArmijoGoldsteinLS() + self.linear_solver = om.DirectSolver(assemble_jac=True) diff --git a/aviary/mission/flops_based/phases/balanced_field_trajectory.py b/aviary/mission/flops_based/phases/balanced_field_trajectory.py new file mode 100644 index 000000000..0c9a90a40 --- /dev/null +++ b/aviary/mission/flops_based/phases/balanced_field_trajectory.py @@ -0,0 +1,530 @@ +""" +Define utilities for building detailed takeoff phases and the typical takeoff trajectory. + +Classes +------- +TakeoffBrakeReleaseToDecisionSpeed : a phase builder for the first phase of takeoff, from +brake release to decision speed, the maximum speed at which takeoff can be safely brought +to full stop using zero thrust while braking + +TakeoffDecisionSpeedToRotate : a phase builder for the second phase of takeoff, from +decision speed to rotation + +TakeoffDecisionSpeedBrakeDelay : a phase builder for the second phase of aborted takeoff, +from decision speed to brake application + +TakeoffRotateToLiftoff : a phase builder for the third phase of takeoff, from rotation to +liftoff + +TakeoffLiftoffToObstacle : a phase builder for the fourth phase of takeoff, from liftoff +to clearing the required obstacle + +TakeoffObstacleToMicP2 : a phase builder for the fifth phase of takeoff, from +clearing the required obstacle to the P2 mic lication; this phase is required for +acoustic calculations + +TakeoffMicP2ToEngineCutback : a phase builder for the sixth phase of takeoff, from the +P2 mic location to engine cutback; this phase is required for acoustic calculations + +TakeoffEngineCutback : a phase builder for the seventh phase of takeoff, from +start to finish of engine cutback; this phase is required for acoustic calculations + +TakeoffEngineCutbackToMicP1 : a phase builder for the eighth phase of takeoff, from +engine cutback to the P1 mic lication; this phase is required for acoustic calculations + +TakeoffMicP1ToClimb : a phase builder for the ninth phase of takeoff, from +P1 mic location to climb; this phase is required for acoustic calculations + +TakeoffBrakeToAbort : a phase builder for the last phase of aborted takeoff, from brake +application to full stop + +TakeoffTrajectory : a trajectory builder for detailed takeoff +""" + +from collections import namedtuple + +import dymos as dm +from openmdao.solvers.solver import NonlinearSolver, LinearSolver + +from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE +from aviary.mission.initial_guess_builders import ( + InitialGuessControl, + InitialGuessIntegrationVariable, + InitialGuessParameter, + InitialGuessState, +) +from aviary.mission.phase_builder_base import PhaseBuilderBase +from aviary.utils.aviary_options_dict import AviaryOptionsDictionary +from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.variables import Aircraft, Dynamic, Mission + + +# VR_RATIO = 1.18 + + +def _init_initial_guess_meta_data(cls: PhaseBuilderBase): + """Create default initial guess meta data preset with common items.""" + cls._initial_guesses_meta_data_ = {} + + cls._add_initial_guess_meta_data( + InitialGuessIntegrationVariable(), + desc='initial guess for initial time and duration specified as a tuple', + ) + + cls._add_initial_guess_meta_data( + InitialGuessState('distance'), desc='initial guess for horizontal distance traveled' + ) + + cls._add_initial_guess_meta_data(InitialGuessState('velocity'), desc='initial guess for speed') + + cls._add_initial_guess_meta_data(InitialGuessState('mass'), desc='initial guess for mass') + cls._add_initial_guess_meta_data( + InitialGuessState(Dynamic.Mission.ALTITUDE), desc='initial guess for altitude' + ) + + cls._add_initial_guess_meta_data( + InitialGuessControl('throttle'), desc='initial guess for throttle' + ) + + cls._add_initial_guess_meta_data(InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK)) + + cls._add_initial_guess_meta_data(InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE)) + + cls._add_initial_guess_meta_data(InitialGuessParameter(Dynamic.Mission.FLIGHT_PATH_ANGLE)) + cls._add_initial_guess_meta_data(InitialGuessParameter('dV1')) + cls._add_initial_guess_meta_data(InitialGuessParameter('dVEF')) + + return cls + + +class BalancedFieldPhaseOptions(AviaryOptionsDictionary): + def declare_options(self): + self.declare( + name='max_duration', + default=1000.0, + units='s', + desc='Upper bound on duration for this phase.', + ) + + self.declare( + name='time_duration_ref', default=10.0, units='s', desc='Scale factor ref for duration.' + ) + + self.declare( + name='distance_max', default=1000.0, units='ft', desc='Upper bound for distance.' + ) + + self.declare( + name='max_velocity', default=100.0, units='ft/s', desc='Upper bound for velocity.' + ) + + self.declare( + name='terminal_condition', + values=[ + 'VEF', + 'V1', + 'VR', + 'LIFTOFF', + 'CLIMB_GRADIENT', + 'MAX_ALPHA', + 'OBSTACLE', + 'STOP', + ], + allow_none=True, + default=None, + desc='The condition which governs the end of the phase.', + ) + + self.declare( + name='climbing', + types=bool, + default=False, + desc='If False, assume aircraft is operating on the runway.', + ) + + self.declare( + name='friction_key', + types=str, + default=Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT, + desc='Friction Coefficient to use in the phase', + ) + + self.declare( + name='pitch_control', + values=['ALPHA_FIXED', 'ALPHA_RATE_FIXED', 'GAMMA_FIXED'], + default='ALPHA_FIXED', + desc='Specifies how alpha is controlled - Alpha can be a fixed parameter, specified via a fixed rate parameter, ' + 'or whether the climb gradient is constant', + ) + + self.declare( + name='nonlinear_solver', + types=(NonlinearSolver,), + allow_none=True, + default=None, + desc='Nonlinear solver applied to the phase if it needs to solve for the terminal speed condition.', + ) + + self.declare( + name='linear_solver', + types=(LinearSolver,), + allow_none=True, + default=None, + desc='Linear solver applied to the phase if it needs to solve for the terminal speed condition.', + ) + + +@_init_initial_guess_meta_data +class BalancedFieldPhaseBuilder(PhaseBuilderBase): + """ + Define a phase builder for detailed takeoff phases. + + Attributes + ---------- + name : str ('takeoff_brake_release') + object label + + user_options : AviaryValues () + state/path constraint values and flags + + supported options: + - max_duration (1000.0, 's') + - time_duration_ref (10.0, 's') + - distance_max (1000.0, 'ft') + - max_velocity (100.0, 'ft/s') + + initial_guesses : AviaryValues () + state/path beginning values to be set on the problem + + supported options: + - time + - range + - velocity + - mass + - throttle + - angle_of_attack + + ode_class : type (None) + advanced: the type of system defining the ODE + + transcription : "Dymos transcription object" (None) + advanced: an object providing the transcription technique of the + optimal control problem + + default_name : str + class attribute: derived type customization point; the default value + for name + + default_ode_class : type + class attribute: derived type customization point; the default value + for ode_class used by build_phase + + Methods + ------- + build_phase + make_default_transcription + """ + + __slots__ = () + + default_name = 'detailed_takeoff' + default_ode_class = TakeoffODE + default_options_class = BalancedFieldPhaseOptions + + def build_phase(self, aviary_options=None): + """ + Return a new phase object for analysis using these constraints. + + If ode_class is None, default_ode_class is used. + + If transcription is None, the return value from calling + make_default_transcription is used. + + Parameters + ---------- + aviary_options : AviaryValues (empty) + collection of Aircraft/Mission specific options + + Returns + ------- + dymos.Phase + """ + phase: dm.Phase = super().build_phase(aviary_options) + + user_options: AviaryValues = self.user_options + + max_duration, units = user_options['max_duration'] + duration_ref = user_options.get_val('time_duration_ref', units) + climbing = user_options['climbing'] + + phase.set_time_options( + fix_initial=True, + duration_bounds=(0.001, max_duration), + duration_ref=duration_ref, + units=units, + ) + + distance_max, units = user_options['distance_max'] + + phase.add_state( + Dynamic.Mission.DISTANCE, + fix_initial=True, + lower=0, + ref=distance_max, + defect_ref=distance_max, + units=units, + upper=distance_max, + rate_source=Dynamic.Mission.DISTANCE_RATE, + ) + + max_velocity, units = user_options['max_velocity'] + + phase.add_state( + Dynamic.Mission.VELOCITY, + fix_initial=True, + lower=0, + ref=max_velocity, + defect_ref=max_velocity, + units=units, + upper=max_velocity, + rate_source=Dynamic.Mission.VELOCITY_RATE, + ) + + phase.add_state( + Dynamic.Vehicle.MASS, + fix_initial=True, + fix_final=False, + lower=0.0, + upper=1e9, + ref=5e4, + units='kg', + rate_source=Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE_TOTAL, + targets=Dynamic.Vehicle.MASS, + ) + + if climbing: + phase.add_state( + Dynamic.Mission.FLIGHT_PATH_ANGLE, + fix_initial=True, + lower=0, + ref=1.0, + upper=1.5, + defect_ref=1.0, + units='rad', + rate_source=Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, + ) + + phase.add_state( + Dynamic.Mission.ALTITUDE, + fix_initial=False, + lower=0, + ref=1.0, + defect_ref=1.0, + units='ft', + rate_source=Dynamic.Mission.ALTITUDE_RATE, + ) + + # TODO: Energy phase places this under an if num_engines > 0. + phase.add_control( + Dynamic.Vehicle.Propulsion.THROTTLE, + targets=Dynamic.Vehicle.Propulsion.THROTTLE, + units='unitless', + opt=False, + ) + + phase.add_parameter('VR_ratio', val=1.05, units='unitless', opt=False) + + if user_options['pitch_control'] == 'ALPHA_FIXED': + phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') + elif user_options['pitch_control'] == 'ALPHA_RATE_FIXED': + phase.add_parameter( + Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, val=2.0, opt=False, units='deg/s' + ) + phase.add_state( + Dynamic.Vehicle.ANGLE_OF_ATTACK, + fix_initial=True, + fix_final=False, + ref=1.0, + units='deg', + rate_source=Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, + targets=Dynamic.Vehicle.ANGLE_OF_ATTACK, + ) + else: + phase.add_timeseries_output(Dynamic.Vehicle.ANGLE_OF_ATTACK, units='deg') + + phase.add_timeseries_output( + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + output_name=Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + units='lbf', + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' + ) + + # Define velocity to go based on definition of terminal speed, if applicable. + terminal_condition = user_options['terminal_condition'] + if terminal_condition == 'V1': + # Propagate until speed is the decision speed. + # In balanced field applications, dV1 will be + # set such that a rejected takeoff and a + # takeoff to a 35 ft altitude for obstacle clearance + # require the same range. + phase.add_calc_expr( + f'v_to_go = velocity - (VR_ratio * v_stall - dV1)', + velocity={'units': 'kn'}, + dV1={'units': 'kn'}, + v_to_go={'units': 'kn'}, + v_stall={'units': 'kn'}, + VR_ratio={'units': 'unitless'}, + ) + phase.add_parameter( + 'dV1', + opt=False, + val=1.0, + units='kn', + desc='Decision speed delta below rotation speed.', + ) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + elif terminal_condition == 'VR': + # Propagate until speed is the rotation speed. + phase.add_calc_expr( + 'v_to_go = velocity - (VR_ratio * v_stall)', + velocity={'units': 'kn'}, + VR_ratio={'units': 'unitless'}, + v_stall={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + phase.add_boundary_balance( + param='t_duration', + name='v_to_go', + tgt_val=0.0, + loc='final', + lower=-1000, + upper=1000, + ref=10.0, + ) + elif terminal_condition == 'VEF': + # Propagate until engine failure. + # Note we expect dVEF to be negative here. + # In balanced field applications, dVEF will be set + # such that it occurs {pilot_reaction_time} seconds + # before V1. + phase.add_calc_expr( + 'v_to_go = velocity - (VR_ratio * v_stall - dV1 - dVEF)', + velocity={'units': 'kn'}, + VR_ratio={'units': 'unitless'}, + dV1={'units': 'kn'}, + v_to_go={'units': 'kn'}, + v_stall={'units': 'kn'}, + dVEF={'units': 'kn'}, + ) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + phase.add_parameter( + 'dVEF', + opt=False, + val=3.0, + units='kn', + desc='Engine failure speed delta below decision speed.', + ) + phase.add_parameter( + 'dV1', + opt=False, + val=1.0, + units='kn', + desc='Decision speed delta below rotation speed.', + ) + elif terminal_condition == 'MAX_ALPHA': + # Propagate until velocity is the desired literal value. + phase.add_boundary_balance( + param='t_duration', + name=Dynamic.Vehicle.ANGLE_OF_ATTACK, + tgt_val=12.0, + lower=0.0001, + upper=1000, + eq_units='deg', + loc='final', + ) + elif terminal_condition == 'LIFTOFF': + # Propagate until velocity is the desired literal value. + phase.add_boundary_balance( + param='t_duration', + name='takeoff_eom.ground_normal_force', + tgt_val=0.0, + lower=0.0001, + upper=1000, + eq_units='MN', + loc='final', + ) + elif terminal_condition == 'CLIMB_GRADIENT': + phase.add_calc_expr( + 'climb_gradient = tan(flight_path_angle)', + climb_gradient={'units': 'unitless'}, + flight_path_angle={'units': 'rad'}, + ) + phase.add_boundary_balance( + param='t_duration', + name='climb_gradient', + tgt_val=0.024, + lower=0.0001, + upper=1000, + eq_units='unitless', + loc='final', + ) + elif terminal_condition == 'OBSTACLE': + phase.add_boundary_balance( + param='t_duration', + name=Dynamic.Mission.ALTITUDE, + tgt_val=35, + lower=0.0001, + upper=1000, + eq_units='ft', + loc='final', + ) + elif terminal_condition == 'STOP': + phase.add_boundary_balance( + param='t_duration', + name=Dynamic.Mission.VELOCITY, + tgt_val=5.0, + lower=0.0001, + upper=1000, + eq_units='kn', + loc='final', + ) + elif terminal_condition is None: + # Propagate for t_duration + pass + else: + raise ValueError( + f'Unrecognized value for terminal_speed ({terminal_condition}).' + 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' + ) + + if phase.boundary_balance_options: + phase.options['auto_order'] = True + + if user_options['nonlinear_solver'] is not None: + phase.nonlinear_solver = user_options['nonlinear_solver'] + + if user_options['linear_solver'] is not None: + phase.linear_solver = user_options['linear_solver'] + + phase.add_timeseries_output( + 'v_over_v_stall', output_name='v_over_v_stall', units='unitless' + ) + + return phase + + def make_default_transcription(self): + """Return a transcription object to be used by default in build_phase.""" + transcription = dm.PicardShooting(num_segments=1, nodes_per_seg=10) + + return transcription + + def _extra_ode_init_kwargs(self): + """Return extra kwargs required for initializing the ODE.""" + return { + 'climbing': self.user_options['climbing'], + 'pitch_control': self.user_options['pitch_control'], + 'friction_key': self.user_options['friction_key'], + } diff --git a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py index f91ba7fcf..26b706fca 100644 --- a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py +++ b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py @@ -41,10 +41,13 @@ TakeoffTrajectory : a trajectory builder for detailed takeoff """ +from cmath import phase from collections import namedtuple +from platform import node import dymos as dm import openmdao.api as om +from openmdao.solvers.solver import NonlinearSolver, LinearSolver from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE from aviary.mission.initial_guess_builders import ( @@ -88,7 +91,7 @@ def _init_initial_guess_meta_data(cls: PhaseBuilderBase): return cls -class TakeoffBrakeReleaseToDecisionSpeedOptions(AviaryOptionsDictionary): +class DetailedTakeoffPhaseOptions(AviaryOptionsDictionary): def declare_options(self): self.declare( name='max_duration', @@ -109,6 +112,361 @@ def declare_options(self): name='max_velocity', default=100.0, units='ft/s', desc='Upper bound for velocity.' ) + self.declare( + name='terminal_condition', + values=['VEF', 'V1', 'VR', 'LIFTOFF', 'CLIMB_GRADIENT', 'OBSTACLE'], + allow_none=True, + default=None, + desc='The condition which governs the end of the phase.', + ) + + self.declare( + name='climbing', + types=bool, + default=False, + desc='If False, assume aircraft is operating on the runway.', + ) + + self.declare( + name='pitch_control', + values=['alpha_fixed', 'alpha_rate_fixed', 'gamma_fixed'], + default='alpha_fixed', + desc='Specifies how alpha is controlled - Alpha can be a fixed parameter, specified via a fixed rate parameter, ' \ + 'or whether the climb gradient is constant', + ) + + self.declare( + name='nonlinear_solver', + types=(NonlinearSolver,), + allow_none=True, + default=None, + desc='Nonlinear solver applied to the phase if it needs to solve for the terminal speed condition.', + ) + + self.declare( + name='linear_solver', + types=(LinearSolver,), + allow_none=True, + default=None, + desc='Linear solver applied to the phase if it needs to solve for the terminal speed condition.', + ) + + +@_init_initial_guess_meta_data +class DetailedTakeoffPhaseBuilder(PhaseBuilderBase): + """ + Define a phase builder for detailed takeoff phases. + + Attributes + ---------- + name : str ('takeoff_brake_release') + object label + + user_options : AviaryValues () + state/path constraint values and flags + + supported options: + - max_duration (1000.0, 's') + - time_duration_ref (10.0, 's') + - distance_max (1000.0, 'ft') + - max_velocity (100.0, 'ft/s') + + initial_guesses : AviaryValues () + state/path beginning values to be set on the problem + + supported options: + - time + - range + - velocity + - mass + - throttle + - angle_of_attack + + ode_class : type (None) + advanced: the type of system defining the ODE + + transcription : "Dymos transcription object" (None) + advanced: an object providing the transcription technique of the + optimal control problem + + default_name : str + class attribute: derived type customization point; the default value + for name + + default_ode_class : type + class attribute: derived type customization point; the default value + for ode_class used by build_phase + + Methods + ------- + build_phase + make_default_transcription + """ + + __slots__ = () + + default_name = 'detailed_takeoff' + default_ode_class = TakeoffODE + default_options_class = DetailedTakeoffPhaseOptions + + def build_phase(self, aviary_options=None): + """ + Return a new phase object for analysis using these constraints. + + If ode_class is None, default_ode_class is used. + + If transcription is None, the return value from calling + make_default_transcription is used. + + Parameters + ---------- + aviary_options : AviaryValues (empty) + collection of Aircraft/Mission specific options + + Returns + ------- + dymos.Phase + """ + phase: dm.Phase = super().build_phase(aviary_options) + + user_options: AviaryValues = self.user_options + + max_duration, units = user_options['max_duration'] + duration_ref = user_options.get_val('time_duration_ref', units) + climbing = user_options['climbing'] + + phase.set_time_options( + fix_initial=True, + duration_bounds=(0.001, max_duration), + duration_ref=duration_ref, + units=units, + ) + + distance_max, units = user_options['distance_max'] + + phase.add_state( + Dynamic.Mission.DISTANCE, + fix_initial=True, + lower=0, + ref=distance_max, + defect_ref=distance_max, + units=units, + upper=distance_max, + rate_source=Dynamic.Mission.DISTANCE_RATE, + ) + + max_velocity, units = user_options['max_velocity'] + + phase.add_state( + Dynamic.Mission.VELOCITY, + fix_initial=True, + lower=0, + ref=max_velocity, + defect_ref=max_velocity, + units=units, + upper=max_velocity, + rate_source=Dynamic.Mission.VELOCITY_RATE, + ) + + phase.add_state( + Dynamic.Vehicle.MASS, + fix_initial=True, + fix_final=False, + lower=0.0, + upper=1e9, + ref=5e4, + units='kg', + rate_source=Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE_TOTAL, + targets=Dynamic.Vehicle.MASS, + ) + + if climbing: + phase.add_state( + Dynamic.Mission.FLIGHT_PATH_ANGLE, + fix_initial=True, + lower=0, + ref=1.0, + upper=1.5, + defect_ref=1.0, + units='rad', + rate_source=Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE, + ) + + phase.add_state( + Dynamic.Mission.ALTITUDE, + fix_initial=False, + lower=0, + ref=1.0, + defect_ref=1.0, + units='ft', + rate_source=Dynamic.Mission.ALTITUDE_RATE, + ) + + # TODO: Energy phase places this under an if num_engines > 0. + phase.add_control( + Dynamic.Vehicle.Propulsion.THROTTLE, + targets=Dynamic.Vehicle.Propulsion.THROTTLE, + units='unitless', + opt=False, + ) + + if user_options['pitch_control'] == 'alpha_fixed': + phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') + elif user_options['pitch_control'] == 'alpha_rate_fixed': + phase.add_parameter( + Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, val=2.0, opt=False, units='deg/s' + ) + phase.add_state( + Dynamic.Vehicle.ANGLE_OF_ATTACK, + fix_initial=True, + fix_final=False, + ref=1.0, + units='deg', + rate_source=Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE, + targets=Dynamic.Vehicle.ANGLE_OF_ATTACK, + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + output_name=Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + units='lbf', + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' + ) + + # Define velocity to go based on definition of terminal speed, if applicable. + terminal_condition = user_options['terminal_condition'] + if terminal_condition == 'V1': + # Propagate until speed is the decision speed. + # In balanced field applications, dV1 will be + # set such that a rejected takeoff and a + # takeoff to a 35 ft altitude for obstacle clearance + # require the same range. + phase.add_calc_expr( + 'v_to_go = velocity - (dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + elif terminal_condition == 'VR': + # Propagate until speed is the rotation speed. + # phase.add_calc_expr( + # 'v_to_go = velocity - (dVR + dV1 + v_stall)', + # velocity={'units': 'kn'}, + # dv1={'units': 'kn'}, + # dVR={'units': 'kn'}, + # v_to_go={'units': 'kn'}, + # ) + pass + phase.add_boundary_balance( + param='t_duration', name='v_over_v_stall', tgt_val=1.05, loc='final' + ) + elif terminal_condition == 'VEF': + # Propagate until engine failure. + # Note we expect dVEF to be negative here. + # In balanced field applications, dVEF will be set + # such that it occurs {pilot_reaction_time} seconds + # before V1. + phase.add_calc_expr( + 'v_to_go = velocity - (dVEF + dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + dVEF={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + elif terminal_condition == 'LIFTOFF': + # Propagate until velocity is the desired literal value. + phase.add_boundary_balance( + param='t_duration', + name='takeoff_eom.ground_normal_force', + tgt_val=0.0, + lower=0.0001, + upper=20, + eq_units='MN', + loc='final', + ) + elif terminal_condition == 'CLIMB_GRADIENT': + phase.add_calc_expr( + 'climb_gradient = tan(flight_path_angle)', + climb_gradient={'units': 'unitless'}, + flight_path_angle={'units': 'rad'}, + ) + phase.add_boundary_balance( + param='t_duration', + name='climb_gradient', + tgt_val=0.024, + lower=0.0001, + upper=20, + eq_units='unitless', + loc='final', + ) + elif terminal_condition == 'OBSTACLE': + phase.add_boundary_balance( + param='t_duration', + name=Dynamic.Mission.ALTITUDE, + tgt_val=35, + lower=0.0001, + upper=100, + eq_units='ft', + loc='final', + ) + elif terminal_condition is None: + # Propagate for t_duration + pass + else: + raise ValueError( + f'Unrecognized value for terminal_speed ({terminal_condition}).' + 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' + ) + + phase.add_parameter( + 'dV1', opt=False, val=10.0, units='kn', desc='Decision speed delta above stall speed.' + ) + # phase.add_parameter('dVEF', opt=False, val=10.0, desc='Decision speed delta below decision speed.') + + if phase.boundary_balance_options: + phase.options['auto_order'] = True + + if user_options['nonlinear_solver'] is not None: + phase.nonlinear_solver = user_options['nonlinear_solver'] + + if user_options['linear_solver'] is not None: + phase.linear_solver = user_options['linear_solver'] + + return phase + + def make_default_transcription(self): + """Return a transcription object to be used by default in build_phase.""" + transcription = dm.Radau(num_segments=3, order=3, compressed=True) + transcription = dm.PicardShooting(num_segments=1, nodes_per_seg=10) + + return transcription + + def _extra_ode_init_kwargs(self): + """Return extra kwargs required for initializing the ODE.""" + return { + 'climbing': self.user_options['climbing'], + 'pitch_control': self.user_options['pitch_control'], + 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT, + } + + +DetailedTakeoffPhaseBuilder._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK) +) + +DetailedTakeoffPhaseBuilder._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK_RATE) +) + +DetailedTakeoffPhaseBuilder._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Mission.FLIGHT_PATH_ANGLE) +) + @_init_initial_guess_meta_data class TakeoffBrakeReleaseToDecisionSpeed(PhaseBuilderBase): @@ -167,7 +525,7 @@ class attribute: derived type customization point; the default value default_name = 'takeoff_brake_release' default_ode_class = TakeoffODE - default_options_class = TakeoffBrakeReleaseToDecisionSpeedOptions + default_options_class = DetailedTakeoffPhaseOptions def build_phase(self, aviary_options=None): """ @@ -196,7 +554,7 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=True, - duration_bounds=(1, max_duration), + duration_bounds=(0.1, max_duration), duration_ref=duration_ref, units=units, ) @@ -259,11 +617,252 @@ def build_phase(self, aviary_options=None): Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' ) + # Define velocity to go based on definition of terminal speed, if applicable. + terminal_speed = user_options['terminal_speed'] + if terminal_speed == 'V1': + # Propagate until speed is the decision speed. + # In balanced field applications, dV1 will be + # set such that a rejected takeoff and a + # takeoff to a 35 ft altitude for obstacle clearance + # require the same range. + phase.add_calc_expr( + 'v_to_go = velocity - (dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif terminal_speed == 'VR': + # Propagate until speed is the rotation speed. + phase.add_calc_expr( + 'v_to_go = velocity - (dVR + dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + dVR={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif terminal_speed == 'VEF': + # Propagate until engine failure. + # Note we expect dVEF to be negative here. + # In balanced field applications, dVEF will be set + # such that it occurs {pilot_reaction_time} seconds + # before V1. + phase.add_calc_expr( + 'v_to_go = velocity - (dVEF + dV1 + v_stall)', + velocity={'units': 'kn'}, + dv1={'units': 'kn'}, + dVEF={'units': 'kn'}, + v_to_go={'units': 'kn'}, + ) + v_to_go_calc = True + elif isinstance(terminal_speed, (float, int)): + # Propagate until velocity is the desired literal value. + phase.add_boundary_balance( + param='t_duration', name='velocity', tgt_val=terminal_speed, loc='final' + ) + phase.options['auto_order'] = True + elif terminal_speed is None: + # Propagate for t_duration + pass + else: + raise ValueError( + f'Unrecognized value for terminal_speed ({terminal_speed}).' + 'Must be one of "VEF", "VR", "V1" or a literal numerical value.' + ) + + if v_to_go_calc: + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + phase.options['auto_order'] = True + + if user_options['nonlinear_solver'] is not None: + phase.nonlinear_solver = user_options['nonlinear_solver'] + + if user_options['linear_solver'] is not None: + phase.linear_solver = user_options['linear_solver'] + return phase def make_default_transcription(self): """Return a transcription object to be used by default in build_phase.""" transcription = dm.Radau(num_segments=3, order=3, compressed=True) + transcription = dm.PicardShooting(num_segments=1, nodes_per_seg=10) + + return transcription + + def _extra_ode_init_kwargs(self): + """Return extra kwargs required for initializing the ODE.""" + return {'climbing': False, 'friction_key': Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT} + + +TakeoffBrakeReleaseToDecisionSpeed._add_initial_guess_meta_data( + InitialGuessParameter(Dynamic.Vehicle.ANGLE_OF_ATTACK) +) + + +@_init_initial_guess_meta_data +class TakeoffBrakeReleaseToEngineFailure(PhaseBuilderBase): + """ + Define a phase builder for the first phase of takeoff, from brake release to decision + speed, the maximum speed at which takeoff can be safely brought to full stop using + zero thrust while braking. + + Attributes + ---------- + name : str ('takeoff_brake_release') + object label + + user_options : AviaryValues () + state/path constraint values and flags + + supported options: + - max_duration (1000.0, 's') + - time_duration_ref (10.0, 's') + - distance_max (1000.0, 'ft') + - max_velocity (100.0, 'ft/s') + + initial_guesses : AviaryValues () + state/path beginning values to be set on the problem + + supported options: + - time + - range + - velocity + - mass + - throttle + - angle_of_attack + + ode_class : type (None) + advanced: the type of system defining the ODE + + transcription : "Dymos transcription object" (None) + advanced: an object providing the transcription technique of the + optimal control problem + + default_name : str + class attribute: derived type customization point; the default value + for name + + default_ode_class : type + class attribute: derived type customization point; the default value + for ode_class used by build_phase + + Methods + ------- + build_phase + make_default_transcription + """ + + __slots__ = () + + default_name = 'takeoff_to_engine_failure' + default_ode_class = TakeoffODE + default_options_class = DetailedTakeoffPhaseOptions + + def build_phase(self, aviary_options=None): + """ + Return a new phase object for analysis using these constraints. + + If ode_class is None, default_ode_class is used. + + If transcription is None, the return value from calling + make_default_transcription is used. + + Parameters + ---------- + aviary_options : AviaryValues (empty) + collection of Aircraft/Mission specific options + + Returns + ------- + dymos.Phase + """ + phase: dm.Phase = super().build_phase(aviary_options) + + user_options: AviaryValues = self.user_options + + max_duration, units = user_options['max_duration'] + duration_ref = user_options.get_val('time_duration_ref', units) + + phase.set_time_options( + fix_initial=True, + duration_bounds=(0.1, max_duration), + duration_ref=duration_ref, + units=units, + ) + + distance_max, units = user_options['distance_max'] + + phase.add_state( + Dynamic.Mission.DISTANCE, + fix_initial=True, + lower=0, + ref=distance_max, + defect_ref=distance_max, + units=units, + upper=distance_max, + rate_source=Dynamic.Mission.DISTANCE_RATE, + ) + + max_velocity, units = user_options['max_velocity'] + + phase.add_state( + Dynamic.Mission.VELOCITY, + fix_initial=True, + lower=0, + ref=max_velocity, + defect_ref=max_velocity, + units=units, + upper=max_velocity, + rate_source=Dynamic.Mission.VELOCITY_RATE, + ) + + phase.add_state( + Dynamic.Vehicle.MASS, + fix_initial=True, + fix_final=False, + lower=0.0, + upper=1e9, + ref=5e4, + units='kg', + rate_source=Dynamic.Vehicle.Propulsion.FUEL_FLOW_RATE_NEGATIVE_TOTAL, + targets=Dynamic.Vehicle.MASS, + ) + + # TODO: Energy phase places this under an if num_engines > 0. + # phase.add_control( + # Dynamic.Vehicle.Propulsion.THROTTLE, + # targets=Dynamic.Vehicle.Propulsion.THROTTLE, + # units='unitless', + # opt=False, + # ) + + # phase.add_parameter(Dynamic.Vehicle.Propulsion.THROTTLE, + # targets=Dynamic.Vehicle.Propulsion.THROTTLE, + # val=1.0, opt=False, units='unitless') + phase.add_parameter(Dynamic.Vehicle.ANGLE_OF_ATTACK, val=0.0, opt=False, units='deg') + + phase.add_parameter('v_ef', val=100.0, opt=False, units='kn') + phase.add_calc_expr( + 'v_to_go = v - v_ef', v={'units': 'kn'}, v_ef={'units': 'kn'}, v_to_go={'units': 'kn'} + ) + phase.add_boundary_balance(param='t_duration', name='v_to_go', tgt_val=0.0, loc='final') + + phase.add_timeseries_output( + Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + output_name=Dynamic.Vehicle.Propulsion.THRUST_TOTAL, + units='lbf', + ) + + phase.add_timeseries_output( + Dynamic.Vehicle.DRAG, output_name=Dynamic.Vehicle.DRAG, units='lbf' + ) + + return phase + + def make_default_transcription(self): + """Return a transcription object to be used by default in build_phase.""" + transcription = dm.PicardShooting(num_segments=1, nodes_per_seg=11) return transcription @@ -394,8 +993,8 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -725,8 +1324,8 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -980,8 +1579,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -1292,8 +1891,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -1596,8 +2195,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -2168,8 +2767,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -2468,8 +3067,8 @@ def build_phase(self, aviary_options: AviaryValues = None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -2723,8 +3322,8 @@ def build_phase(self, aviary_options=None): phase.set_time_options( fix_initial=False, - duration_bounds=(1, max_duration), - initial_bounds=(1, initial_ref), + duration_bounds=(0.1, max_duration), + initial_bounds=(0.1, initial_ref), duration_ref=duration_ref, initial_ref=initial_ref, units=units, @@ -2815,9 +3414,11 @@ def __init__(self, name=None): self.name = name - self._brake_release_to_decision_speed = None + # self._brake_release_to_decision_speed = None self._decision_speed_to_rotate = None self._rotate_to_liftoff = None + self._liftoff_to_climb_gradient = None + self._climb_gradient_to_obstacle = None self._liftoff_to_obstacle = None self._obstacle_to_mic_p2 = None self._mic_p2_to_engine_cutback = None @@ -2870,6 +3471,24 @@ def set_rotate_to_liftoff(self, phase_builder: PhaseBuilderBase): """ self._rotate_to_liftoff = phase_builder + def set_liftoff_to_climb_gradient(self, phase_builder: PhaseBuilderBase): + """ + Assign the phase builder for the phase from liftoff until the required climb gradient is reached. + + Args: + phase_builder (PhaseBuilderBase): _description_ + """ + self._liftoff_to_climb_gradient = phase_builder + + def set_climb_gradient_to_obstacle(self, phase_builder: PhaseBuilderBase): + """ + Assign the phase builder for the phase from achievement of climb gradient until obstacle clearance. + + Args: + phase_builder (PhaseBuilderBase): _description_ + """ + self._climb_gradient_to_obstacle = phase_builder + def set_liftoff_to_obstacle(self, phase_builder: PhaseBuilderBase): """ Assign a phase builder for the short period between liftoff and clearing the @@ -3050,95 +3669,104 @@ def _add_phases(self, aviary_options: AviaryValues): self._add_phase(self._rotate_to_liftoff, aviary_options) - self._add_phase(self._liftoff_to_obstacle, aviary_options) + self._add_phase(self._liftoff_to_climb_gradient, aviary_options) - obstacle_to_mic_p2 = self._obstacle_to_mic_p2 + self._add_phase(self._climb_gradient_to_obstacle, aviary_options) - if obstacle_to_mic_p2 is not None: - self._add_phase(obstacle_to_mic_p2, aviary_options) + # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 - self._add_phase(self._mic_p2_to_engine_cutback, aviary_options) + # if obstacle_to_mic_p2 is not None: + # self._add_phase(obstacle_to_mic_p2, aviary_options) - self._add_phase(self._engine_cutback, aviary_options) + # self._add_phase(self._mic_p2_to_engine_cutback, aviary_options) - self._add_phase(self._engine_cutback_to_mic_p1, aviary_options) + # self._add_phase(self._engine_cutback, aviary_options) - self._add_phase(self._mic_p1_to_climb, aviary_options) + # self._add_phase(self._engine_cutback_to_mic_p1, aviary_options) - decision_speed_to_brake = self._decision_speed_to_brake + # self._add_phase(self._mic_p1_to_climb, aviary_options) - if decision_speed_to_brake is not None: - self._add_phase(decision_speed_to_brake, aviary_options) + # decision_speed_to_brake = self._decision_speed_to_brake - self._add_phase(self._brake_to_abort, aviary_options) + # if decision_speed_to_brake is not None: + # self._add_phase(decision_speed_to_brake, aviary_options) + + # self._add_phase(self._brake_to_abort, aviary_options) def _link_phases(self): traj: dm.Trajectory = self._traj brake_release_name = self._brake_release_to_decision_speed.name decision_speed_name = self._decision_speed_to_rotate.name + rotate_name = self._rotate_to_liftoff.name + liftoff_name = self._liftoff_to_climb_gradient.name + climb_gradient_name = self._climb_gradient_to_obstacle.name - basic_vars = ['time', 'distance', 'velocity', 'mass'] + traj.link_phases( + [brake_release_name, decision_speed_name, rotate_name, liftoff_name, climb_gradient_name], + vars=['time', 'distance', 'velocity', 'mass'], + connected=True, + ) - traj.link_phases([brake_release_name, decision_speed_name], vars=basic_vars) + traj.link_phases([rotate_name, liftoff_name], vars=['angle_of_attack',], connected=True) - rotate_name = self._rotate_to_liftoff.name + traj.link_phases([liftoff_name, climb_gradient_name], vars=['flight_path_angle', 'altitude'], connected=True) - ext_vars = basic_vars + ['angle_of_attack'] + # ext_vars = basic_vars + ['angle_of_attack'] - traj.link_phases([decision_speed_name, rotate_name], vars=ext_vars) + # traj.link_phases([decision_speed_name, rotate_name], vars=ext_vars) - liftoff_name = self._liftoff_to_obstacle.name + # liftoff_name = self._liftoff_to_obstacle.name - traj.link_phases([rotate_name, liftoff_name], vars=ext_vars) + # traj.link_phases([rotate_name, liftoff_name], vars=ext_vars) - obstacle_to_mic_p2 = self._obstacle_to_mic_p2 + # obstacle_to_mic_p2 = self._obstacle_to_mic_p2 - if obstacle_to_mic_p2 is not None: - obstacle_to_mic_p2_name = obstacle_to_mic_p2.name - mic_p2_to_engine_cutback_name = self._mic_p2_to_engine_cutback.name - engine_cutback_name = self._engine_cutback.name - engine_cutback_to_mic_p1_name = self._engine_cutback_to_mic_p1.name - mic_p1_to_climb_name = self._mic_p1_to_climb.name + # if obstacle_to_mic_p2 is not None: + # obstacle_to_mic_p2_name = obstacle_to_mic_p2.name + # mic_p2_to_engine_cutback_name = self._mic_p2_to_engine_cutback.name + # engine_cutback_name = self._engine_cutback.name + # engine_cutback_to_mic_p1_name = self._engine_cutback_to_mic_p1.name + # mic_p1_to_climb_name = self._mic_p1_to_climb.name - acoustics_vars = ext_vars + [Dynamic.Mission.FLIGHT_PATH_ANGLE, 'altitude'] + # acoustics_vars = ext_vars + [Dynamic.Mission.FLIGHT_PATH_ANGLE, 'altitude'] - traj.link_phases([liftoff_name, obstacle_to_mic_p2_name], vars=acoustics_vars) + # traj.link_phases([liftoff_name, obstacle_to_mic_p2_name], vars=acoustics_vars) - traj.link_phases( - [obstacle_to_mic_p2_name, mic_p2_to_engine_cutback_name], vars=acoustics_vars - ) + # traj.link_phases( + # [obstacle_to_mic_p2_name, mic_p2_to_engine_cutback_name], vars=acoustics_vars + # ) - traj.link_phases( - [mic_p2_to_engine_cutback_name, engine_cutback_name], vars=acoustics_vars - ) + # traj.link_phases( + # [mic_p2_to_engine_cutback_name, engine_cutback_name], vars=acoustics_vars + # ) - traj.link_phases( - [engine_cutback_name, engine_cutback_to_mic_p1_name], vars=acoustics_vars - ) + # traj.link_phases( + # [engine_cutback_name, engine_cutback_to_mic_p1_name], vars=acoustics_vars + # ) - traj.link_phases( - [engine_cutback_to_mic_p1_name, mic_p1_to_climb_name], vars=acoustics_vars - ) + # traj.link_phases( + # [engine_cutback_to_mic_p1_name, mic_p1_to_climb_name], vars=acoustics_vars + # ) - decision_speed_to_brake = self._decision_speed_to_brake + # decision_speed_to_brake = self._decision_speed_to_brake - if decision_speed_to_brake is not None: - brake_name = decision_speed_to_brake.name - abort_name = self._brake_to_abort.name + # if decision_speed_to_brake is not None: + # brake_name = decision_speed_to_brake.name + # abort_name = self._brake_to_abort.name - traj.link_phases([brake_release_name, brake_name], vars=basic_vars) - traj.link_phases([brake_name, abort_name], vars=basic_vars) + # traj.link_phases([brake_release_name, brake_name], vars=basic_vars) + # traj.link_phases([brake_name, abort_name], vars=basic_vars) - traj.add_linkage_constraint( - phase_a=abort_name, - var_a='distance', - loc_a='final', - phase_b=liftoff_name, - var_b='distance', - loc_b='final', - ref=self._balanced_field_ref, - ) + # traj.add_linkage_constraint( + # phase_a=abort_name, + # var_a='distance', + # loc_a='final', + # phase_b=liftoff_name, + # var_b='distance', + # loc_b='final', + # ref=self._balanced_field_ref, + # ) def _add_phase(self, phase_builder: PhaseBuilderBase, aviary_options: AviaryValues): name = phase_builder.name diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field.py b/aviary/mission/flops_based/phases/test/test_balanced_field.py new file mode 100644 index 000000000..c8357ef34 --- /dev/null +++ b/aviary/mission/flops_based/phases/test/test_balanced_field.py @@ -0,0 +1,413 @@ +import aviary.api as av +import warnings +import unittest +import dymos as dm +import openmdao.api as om +from aviary.api import Dynamic, Mission +from aviary.core.aviary_group import AviaryGroup +from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData +from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import inputs +from aviary.utils.preprocessors import preprocess_options + + +class TestTakeoffToEngineFailureTest(unittest.TestCase): + """Test takeoff phase builder.""" + + def test_case1(self): + aviary_options = inputs.deepcopy() + + # This builder can be used for both takeoff and landing phases + aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS + ) + + # fmt: off + takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } + } + # fmt: off + + # when using spoilers, add a few more options + takeoff_spoiler_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT), + 'spoiler_lift_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT), + } + } + + # We also need propulsion analysis for takeoff and landing. No additional configuration + # is needed for this builder + engines = [av.build_engine_deck(aviary_options)] + preprocess_options(aviary_options, engine_models=engines) + prop_builder = av.CorePropulsionBuilder(engine_models=engines) + + # BRAKE RELEASE TO DECISION SPEED + takeoff_brake_release_user_options = av.AviaryValues() + + takeoff_brake_release_user_options.set_val('max_duration', val=60.0, units='s') + takeoff_brake_release_user_options.set_val('time_duration_ref', val=60.0, units='s') + takeoff_brake_release_user_options.set_val('distance_max', val=7500.0, units='ft') + takeoff_brake_release_user_options.set_val('max_velocity', val=167.85, units='kn') + takeoff_brake_release_user_options.set_val('terminal_condition', val='V1') + + tobl_nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) + tobl_nl_solver.linesearch = om.BoundsEnforceLS() + + takeoff_brake_release_user_options.set_val('nonlinear_solver', val=tobl_nl_solver) + takeoff_brake_release_user_options.set_val('linear_solver', val=om.DirectSolver()) + + takeoff_v1_to_vr_initial_guesses = av.AviaryValues() + + takeoff_v1_to_vr_initial_guesses.set_val('time', [0.0, 30.0], 's') + takeoff_v1_to_vr_initial_guesses.set_val('distance', [0.0, 4100.0], 'ft') + takeoff_v1_to_vr_initial_guesses.set_val('velocity', [0.01, 150.0], 'kn') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + takeoff_v1_to_vr_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + takeoff_v1_to_vr_initial_guesses.set_val('throttle', 1.0) + takeoff_v1_to_vr_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + takeoff_brake_release_to_decision_speed_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_brake_release_to_decision_speed', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=takeoff_brake_release_user_options, + initial_guesses=takeoff_v1_to_vr_initial_guesses, + ) + + # DECISION SPEED TO ROTATION + + takeoff_v1_to_vr_user_options = av.AviaryValues() + + takeoff_v1_to_vr_user_options.set_val('max_duration', val=90.0, units='s') + takeoff_v1_to_vr_user_options.set_val('time_duration_ref', val=60.0, units='s') + takeoff_v1_to_vr_user_options.set_val('distance_max', val=15000.0, units='ft') + takeoff_v1_to_vr_user_options.set_val('max_velocity', val=167.85, units='kn') + takeoff_v1_to_vr_user_options.set_val('terminal_condition', val='VR') + + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) + nl_solver.linesearch = om.BoundsEnforceLS() + + takeoff_v1_to_vr_user_options.set_val('nonlinear_solver', val=nl_solver) + takeoff_v1_to_vr_user_options.set_val('linear_solver', val=om.DirectSolver()) + + takeoff_v1_to_vr_initial_guesses = av.AviaryValues() + + takeoff_v1_to_vr_initial_guesses.set_val('time', [30.0, 1.0], 's') + takeoff_v1_to_vr_initial_guesses.set_val('distance', [4100.0, 4800.0], 'ft') + takeoff_v1_to_vr_initial_guesses.set_val('velocity', [70., 150.0], 'kn') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + takeoff_v1_to_vr_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + takeoff_v1_to_vr_initial_guesses.set_val('throttle', 1.0) + takeoff_v1_to_vr_initial_guesses.set_val('angle_of_attack', 0.0, 'deg') + + takeoff_decision_speed_to_rotate_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_decision_speed_to_rotate', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=takeoff_v1_to_vr_user_options, + initial_guesses=takeoff_v1_to_vr_initial_guesses, + ) + + # ROTATION TO LIFTOFF + + vr_to_liftoff_user_options = av.AviaryValues() + + vr_to_liftoff_user_options.set_val('max_duration', val=90.0, units='s') + vr_to_liftoff_user_options.set_val('time_duration_ref', val=60.0, units='s') + vr_to_liftoff_user_options.set_val('distance_max', val=15000.0, units='ft') + vr_to_liftoff_user_options.set_val('max_velocity', val=167.85, units='kn') + vr_to_liftoff_user_options.set_val('pitch_control', val='alpha_rate_fixed', units='unitless') + vr_to_liftoff_user_options.set_val('terminal_condition', val='LIFTOFF') + + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) + nl_solver.linesearch = om.BoundsEnforceLS() + + vr_to_liftoff_user_options.set_val('nonlinear_solver', val=nl_solver) + vr_to_liftoff_user_options.set_val('linear_solver', val=om.DirectSolver()) + + vr_to_liftoff_initial_guesses = av.AviaryValues() + + vr_to_liftoff_initial_guesses.set_val('time', [31.0, 5.0], 's') + vr_to_liftoff_initial_guesses.set_val('distance', [4800.0, 5500.0], 'ft') + vr_to_liftoff_initial_guesses.set_val('velocity', [100., 120.0], 'kn') + vr_to_liftoff_initial_guesses.set_val('angle_of_attack', [0.0, 5.0], 'deg') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + vr_to_liftoff_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + vr_to_liftoff_initial_guesses.set_val('throttle', 1.0) + vr_to_liftoff_initial_guesses.set_val('angle_of_attack_rate', 2.0, units='deg/s') + + vr_to_liftoff_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_rotate_to_liftoff', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=vr_to_liftoff_user_options, + initial_guesses=vr_to_liftoff_initial_guesses, + ) + + # LIFTOFF TO CLIMB GRADIENT + + liftoff_to_climb_gradient_user_options = av.AviaryValues() + + liftoff_to_climb_gradient_user_options.set_val('max_duration', val=90.0, units='s') + liftoff_to_climb_gradient_user_options.set_val('time_duration_ref', val=60.0, units='s') + liftoff_to_climb_gradient_user_options.set_val('distance_max', val=15000.0, units='ft') + liftoff_to_climb_gradient_user_options.set_val('max_velocity', val=167.85, units='kn') + liftoff_to_climb_gradient_user_options.set_val('pitch_control', val='alpha_rate_fixed', units='unitless') + liftoff_to_climb_gradient_user_options.set_val('climbing', val=True, units='unitless') + liftoff_to_climb_gradient_user_options.set_val('terminal_condition', val='CLIMB_GRADIENT') + + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6) + nl_solver.linesearch = om.BoundsEnforceLS() + + liftoff_to_climb_gradient_user_options.set_val('nonlinear_solver', val=nl_solver) + liftoff_to_climb_gradient_user_options.set_val('linear_solver', val=om.DirectSolver()) + + liftoff_to_climb_gradient_initial_guesses = av.AviaryValues() + + liftoff_to_climb_gradient_initial_guesses.set_val('time', [35.0, 1.0], 's') + liftoff_to_climb_gradient_initial_guesses.set_val('distance', [5000.0, 6000.0], 'ft') + liftoff_to_climb_gradient_initial_guesses.set_val('velocity', [120., 100.0], 'kn') + liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack', [5.0, 10.0], 'deg') + liftoff_to_climb_gradient_initial_guesses.set_val('flight_path_angle', [0.0, 2.0], 'deg') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + liftoff_to_climb_gradient_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + liftoff_to_climb_gradient_initial_guesses.set_val('throttle', 1.0) + liftoff_to_climb_gradient_initial_guesses.set_val('angle_of_attack_rate', 2.0, units='deg/s') + + liftoff_to_climb_gradient_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_liftoff_to_climb_gradient', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=liftoff_to_climb_gradient_user_options, + initial_guesses=liftoff_to_climb_gradient_initial_guesses, + ) + + # CLIMB GRADIENT TO OBSTACLE + + climb_gradient_to_obstacle_user_options = av.AviaryValues() + + climb_gradient_to_obstacle_user_options.set_val('max_duration', val=90.0, units='s') + climb_gradient_to_obstacle_user_options.set_val('time_duration_ref', val=60.0, units='s') + climb_gradient_to_obstacle_user_options.set_val('distance_max', val=15000.0, units='ft') + climb_gradient_to_obstacle_user_options.set_val('max_velocity', val=200., units='kn') + climb_gradient_to_obstacle_user_options.set_val('pitch_control', val='gamma_fixed', units='unitless') + climb_gradient_to_obstacle_user_options.set_val('climbing', val=True, units='unitless') + climb_gradient_to_obstacle_user_options.set_val('terminal_condition', val='OBSTACLE') + + nl_solver = om.NewtonSolver(solve_subsystems=True, maxiter=100, iprint=2, atol=1.0e-6, rtol=1.0e-6, debug_print=False) + nl_solver.linesearch = om.BoundsEnforceLS() + + climb_gradient_to_obstacle_user_options.set_val('nonlinear_solver', val=nl_solver) + climb_gradient_to_obstacle_user_options.set_val('linear_solver', val=om.DirectSolver()) + + climb_gradient_to_obstacle_initial_guesses = av.AviaryValues() + + climb_gradient_to_obstacle_initial_guesses.set_val('time', [35.0, 3.0], 's') + climb_gradient_to_obstacle_initial_guesses.set_val('distance', [5500.0, 5800.0], 'ft') + climb_gradient_to_obstacle_initial_guesses.set_val('velocity', [120., 120.0], 'kn') + climb_gradient_to_obstacle_initial_guesses.set_val('flight_path_angle', [2.0, 2.0], 'deg') + + gross_mass_units = 'lbm' + gross_mass = inputs.get_val(Mission.Design.GROSS_MASS, gross_mass_units) + climb_gradient_to_obstacle_initial_guesses.set_val('mass', gross_mass, gross_mass_units) + + climb_gradient_to_obstacle_initial_guesses.set_val('throttle', 1.0) + + climb_gradient_to_obstacle_builder = av.DetailedTakeoffPhaseBuilder( + 'takeoff_climb_gradient_to_obstacle', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=climb_gradient_to_obstacle_user_options, + initial_guesses=climb_gradient_to_obstacle_initial_guesses, + ) + + # from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( + # takeoff_decision_speed_builder, + # takeoff_engine_cutback_builder, + # takeoff_engine_cutback_to_mic_p1_builder, + # takeoff_liftoff_builder, + # takeoff_liftoff_user_options, + # takeoff_mic_p1_to_climb_builder, + # takeoff_mic_p2_builder, + # takeoff_mic_p2_to_engine_cutback_builder, + # takeoff_rotate_builder, + # ) + from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems + from aviary.variable_info.functions import setup_model_options + + takeoff_trajectory_builder = av.DetailedTakeoffTrajectoryBuilder('detailed_takeoff') + + takeoff_trajectory_builder.set_brake_release_to_decision_speed(takeoff_brake_release_to_decision_speed_builder) + + takeoff_trajectory_builder.set_decision_speed_to_rotate(takeoff_decision_speed_to_rotate_builder) + + takeoff_trajectory_builder.set_rotate_to_liftoff(vr_to_liftoff_builder) + + takeoff_trajectory_builder.set_liftoff_to_climb_gradient(liftoff_to_climb_gradient_builder) + + takeoff_trajectory_builder.set_climb_gradient_to_obstacle(climb_gradient_to_obstacle_builder) + + # takeoff_trajectory_builder.set_liftoff_to_obstacle(takeoff_liftoff_builder) + + # takeoff_trajectory_builder.set_obstacle_to_mic_p2(takeoff_mic_p2_builder) + + # takeoff_trajectory_builder.set_mic_p2_to_engine_cutback(takeoff_mic_p2_to_engine_cutback_builder) + + # takeoff_trajectory_builder.set_engine_cutback(takeoff_engine_cutback_builder) + + # takeoff_trajectory_builder.set_engine_cutback_to_mic_p1(takeoff_engine_cutback_to_mic_p1_builder) + + # takeoff_trajectory_builder.set_mic_p1_to_climb(takeoff_mic_p1_to_climb_builder) + + takeoff = om.Problem() # model=AviaryGroup(aviary_options=aviary_options, aviary_metadata=BaseMetaData)) + + # default subsystems + default_premission_subsystems = get_default_premission_subsystems('FLOPS', engines) + + # Upstream pre-mission analysis for aero + takeoff.model.add_subsystem( + 'core_subsystems', + av.CorePreMission( + aviary_options=aviary_options, + subsystems=default_premission_subsystems, + ), + promotes_inputs=['*'], + promotes_outputs=['*'], + ) + + # Instantiate the trajectory and add the phases + traj = dm.Trajectory() + takeoff.model.add_subsystem('traj', traj) + + takeoff_trajectory_builder.build_trajectory( + aviary_options=aviary_options, model=takeoff.model, traj=traj + ) + + # distance_max, units = takeoff_liftoff_user_options.get_item('distance_max') + # liftoff = takeoff_trajectory_builder.get_phase('takeoff_liftoff') + + # liftoff.add_objective(Dynamic.Mission.DISTANCE, loc='final', ref=distance_max, units=units) + + # Insert a constraint for a fake decision speed, until abort is added. + # takeoff.model.add_constraint( + # 'traj.takeoff_brake_release.states:velocity', equals=149.47, units='kn', ref=150.0, indices=[-1] + # ) + + # takeoff.model.add_constraint( + # 'traj.takeoff_decision_speed.states:velocity', + # equals=155.36, + # units='kn', + # ref=159.0, + # indices=[-1], + # ) + + varnames = [ + av.Aircraft.Wing.AREA, + av.Aircraft.Wing.ASPECT_RATIO, + av.Aircraft.Wing.SPAN, + ] + av.set_aviary_input_defaults(takeoff.model, varnames, aviary_options) + + setup_model_options(takeoff, aviary_options) + + # suppress warnings: + # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' + with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + takeoff.setup(check=False) + + av.set_aviary_initial_values(takeoff, aviary_options) + + takeoff_trajectory_builder.apply_initial_guesses(takeoff, 'traj') + + takeoff.final_setup() + + # takeoff.model.run_apply_nonlinear() + + # takeoff.model.list_vars(print_arrays=True) + + takeoff.run_model() + + takeoff.model.traj.phases.takeoff_climb_gradient_to_obstacle.ode_iter_group.segment_prop_group.ode_all.takeoff_eom.list_vars(print_arrays=True) + + om.n2(takeoff) + + # vars = takeoff.model.list_vars(print_arrays=True, units=True, prom_name=True, return_format='dict', out_stream=None) + + # vars = {meta['prom_name']: meta for meta in vars.values()} + + # systems = set([path.rsplit('.', 1)[0] for path in vars.keys()]) + + # print('\n'.join(vars.keys())) + + # for sys in takeoff.model.system_iter(include_self=True, recurse=True): + # sys.list_vars(prom_name=True, units=True) + + + # from textual.app import App, ComposeResult + # from textual.widgets import DataTable, Collapsible + + + # class TableApp(App): + + # def __init__(self, systems): + # self._systems = systems + + # def compose(self) -> ComposeResult: + # for sys, vars in self._systems.items(): + # with Collapsible(title=sys.pathname): + # yield DataTable() + + # def on_mount(self) -> None: + # table = self.query_one(DataTable) + # table.add_columns('promoted name', 'units') + # print(table) + # # table.add_rows(ROWS[1:]) + + # systems = {} + + # for sys in takeoff.model.system_iter(include_self=True, recurse=True): + # vars = sys.list_vars(prom_name=True, units=True, out_stream=None) + # systems[sys] = vars + + # app = TableApp(systems) + # app.run() + + # takeoff.check_partials(compact_print=True) + # takeoff.model.run_apply_nonlinear() + # takeoff.model.list_vars(print_arrays=True) + # om.n2(takeoff.model) + + +if __name__ == '__main__': + unittest.main() diff --git a/aviary/mission/flops_based/phases/test/test_balanced_field2.py b/aviary/mission/flops_based/phases/test/test_balanced_field2.py new file mode 100644 index 000000000..3899fd2b6 --- /dev/null +++ b/aviary/mission/flops_based/phases/test/test_balanced_field2.py @@ -0,0 +1,121 @@ +import unittest +import warnings + +import dymos as dm +import openmdao.api as om + +import aviary.api as av +from aviary.api import Mission +from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import inputs +from aviary.utils.preprocessors import preprocess_options + + +class TestBalancedField(unittest.TestCase): + """Test takeoff phase builder.""" + + def test_balanced_field_2(self): + # TODO: Why doesn't aviary options respect keys() and values() if its dict-like? + aviary_options = inputs.deepcopy() + + # This builder can be used for both takeoff and landing phases + aero_builder = av.CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=av.LegacyCode.FLOPS + ) + + # fmt: off + takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } + } + # fmt: off + + # when using spoilers, add a few more options + takeoff_spoiler_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT), + 'spoiler_lift_coefficient': inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT), + } + } + + # We also need propulsion analysis for takeoff and landing. No additional configuration + # is needed for this builder + engines = [av.build_engine_deck(aviary_options)] + preprocess_options(aviary_options, engine_models=engines) + prop_builder = av.CorePropulsionBuilder(engine_models=engines) + + balanced_field_user_options = av.AviaryValues() + + from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems + from aviary.variable_info.functions import setup_model_options + + takeoff_trajectory_builder = av.BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) + + test_problem = om.Problem() + + # default subsystems + default_premission_subsystems = get_default_premission_subsystems('FLOPS', engines) + + # Upstream pre-mission analysis for aero + test_problem.model.add_subsystem( + 'core_subsystems', + av.CorePreMission( + aviary_options=aviary_options, + subsystems=default_premission_subsystems, + ), + promotes_inputs=['*'], + promotes_outputs=['*'], + ) + + # Instantiate the trajectory and add the phases + traj = takeoff_trajectory_builder.build_trajectory(aviary_options=aviary_options, model=test_problem.model) + + varnames = [ + av.Aircraft.Wing.AREA, + av.Aircraft.Wing.ASPECT_RATIO, + av.Aircraft.Wing.SPAN, + ] + av.set_aviary_input_defaults(test_problem.model, varnames, aviary_options) + + setup_model_options(test_problem, aviary_options) + + # suppress warnings: + # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' + with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + test_problem.setup(check=False) + + av.set_aviary_initial_values(test_problem, aviary_options) + + takeoff_trajectory_builder.apply_initial_guesses(test_problem, 'traj') + + test_problem.final_setup() + + + import dymos + dymos.run_problem(test_problem, run_driver=False, make_plots=True) + + print(test_problem.get_reports_dir()) + + +if __name__ == '__main__': + unittest.main() diff --git a/aviary/mission/height_energy_problem_configurator.py b/aviary/mission/height_energy_problem_configurator.py index 7599f12bb..acf3ecd04 100644 --- a/aviary/mission/height_energy_problem_configurator.py +++ b/aviary/mission/height_energy_problem_configurator.py @@ -451,7 +451,10 @@ def _add_post_mission_takeoff_systems(self, aviary_group): if phase_options.get('mach_optimize', False): # Create an ExecComp to compute the difference in mach mach_diff_comp = om.ExecComp( - 'mach_resid_for_connecting_takeoff = final_mach - initial_mach' + 'mach_resid_for_connecting_takeoff = final_mach - initial_mach', + mach_resid_for_connecting_takeoff={'units': 'unitless'}, + final_mach={'units': 'unitless'}, + initial_mach={'units': 'unitless'}, ) aviary_group.add_subsystem('mach_diff_comp', mach_diff_comp) diff --git a/aviary/mission/initial_guess_builders.py b/aviary/mission/initial_guess_builders.py index 402a87919..9c1c083cd 100644 --- a/aviary/mission/initial_guess_builders.py +++ b/aviary/mission/initial_guess_builders.py @@ -45,28 +45,18 @@ def __init__(self, key): def apply_initial_guess( self, prob: om.Problem, traj_name, phase: dm.Phase, phase_name, val, units ): - """Set the initial guess on the problem.""" - complete_key = self._get_complete_key(traj_name, phase_name) - - # TODO: this is a short term hack in need of an appropriate long term solution - # - to interpolate, or not to interpolate: that is the question - # - the solution should probably be a value decoration (a wrapper) that is - # both lightweight and easy to check and unpack - if isinstance(val, np.ndarray) or (isinstance(val, Sequence) and not isinstance(val, str)): - val = phase.interp(self.key, val) - - try: - prob.set_val(complete_key, val, units) - except KeyError: - complete_key = complete_key.replace('polynomial_controls', 'controls') - prob.set_val(complete_key, val, units) - - def _get_complete_key(self, traj_name, phase_name): - """Compose the complete key for setting the initial guess.""" - _ = traj_name - _ = phase_name - - return self.key + if self.key in phase.state_options: + phase.set_state_val(self.key, val, units=units) + elif self.key in phase.control_options: + phase.set_control_val(self.key, val, units=units) + elif self.key in phase.parameter_options: + phase.set_parameter_val(self.key, val, units=units) + elif self.key == phase.time_options['name']: + prob.set_integ_var_val(initial=val[0], duration=val[1], units=units) + else: + pass + # raise ValueError(f'{phase.msginfo} Attempting to apply initial guess for {self.key}.\n' + # 'Not find in the states, control, parameters, or integration variable of the phase.') class InitialGuessControl(InitialGuess): @@ -166,11 +156,5 @@ def __init__(self, key='time'): def apply_initial_guess( self, prob: om.Problem, traj_name, phase: dm.Phase, phase_name, val, units ): - _ = phase - - name = f'{traj_name}.{phase_name}.t_initial' t_initial, t_duration = val - prob.set_val(name, t_initial, units) - - name = f'{traj_name}.{phase_name}.t_duration' - prob.set_val(name, t_duration, units) + phase.set_integ_var_val(initial=t_initial, duration=t_duration, units=units) diff --git a/aviary/models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv b/aviary/models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv index 6cd2b15fd..f878ee3b4 100644 --- a/aviary/models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv +++ b/aviary/models/aircraft/advanced_single_aisle/advanced_single_aisle_FLOPS.csv @@ -182,7 +182,7 @@ mission:summary:fuel_flow_scaler,1,unitless mission:takeoff:airport_altitude,0,ft mission:takeoff:angle_of_attack_runway,0,deg mission:takeoff:braking_friction_coefficient,0.35,unitless -mission:takeoff:drag_coefficient_min,0.045,unitless +mission:takeoff:drag_coefficient_min,0.05,unitless mission:takeoff:final_altitude,35,ft mission:takeoff:fuel_simple,577,lbm mission:takeoff:lift_coefficient_max,2,unitless diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py index 1af28385b..236c14fc3 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py @@ -4,7 +4,7 @@ import openmdao.api as om from openmdao.utils.assert_utils import assert_near_equal -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values from aviary.utils.preprocessors import preprocess_options @@ -277,7 +277,7 @@ def test_n3cc_drag(self): 0.03291, 0.03596, 0.04024, 0.04509, 0.05057, 0.05668, 0.06342 ], [ - 0.02274, 0.02293, 0.02366, 0.02494, 0.02659, 0.02820, 0.03004, 0.03244, + 0.02274, 0.02293, 0.02366, 0.02494, 0.02659, 0.02820, 0.03004, 0.03244, 0.03596, 0.04066, 0.04560, 0.05224, 0.06038, 0.07001, 0.08114 ], [ @@ -402,32 +402,32 @@ def test_large_single_aisle_2_drag(self): 0.03007, 0.03246, 0.03509, 0.03804, 0.04134, 0.04521, 0.04987 ], [ - + 0.02279, 0.02269, 0.02293, 0.02350, 0.02441, 0.02555, 0.02685, 0.02829, 0.03002, 0.03235, 0.03499, 0.03794, 0.04124, 0.04511, 0.04977 ], [ - + 0.02297, 0.02282, 0.02303, 0.02360, 0.02454, 0.02571, 0.02699, 0.02843, 0.03016, 0.03241, 0.03502, 0.03803, 0.04140, 0.04537, 0.05014 ], [ - + 0.02328, 0.02306, 0.02323, 0.02380, 0.02478, 0.02598, 0.02724, 0.02868, 0.03042, 0.03260, 0.03512, 0.03860, 0.04232, 0.04671, 0.05204 ], [ - + 0.02399, 0.02360, 0.02370, 0.02428, 0.02533, 0.02662, 0.02789, 0.02933, 0.03129, 0.03403, 0.03737, 0.04141, 0.04587, 0.05069, 0.05587 ], [ - + 0.02567, 0.02496, 0.02490, 0.02548, 0.02670, 0.02821, 0.02970, 0.03157, 0.03426, 0.03794, 0.04203, 0.04630, 0.05183, 0.05791, 0.06470 ], [ - + 0.04212, 0.04044, 0.03992, 0.04052, 0.04226, 0.04476, 0.04771, 0.05158, 0.05638, 0.06166, 0.06690, 0.07238, 0.07763, 0.08296, 0.08858 ] diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py index 3a21f6892..f7f0290ca 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py @@ -11,7 +11,7 @@ from aviary.interface.methods_for_level2 import AviaryProblem from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.subsystems.atmosphere.atmosphere import Atmosphere -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import set_aviary_initial_values diff --git a/aviary/subsystems/core_postmission.py b/aviary/subsystems/core_postmission.py new file mode 100644 index 000000000..76a92b869 --- /dev/null +++ b/aviary/subsystems/core_postmission.py @@ -0,0 +1,62 @@ +import openmdao.api as om + +from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.functions import override_aviary_vars +from aviary.variable_info.variable_meta_data import _MetaData + + +class CorePostMission(om.Group): + """Group that contains all post-mission groups of core Aviary subsystems: (performance). + """ + + def initialize(self): + self.options.declare( + 'aviary_options', + types=AviaryValues, + desc='collection of Aircraft/Mission specific options', + ) + self.options.declare('subsystems', desc='list of core subsystem builders') + self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + + self.options.declare( + 'phase_info', + desc='The phase_info dict for all phases', + types=dict, + ) + + self.options.declare( + 'phase_mission_bus_lengths', + desc='Mapping from phase names to the lengths of the mission_bus_variables timeseries', + types=dict, + ) + + self.options.declare( + 'post_mission_info', + desc='The post_mission portion of the phase_info.', + types=dict, + ) + + def setup(self, **kwargs): + # rely on openMDAO's auto-ordering for this group + self.options['auto_order'] = True + + aviary_options = self.options['aviary_options'] + phase_info = self.options['phase_info'] + phase_mission_bus_lengths = self.options['phase_mission_bus_lengths'] + post_mission_info = self.options['post_mission_info'] + core_subsystems = self.options['subsystems'] + + for subsystem in core_subsystems: + pre_mission_system = subsystem.build_post_mission( + aviary_options, + phase_info, + phase_mission_bus_lengths, + post_mission_info=post_mission_info, + ) + if pre_mission_system is not None: + self.add_subsystem( + subsystem.name, + pre_mission_system, + promotes_inputs=['*'], + promotes_outputs=['*'], + ) diff --git a/aviary/subsystems/premission.py b/aviary/subsystems/core_premission.py similarity index 86% rename from aviary/subsystems/premission.py rename to aviary/subsystems/core_premission.py index b55a49c3d..63930a61f 100644 --- a/aviary/subsystems/premission.py +++ b/aviary/subsystems/core_premission.py @@ -1,16 +1,14 @@ -import openmdao import openmdao.api as om -from packaging import version from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.functions import override_aviary_vars from aviary.variable_info.variable_meta_data import _MetaData -use_new_openmdao_syntax = version.parse(openmdao.__version__) >= version.parse('3.28') - class CorePreMission(om.Group): - """Group that contains all pre-mission groups of core Aviary subsystems (geometry, mass, propulsion, aerodynamics).""" + """Group that contains all pre-mission groups of core Aviary subsystems: + (geometry, mass, propulsion, aerodynamics, performance). + """ def initialize(self): self.options.declare( @@ -20,6 +18,7 @@ def initialize(self): ) self.options.declare('subsystems', desc='list of core subsystem builders') self.options.declare('meta_data', desc='problem metadata', default=_MetaData) + # NOTE this flag is only needed for tests - in AviaryProblem it should always be False self.options.declare( 'process_overrides', @@ -30,9 +29,8 @@ def initialize(self): ) def setup(self, **kwargs): - if use_new_openmdao_syntax: - # rely on openMDAO's auto-ordering for this group - self.options['auto_order'] = True + # rely on openMDAO's auto-ordering for this group + self.options['auto_order'] = True aviary_options = self.options['aviary_options'] core_subsystems = self.options['subsystems'] diff --git a/aviary/subsystems/geometry/gasp_based/test/test_override.py b/aviary/subsystems/geometry/gasp_based/test/test_override.py index ef8748998..468bcc745 100644 --- a/aviary/subsystems/geometry/gasp_based/test/test_override.py +++ b/aviary/subsystems/geometry/gasp_based/test/test_override.py @@ -7,7 +7,7 @@ from aviary.interface.methods_for_level2 import AviaryGroup from aviary.subsystems.aerodynamics.gasp_based.gaspaero import AeroGeom -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.process_input_decks import create_vehicle diff --git a/aviary/subsystems/performance/balanced_field_submodel.py b/aviary/subsystems/performance/balanced_field_submodel.py new file mode 100644 index 000000000..08052d16f --- /dev/null +++ b/aviary/subsystems/performance/balanced_field_submodel.py @@ -0,0 +1,139 @@ +""" +Group containing a submodel component with detailed landing. +""" +import warnings + +import openmdao.api as om + +from aviary.mission.balanced_field_traj_builder import BalancedFieldTrajectoryBuilder +from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder +from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.aviary_values import AviaryValues +from aviary.utils.functions import set_aviary_initial_values +from aviary.variable_info.enums import LegacyCode +from aviary.variable_info.functions import setup_model_options +from aviary.variable_info.variables import Aircraft, Mission, Settings + + +def create_balance_field_subprob(aviary_inputs, use_spoiler=False): + + subprob = create_prob(aviary_inputs, use_spoiler) + + comp = AviarySubmodelComp( + problem=subprob, + inputs=[ + Aircraft.Wing.AREA, + Aircraft.Wing.ASPECT_RATIO, + Aircraft.Wing.HEIGHT, + Aircraft.Wing.SPAN, + Mission.Takeoff.DRAG_COEFFICIENT_MIN, + Mission.Takeoff.LIFT_COEFFICIENT_MAX, + ('traj.takeoff_brake_release_to_engine_failure.initial_states:mass', Mission.Summary.GROSS_MASS), + ], + outputs=[ + ('traj.takeoff_climb_gradient_to_obstacle.final_states:distance', 'distance_obstacle'), + ] + ) + + return comp + + +def create_prob(aviary_inputs, use_spoiler=False): + """ + Return a problem + """ + aero_builder = CoreAerodynamicsBuilder( + name='low_speed_aero', code_origin=LegacyCode.FLOPS + ) + + # fmt: off + takeoff_subsystem_options = { + 'low_speed_aero': { + 'method': 'low_speed', + 'ground_altitude': 0.0, # units='m' + 'angles_of_attack': [ + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, + ], # units='deg' + 'lift_coefficients': [ + 0.5178, 0.6, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, + 1.35, 1.5, 1.6, 1.7, 1.8, 1.85, 1.9, 1.95, + ], + 'drag_coefficients': [ + 0.0674, 0.065, 0.065, 0.07, 0.072, 0.076, 0.084, 0.09, + 0.10, 0.11, 0.12, 0.13, 0.15, 0.16, 0.18, 0.20, + ], + 'lift_coefficient_factor': 1.0, + 'drag_coefficient_factor': 1.0, + } + } + # fmt: off + + # when using spoilers, add a few more options + if use_spoiler: + + spoiler_drag = aviary_inputs.get_val(Mission.Takeoff.SPOILER_DRAG_COEFFICIENT) + spoiler_lift = aviary_inputs.get_val(Mission.Takeoff.SPOILER_LIFT_COEFFICIENT) + + takeoff_subsystem_options = { + 'low_speed_aero': { + **takeoff_subsystem_options['low_speed_aero'], + 'use_spoilers': True, + 'spoiler_drag_coefficient': spoiler_drag, + 'spoiler_lift_coefficient': spoiler_lift, + } + } + + # We also need propulsion analysis for takeoff and landing. No additional configuration + # is needed for this builder + engines = [build_engine_deck(aviary_inputs)] + # Note that the aviary_inputs is already in a pre-processed state. + prop_builder = CorePropulsionBuilder(engine_models=engines) + + balanced_field_user_options = AviaryValues() + + dto_build = BalancedFieldTrajectoryBuilder('balanced_field_traj', + core_subsystems=[aero_builder, prop_builder], + subsystem_options=takeoff_subsystem_options, + user_options=balanced_field_user_options) + + subprob = om.Problem() + + #ivc = om.IndepVarComp(Mission.Summary.GROSS_MASS, val=1.0, units='lbm') + #subprob.model.add_subsystem('takeoff_mass_ivc', ivc, promotes=['*']) + #subprob.model.connect( + # Mission.Summary.GROSS_MASS, + # "traj.takeoff_brake_release_to_engine_failure.states:mass", + # flat_src_indices=[0], + #) + + # Instantiate the trajectory and add the phases + traj = dto_build.build_trajectory(aviary_options=aviary_inputs, model=subprob.model) + + setup_model_options(subprob, aviary_inputs) + #subprob.set_solver_print(2) + + # This is kind of janky, but we need these after the subproblem sets it up. + subprob.aviary_inputs = aviary_inputs + subprob.dto_build = dto_build + + return subprob + + +class AviarySubmodelComp(om.SubmodelComp): + """ + We need to subclass so that we can set the initial conditions. + """ + def setup(self): + # suppress warnings: + # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' + with warnings.catch_warnings(): + warnings.simplefilter('ignore', om.PromotionWarning) + super().setup() + + + sub = self._subprob + set_aviary_initial_values(sub, sub.aviary_inputs) + sub.dto_build.apply_initial_guesses(sub, 'traj') + + sub.final_setup() diff --git a/aviary/subsystems/performance/performance_builder.py b/aviary/subsystems/performance/performance_builder.py index 5a18acadb..03caea575 100644 --- a/aviary/subsystems/performance/performance_builder.py +++ b/aviary/subsystems/performance/performance_builder.py @@ -1,3 +1,6 @@ +from aviary.subsystems.performance.balanced_field_submodel import ( + create_balance_field_subprob, +) from aviary.subsystems.performance.performance_premission import PerformancePremission from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase @@ -13,3 +16,12 @@ def __init__(self, name=None, meta_data=None): def build_pre_mission(self, aviary_inputs, **kwargs): return PerformancePremission() + + def build_post_mission(self, aviary_inputs, phase_info, phase_mission_bus_lengths, **kwargs): + + if 'post_mission_info' in kwargs: + post = kwargs['post_mission_info'] + if post.get('balanced_field', False): + return create_balance_field_subprob(aviary_inputs) + + return \ No newline at end of file diff --git a/aviary/subsystems/test/test_flops_based_premission.py b/aviary/subsystems/test/test_flops_based_premission.py index 82e90846d..56995cb73 100644 --- a/aviary/subsystems/test/test_flops_based_premission.py +++ b/aviary/subsystems/test/test_flops_based_premission.py @@ -4,7 +4,7 @@ from openmdao.utils.testing_utils import use_tempdirs from parameterized import parameterized -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/subsystems/test/test_gasp_based_premission.py b/aviary/subsystems/test/test_gasp_based_premission.py index d3f3ce4fd..47c22fdaa 100644 --- a/aviary/subsystems/test/test_gasp_based_premission.py +++ b/aviary/subsystems/test/test_gasp_based_premission.py @@ -4,7 +4,7 @@ from openmdao.utils.testing_utils import use_tempdirs from aviary.interface.methods_for_level2 import AviaryProblem -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/subsystems/test/test_premission.py b/aviary/subsystems/test/test_premission.py index 16335bfb5..87c7c9e72 100644 --- a/aviary/subsystems/test/test_premission.py +++ b/aviary/subsystems/test/test_premission.py @@ -10,7 +10,7 @@ from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.subsystems.geometry.geometry_builder import CoreGeometryBuilder from aviary.subsystems.mass.mass_builder import CoreMassBuilder -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.aviary_values import get_items, get_keys diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py index 4475f8adc..f47f12a07 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py @@ -19,7 +19,7 @@ from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( inputs as _inputs, ) -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values, set_aviary_input_defaults from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py index bdb1b511e..0e66eaf45 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py @@ -17,7 +17,7 @@ from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( landing_trajectory_builder as _landing_trajectory_builder, ) -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values, set_aviary_input_defaults from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py index d4bdb7216..f1b01d7d6 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py @@ -17,7 +17,7 @@ from aviary.models.aircraft.advanced_single_aisle.advanced_single_aisle_data import ( takeoff_trajectory_builder as _takeoff_trajectory_builder, ) -from aviary.subsystems.premission import CorePreMission +from aviary.subsystems.core_premission import CorePreMission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import set_aviary_initial_values, set_aviary_input_defaults from aviary.utils.preprocessors import preprocess_options diff --git a/aviary/variable_info/variables.py b/aviary/variable_info/variables.py index b4570fb97..4074b9a83 100644 --- a/aviary/variable_info/variables.py +++ b/aviary/variable_info/variables.py @@ -653,6 +653,7 @@ class Vehicle: """Vehicle properties and states in a vehicle-fixed reference frame.""" ANGLE_OF_ATTACK = 'angle_of_attack' + ANGLE_OF_ATTACK_RATE = 'angle_of_attack_rate' BATTERY_STATE_OF_CHARGE = 'battery_state_of_charge' CUMULATIVE_ELECTRIC_ENERGY_USED = 'cumulative_electric_energy_used' DRAG = 'drag' @@ -782,6 +783,7 @@ class Takeoff: DRAG_COEFFICIENT_FLAP_INCREMENT = 'mission:takeoff:drag_coefficient_flap_increment' DRAG_COEFFICIENT_MIN = 'mission:takeoff:drag_coefficient_min' + ENGINE_FAILURE_SPEED_INCREMENT = 'mission:takeoff:engine_failure_speed_increment' FIELD_LENGTH = 'mission:takeoff:field_length' FINAL_ALTITUDE = 'mission:takeoff:final_altitude' FINAL_MACH = 'mission:takeoff:final_mach'