From ecaf2102c4d2e650083d324b3345305aa5551df0 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Fri, 20 Dec 2024 15:45:18 -0500 Subject: [PATCH 01/38] first attempt at separating 2DOF and HE elements from eachother in methods_for_level_2 __init__() and load_inputs() functions. Enums was updated to have a custom input for the problem builder, elements inside load_inpus() were rearranged to simplify the number of additional functions per builder to be only 2. These consist of an initial guesses and defaults function, and a default location to check for phase_info if none is provided. --- aviary/core/AviaryGroup.py | 121 ++++++++ aviary/core/PostMissionGroup.py | 14 + aviary/core/PreMissionGroup.py | 24 ++ aviary/interface/methods_for_level2.py | 267 +++--------------- aviary/mission/problem_builder_2DOF.py | 80 ++++++ .../mission/problem_builder_height_energy.py | 70 +++++ aviary/variable_info/enums.py | 1 + 7 files changed, 353 insertions(+), 224 deletions(-) create mode 100644 aviary/core/AviaryGroup.py create mode 100644 aviary/core/PostMissionGroup.py create mode 100644 aviary/core/PreMissionGroup.py create mode 100644 aviary/mission/problem_builder_2DOF.py create mode 100644 aviary/mission/problem_builder_height_energy.py diff --git a/aviary/core/AviaryGroup.py b/aviary/core/AviaryGroup.py new file mode 100644 index 000000000..1705bc86c --- /dev/null +++ b/aviary/core/AviaryGroup.py @@ -0,0 +1,121 @@ +import openmdao.api as om +from aviary.utils.aviary_values import AviaryValues +from openmdao.utils.mpi import MPI +from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.variable_info.enums import AnalysisScheme, ProblemType, EquationsOfMotion + + +class AviaryGroup(om.Group): + """ + A standard OpenMDAO group that handles Aviary's promotions in the configure + method. This assures that we only call set_input_defaults on variables + that are present in the model. + """ + + def initialize(self): + """declare options""" + self.options.declare( + 'aviary_options', types=AviaryValues, + desc='collection of Aircraft/Mission specific options') + self.options.declare( + 'aviary_metadata', types=dict, + desc='metadata dictionary of the full aviary problem.') + self.options.declare( + 'phase_info', types=dict, + desc='phase-specific settings.') + self.builder = [] + + def configure(self): + """ + Configure the Aviary group + """ + aviary_options = self.options['aviary_options'] + aviary_metadata = self.options['aviary_metadata'] + + # Find promoted name of every input in the model. + all_prom_inputs = [] + + # We can call list_inputs on the subsystems. + for system in self.system_iter(recurse=False): + var_abs = system.list_inputs(out_stream=None, val=False) + var_prom = [v['prom_name'] for k, v in var_abs] + all_prom_inputs.extend(var_prom) + + # Calls to promotes aren't handled until this group resolves. + # Here, we address anything promoted with an alias in AviaryProblem. + input_meta = system._var_promotes['input'] + var_prom = [v[0][1] for v in input_meta if isinstance(v[0], tuple)] + all_prom_inputs.extend(var_prom) + var_prom = [v[0] for v in input_meta if not isinstance(v[0], tuple)] + all_prom_inputs.extend(var_prom) + + if MPI and self.comm.size > 1: + # Under MPI, promotion info only lives on rank 0, so broadcast. + all_prom_inputs = self.comm.bcast(all_prom_inputs, root=0) + + for key in aviary_metadata: + + if ':' not in key or key.startswith('dynamic:'): + continue + + if aviary_metadata[key]['option']: + continue + + # Skip anything that is not presently an input. + if key not in all_prom_inputs: + continue + + if key in aviary_options: + val, units = aviary_options.get_item(key) + else: + val = aviary_metadata[key]['default_value'] + units = aviary_metadata[key]['units'] + + if val is None: + # optional, but no default value + continue + + self.set_input_defaults(key, val=val, units=units) + + # try to get all the possible EOMs from the Enums rather than specifically calling the names here + # This will require some modifications to the enums + mission_method = aviary_options.get_val( + Settings.EQUATIONS_OF_MOTION) + + # Temporarily add extra stuff here, probably patched soon + if mission_method is 'height_energy': + phase_info = self.options['phase_info'] + + # Set a more appropriate solver for dymos when the phases are linked. + if MPI and isinstance(self.traj.phases.linear_solver, om.PETScKrylov): + + # When any phase is connected with input_initial = True, dymos puts + # a jacobi solver in the phases group. This is necessary in case + # the phases are cyclic. However, this causes some problems + # with the newton solvers in Aviary, exacerbating issues with + # solver tolerances at multiple levels. Since Aviary's phases + # are basically in series, the jacobi solver is a much better + # choice and should be able to handle it in a couple of + # iterations. + self.traj.phases.linear_solver = om.LinearBlockJac(maxiter=5) + + # Due to recent changes in dymos, there is now a solver in any phase + # that has connected initial states. It is not clear that this solver + # is necessary except in certain corner cases that do not apply to the + # Aviary trajectory. In our case, this solver merely addresses a lag + # in the state input component. Since this solver can cause some + # numerical problems, and can slow things down, we need to move it down + # into the state interp component. + # TODO: Future updates to dymos may make this unneccesary. + for phase in self.traj.phases.system_iter(recurse=False): + + # Don't move the solvers if we are using solve segements. + if phase_info[phase.name]['user_options'].get('solve_for_distance'): + continue + + phase.nonlinear_solver = om.NonlinearRunOnce() + phase.linear_solver = om.LinearRunOnce() + if isinstance(phase.indep_states, om.ImplicitComponent): + phase.indep_states.nonlinear_solver = \ + om.NewtonSolver(solve_subsystems=True) + phase.indep_states.linear_solver = om.DirectSolver(rhs_checking=True) diff --git a/aviary/core/PostMissionGroup.py b/aviary/core/PostMissionGroup.py new file mode 100644 index 000000000..78eada104 --- /dev/null +++ b/aviary/core/PostMissionGroup.py @@ -0,0 +1,14 @@ +import openmdao.api as om +from aviary.utils.aviary_values import AviaryValues +from aviary.utils.functions import promote_aircraft_and_mission_vars + + +class PostMissionGroup(om.Group): + """OpenMDAO group that holds all post-mission systems""" + + def configure(self): + """ + Congigure this group for post-mission. + Promote aircraft and mission variables. + """ + promote_aircraft_and_mission_vars(self) diff --git a/aviary/core/PreMissionGroup.py b/aviary/core/PreMissionGroup.py new file mode 100644 index 000000000..982ae0e71 --- /dev/null +++ b/aviary/core/PreMissionGroup.py @@ -0,0 +1,24 @@ +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 + + +class PreMissionGroup(om.Group): + """OpenMDAO group that holds all pre-mission systems""" + + def configure(self): + """ + Configure this group for pre-mission. + Promote aircraft and mission variables. + Override output aviary variables. + """ + external_outputs = promote_aircraft_and_mission_vars(self) + + pre_mission = self.core_subsystems + override_aviary_vars( + pre_mission, + pre_mission.options["aviary_options"], + external_overrides=external_outputs, + manual_overrides=pre_mission.manual_overrides, + ) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 01d141f07..b31614df8 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -42,32 +42,39 @@ from aviary.mission.gasp_based.phases.v_rotate_comp import VRotateComp from aviary.mission.gasp_based.polynomial_fit import PolynomialFit from aviary.mission.phase_builder_base import PhaseBuilderBase +from aviary.mission.problem_builder_2DOF import AviaryProblemBuilder_2DOF +from aviary.mission.problem_builder_height_energy import AviaryProblemBuilder_HE 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.propulsion.utils import build_engine_deck from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder from aviary.utils.aviary_values import AviaryValues -from aviary.utils.functions import create_opts2vals, add_opts2vals, promote_aircraft_and_mission_vars, wrapped_convert_units +from aviary.utils.functions import create_opts2vals, add_opts2vals, wrapped_convert_units from aviary.utils.functions import convert_strings_to_data, set_value from aviary.utils.merge_variable_metadata import merge_meta_data from aviary.utils.preprocessors import preprocess_options -from aviary.utils.process_input_decks import create_vehicle, update_GASP_options, initialization_guessing +from aviary.utils.process_input_decks import create_vehicle from aviary.variable_info.enums import AnalysisScheme, ProblemType, EquationsOfMotion, LegacyCode, Verbosity -from aviary.variable_info.functions import setup_trajectory_params, override_aviary_vars +from aviary.variable_info.functions import setup_trajectory_params from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData +from aviary.core.PostMissionGroup import PostMissionGroup +from aviary.core.PreMissionGroup import PreMissionGroup +from aviary.core.AviaryGroup import AviaryGroup +from aviary.utils.process_input_decks import initialization_guessing, update_GASP_options + FLOPS = LegacyCode.FLOPS GASP = LegacyCode.GASP TWO_DEGREES_OF_FREEDOM = EquationsOfMotion.TWO_DEGREES_OF_FREEDOM HEIGHT_ENERGY = EquationsOfMotion.HEIGHT_ENERGY SOLVED_2DOF = EquationsOfMotion.SOLVED_2DOF +CUSTOM = EquationsOfMotion.CUSTOM if hasattr(TranscriptionBase, 'setup_polynomial_controls'): use_new_dymos_syntax = False @@ -75,150 +82,6 @@ use_new_dymos_syntax = True -class PreMissionGroup(om.Group): - """OpenMDAO group that holds all pre-mission systems""" - - def configure(self): - """ - Configure this group for pre-mission. - Promote aircraft and mission variables. - Override output aviary variables. - """ - external_outputs = promote_aircraft_and_mission_vars(self) - - pre_mission = self.core_subsystems - override_aviary_vars( - pre_mission, - pre_mission.options["aviary_options"], - external_overrides=external_outputs, - manual_overrides=pre_mission.manual_overrides, - ) - - -class PostMissionGroup(om.Group): - """OpenMDAO group that holds all post-mission systems""" - - def configure(self): - """ - Congigure this group for post-mission. - Promote aircraft and mission variables. - """ - promote_aircraft_and_mission_vars(self) - - -class AviaryGroup(om.Group): - """ - A standard OpenMDAO group that handles Aviary's promotions in the configure - method. This assures that we only call set_input_defaults on variables - that are present in the model. - """ - - def initialize(self): - """declare options""" - self.options.declare( - 'aviary_options', types=AviaryValues, - desc='collection of Aircraft/Mission specific options') - self.options.declare( - 'aviary_metadata', types=dict, - desc='metadata dictionary of the full aviary problem.') - self.options.declare( - 'phase_info', types=dict, - desc='phase-specific settings.') - - def configure(self): - """ - Configure the Aviary group - """ - aviary_options = self.options['aviary_options'] - aviary_metadata = self.options['aviary_metadata'] - - # Find promoted name of every input in the model. - all_prom_inputs = [] - - # We can call list_inputs on the subsystems. - for system in self.system_iter(recurse=False): - var_abs = system.list_inputs(out_stream=None, val=False) - var_prom = [v['prom_name'] for k, v in var_abs] - all_prom_inputs.extend(var_prom) - - # Calls to promotes aren't handled until this group resolves. - # Here, we address anything promoted with an alias in AviaryProblem. - input_meta = system._var_promotes['input'] - var_prom = [v[0][1] for v in input_meta if isinstance(v[0], tuple)] - all_prom_inputs.extend(var_prom) - var_prom = [v[0] for v in input_meta if not isinstance(v[0], tuple)] - all_prom_inputs.extend(var_prom) - - if MPI and self.comm.size > 1: - # Under MPI, promotion info only lives on rank 0, so broadcast. - all_prom_inputs = self.comm.bcast(all_prom_inputs, root=0) - - for key in aviary_metadata: - - if ':' not in key or key.startswith('dynamic:'): - continue - - if aviary_metadata[key]['option']: - continue - - # Skip anything that is not presently an input. - if key not in all_prom_inputs: - continue - - if key in aviary_options: - val, units = aviary_options.get_item(key) - else: - val = aviary_metadata[key]['default_value'] - units = aviary_metadata[key]['units'] - - if val is None: - # optional, but no default value - continue - - self.set_input_defaults(key, val=val, units=units) - - # The section below this contains some manipulations of the dymos solver - # structure for height energy. - if aviary_options.get_val(Settings.EQUATIONS_OF_MOTION) is not HEIGHT_ENERGY: - return - - phase_info = self.options['phase_info'] - - # Set a more appropriate solver for dymos when the phases are linked. - if MPI and isinstance(self.traj.phases.linear_solver, om.PETScKrylov): - - # When any phase is connected with input_initial = True, dymos puts - # a jacobi solver in the phases group. This is necessary in case - # the phases are cyclic. However, this causes some problems - # with the newton solvers in Aviary, exacerbating issues with - # solver tolerances at multiple levels. Since Aviary's phases - # are basically in series, the jacobi solver is a much better - # choice and should be able to handle it in a couple of - # iterations. - self.traj.phases.linear_solver = om.LinearBlockJac(maxiter=5) - - # Due to recent changes in dymos, there is now a solver in any phase - # that has connected initial states. It is not clear that this solver - # is necessary except in certain corner cases that do not apply to the - # Aviary trajectory. In our case, this solver merely addresses a lag - # in the state input component. Since this solver can cause some - # numerical problems, and can slow things down, we need to move it down - # into the state interp component. - # TODO: Future updates to dymos may make this unneccesary. - for phase in self.traj.phases.system_iter(recurse=False): - - # Don't move the solvers if we are using solve segements. - if phase_info[phase.name]['user_options'].get('solve_for_distance'): - continue - - phase.nonlinear_solver = om.NonlinearRunOnce() - phase.linear_solver = om.LinearRunOnce() - if isinstance(phase.indep_states, om.ImplicitComponent): - phase.indep_states.nonlinear_solver = \ - om.NewtonSolver(solve_subsystems=True) - phase.indep_states.linear_solver = om.DirectSolver(rhs_checking=True) - - class AviaryProblem(om.Problem): """ Main class for instantiating, formulating, and solving Aviary problems. @@ -258,9 +121,11 @@ def __init__(self, analysis_scheme=AnalysisScheme.COLLOCATION, **kwargs): self.regular_phases = [] self.reserve_phases = [] + self.builder = None def load_inputs( self, aviary_inputs, phase_info=None, engine_builders=None, + problem_builder=None, meta_data=BaseMetaData, verbosity=Verbosity.BRIEF): """ This method loads the aviary_values inputs and options that the @@ -277,20 +142,28 @@ def load_inputs( ## LOAD INPUT FILE ### # Create AviaryValues object from file (or process existing AviaryValues object # with default values from metadata) and generate initial guesses - aviary_inputs, initialization_guesses = create_vehicle( + self.aviary_inputs, self.initialization_guesses = create_vehicle( aviary_inputs, meta_data=meta_data, verbosity=verbosity) # pull which methods will be used for subsystems and mission - self.mission_method = mission_method = aviary_inputs.get_val( + self.mission_method = mission_method = self.aviary_inputs.get_val( Settings.EQUATIONS_OF_MOTION) - self.mass_method = mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) - if mission_method is TWO_DEGREES_OF_FREEDOM or mass_method is GASP: - aviary_inputs = update_GASP_options(aviary_inputs) - initialization_guesses = initialization_guessing(aviary_inputs, initialization_guesses, - engine_builders) - self.aviary_inputs = aviary_inputs - self.initialization_guesses = initialization_guesses + # Determine which problem builder to use based on mission_method + if mission_method is HEIGHT_ENERGY: + self.builder = AviaryProblemBuilder_HE() + elif mission_method is TWO_DEGREES_OF_FREEDOM: + self.builder = AviaryProblemBuilder_2DOF() + elif mission_method is CUSTOM: + if problem_builder: + self.builder = problem_builder() + # TODO: make draft / example custom builder + else: + raise ValueError( + f'When using "settings:equations_of_motion,custom", a problem_builder must be specified in load_inputs().') + else: + raise ValueError( + f'settings:equations_of_motion must be one of: height_energy, 2DOF, or custom') ## LOAD PHASE_INFO ### if phase_info is None: @@ -311,19 +184,9 @@ def load_inputs( # if verbosity level is BRIEF or higher, print that we're using the outputted phase info if verbosity is not None and verbosity >= Verbosity.BRIEF: print('Using outputted phase_info from current working directory') - else: - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - if self.analysis_scheme is AnalysisScheme.COLLOCATION: - from aviary.interface.default_phase_info.two_dof import phase_info - elif self.analysis_scheme is AnalysisScheme.SHOOTING: - from aviary.interface.default_phase_info.two_dof_fiti import phase_info, \ - phase_info_parameterization - phase_info, _ = phase_info_parameterization( - phase_info, None, self.aviary_inputs) - - elif self.mission_method is HEIGHT_ENERGY: - from aviary.interface.default_phase_info.height_energy import phase_info + + phase_info = self.builder.phase_info_default_location(self) if verbosity is not None and verbosity >= Verbosity.BRIEF: print('Loaded default phase_info for ' @@ -343,66 +206,22 @@ def load_inputs( if 'pre_mission' in phase_info: self.pre_mission_info = phase_info['pre_mission'] else: - self.pre_mission_info = {'include_takeoff': True, - 'external_subsystems': []} + self.pre_mission_info = None if 'post_mission' in phase_info: self.post_mission_info = phase_info['post_mission'] else: - self.post_mission_info = {'include_landing': True, - 'external_subsystems': []} - - if engine_builders is None: - engine_builders = build_engine_deck(aviary_inputs) - self.engine_builders = engine_builders - - self.aviary_inputs = aviary_inputs - - if mission_method is TWO_DEGREES_OF_FREEDOM: - aviary_inputs.set_val(Mission.Summary.CRUISE_MASS_FINAL, - val=self.initialization_guesses['cruise_mass_final'], units='lbm') - aviary_inputs.set_val(Mission.Summary.GROSS_MASS, - val=self.initialization_guesses['actual_takeoff_mass'], units='lbm') - - # Commonly referenced values - self.cruise_alt = aviary_inputs.get_val( - Mission.Design.CRUISE_ALTITUDE, units='ft') - self.problem_type = aviary_inputs.get_val(Settings.PROBLEM_TYPE) - self.mass_defect = aviary_inputs.get_val('mass_defect', units='lbm') - - self.cruise_mass_final = aviary_inputs.get_val( - Mission.Summary.CRUISE_MASS_FINAL, units='lbm') - - if self.post_mission_info is True and 'target_range' in self.post_mission_info: - self.target_range = wrapped_convert_units( - phase_info['post_mission']['target_range'], 'NM') - aviary_inputs.set_val(Mission.Summary.RANGE, - self.target_range, units='NM') - else: - self.target_range = aviary_inputs.get_val( - Mission.Design.RANGE, units='NM') - aviary_inputs.set_val(Mission.Summary.RANGE, aviary_inputs.get_val( - Mission.Design.RANGE, units='NM'), units='NM') - self.cruise_mach = aviary_inputs.get_val(Mission.Design.MACH) - self.require_range_residual = True - - elif mission_method is HEIGHT_ENERGY: - self.problem_type = aviary_inputs.get_val(Settings.PROBLEM_TYPE) - aviary_inputs.set_val(Mission.Summary.GROSS_MASS, - val=self.initialization_guesses['actual_takeoff_mass'], units='lbm') - if 'target_range' in self.post_mission_info: - aviary_inputs.set_val(Mission.Summary.RANGE, wrapped_convert_units( - phase_info['post_mission']['target_range'], 'NM'), units='NM') - self.require_range_residual = True - self.target_range = wrapped_convert_units( - phase_info['post_mission']['target_range'], 'NM') - else: - self.require_range_residual = False - # still instantiate target_range because it is used for default guesses for phase comps - self.target_range = aviary_inputs.get_val( - Mission.Design.RANGE, units='NM') + self.post_mission_info = None + + self.problem_type = self.aviary_inputs.get_val(Settings.PROBLEM_TYPE) + + self.builder.initial_guesses(self, engine_builders) + # This function sets all the following defaults if they were not already set + # self.engine_builders, self.mass_method, self.pre_mission_info, self_post_mission_info + # self.require_range_residual, self.target_range + # other specific self.*** are defined in here as well that are specific to each builder - return aviary_inputs + return self.aviary_inputs def _update_metadata_from_subsystems(self): self.meta_data = BaseMetaData.copy() diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py new file mode 100644 index 000000000..974431ba1 --- /dev/null +++ b/aviary/mission/problem_builder_2DOF.py @@ -0,0 +1,80 @@ +from aviary.variable_info.variables import Settings +# from aviary.utils.functions import wrapped_convert_units +# from aviary.variable_info.enums import LegacyCode +from aviary.variable_info.enums import AnalysisScheme +from aviary.utils.process_input_decks import update_GASP_options +from aviary.utils.process_input_decks import initialization_guessing +# from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.functions import create_opts2vals, add_opts2vals, wrapped_convert_units + + +class AviaryProblemBuilder_2DOF(): + """ + A 2DOF specific builder that customizes AviaryProblem() for use with + two degree of freedom phases. + """ + + def initial_guesses(self, prob, engine_builders): + # TODO: This should probably be moved to the set_initial_guesses() method in AviaryProblem class + # Defines how the problem should build it's initial guesses for load_inputs() + # this modifies mass_method, initialization_guesses, and aviary_values + + prob.mass_method = prob.aviary_inputs.get_val(Settings.MASS_METHOD) + + if engine_builders is None: + engine_builders = build_engine_deck(prob.aviary_inputs) + prob.engine_builders = engine_builders + + prob.aviary_inputs = update_GASP_options(prob.aviary_inputs) + + prob.initialization_guesses = initialization_guessing( + prob.aviary_inputs, prob.initialization_guesses, prob.engine_builders) + + prob.aviary_inputs.set_val(Mission.Summary.CRUISE_MASS_FINAL, + val=prob.initialization_guesses['cruise_mass_final'], units='lbm') + prob.aviary_inputs.set_val(Mission.Summary.GROSS_MASS, + val=prob.initialization_guesses['actual_takeoff_mass'], units='lbm') + + # Deal with missing defaults in phase info: + if prob.pre_mission_info is None: + prob.pre_mission_info = {'include_takeoff': True, + 'external_subsystems': []} + if prob.post_mission_info is None: + prob.post_mission_info = {'include_landing': True, + 'external_subsystems': []} + + # Commonly referenced values + prob.cruise_alt = prob.aviary_inputs.get_val( + Mission.Design.CRUISE_ALTITUDE, units='ft') + prob.mass_defect = prob.aviary_inputs.get_val('mass_defect', units='lbm') + + prob.cruise_mass_final = prob.aviary_inputs.get_val( + Mission.Summary.CRUISE_MASS_FINAL, units='lbm') + + if 'target_range' in prob.post_mission_info: + prob.target_range = wrapped_convert_units( + prob.post_mission_info['post_mission']['target_range'], 'NM') + prob.aviary_inputs.set_val(Mission.Summary.RANGE, + prob.target_range, units='NM') + else: + prob.target_range = prob.aviary_inputs.get_val( + Mission.Design.RANGE, units='NM') + prob.aviary_inputs.set_val(Mission.Summary.RANGE, prob.aviary_inputs.get_val( + Mission.Design.RANGE, units='NM'), units='NM') + prob.cruise_mach = prob.aviary_inputs.get_val(Mission.Design.MACH) + prob.require_range_residual = True + + def phase_info_default_location(self, prob): + # Set the location of the default phase info for the EOM if no phase_info is specified + + if prob.analysis_scheme is AnalysisScheme.COLLOCATION: + from aviary.interface.default_phase_info.two_dof import phase_info + elif prob.analysis_scheme is AnalysisScheme.SHOOTING: + from aviary.interface.default_phase_info.two_dof_fiti import phase_info, \ + phase_info_parameterization + phase_info, _ = phase_info_parameterization( + phase_info, None, prob.aviary_inputs) + + return phase_info diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py new file mode 100644 index 000000000..6c817c338 --- /dev/null +++ b/aviary/mission/problem_builder_height_energy.py @@ -0,0 +1,70 @@ +from aviary.variable_info.variables import Settings +# from aviary.utils.functions import wrapped_convert_units +# from dymos.transcriptions.transcription_base import TranscriptionBase +# from aviary.mission.flops_based.phases.build_landing import Landing +# import openmdao.api as om +# from openmdao.utils.mpi import MPI +# from aviary.mission.phase_builder_base import PhaseBuilderBase +# from aviary.mission.energy_phase import EnergyPhase +from aviary.utils.process_input_decks import update_GASP_options, initialization_guessing +from aviary.variable_info.enums import LegacyCode +from aviary.variable_info.enums import AnalysisScheme +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.utils.functions import create_opts2vals, add_opts2vals, wrapped_convert_units + + +class AviaryProblemBuilder_HE(): + """ + A Height-Energy specific builder that customizes AviaryProblem() for use with + height energy phases. + """ + + def initial_guesses(self, prob, engine_builders): + # TODO: This should probably be moved to the set_initial_guesses() method in AviaryProblem class + # Defines how the problem should build it's initial guesses for load_inputs() + # this modifies mass_method, initialization_guesses, and aviary_values + + prob.mass_method = prob.aviary_inputs.get_val(Settings.MASS_METHOD) + + if prob.mass_method is LegacyCode.GASP: + prob.aviary_inputs = update_GASP_options(prob.aviary_inputs) + + if engine_builders is None: + engine_builders = build_engine_deck(prob.aviary_inputs) + prob.engine_builders = engine_builders + + prob.initialization_guesses = initialization_guessing( + prob.aviary_inputs, prob.initialization_guesses, prob.engine_builders) + + # Deal with missing defaults in phase info: + if prob.pre_mission_info is None: + prob.pre_mission_info = {'include_takeoff': True, + 'external_subsystems': []} + if prob.post_mission_info is None: + prob.post_mission_info = {'include_landing': True, + 'external_subsystems': []} + + # Commonly referenced values + prob.aviary_inputs.set_val( + Mission.Summary.GROSS_MASS, val=prob.initialization_guesses['actual_takeoff_mass'], units='lbm') + + if 'target_range' in prob.post_mission_info: + prob.aviary_inputs.set_val(Mission.Summary.RANGE, wrapped_convert_units( + prob.post_mission_info['target_range'], 'NM'), units='NM') + prob.require_range_residual = True + prob.target_range = wrapped_convert_units( + prob.post_mission_info['target_range'], 'NM') + else: + prob.require_range_residual = False + # still instantiate target_range because it is used for default guesses for phase comps + prob.target_range = prob.aviary_inputs.get_val( + Mission.Design.RANGE, units='NM') + + def phase_info_default_location(self, prob): + # Set the location of the default phase info for the EOM if no phase_info is specified + + if prob.analysis_scheme is AnalysisScheme.COLLOCATION: + from aviary.interface.default_phase_info.height_energy import phase_info + + return phase_info diff --git a/aviary/variable_info/enums.py b/aviary/variable_info/enums.py index 75d7392b7..9d00d1787 100644 --- a/aviary/variable_info/enums.py +++ b/aviary/variable_info/enums.py @@ -61,6 +61,7 @@ class EquationsOfMotion(Enum): HEIGHT_ENERGY = 'height_energy' TWO_DEGREES_OF_FREEDOM = '2DOF' SOLVED_2DOF = 'solved_2DOF' + CUSTOM = 'custom' @unique From 35fce0d1600a5b762d9dc1287550b92366c32ae7 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 Jan 2025 16:41:42 -0500 Subject: [PATCH 02/38] move TO to the builders --- aviary/interface/methods_for_level2.py | 308 ++++-------------- aviary/mission/problem_builder_2DOF.py | 111 ++++++- .../mission/problem_builder_height_energy.py | 34 +- 3 files changed, 186 insertions(+), 267 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index d83358de1..8b8d1ce67 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -1,4 +1,3 @@ -from dymos.transcriptions.transcription_base import TranscriptionBase import csv import warnings import inspect @@ -12,6 +11,7 @@ import numpy as np import dymos as dm +from dymos.transcriptions.transcription_base import TranscriptionBase from dymos.utils.misc import _unspecified import openmdao.api as om @@ -24,7 +24,6 @@ from aviary.interface.utils.check_phase_info import check_phase_info from aviary.mission.energy_phase import EnergyPhase from aviary.mission.flops_based.phases.build_landing import Landing -from aviary.mission.flops_based.phases.build_takeoff import Takeoff from aviary.mission.twodof_phase import TwoDOFPhase from aviary.mission.gasp_based.idle_descent_estimation import add_descent_estimation_as_submodel from aviary.mission.gasp_based.ode.params import ParamPort @@ -38,7 +37,6 @@ from aviary.mission.gasp_based.phases.ascent_phase import AscentPhase from aviary.mission.gasp_based.phases.descent_phase import DescentPhase from aviary.mission.gasp_based.ode.landing_ode import LandingSegment -from aviary.mission.gasp_based.ode.taxi_ode import TaxiSegment from aviary.mission.gasp_based.phases.v_rotate_comp import VRotateComp from aviary.mission.gasp_based.polynomial_fit import PolynomialFit from aviary.mission.phase_builder_base import PhaseBuilderBase @@ -66,7 +64,6 @@ from aviary.core.PostMissionGroup import PostMissionGroup from aviary.core.PreMissionGroup import PreMissionGroup from aviary.core.AviaryGroup import AviaryGroup -from aviary.utils.process_input_decks import initialization_guessing, update_GASP_options FLOPS = LegacyCode.FLOPS GASP = LegacyCode.GASP @@ -139,6 +136,7 @@ def load_inputs( """ # compatibility with being passed int for verbosity verbosity = Verbosity(verbosity) + ## LOAD INPUT FILE ### # Create AviaryValues object from file (or process existing AviaryValues object # with default values from metadata) and generate initial guesses @@ -149,7 +147,6 @@ def load_inputs( self.mission_method = mission_method = self.aviary_inputs.get_val( Settings.EQUATIONS_OF_MOTION) -<<<<<<< HEAD # Determine which problem builder to use based on mission_method if mission_method is HEIGHT_ENERGY: self.builder = AviaryProblemBuilder_HE() @@ -165,15 +162,6 @@ def load_inputs( else: raise ValueError( f'settings:equations_of_motion must be one of: height_energy, 2DOF, or custom') -======= - if mission_method is TWO_DEGREES_OF_FREEDOM or mass_method is GASP: - # TODO this should be a preprocessor step if it is required here - aviary_inputs = update_GASP_options(aviary_inputs) - initialization_guesses = initialization_guessing( - aviary_inputs, initialization_guesses, engine_builders) - self.aviary_inputs = aviary_inputs - self.initialization_guesses = initialization_guesses ->>>>>>> d4cc805780fef8012d81b48886b46f2b9e3a20b2 ## LOAD PHASE_INFO ### if phase_info is None: @@ -232,116 +220,7 @@ def load_inputs( # self.require_range_residual, self.target_range # other specific self.*** are defined in here as well that are specific to each builder -<<<<<<< HEAD return self.aviary_inputs -======= - if mission_method is TWO_DEGREES_OF_FREEDOM: - aviary_inputs.set_val( - Mission.Summary.CRUISE_MASS_FINAL, - val=self.initialization_guesses['cruise_mass_final'], - units='lbm') - aviary_inputs.set_val( - Mission.Summary.GROSS_MASS, - val=self.initialization_guesses['actual_takeoff_mass'], - units='lbm') - - # Commonly referenced values - self.cruise_alt = aviary_inputs.get_val( - Mission.Design.CRUISE_ALTITUDE, units='ft') - self.problem_type = aviary_inputs.get_val(Settings.PROBLEM_TYPE) - self.mass_defect = aviary_inputs.get_val('mass_defect', units='lbm') - - self.cruise_mass_final = aviary_inputs.get_val( - Mission.Summary.CRUISE_MASS_FINAL, units='lbm') - - if self.post_mission_info is True and 'target_range' in self.post_mission_info: - self.target_range = wrapped_convert_units( - phase_info['post_mission']['target_range'], 'NM') - aviary_inputs.set_val(Mission.Summary.RANGE, - self.target_range, units='NM') - else: - self.target_range = aviary_inputs.get_val( - Mission.Design.RANGE, units='NM') - aviary_inputs.set_val(Mission.Summary.RANGE, aviary_inputs.get_val( - Mission.Design.RANGE, units='NM'), units='NM') - self.cruise_mach = aviary_inputs.get_val(Mission.Design.MACH) - self.require_range_residual = True - - elif mission_method is HEIGHT_ENERGY: - self.problem_type = aviary_inputs.get_val(Settings.PROBLEM_TYPE) - aviary_inputs.set_val( - Mission.Summary.GROSS_MASS, - val=self.initialization_guesses['actual_takeoff_mass'], - units='lbm') - if 'target_range' in self.post_mission_info: - aviary_inputs.set_val(Mission.Summary.RANGE, wrapped_convert_units( - phase_info['post_mission']['target_range'], 'NM'), units='NM') - self.require_range_residual = True - self.target_range = wrapped_convert_units( - phase_info['post_mission']['target_range'], 'NM') - else: - self.require_range_residual = False - # still instantiate target_range because it is used for default guesses - # for phase comps - self.target_range = aviary_inputs.get_val( - Mission.Design.RANGE, units='NM') - - return aviary_inputs ->>>>>>> d4cc805780fef8012d81b48886b46f2b9e3a20b2 - - def _update_metadata_from_subsystems(self): - self.meta_data = BaseMetaData.copy() - - # loop through phase_info and external subsystems - for phase_name in self.phase_info: - external_subsystems = self._get_all_subsystems( - self.phase_info[phase_name]['external_subsystems']) - for subsystem in external_subsystems: - meta_data = subsystem.meta_data.copy() - self.meta_data = merge_meta_data([self.meta_data, meta_data]) - - def phase_separator(self): - """ - This method checks for reserve=True & False - Returns an error if a non-reserve phase is specified after a reserve phase. - return two dictionaries of phases: regular_phases and reserve_phases - For shooting trajectories, this will also check if a phase is part of the descent - """ - - # Check to ensure no non-reserve phases are specified after reserve phases - start_reserve = False - raise_error = False - for idx, phase_name in enumerate(self.phase_info): - if 'user_options' in self.phase_info[phase_name]: - if 'reserve' in self.phase_info[phase_name]["user_options"]: - if self.phase_info[phase_name]["user_options"]["reserve"] is False: - # This is a regular phase - self.regular_phases.append(phase_name) - if start_reserve is True: - raise_error = True - else: - # This is a reserve phase - self.reserve_phases.append(phase_name) - start_reserve = True - else: - # This is a regular phase by default - self.regular_phases.append(phase_name) - if start_reserve is True: - raise_error = True - - if raise_error is True: - raise ValueError( - f'In phase_info, reserve=False cannot be specified after a phase where reserve=True. ' - f'All reserve phases must happen after non-reserve phases. ' - f'Regular Phases : {self.regular_phases} | ' - f'Reserve Phases : {self.reserve_phases} ') - - if self.analysis_scheme is AnalysisScheme.SHOOTING: - self.descent_phases = {} - for name, info in self.phase_info.items(): - descent = info.get('descent_phase', False) - if descent: - self.descent_phases[name] = info def check_and_preprocess_inputs(self): """ @@ -472,9 +351,66 @@ def check_and_preprocess_inputs(self): 'core_subsystems': default_mission_subsystems} self._update_metadata_from_subsystems() + self._check_reserve_phase_separation() - if self.mission_method in (HEIGHT_ENERGY, SOLVED_2DOF, TWO_DEGREES_OF_FREEDOM): - self.phase_separator() + def _update_metadata_from_subsystems(self): + """ + Merge metadata from user-defined subsystems into problem metadata. + """ + self.meta_data = BaseMetaData.copy() + + # loop through phase_info and external subsystems + for phase_name in self.phase_info: + + external_subsystems = self._get_all_subsystems( + self.phase_info[phase_name]['external_subsystems']) + + for subsystem in external_subsystems: + meta_data = subsystem.meta_data.copy() + self.meta_data = merge_meta_data([self.meta_data, meta_data]) + + def _check_reserve_phase_separation(self): + """ + This method checks for reserve=True & False + Returns an error if a non-reserve phase is specified after a reserve phase. + return two dictionaries of phases: regular_phases and reserve_phases + For shooting trajectories, this will also check if a phase is part of the descent + """ + + # Check to ensure no non-reserve phases are specified after reserve phases + start_reserve = False + raise_error = False + for idx, phase_name in enumerate(self.phase_info): + if 'user_options' in self.phase_info[phase_name]: + if 'reserve' in self.phase_info[phase_name]["user_options"]: + if self.phase_info[phase_name]["user_options"]["reserve"] is False: + # This is a regular phase + self.regular_phases.append(phase_name) + if start_reserve is True: + raise_error = True + else: + # This is a reserve phase + self.reserve_phases.append(phase_name) + start_reserve = True + else: + # This is a regular phase by default + self.regular_phases.append(phase_name) + if start_reserve is True: + raise_error = True + + if raise_error is True: + raise ValueError( + f'In phase_info, reserve=False cannot be specified after a phase where reserve=True. ' + f'All reserve phases must happen after non-reserve phases. ' + f'Regular Phases : {self.regular_phases} | ' + f'Reserve Phases : {self.reserve_phases} ') + + if self.analysis_scheme is AnalysisScheme.SHOOTING: + self.descent_phases = {} + for name, info in self.phase_info.items(): + descent = info.get('descent_phase', False) + if descent: + self.descent_phases[name] = info def add_pre_mission_systems(self): """ @@ -524,126 +460,8 @@ def add_pre_mission_systems(self): promotes_inputs=['*'], promotes_outputs=['*']) - if not self.pre_mission_info['include_takeoff']: - return - - # Check for 2DOF mission method - # NOTE should solved trigger this as well? - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - self._add_two_dof_takeoff_systems() - - # Check for HE mission method - elif self.mission_method is HEIGHT_ENERGY: - self._add_height_energy_takeoff_systems() - - def _add_height_energy_takeoff_systems(self): - # Initialize takeoff options - takeoff_options = Takeoff( - airport_altitude=0., # ft - num_engines=self.aviary_inputs.get_val(Aircraft.Engine.NUM_ENGINES) - ) - - # Build and add takeoff subsystem - takeoff = takeoff_options.build_phase(False) - self.model.add_subsystem( - 'takeoff', takeoff, promotes_inputs=['aircraft:*', 'mission:*'], - promotes_outputs=['mission:*']) - - def _add_two_dof_takeoff_systems(self): - # Create options to values - OptionsToValues = create_opts2vals( - [Aircraft.CrewPayload.NUM_PASSENGERS, - Mission.Design.CRUISE_ALTITUDE, ]) - - add_opts2vals(self.model, OptionsToValues, self.aviary_inputs) - - if self.analysis_scheme is AnalysisScheme.SHOOTING: - self._add_fuel_reserve_component( - post_mission=False, reserves_name='reserve_fuel_estimate') - add_default_sgm_args(self.descent_phases, self.ode_args) - add_descent_estimation_as_submodel( - self, - phases=self.descent_phases, - cruise_mach=self.cruise_mach, - cruise_alt=self.cruise_alt, - reserve_fuel='reserve_fuel_estimate', - all_subsystems=self._get_all_subsystems(), - ) - - # Add thrust-to-weight ratio subsystem - self.model.add_subsystem( - 'tw_ratio', - om.ExecComp( - f'TW_ratio = Fn_SLS / (takeoff_mass * {GRAV_ENGLISH_LBM})', - TW_ratio={'units': "unitless"}, - Fn_SLS={'units': 'lbf'}, - takeoff_mass={'units': 'lbm'}, - ), - promotes_inputs=[('Fn_SLS', Aircraft.Propulsion.TOTAL_SCALED_SLS_THRUST), - ('takeoff_mass', Mission.Summary.GROSS_MASS)], - promotes_outputs=[('TW_ratio', Aircraft.Design.THRUST_TO_WEIGHT_RATIO)], - ) - - self.cruise_alt = self.aviary_inputs.get_val( - Mission.Design.CRUISE_ALTITUDE, units='ft') - - if self.analysis_scheme is AnalysisScheme.COLLOCATION: - # Add event transformation subsystem - self.model.add_subsystem( - "event_xform", - om.ExecComp( - ["t_init_gear=m*tau_gear+b", "t_init_flaps=m*tau_flaps+b"], - t_init_gear={"units": "s"}, # initial time that gear comes up - t_init_flaps={"units": "s"}, # initial time that flaps retract - tau_gear={"units": "unitless"}, - tau_flaps={"units": "unitless"}, - m={"units": "s"}, - b={"units": "s"}, - ), - promotes_inputs=[ - "tau_gear", # design var - "tau_flaps", # design var - ("m", Mission.Takeoff.ASCENT_DURATION), - ("b", Mission.Takeoff.ASCENT_T_INTIIAL), - ], - promotes_outputs=["t_init_gear", "t_init_flaps"], # link to h_fit - ) - - # Add taxi subsystem - self.model.add_subsystem( - "taxi", TaxiSegment(**(self.ode_args)), - promotes_inputs=['aircraft:*', 'mission:*'], - ) - - # Calculate speed at which to initiate rotation - self.model.add_subsystem( - "vrot", - om.ExecComp( - "Vrot = ((2 * mass * g) / (rho * wing_area * CLmax))**0.5 + dV1 + dVR", - Vrot={"units": "ft/s"}, - mass={"units": "lbm"}, - CLmax={"units": "unitless"}, - g={"units": "lbf/lbm", "val": GRAV_ENGLISH_LBM}, - rho={"units": "slug/ft**3", "val": RHO_SEA_LEVEL_ENGLISH}, - wing_area={"units": "ft**2"}, - dV1={ - "units": "ft/s", - "desc": "Increment of engine failure decision speed above stall", - }, - dVR={ - "units": "ft/s", - "desc": "Increment of takeoff rotation speed above engine failure " - "decision speed", - }, - ), - promotes_inputs=[ - ("wing_area", Aircraft.Wing.AREA), - ("dV1", Mission.Takeoff.DECISION_SPEED_INCREMENT), - ("dVR", Mission.Takeoff.ROTATION_SPEED_INCREMENT), - ("CLmax", Mission.Takeoff.LIFT_COEFFICIENT_MAX), - ], - promotes_outputs=[('Vrot', Mission.Takeoff.ROTATION_VELOCITY)] - ) + if self.pre_mission_info['include_takeoff']: + self.builder.add_takeoff_systems(self) def _add_premission_external_subsystems(self): """ diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 974431ba1..0a9bd9f44 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -1,18 +1,14 @@ -from aviary.variable_info.variables import Settings -# from aviary.utils.functions import wrapped_convert_units -# from aviary.variable_info.enums import LegacyCode -from aviary.variable_info.enums import AnalysisScheme -from aviary.utils.process_input_decks import update_GASP_options -from aviary.utils.process_input_decks import initialization_guessing -# from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.mission.gasp_based.ode.taxi_ode import TaxiSegment from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import create_opts2vals, add_opts2vals, wrapped_convert_units +from aviary.utils.process_input_decks import initialization_guessing, update_GASP_options +from aviary.variable_info.enums import AnalysisScheme +from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings class AviaryProblemBuilder_2DOF(): """ - A 2DOF specific builder that customizes AviaryProblem() for use with + A 2DOF specific builder that customizes AviaryProblem() for use with two degree of freedom phases. """ @@ -78,3 +74,100 @@ def phase_info_default_location(self, prob): phase_info, None, prob.aviary_inputs) return phase_info + + def add_takeoff_systems(self, prob): + # Create options to values + OptionsToValues = create_opts2vals( + [Aircraft.CrewPayload.NUM_PASSENGERS, + Mission.Design.CRUISE_ALTITUDE, ]) + + add_opts2vals(prob.model, OptionsToValues, prob.aviary_inputs) + + if prob.analysis_scheme is AnalysisScheme.SHOOTING: + prob._add_fuel_reserve_component( + post_mission=False, reserves_name='reserve_fuel_estimate') + add_default_sgm_args(prob.descent_phases, prob.ode_args) + add_descent_estimation_as_submodel( + prob, + phases=prob.descent_phases, + cruise_mach=prob.cruise_mach, + cruise_alt=prob.cruise_alt, + reserve_fuel='reserve_fuel_estimate', + all_subsystems=prob._get_all_subsystems(), + ) + + # Add thrust-to-weight ratio subsystem + prob.model.add_subsystem( + 'tw_ratio', + om.ExecComp( + f'TW_ratio = Fn_SLS / (takeoff_mass * {GRAV_ENGLISH_LBM})', + TW_ratio={'units': "unitless"}, + Fn_SLS={'units': 'lbf'}, + takeoff_mass={'units': 'lbm'}, + ), + promotes_inputs=[('Fn_SLS', Aircraft.Propulsion.TOTAL_SCALED_SLS_THRUST), + ('takeoff_mass', Mission.Summary.GROSS_MASS)], + promotes_outputs=[('TW_ratio', Aircraft.Design.THRUST_TO_WEIGHT_RATIO)], + ) + + prob.cruise_alt = prob.aviary_inputs.get_val( + Mission.Design.CRUISE_ALTITUDE, units='ft') + + if prob.analysis_scheme is AnalysisScheme.COLLOCATION: + # Add event transformation subsystem + prob.model.add_subsystem( + "event_xform", + om.ExecComp( + ["t_init_gear=m*tau_gear+b", "t_init_flaps=m*tau_flaps+b"], + t_init_gear={"units": "s"}, # initial time that gear comes up + t_init_flaps={"units": "s"}, # initial time that flaps retract + tau_gear={"units": "unitless"}, + tau_flaps={"units": "unitless"}, + m={"units": "s"}, + b={"units": "s"}, + ), + promotes_inputs=[ + "tau_gear", # design var + "tau_flaps", # design var + ("m", Mission.Takeoff.ASCENT_DURATION), + ("b", Mission.Takeoff.ASCENT_T_INTIIAL), + ], + promotes_outputs=["t_init_gear", "t_init_flaps"], # link to h_fit + ) + + # Add taxi subsystem + prob.model.add_subsystem( + "taxi", TaxiSegment(**(prob.ode_args)), + promotes_inputs=['aircraft:*', 'mission:*'], + ) + + # Calculate speed at which to initiate rotation + prob.model.add_subsystem( + "vrot", + om.ExecComp( + "Vrot = ((2 * mass * g) / (rho * wing_area * CLmax))**0.5 + dV1 + dVR", + Vrot={"units": "ft/s"}, + mass={"units": "lbm"}, + CLmax={"units": "unitless"}, + g={"units": "lbf/lbm", "val": GRAV_ENGLISH_LBM}, + rho={"units": "slug/ft**3", "val": RHO_SEA_LEVEL_ENGLISH}, + wing_area={"units": "ft**2"}, + dV1={ + "units": "ft/s", + "desc": "Increment of engine failure decision speed above stall", + }, + dVR={ + "units": "ft/s", + "desc": "Increment of takeoff rotation speed above engine failure " + "decision speed", + }, + ), + promotes_inputs=[ + ("wing_area", Aircraft.Wing.AREA), + ("dV1", Mission.Takeoff.DECISION_SPEED_INCREMENT), + ("dVR", Mission.Takeoff.ROTATION_SPEED_INCREMENT), + ("CLmax", Mission.Takeoff.LIFT_COEFFICIENT_MAX), + ], + promotes_outputs=[('Vrot', Mission.Takeoff.ROTATION_VELOCITY)] + ) + diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 6c817c338..edcb52904 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -1,23 +1,16 @@ -from aviary.variable_info.variables import Settings -# from aviary.utils.functions import wrapped_convert_units -# from dymos.transcriptions.transcription_base import TranscriptionBase -# from aviary.mission.flops_based.phases.build_landing import Landing -# import openmdao.api as om -# from openmdao.utils.mpi import MPI -# from aviary.mission.phase_builder_base import PhaseBuilderBase -# from aviary.mission.energy_phase import EnergyPhase +from aviary.mission.flops_based.phases.build_takeoff import Takeoff +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.functions import wrapped_convert_units from aviary.utils.process_input_decks import update_GASP_options, initialization_guessing -from aviary.variable_info.enums import LegacyCode from aviary.variable_info.enums import AnalysisScheme -from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.variable_info.enums import LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings -from aviary.utils.functions import create_opts2vals, add_opts2vals, wrapped_convert_units class AviaryProblemBuilder_HE(): """ - A Height-Energy specific builder that customizes AviaryProblem() for use with - height energy phases. + A Height-Energy specific builder that customizes AviaryProblem() for use with + height energy phases. """ def initial_guesses(self, prob, engine_builders): @@ -28,6 +21,7 @@ def initial_guesses(self, prob, engine_builders): prob.mass_method = prob.aviary_inputs.get_val(Settings.MASS_METHOD) if prob.mass_method is LegacyCode.GASP: + # Support for GASP mass methods with HE. prob.aviary_inputs = update_GASP_options(prob.aviary_inputs) if engine_builders is None: @@ -68,3 +62,17 @@ def phase_info_default_location(self, prob): from aviary.interface.default_phase_info.height_energy import phase_info return phase_info + + def add_takeoff_systems(self, prob): + # Initialize takeoff options + takeoff_options = Takeoff( + airport_altitude=0., # ft + num_engines=prob.aviary_inputs.get_val(Aircraft.Engine.NUM_ENGINES) + ) + + # Build and add takeoff subsystem + takeoff = takeoff_options.build_phase(False) + prob.model.add_subsystem( + 'takeoff', takeoff, promotes_inputs=['aircraft:*', 'mission:*'], + promotes_outputs=['mission:*']) + From c02d33adf2c6489e36f260bf9fb1a5d18b677f2c Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 Jan 2025 16:47:41 -0500 Subject: [PATCH 03/38] cleanup --- aviary/mission/problem_builder_height_energy.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index edcb52904..83fd0fe71 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -73,6 +73,9 @@ def add_takeoff_systems(self, prob): # Build and add takeoff subsystem takeoff = takeoff_options.build_phase(False) prob.model.add_subsystem( - 'takeoff', takeoff, promotes_inputs=['aircraft:*', 'mission:*'], - promotes_outputs=['mission:*']) + 'takeoff', + takeoff, + promotes_inputs=['aircraft:*', 'mission:*'], + promotes_outputs=['mission:*'], + ) From f14e400d235c270a7b79b4353bfd3dee2175fef4 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 Jan 2025 17:22:18 -0500 Subject: [PATCH 04/38] debugging --- aviary/mission/problem_builder_2DOF.py | 37 +++++++++++-------- .../mission/problem_builder_height_energy.py | 23 +++++++----- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 0a9bd9f44..65786d8ef 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -17,21 +17,28 @@ def initial_guesses(self, prob, engine_builders): # Defines how the problem should build it's initial guesses for load_inputs() # this modifies mass_method, initialization_guesses, and aviary_values - prob.mass_method = prob.aviary_inputs.get_val(Settings.MASS_METHOD) + aviary_inputs = prob.aviary_inputs + prob.mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) if engine_builders is None: - engine_builders = build_engine_deck(prob.aviary_inputs) + engine_builders = build_engine_deck(aviary_inputs) prob.engine_builders = engine_builders - prob.aviary_inputs = update_GASP_options(prob.aviary_inputs) + aviary_inputs = update_GASP_options(aviary_inputs) prob.initialization_guesses = initialization_guessing( - prob.aviary_inputs, prob.initialization_guesses, prob.engine_builders) + aviary_inputs, prob.initialization_guesses, prob.engine_builders) - prob.aviary_inputs.set_val(Mission.Summary.CRUISE_MASS_FINAL, - val=prob.initialization_guesses['cruise_mass_final'], units='lbm') - prob.aviary_inputs.set_val(Mission.Summary.GROSS_MASS, - val=prob.initialization_guesses['actual_takeoff_mass'], units='lbm') + aviary_inputs.set_val( + Mission.Summary.CRUISE_MASS_FINAL, + val=prob.initialization_guesses['cruise_mass_final'], + units='lbm', + ) + aviary_inputs.set_val( + Mission.Summary.GROSS_MASS, + val=prob.initialization_guesses['actual_takeoff_mass'], + units='lbm', + ) # Deal with missing defaults in phase info: if prob.pre_mission_info is None: @@ -42,24 +49,24 @@ def initial_guesses(self, prob, engine_builders): 'external_subsystems': []} # Commonly referenced values - prob.cruise_alt = prob.aviary_inputs.get_val( + prob.cruise_alt = aviary_inputs.get_val( Mission.Design.CRUISE_ALTITUDE, units='ft') - prob.mass_defect = prob.aviary_inputs.get_val('mass_defect', units='lbm') + prob.mass_defect = aviary_inputs.get_val('mass_defect', units='lbm') - prob.cruise_mass_final = prob.aviary_inputs.get_val( + prob.cruise_mass_final = aviary_inputs.get_val( Mission.Summary.CRUISE_MASS_FINAL, units='lbm') if 'target_range' in prob.post_mission_info: prob.target_range = wrapped_convert_units( prob.post_mission_info['post_mission']['target_range'], 'NM') - prob.aviary_inputs.set_val(Mission.Summary.RANGE, + aviary_inputs.set_val(Mission.Summary.RANGE, prob.target_range, units='NM') else: - prob.target_range = prob.aviary_inputs.get_val( + prob.target_range = aviary_inputs.get_val( Mission.Design.RANGE, units='NM') - prob.aviary_inputs.set_val(Mission.Summary.RANGE, prob.aviary_inputs.get_val( + aviary_inputs.set_val(Mission.Summary.RANGE, aviary_inputs.get_val( Mission.Design.RANGE, units='NM'), units='NM') - prob.cruise_mach = prob.aviary_inputs.get_val(Mission.Design.MACH) + prob.cruise_mach = aviary_inputs.get_val(Mission.Design.MACH) prob.require_range_residual = True def phase_info_default_location(self, prob): diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 83fd0fe71..a4b8d7041 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -18,18 +18,19 @@ def initial_guesses(self, prob, engine_builders): # Defines how the problem should build it's initial guesses for load_inputs() # this modifies mass_method, initialization_guesses, and aviary_values - prob.mass_method = prob.aviary_inputs.get_val(Settings.MASS_METHOD) + aviary_inputs = prob.aviary_inputs + prob.mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) if prob.mass_method is LegacyCode.GASP: # Support for GASP mass methods with HE. - prob.aviary_inputs = update_GASP_options(prob.aviary_inputs) + aviary_inputs = update_GASP_options(aviary_inputs) if engine_builders is None: - engine_builders = build_engine_deck(prob.aviary_inputs) + engine_builders = build_engine_deck(aviary_inputs) prob.engine_builders = engine_builders prob.initialization_guesses = initialization_guessing( - prob.aviary_inputs, prob.initialization_guesses, prob.engine_builders) + aviary_inputs, prob.initialization_guesses, prob.engine_builders) # Deal with missing defaults in phase info: if prob.pre_mission_info is None: @@ -40,19 +41,23 @@ def initial_guesses(self, prob, engine_builders): 'external_subsystems': []} # Commonly referenced values - prob.aviary_inputs.set_val( - Mission.Summary.GROSS_MASS, val=prob.initialization_guesses['actual_takeoff_mass'], units='lbm') + aviary_inputs.set_val( + Mission.Summary.GROSS_MASS, + val=prob.initialization_guesses['actual_takeoff_mass'], + units='lbm' + ) if 'target_range' in prob.post_mission_info: - prob.aviary_inputs.set_val(Mission.Summary.RANGE, wrapped_convert_units( + aviary_inputs.set_val(Mission.Summary.RANGE, wrapped_convert_units( prob.post_mission_info['target_range'], 'NM'), units='NM') prob.require_range_residual = True prob.target_range = wrapped_convert_units( prob.post_mission_info['target_range'], 'NM') else: prob.require_range_residual = False - # still instantiate target_range because it is used for default guesses for phase comps - prob.target_range = prob.aviary_inputs.get_val( + # still instantiate target_range because it is used for default guesses + # for phase comps + prob.target_range = aviary_inputs.get_val( Mission.Design.RANGE, units='NM') def phase_info_default_location(self, prob): From ebc5836b322b6b4bc135e5a8867dd721a6a964aa Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 15 Jan 2025 17:54:55 -0500 Subject: [PATCH 05/38] Fixed a bug from separting the AviaryGroup --- aviary/core/AviaryGroup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aviary/core/AviaryGroup.py b/aviary/core/AviaryGroup.py index 1705bc86c..fae28b87f 100644 --- a/aviary/core/AviaryGroup.py +++ b/aviary/core/AviaryGroup.py @@ -1,8 +1,11 @@ import openmdao.api as om -from aviary.utils.aviary_values import AviaryValues from openmdao.utils.mpi import MPI + +from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.enums import EquationsOfMotion from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings -from aviary.variable_info.enums import AnalysisScheme, ProblemType, EquationsOfMotion + +HEIGHT_ENERGY = EquationsOfMotion.HEIGHT_ENERGY class AviaryGroup(om.Group): @@ -83,7 +86,7 @@ def configure(self): Settings.EQUATIONS_OF_MOTION) # Temporarily add extra stuff here, probably patched soon - if mission_method is 'height_energy': + if mission_method is HEIGHT_ENERGY: phase_info = self.options['phase_info'] # Set a more appropriate solver for dymos when the phases are linked. From 50aa62dbefe64e35f0c4a97f88da5624a3a9ffee Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 21 Jan 2025 16:51:17 -0500 Subject: [PATCH 06/38] A few more migrations. --- aviary/interface/methods_for_level2.py | 122 +++--------------- aviary/mission/problem_builder_2DOF.py | 22 ++++ .../mission/problem_builder_height_energy.py | 83 ++++++++++++ 3 files changed, 120 insertions(+), 107 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 8b8d1ce67..0239d0406 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -23,7 +23,6 @@ from aviary.interface.default_phase_info.two_dof_fiti import add_default_sgm_args from aviary.interface.utils.check_phase_info import check_phase_info from aviary.mission.energy_phase import EnergyPhase -from aviary.mission.flops_based.phases.build_landing import Landing from aviary.mission.twodof_phase import TwoDOFPhase from aviary.mission.gasp_based.idle_descent_estimation import add_descent_estimation_as_submodel from aviary.mission.gasp_based.ode.params import ParamPort @@ -36,7 +35,6 @@ from aviary.mission.gasp_based.phases.accel_phase import AccelPhase from aviary.mission.gasp_based.phases.ascent_phase import AscentPhase from aviary.mission.gasp_based.phases.descent_phase import DescentPhase -from aviary.mission.gasp_based.ode.landing_ode import LandingSegment from aviary.mission.gasp_based.phases.v_rotate_comp import VRotateComp from aviary.mission.gasp_based.polynomial_fit import PolynomialFit from aviary.mission.phase_builder_base import PhaseBuilderBase @@ -901,14 +899,11 @@ def add_post_mission_systems(self, include_landing=True): A user can override this with their own postmission systems. """ - if self.pre_mission_info['include_takeoff'] and self.mission_method is HEIGHT_ENERGY: - self._add_post_mission_takeoff_systems() + if self.pre_mission_info['include_takeoff']: + self.builder.add_post_mission_takeoff_systems() if include_landing and self.post_mission_info['include_landing']: - if self.mission_method is HEIGHT_ENERGY: - self._add_height_energy_landing_systems() - elif self.mission_method is TWO_DEGREES_OF_FREEDOM: - self._add_two_dof_landing_systems() + self.builder.add_landing_systems(self) self.model.add_subsystem('post_mission', self.post_mission, promotes_inputs=['*'], @@ -1506,9 +1501,11 @@ def add_design_variables(self): """ Adds design variables to the Aviary problem. - Depending on the mission model and problem type, different design variables and constraints are added. + Depending on the mission model and problem type, different design variables and constraints + are added. - If using the FLOPS model, a design variable is added for the gross mass of the aircraft, with a lower bound of 10 lbm and an upper bound of 900,000 lbm. + If using the FLOPS model, a design variable is added for the gross mass of the aircraft, + with a lower bound of 10 lbm and an upper bound of 900,000 lbm. If using the GASP model, the following design variables are added depending on the mission type: - the initial thrust-to-weight ratio of the aircraft during ascent @@ -1520,11 +1517,16 @@ def add_design_variables(self): - the initial altitude of the aircraft with gear extended is constrained to be 50 ft - the initial altitude of the aircraft with flaps extended is constrained to be 400 ft - If solving a sizing problem, a design variable is added for the gross mass of the aircraft, and another for the gross mass of the aircraft computed during the mission. A constraint is also added to ensure that the residual range is zero. + If solving a sizing problem, a design variable is added for the gross mass of the aircraft, + and another for the gross mass of the aircraft computed during the mission. A constraint + is also added to ensure that the residual range is zero. - If solving an alternate problem, only a design variable for the gross mass of the aircraft computed during the mission is added. A constraint is also added to ensure that the residual range is zero. + If solving an alternate problem, only a design variable for the gross mass of the aircraft + computed during the mission is added. A constraint is also added to ensure that the residual + range is zero. - In all cases, a design variable is added for the final cruise mass of the aircraft, with no upper bound, and a residual mass constraint is added to ensure that the mass balances. + In all cases, a design variable is added for the final cruise mass of the aircraft, with + no upper bound, and a residual mass constraint is added to ensure that the mass balances. """ # add the engine builder `get_design_vars` dict to a collected dict from @@ -2460,100 +2462,6 @@ def _get_all_subsystems(self, external_subsystems=None): return all_subsystems - def _add_height_energy_landing_systems(self): - landing_options = Landing( - ref_wing_area=self.aviary_inputs.get_val( - Aircraft.Wing.AREA, units='ft**2'), - Cl_max_ldg=self.aviary_inputs.get_val( - Mission.Landing.LIFT_COEFFICIENT_MAX) # no units - ) - - landing = landing_options.build_phase(False) - self.model.add_subsystem( - 'landing', landing, promotes_inputs=['aircraft:*', 'mission:*'], - promotes_outputs=['mission:*']) - - last_flight_phase_name = list(self.phase_info.keys())[-1] - control_type_string = 'control_values' - if self.phase_info[last_flight_phase_name]['user_options'].get( - 'use_polynomial_control', True): - if not use_new_dymos_syntax: - control_type_string = 'polynomial_control_values' - - last_regular_phase = self.regular_phases[-1] - self.model.connect(f'traj.{last_regular_phase}.states:mass', - Mission.Landing.TOUCHDOWN_MASS, src_indices=[-1]) - self.model.connect(f'traj.{last_regular_phase}.{control_type_string}:altitude', - Mission.Landing.INITIAL_ALTITUDE, - src_indices=[0]) - - def _add_post_mission_takeoff_systems(self): - first_flight_phase_name = list(self.phase_info.keys())[0] - connect_takeoff_to_climb = not self.phase_info[first_flight_phase_name][ - 'user_options'].get('add_initial_mass_constraint', True) - - if connect_takeoff_to_climb: - self.model.connect(Mission.Takeoff.FINAL_MASS, - f'traj.{first_flight_phase_name}.initial_states:mass') - self.model.connect(Mission.Takeoff.GROUND_DISTANCE, - f'traj.{first_flight_phase_name}.initial_states:distance') - - control_type_string = 'control_values' - if self.phase_info[first_flight_phase_name]['user_options'].get( - 'use_polynomial_control', True): - if not use_new_dymos_syntax: - control_type_string = 'polynomial_control_values' - - if self.phase_info[first_flight_phase_name]['user_options'].get( - 'optimize_mach', False): - # Create an ExecComp to compute the difference in mach - mach_diff_comp = om.ExecComp( - 'mach_resid_for_connecting_takeoff = final_mach - initial_mach') - self.model.add_subsystem('mach_diff_comp', mach_diff_comp) - - # Connect the inputs to the mach difference component - self.model.connect(Mission.Takeoff.FINAL_MACH, - 'mach_diff_comp.final_mach') - self.model.connect( - f'traj.{first_flight_phase_name}.{control_type_string}:mach', - 'mach_diff_comp.initial_mach', src_indices=[0]) - - # Add constraint for mach difference - self.model.add_constraint( - 'mach_diff_comp.mach_resid_for_connecting_takeoff', equals=0.0) - - if self.phase_info[first_flight_phase_name]['user_options'].get( - 'optimize_altitude', False): - # Similar steps for altitude difference - alt_diff_comp = om.ExecComp( - 'altitude_resid_for_connecting_takeoff = final_altitude - initial_altitude', units='ft') - self.model.add_subsystem('alt_diff_comp', alt_diff_comp) - - self.model.connect(Mission.Takeoff.FINAL_ALTITUDE, - 'alt_diff_comp.final_altitude') - self.model.connect( - f'traj.{first_flight_phase_name}.{control_type_string}:altitude', - 'alt_diff_comp.initial_altitude', src_indices=[0]) - - self.model.add_constraint( - 'alt_diff_comp.altitude_resid_for_connecting_takeoff', equals=0.0) - - def _add_two_dof_landing_systems(self): - self.model.add_subsystem( - "landing", - LandingSegment( - **(self.ode_args)), - promotes_inputs=['aircraft:*', 'mission:*', - (Dynamic.Vehicle.MASS, Mission.Landing.TOUCHDOWN_MASS)], - promotes_outputs=['mission:*'], - ) - self.model.connect( - 'pre_mission.interference_independent_of_shielded_area', - 'landing.interference_independent_of_shielded_area') - self.model.connect( - 'pre_mission.drag_loss_due_to_shielded_wing_area', - 'landing.drag_loss_due_to_shielded_wing_area') - def _add_objectives(self): "add objectives and some constraints" self.model.add_subsystem( diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 65786d8ef..b18bfe4c0 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -1,3 +1,4 @@ +from aviary.mission.gasp_based.ode.landing_ode import LandingSegment from aviary.mission.gasp_based.ode.taxi_ode import TaxiSegment from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import create_opts2vals, add_opts2vals, wrapped_convert_units @@ -178,3 +179,24 @@ def add_takeoff_systems(self, prob): promotes_outputs=[('Vrot', Mission.Takeoff.ROTATION_VELOCITY)] ) + def add_post_mission_takeoff_systems(self, prob): + pass + + def add_landing_systems(self, prob): + + prob.model.add_subsystem( + "landing", + LandingSegment( + **(prob.ode_args)), + promotes_inputs=['aircraft:*', 'mission:*', + (Dynamic.Vehicle.MASS, Mission.Landing.TOUCHDOWN_MASS)], + promotes_outputs=['mission:*'], + ) + + prob.model.connect( + 'pre_mission.interference_independent_of_shielded_area', + 'landing.interference_independent_of_shielded_area') + prob.model.connect( + 'pre_mission.drag_loss_due_to_shielded_wing_area', + 'landing.drag_loss_due_to_shielded_wing_area') + diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index a4b8d7041..4ac993b54 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -1,3 +1,4 @@ +from aviary.mission.flops_based.phases.build_landing import Landing from aviary.mission.flops_based.phases.build_takeoff import Takeoff from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.functions import wrapped_convert_units @@ -84,3 +85,85 @@ def add_takeoff_systems(self, prob): promotes_outputs=['mission:*'], ) + def add_post_mission_takeoff_systems(self, prob): + + first_flight_phase_name = list(prob.phase_info.keys())[0] + connect_takeoff_to_climb = not prob.phase_info[first_flight_phase_name][ + 'user_options'].get('add_initial_mass_constraint', True) + + if connect_takeoff_to_climb: + prob.model.connect(Mission.Takeoff.FINAL_MASS, + f'traj.{first_flight_phase_name}.initial_states:mass') + prob.model.connect(Mission.Takeoff.GROUND_DISTANCE, + f'traj.{first_flight_phase_name}.initial_states:distance') + + control_type_string = 'control_values' + if prob.phase_info[first_flight_phase_name]['user_options'].get( + 'use_polynomial_control', True): + if not use_new_dymos_syntax: + control_type_string = 'polynomial_control_values' + + if prob.phase_info[first_flight_phase_name]['user_options'].get( + 'optimize_mach', False): + # Create an ExecComp to compute the difference in mach + mach_diff_comp = om.ExecComp( + 'mach_resid_for_connecting_takeoff = final_mach - initial_mach') + prob.model.add_subsystem('mach_diff_comp', mach_diff_comp) + + # Connect the inputs to the mach difference component + prob.model.connect(Mission.Takeoff.FINAL_MACH, + 'mach_diff_comp.final_mach') + prob.model.connect( + f'traj.{first_flight_phase_name}.{control_type_string}:mach', + 'mach_diff_comp.initial_mach', src_indices=[0]) + + # Add constraint for mach difference + prob.model.add_constraint( + 'mach_diff_comp.mach_resid_for_connecting_takeoff', equals=0.0) + + if prob.phase_info[first_flight_phase_name]['user_options'].get( + 'optimize_altitude', False): + # Similar steps for altitude difference + alt_diff_comp = om.ExecComp( + 'altitude_resid_for_connecting_takeoff = final_altitude - initial_altitude', units='ft') + prob.model.add_subsystem('alt_diff_comp', alt_diff_comp) + + prob.model.connect(Mission.Takeoff.FINAL_ALTITUDE, + 'alt_diff_comp.final_altitude') + prob.model.connect( + f'traj.{first_flight_phase_name}.{control_type_string}:altitude', + 'alt_diff_comp.initial_altitude', src_indices=[0]) + + prob.model.add_constraint( + 'alt_diff_comp.altitude_resid_for_connecting_takeoff', equals=0.0) + + def add_landing_systems(self, prob): + + landing_options = Landing( + ref_wing_area=prob.aviary_inputs.get_val( + Aircraft.Wing.AREA, units='ft**2'), + Cl_max_ldg=prob.aviary_inputs.get_val( + Mission.Landing.LIFT_COEFFICIENT_MAX) # no units + ) + + landing = landing_options.build_phase(False) + + prob.model.add_subsystem( + 'landing', landing, promotes_inputs=['aircraft:*', 'mission:*'], + promotes_outputs=['mission:*']) + + last_flight_phase_name = list(prob.phase_info.keys())[-1] + + control_type_string = 'control_values' + if prob.phase_info[last_flight_phase_name]['user_options'].get( + 'use_polynomial_control', True): + if not use_new_dymos_syntax: + control_type_string = 'polynomial_control_values' + + last_regular_phase = prob.regular_phases[-1] + prob.model.connect(f'traj.{last_regular_phase}.states:mass', + Mission.Landing.TOUCHDOWN_MASS, src_indices=[-1]) + prob.model.connect(f'traj.{last_regular_phase}.{control_type_string}:altitude', + Mission.Landing.INITIAL_ALTITUDE, + src_indices=[0]) + From 4670da35c4092dca68a03330ff449f06bdae7957 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 21 Jan 2025 16:56:08 -0500 Subject: [PATCH 07/38] A few more migrations. --- aviary/interface/methods_for_level2.py | 2 +- aviary/mission/problem_builder_2DOF.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 0239d0406..8a497dd01 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -900,7 +900,7 @@ def add_post_mission_systems(self, include_landing=True): """ if self.pre_mission_info['include_takeoff']: - self.builder.add_post_mission_takeoff_systems() + self.builder.add_post_mission_takeoff_systems(self) if include_landing and self.post_mission_info['include_landing']: self.builder.add_landing_systems(self) diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index b18bfe4c0..0c93eae33 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -1,3 +1,4 @@ +from aviary.interface.default_phase_info.two_dof_fiti import add_default_sgm_args from aviary.mission.gasp_based.ode.landing_ode import LandingSegment from aviary.mission.gasp_based.ode.taxi_ode import TaxiSegment from aviary.subsystems.propulsion.utils import build_engine_deck From 5443f9f745294152b8ce00ba23e88258c22b211a Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 21 Jan 2025 17:29:07 -0500 Subject: [PATCH 08/38] Need to add a builder for solved 2dof --- aviary/interface/methods_for_level2.py | 8 -------- aviary/mission/problem_builder_2DOF.py | 4 ++++ aviary/mission/problem_builder_height_energy.py | 9 +++++++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 8a497dd01..3f7c9830c 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -11,7 +11,6 @@ import numpy as np import dymos as dm -from dymos.transcriptions.transcription_base import TranscriptionBase from dymos.utils.misc import _unspecified import openmdao.api as om @@ -19,12 +18,10 @@ from openmdao.utils.mpi import MPI from openmdao.utils.reports_system import _default_reports -from aviary.constants import GRAV_ENGLISH_LBM, RHO_SEA_LEVEL_ENGLISH from aviary.interface.default_phase_info.two_dof_fiti import add_default_sgm_args from aviary.interface.utils.check_phase_info import check_phase_info from aviary.mission.energy_phase import EnergyPhase from aviary.mission.twodof_phase import TwoDOFPhase -from aviary.mission.gasp_based.idle_descent_estimation import add_descent_estimation_as_submodel from aviary.mission.gasp_based.ode.params import ParamPort from aviary.mission.gasp_based.phases.time_integration_traj import FlexibleTraj from aviary.mission.gasp_based.phases.groundroll_phase import GroundrollPhase @@ -71,11 +68,6 @@ SOLVED_2DOF = EquationsOfMotion.SOLVED_2DOF CUSTOM = EquationsOfMotion.CUSTOM -if hasattr(TranscriptionBase, 'setup_polynomial_controls'): - use_new_dymos_syntax = False -else: - use_new_dymos_syntax = True - class AviaryProblem(om.Problem): """ diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 0c93eae33..8eda9ecc8 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -1,4 +1,8 @@ +import openmdao.api as om + +from aviary.constants import GRAV_ENGLISH_LBM, RHO_SEA_LEVEL_ENGLISH from aviary.interface.default_phase_info.two_dof_fiti import add_default_sgm_args +from aviary.mission.gasp_based.idle_descent_estimation import add_descent_estimation_as_submodel from aviary.mission.gasp_based.ode.landing_ode import LandingSegment from aviary.mission.gasp_based.ode.taxi_ode import TaxiSegment from aviary.subsystems.propulsion.utils import build_engine_deck diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 4ac993b54..ac70961df 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -1,3 +1,7 @@ +import openmdao.api as om + +from dymos.transcriptions.transcription_base import TranscriptionBase + from aviary.mission.flops_based.phases.build_landing import Landing from aviary.mission.flops_based.phases.build_takeoff import Takeoff from aviary.subsystems.propulsion.utils import build_engine_deck @@ -7,6 +11,11 @@ from aviary.variable_info.enums import LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +if hasattr(TranscriptionBase, 'setup_polynomial_controls'): + use_new_dymos_syntax = False +else: + use_new_dymos_syntax = True + class AviaryProblemBuilder_HE(): """ From e7f38e50a8fbaa2c6a4ef8ec176645993fc8c21a Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 22 Jan 2025 15:24:26 -0500 Subject: [PATCH 09/38] Most of the obvious stuff moved into the problem builders. --- aviary/interface/methods_for_level2.py | 974 ++++-------------- aviary/mission/problem_builder_2DOF.py | 460 ++++++++- .../mission/problem_builder_height_energy.py | 292 +++++- aviary/mission/problem_builder_solved_2DOF.py | 197 ++++ 4 files changed, 1150 insertions(+), 773 deletions(-) create mode 100644 aviary/mission/problem_builder_solved_2DOF.py diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 3f7c9830c..0dc570e17 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -14,45 +14,34 @@ from dymos.utils.misc import _unspecified import openmdao.api as om -from openmdao.core.component import Component -from openmdao.utils.mpi import MPI from openmdao.utils.reports_system import _default_reports from aviary.interface.default_phase_info.two_dof_fiti import add_default_sgm_args from aviary.interface.utils.check_phase_info import check_phase_info -from aviary.mission.energy_phase import EnergyPhase -from aviary.mission.twodof_phase import TwoDOFPhase -from aviary.mission.gasp_based.ode.params import ParamPort from aviary.mission.gasp_based.phases.time_integration_traj import FlexibleTraj -from aviary.mission.gasp_based.phases.groundroll_phase import GroundrollPhase -from aviary.mission.flops_based.phases.groundroll_phase import GroundrollPhase as GroundrollPhaseVelocityIntegrated -from aviary.mission.gasp_based.phases.rotation_phase import RotationPhase -from aviary.mission.gasp_based.phases.climb_phase import ClimbPhase -from aviary.mission.gasp_based.phases.cruise_phase import CruisePhase -from aviary.mission.gasp_based.phases.accel_phase import AccelPhase -from aviary.mission.gasp_based.phases.ascent_phase import AscentPhase -from aviary.mission.gasp_based.phases.descent_phase import DescentPhase + from aviary.mission.gasp_based.phases.v_rotate_comp import VRotateComp from aviary.mission.gasp_based.polynomial_fit import PolynomialFit -from aviary.mission.phase_builder_base import PhaseBuilderBase -from aviary.mission.problem_builder_2DOF import AviaryProblemBuilder_2DOF -from aviary.mission.problem_builder_height_energy import AviaryProblemBuilder_HE +from aviary.mission.problem_builder_2DOF import ProblemBuilder2DOF +from aviary.mission.problem_builder_height_energy import ProblemBuilderHeightEnergy +from aviary.mission.problem_builder_solved_2DOF import ProblemBuilderSolved2DOF 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.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 create_opts2vals, add_opts2vals, wrapped_convert_units +from aviary.utils.functions import wrapped_convert_units from aviary.utils.functions import convert_strings_to_data, set_value from aviary.utils.merge_variable_metadata import merge_meta_data from aviary.utils.preprocessors import preprocess_options -from aviary.utils.process_input_decks import create_vehicle +from aviary.utils.process_input_decks import create_vehicle, update_GASP_options from aviary.variable_info.enums import AnalysisScheme, ProblemType, EquationsOfMotion, LegacyCode, Verbosity -from aviary.variable_info.functions import setup_trajectory_params, override_aviary_vars, setup_model_options +from aviary.variable_info.functions import setup_trajectory_params, setup_model_options from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData @@ -130,18 +119,21 @@ def load_inputs( ## LOAD INPUT FILE ### # Create AviaryValues object from file (or process existing AviaryValues object # with default values from metadata) and generate initial guesses - self.aviary_inputs, self.initialization_guesses = create_vehicle( + aviary_inputs, self.initialization_guesses = create_vehicle( aviary_inputs, meta_data=meta_data, verbosity=verbosity) # pull which methods will be used for subsystems and mission - self.mission_method = mission_method = self.aviary_inputs.get_val( + self.mission_method = mission_method = aviary_inputs.get_val( Settings.EQUATIONS_OF_MOTION) + self.mass_method = mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) # Determine which problem builder to use based on mission_method if mission_method is HEIGHT_ENERGY: - self.builder = AviaryProblemBuilder_HE() + self.builder = ProblemBuilderHeightEnergy() elif mission_method is TWO_DEGREES_OF_FREEDOM: - self.builder = AviaryProblemBuilder_2DOF() + self.builder = ProblemBuilder2DOF() + elif mission_method is SOLVED_2DOF: + self.builder = ProblemBuilderSolved2DOF() elif mission_method is CUSTOM: if problem_builder: self.builder = problem_builder() @@ -153,6 +145,10 @@ def load_inputs( raise ValueError( f'settings:equations_of_motion must be one of: height_energy, 2DOF, or custom') + # TODO this should be a preprocessor step if it is required here + if mission_method is TWO_DEGREES_OF_FREEDOM or mass_method is GASP: + aviary_inputs = update_GASP_options(aviary_inputs) + ## LOAD PHASE_INFO ### if phase_info is None: # check if the user generated a phase_info from gui @@ -202,9 +198,14 @@ def load_inputs( else: self.post_mission_info = None - self.problem_type = self.aviary_inputs.get_val(Settings.PROBLEM_TYPE) + if engine_builders is None: + engine_builders = build_engine_deck(aviary_inputs) + self.engine_builders = engine_builders + + self.problem_type = aviary_inputs.get_val(Settings.PROBLEM_TYPE) + self.aviary_inputs = aviary_inputs - self.builder.initial_guesses(self, engine_builders) + self.builder.initial_guesses(self) # This function sets all the following defaults if they were not already set # self.engine_builders, self.mass_method, self.pre_mission_info, self_post_mission_info # self.require_range_residual, self.target_range @@ -296,7 +297,8 @@ def check_and_preprocess_inputs(self): elif mission_method is TWO_DEGREES_OF_FREEDOM: everything_else_origin = GASP else: - raise ValueError(f'Unknown mission method {self.mission_method}') + # Custom + everything_else_origin = None prop = CorePropulsionBuilder( 'core_propulsion', engine_models=self.engine_builders) @@ -546,40 +548,7 @@ def _get_phase(self, phase_name, phase_idx): default_mission_subsystems = [ subsystems['aerodynamics'], subsystems['propulsion']] - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - if 'groundroll' in phase_name: - phase_builder = GroundrollPhase - elif 'rotation' in phase_name: - phase_builder = RotationPhase - elif 'accel' in phase_name: - phase_builder = AccelPhase - elif 'ascent' in phase_name: - phase_builder = AscentPhase - elif 'climb' in phase_name: - phase_builder = ClimbPhase - elif 'cruise' in phase_name: - phase_builder = CruisePhase - elif 'desc' in phase_name: - phase_builder = DescentPhase - else: - raise ValueError( - f'{phase_name} does not have an associated phase_builder \n phase_name must ' - 'include one of: groundroll, rotation, accel, ascent, climb, cruise, or desc') - - if self.mission_method is HEIGHT_ENERGY: - if 'phase_builder' in phase_options: - phase_builder = phase_options['phase_builder'] - if not issubclass(phase_builder, PhaseBuilderBase): - raise TypeError(f"phase_builder for the phase called " - "{phase_name} must be a PhaseBuilderBase object.") - else: - phase_builder = EnergyPhase - - if self.mission_method is SOLVED_2DOF: - if phase_options['user_options']['ground_roll'] and phase_options['user_options']['fix_initial']: - phase_builder = GroundrollPhaseVelocityIntegrated - else: - phase_builder = TwoDOFPhase + phase_builder = self.builder.get_phase_builder(self, phase_name, phase_options) phase_object = phase_builder.from_phase_info( phase_name, phase_options, default_mission_subsystems, @@ -611,115 +580,7 @@ def _get_phase(self, phase_name, phase_idx): user_options = AviaryValues(phase_options.get('user_options', ())) - try: - fix_initial = user_options.get_val('fix_initial') - except KeyError: - fix_initial = False - - try: - fix_duration = user_options.get_val('fix_duration') - except KeyError: - fix_duration = False - - if 'ascent' in phase_name and self.mission_method is TWO_DEGREES_OF_FREEDOM: - phase.set_time_options( - units="s", - targets="t_curr", - input_initial=True, - input_duration=True, - ) - elif 'cruise' in phase_name and self.mission_method is TWO_DEGREES_OF_FREEDOM: - # Time here is really the independent variable through which we are integrating. - # In the case of the Breguet Range ODE, it's mass. - # We rely on mass being monotonically non-increasing across the phase. - phase.set_time_options( - name='mass', - fix_initial=False, - fix_duration=False, - units="lbm", - targets="mass", - initial_bounds=(0., 1.e7), - initial_ref=100.e3, - duration_bounds=(-1.e7, -1), - duration_ref=50000, - ) - elif 'descent' in phase_name and self.mission_method is TWO_DEGREES_OF_FREEDOM: - duration_ref = user_options.get_val("duration_ref", 's') - phase.set_time_options( - duration_bounds=duration_bounds, - fix_initial=fix_initial, - input_initial=input_initial, - units="s", - duration_ref=duration_ref, - ) - else: - # The rest of the phases includes all Height Energy method phases - # and any 2DOF phases that don't fall into the naming patterns - # above. - input_initial = False - time_units = phase.time_options['units'] - - # Make a good guess for a reasonable intitial time scaler. - try: - initial_bounds = user_options.get_val('initial_bounds', units=time_units) - except KeyError: - initial_bounds = (None, None) - - if initial_bounds[0] is not None and initial_bounds[1] != 0.0: - # Upper bound is good for a ref. - user_options.set_val('initial_ref', initial_bounds[1], - units=time_units) - else: - user_options.set_val('initial_ref', 600., time_units) - - duration_bounds = user_options.get_val("duration_bounds", time_units) - user_options.set_val( - 'duration_ref', (duration_bounds[0] + duration_bounds[1]) / 2., - time_units - ) - if phase_idx > 0: - input_initial = True - - if fix_initial or input_initial: - - if self.comm.size > 1: - # Phases are disconnected to run in parallel, so initial ref is - # valid. - initial_ref = user_options.get_val("initial_ref", time_units) - else: - # Redundant on a fixed input; raises a warning if specified. - initial_ref = None - - phase.set_time_options( - fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, - duration_bounds=user_options.get_val("duration_bounds", time_units), - duration_ref=user_options.get_val("duration_ref", time_units), - initial_ref=initial_ref, - ) - elif phase_name == 'descent' and self.mission_method is HEIGHT_ENERGY: # TODO: generalize this logic for all phases - phase.set_time_options( - fix_initial=False, fix_duration=False, units=time_units, - duration_bounds=user_options.get_val("duration_bounds", time_units), - duration_ref=user_options.get_val("duration_ref", time_units), - initial_bounds=initial_bounds, - initial_ref=user_options.get_val("initial_ref", time_units), - ) - else: # TODO: figure out how to handle this now that fix_initial is dict - phase.set_time_options( - fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, - duration_bounds=user_options.get_val("duration_bounds", time_units), - duration_ref=user_options.get_val("duration_ref", time_units), - initial_bounds=initial_bounds, - initial_ref=user_options.get_val("initial_ref", time_units), - ) - - if 'cruise' not in phase_name and self.mission_method is TWO_DEGREES_OF_FREEDOM: - phase.add_control( - Dynamic.Vehicle.Propulsion.THROTTLE, - targets=Dynamic.Vehicle.Propulsion.THROTTLE, - units='unitless', - opt=False, - ) + self.builder.set_phase_options(self, phase_name, phase_idx, phase, user_options) return phase @@ -823,49 +684,48 @@ def add_subsystem_timeseries_outputs(phase, phase_name): for timeseries in timeseries_to_add: phase.add_timeseries_output(timeseries) - if self.mission_method in (TWO_DEGREES_OF_FREEDOM, HEIGHT_ENERGY, SOLVED_2DOF): - if self.analysis_scheme is AnalysisScheme.COLLOCATION: - self.phase_objects = [] - for phase_idx, phase_name in enumerate(phases): - phase = traj.add_phase( - phase_name, self._get_phase(phase_name, phase_idx)) - add_subsystem_timeseries_outputs(phase, phase_name) - - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - - # In GASP, we still use the phase name to infer the phase type. - # We need this information to be available in the builders. - # TODO - Ultimately we should overhaul all of this. - self.phase_info[phase_name]['phase_type'] = phase_name - - if phase_name == 'ascent': - self._add_groundroll_eq_constraint() - - # loop through phase_info and external subsystems - external_parameters = {} - for phase_name in self.phase_info: - external_parameters[phase_name] = {} - all_subsystems = self._get_all_subsystems( - self.phase_info[phase_name]['external_subsystems']) - for subsystem in all_subsystems: - parameter_dict = subsystem.get_parameters( - phase_info=self.phase_info[phase_name], - aviary_inputs=self.aviary_inputs - ) - for parameter in parameter_dict: - external_parameters[phase_name][parameter] = parameter_dict[parameter] + if self.analysis_scheme is AnalysisScheme.COLLOCATION: + self.phase_objects = [] + for phase_idx, phase_name in enumerate(phases): + phase = traj.add_phase( + phase_name, self._get_phase(phase_name, phase_idx)) + add_subsystem_timeseries_outputs(phase, phase_name) - traj = setup_trajectory_params( - self.model, traj, self.aviary_inputs, phases, meta_data=self.meta_data, - external_parameters=external_parameters) + if self.mission_method is TWO_DEGREES_OF_FREEDOM: - if self.mission_method is HEIGHT_ENERGY: - if not self.pre_mission_info['include_takeoff']: - first_flight_phase_name = list(phase_info.keys())[0] - first_flight_phase = traj._phases[first_flight_phase_name] - first_flight_phase.set_state_options( - Dynamic.Vehicle.MASS, fix_initial=False - ) + # In GASP, we still use the phase name to infer the phase type. + # We need this information to be available in the builders. + # TODO - Ultimately we should overhaul all of this. + self.phase_info[phase_name]['phase_type'] = phase_name + + if phase_name == 'ascent': + self._add_groundroll_eq_constraint() + + # loop through phase_info and external subsystems + external_parameters = {} + for phase_name in self.phase_info: + external_parameters[phase_name] = {} + all_subsystems = self._get_all_subsystems( + self.phase_info[phase_name]['external_subsystems']) + for subsystem in all_subsystems: + parameter_dict = subsystem.get_parameters( + phase_info=self.phase_info[phase_name], + aviary_inputs=self.aviary_inputs + ) + for parameter in parameter_dict: + external_parameters[phase_name][parameter] = parameter_dict[parameter] + + traj = setup_trajectory_params( + self.model, traj, self.aviary_inputs, phases, meta_data=self.meta_data, + external_parameters=external_parameters) + + if self.mission_method is HEIGHT_ENERGY: + if not self.pre_mission_info['include_takeoff']: + first_flight_phase_name = list(phase_info.keys())[0] + first_flight_phase = traj._phases[first_flight_phase_name] + first_flight_phase.set_state_options( + Dynamic.Vehicle.MASS, fix_initial=False + ) self.traj = traj @@ -901,9 +761,8 @@ def add_post_mission_systems(self, include_landing=True): promotes_inputs=['*'], promotes_outputs=['*']) - # Loop through all the phases in this subsystem. + # Add all post-mission external subsystems. for external_subsystem in self.post_mission_info['external_subsystems']: - # Get all the subsystem builders for this phase. subsystem_postmission = external_subsystem.build_post_mission( self.aviary_inputs) @@ -911,149 +770,148 @@ def add_post_mission_systems(self, include_landing=True): self.post_mission.add_subsystem(external_subsystem.name, subsystem_postmission) - if self.mission_method in (HEIGHT_ENERGY, SOLVED_2DOF, TWO_DEGREES_OF_FREEDOM): - # Check if regular_phases[] is accessible - try: - self.regular_phases[0] - except BaseException: - raise ValueError( - f"regular_phases[] dictionary is not accessible." - f" For HEIGHT_ENERGY and SOLVED_2DOF missions, check_and_preprocess_inputs()" - f" must be called before add_post_mission_systems().") + # Check if regular_phases[] is accessible + try: + self.regular_phases[0] + except BaseException: + raise ValueError( + f"regular_phases[] dictionary is not accessible." + f" For HEIGHT_ENERGY and SOLVED_2DOF missions, check_and_preprocess_inputs()" + f" must be called before add_post_mission_systems().") + + # Fuel burn in regular phases + ecomp = om.ExecComp('fuel_burned = initial_mass - mass_final', + initial_mass={'units': 'lbm'}, + mass_final={'units': 'lbm'}, + fuel_burned={'units': 'lbm'}) + + self.post_mission.add_subsystem( + 'fuel_burned', ecomp, + promotes=[('fuel_burned', Mission.Summary.FUEL_BURNED)]) + + if self.analysis_scheme is AnalysisScheme.SHOOTING: + # shooting method currently doesn't have timeseries + self.post_mission.promotes('fuel_burned', [ + ('initial_mass', Mission.Summary.GROSS_MASS), + ('mass_final', Mission.Landing.TOUCHDOWN_MASS), + ]) + else: + if self.pre_mission_info['include_takeoff']: + self.post_mission.promotes('fuel_burned', [ + ('initial_mass', Mission.Summary.GROSS_MASS), + ]) + else: + # timeseries has to be used because Breguet cruise phases don't have + # states + self.model.connect(f"traj.{self.regular_phases[0]}.timeseries.mass", + "fuel_burned.initial_mass", src_indices=[0]) + + self.model.connect(f"traj.{self.regular_phases[-1]}.timeseries.mass", + "fuel_burned.mass_final", src_indices=[-1]) - # Fuel burn in regular phases - ecomp = om.ExecComp('fuel_burned = initial_mass - mass_final', + # Fuel burn in reserve phases + if self.reserve_phases: + ecomp = om.ExecComp('reserve_fuel_burned = initial_mass - mass_final', initial_mass={'units': 'lbm'}, mass_final={'units': 'lbm'}, - fuel_burned={'units': 'lbm'}) + reserve_fuel_burned={'units': 'lbm'}) self.post_mission.add_subsystem( - 'fuel_burned', ecomp, - promotes=[('fuel_burned', Mission.Summary.FUEL_BURNED)]) + 'reserve_fuel_burned', ecomp, promotes=[ + ('reserve_fuel_burned', Mission.Summary.RESERVE_FUEL_BURNED)]) if self.analysis_scheme is AnalysisScheme.SHOOTING: # shooting method currently doesn't have timeseries - self.post_mission.promotes('fuel_burned', [ - ('initial_mass', Mission.Summary.GROSS_MASS), - ('mass_final', Mission.Landing.TOUCHDOWN_MASS), + self.post_mission.promotes('reserve_fuel_burned', [ + ('initial_mass', Mission.Landing.TOUCHDOWN_MASS), ]) + self.model.connect( + f"traj.{self.reserve_phases[-1]}.states:mass", + "reserve_fuel_burned.mass_final", src_indices=[-1]) else: - if self.pre_mission_info['include_takeoff']: - self.post_mission.promotes('fuel_burned', [ - ('initial_mass', Mission.Summary.GROSS_MASS), - ]) - else: - # timeseries has to be used because Breguet cruise phases don't have - # states - self.model.connect(f"traj.{self.regular_phases[0]}.timeseries.mass", - "fuel_burned.initial_mass", src_indices=[0]) - - self.model.connect(f"traj.{self.regular_phases[-1]}.timeseries.mass", - "fuel_burned.mass_final", src_indices=[-1]) - - # Fuel burn in reserve phases - if self.reserve_phases: - ecomp = om.ExecComp('reserve_fuel_burned = initial_mass - mass_final', - initial_mass={'units': 'lbm'}, - mass_final={'units': 'lbm'}, - reserve_fuel_burned={'units': 'lbm'}) + # timeseries has to be used because Breguet cruise phases don't have + # states + self.model.connect( + f"traj.{self.reserve_phases[0]}.timeseries.mass", + "reserve_fuel_burned.initial_mass", src_indices=[0]) + self.model.connect( + f"traj.{self.reserve_phases[-1]}.timeseries.mass", + "reserve_fuel_burned.mass_final", src_indices=[-1]) + + self._add_fuel_reserve_component() + + # TODO: need to add some sort of check that this value is less than the fuel capacity + # TODO: the overall_fuel variable is the burned fuel plus the reserve, but should + # also include the unused fuel, and the hierarchy variable name should be + # more clear + ecomp = om.ExecComp('overall_fuel = (1 + fuel_margin/100)*fuel_burned + reserve_fuel', + overall_fuel={'units': 'lbm', 'shape': 1}, + fuel_margin={"units": "unitless", 'val': 0}, + fuel_burned={'units': 'lbm'}, # from regular_phases only + reserve_fuel={'units': 'lbm', 'shape': 1}, + ) + self.post_mission.add_subsystem( + 'fuel_calc', ecomp, + promotes_inputs=[ + ("fuel_margin", Aircraft.Fuel.FUEL_MARGIN), + ('fuel_burned', Mission.Summary.FUEL_BURNED), + ("reserve_fuel", Mission.Design.RESERVE_FUEL), + ], + promotes_outputs=[('overall_fuel', Mission.Summary.TOTAL_FUEL_MASS)]) + # If a target distance (or time) has been specified for this phase + # distance (or time) is measured from the start of this phase to the end + # of this phase + for phase_name in self.phase_info: + if 'target_distance' in self.phase_info[phase_name]["user_options"]: + target_distance = wrapped_convert_units( + self.phase_info[phase_name]["user_options"] + ["target_distance"], + 'nmi') self.post_mission.add_subsystem( - 'reserve_fuel_burned', ecomp, promotes=[ - ('reserve_fuel_burned', Mission.Summary.RESERVE_FUEL_BURNED)]) - - if self.analysis_scheme is AnalysisScheme.SHOOTING: - # shooting method currently doesn't have timeseries - self.post_mission.promotes('reserve_fuel_burned', [ - ('initial_mass', Mission.Landing.TOUCHDOWN_MASS), - ]) - self.model.connect( - f"traj.{self.reserve_phases[-1]}.states:mass", - "reserve_fuel_burned.mass_final", src_indices=[-1]) - else: - # timeseries has to be used because Breguet cruise phases don't have - # states - self.model.connect( - f"traj.{self.reserve_phases[0]}.timeseries.mass", - "reserve_fuel_burned.initial_mass", src_indices=[0]) - self.model.connect( - f"traj.{self.reserve_phases[-1]}.timeseries.mass", - "reserve_fuel_burned.mass_final", src_indices=[-1]) - - self._add_fuel_reserve_component() - - # TODO: need to add some sort of check that this value is less than the fuel capacity - # TODO: the overall_fuel variable is the burned fuel plus the reserve, but should - # also include the unused fuel, and the hierarchy variable name should be - # more clear - ecomp = om.ExecComp('overall_fuel = (1 + fuel_margin/100)*fuel_burned + reserve_fuel', - overall_fuel={'units': 'lbm', 'shape': 1}, - fuel_margin={"units": "unitless", 'val': 0}, - fuel_burned={'units': 'lbm'}, # from regular_phases only - reserve_fuel={'units': 'lbm', 'shape': 1}, - ) - self.post_mission.add_subsystem( - 'fuel_calc', ecomp, - promotes_inputs=[ - ("fuel_margin", Aircraft.Fuel.FUEL_MARGIN), - ('fuel_burned', Mission.Summary.FUEL_BURNED), - ("reserve_fuel", Mission.Design.RESERVE_FUEL), - ], - promotes_outputs=[('overall_fuel', Mission.Summary.TOTAL_FUEL_MASS)]) + f"{phase_name}_distance_constraint", om.ExecComp( + "distance_resid = target_distance - (final_distance - initial_distance)", + distance_resid={'units': 'nmi'}, + target_distance={'val': target_distance, 'units': 'nmi'}, + final_distance={'units': 'nmi'}, + initial_distance={'units': 'nmi'},)) + self.model.connect( + f"traj.{phase_name}.timeseries.distance", + f"{phase_name}_distance_constraint.final_distance", + src_indices=[-1]) + self.model.connect( + f"traj.{phase_name}.timeseries.distance", + f"{phase_name}_distance_constraint.initial_distance", + src_indices=[0]) + self.model.add_constraint( + f"{phase_name}_distance_constraint.distance_resid", equals=0.0, ref=1e2) - # If a target distance (or time) has been specified for this phase - # distance (or time) is measured from the start of this phase to the end - # of this phase - for phase_name in self.phase_info: - if 'target_distance' in self.phase_info[phase_name]["user_options"]: - target_distance = wrapped_convert_units( - self.phase_info[phase_name]["user_options"] - ["target_distance"], - 'nmi') - self.post_mission.add_subsystem( - f"{phase_name}_distance_constraint", om.ExecComp( - "distance_resid = target_distance - (final_distance - initial_distance)", - distance_resid={'units': 'nmi'}, - target_distance={'val': target_distance, 'units': 'nmi'}, - final_distance={'units': 'nmi'}, - initial_distance={'units': 'nmi'},)) - self.model.connect( - f"traj.{phase_name}.timeseries.distance", - f"{phase_name}_distance_constraint.final_distance", - src_indices=[-1]) - self.model.connect( - f"traj.{phase_name}.timeseries.distance", - f"{phase_name}_distance_constraint.initial_distance", - src_indices=[0]) - self.model.add_constraint( - f"{phase_name}_distance_constraint.distance_resid", equals=0.0, ref=1e2) - - # this is only used for analytic phases with a target duration - if 'target_duration' in self.phase_info[phase_name]["user_options"] and \ - self.phase_info[phase_name]["user_options"].get("analytic", False): - target_duration = wrapped_convert_units( - self.phase_info[phase_name]["user_options"] - ["target_duration"], - 'min') - self.post_mission.add_subsystem( - f"{phase_name}_duration_constraint", om.ExecComp( - "duration_resid = target_duration - (final_time - initial_time)", - duration_resid={'units': 'min'}, - target_duration={'val': target_duration, 'units': 'min'}, - final_time={'units': 'min'}, - initial_time={'units': 'min'},)) - self.model.connect( - f"traj.{phase_name}.timeseries.time", - f"{phase_name}_duration_constraint.final_time", src_indices=[-1]) - self.model.connect( - f"traj.{phase_name}.timeseries.time", - f"{phase_name}_duration_constraint.initial_time", - src_indices=[0]) - self.model.add_constraint( - f"{phase_name}_duration_constraint.duration_resid", equals=0.0, ref=1e2) + # this is only used for analytic phases with a target duration + if 'target_duration' in self.phase_info[phase_name]["user_options"] and \ + self.phase_info[phase_name]["user_options"].get("analytic", False): + target_duration = wrapped_convert_units( + self.phase_info[phase_name]["user_options"] + ["target_duration"], + 'min') + self.post_mission.add_subsystem( + f"{phase_name}_duration_constraint", om.ExecComp( + "duration_resid = target_duration - (final_time - initial_time)", + duration_resid={'units': 'min'}, + target_duration={'val': target_duration, 'units': 'min'}, + final_time={'units': 'min'}, + initial_time={'units': 'min'},)) + self.model.connect( + f"traj.{phase_name}.timeseries.time", + f"{phase_name}_duration_constraint.final_time", src_indices=[-1]) + self.model.connect( + f"traj.{phase_name}.timeseries.time", + f"{phase_name}_duration_constraint.initial_time", + src_indices=[0]) + self.model.add_constraint( + f"{phase_name}_duration_constraint.duration_resid", equals=0.0, ref=1e2) if self.mission_method in (TWO_DEGREES_OF_FREEDOM, HEIGHT_ENERGY): - self._add_objectives() + self._add_comps_for_objs_and_cons() ecomp = om.ExecComp( 'mass_resid = operating_empty_mass + overall_fuel + payload_mass -' @@ -1190,202 +1048,7 @@ def link_phases(self): if len(phases_to_link) > 1: # TODO: hack self.traj.link_phases(phases=phases_to_link, vars=[var], connected=True) - if self.mission_method in (HEIGHT_ENERGY, SOLVED_2DOF): - # connect regular_phases with each other if you are optimizing alt or mach - self._link_phases_helper_with_options( - self.regular_phases, - 'optimize_altitude', - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self._link_phases_helper_with_options( - self.regular_phases, 'optimize_mach', Dynamic.Atmosphere.MACH - ) - - # connect reserve phases with each other if you are optimizing alt or mach - self._link_phases_helper_with_options( - self.reserve_phases, - 'optimize_altitude', - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self._link_phases_helper_with_options( - self.reserve_phases, 'optimize_mach', Dynamic.Atmosphere.MACH - ) - - if self.mission_method is HEIGHT_ENERGY: - # connect mass and distance between all phases regardless of reserve / - # non-reserve status - self.traj.link_phases(phases, ["time"], - ref=None if true_unless_mpi else 1e3, - connected=true_unless_mpi) - self.traj.link_phases(phases, [Dynamic.Vehicle.MASS], - ref=None if true_unless_mpi else 1e6, - connected=true_unless_mpi) - self.traj.link_phases(phases, [Dynamic.Mission.DISTANCE], - ref=None if true_unless_mpi else 1e3, - connected=true_unless_mpi) - - self.model.connect(f'traj.{self.regular_phases[-1]}.timeseries.distance', - 'actual_range', - src_indices=[-1], flat_src_indices=True) - - elif self.mission_method is SOLVED_2DOF: - self.traj.link_phases(phases, [Dynamic.Vehicle.MASS], connected=True) - self.traj.link_phases( - phases, [Dynamic.Mission.DISTANCE], - units='ft', ref=1.e3, connected=False) - self.traj.link_phases(phases, ["time"], connected=False) - - if len(phases) > 2: - self.traj.link_phases( - phases[1:], ["alpha"], units='rad', connected=False) - - elif self.mission_method is TWO_DEGREES_OF_FREEDOM: - if self.analysis_scheme is AnalysisScheme.COLLOCATION: - for ii in range(len(phases) - 1): - phase1, phase2 = phases[ii:ii + 2] - analytic1 = self.phase_info[phase1]['user_options']['analytic'] - analytic2 = self.phase_info[phase2]['user_options']['analytic'] - - if not (analytic1 or analytic2): - # we always want time, distance, and mass to be continuous - states_to_link = { - 'time': true_unless_mpi, - Dynamic.Mission.DISTANCE: true_unless_mpi, - Dynamic.Vehicle.MASS: False, - } - - # if both phases are reserve phases or neither is a reserve phase - # (we are not on the boundary between the regular and reserve missions) - # and neither phase is ground roll or rotation (altitude isn't a state): - # we want altitude to be continous as well - if ((phase1 in self.reserve_phases) == (phase2 in self.reserve_phases)) and \ - not ({"groundroll", "rotation"} & {phase1, phase2}) and \ - not ('accel', 'climb1') == (phase1, phase2): # required for convergence of FwGm - states_to_link[Dynamic.Mission.ALTITUDE] = true_unless_mpi - - # if either phase is rotation, we need to connect velocity - # ascent to accel also requires velocity - if 'rotation' in ( - phase1, phase2) or ( - 'ascent', 'accel') == ( - phase1, phase2): - states_to_link[Dynamic.Mission.VELOCITY] = true_unless_mpi - # if the first phase is rotation, we also need alpha - if phase1 == 'rotation': - states_to_link['alpha'] = False - - for state, connected in states_to_link.items(): - # in initial guesses, all of the states, other than time use - # the same name - initial_guesses1 = self.phase_info[phase1]['initial_guesses'] - initial_guesses2 = self.phase_info[phase2]['initial_guesses'] - - # if a state is in the initial guesses, get the units of the - # initial guess - kwargs = {} - if not connected: - if state in initial_guesses1: - kwargs = {'units': initial_guesses1[state][-1]} - elif state in initial_guesses2: - kwargs = {'units': initial_guesses2[state][-1]} - - self.traj.link_phases( - [phase1, phase2], [state], connected=connected, **kwargs) - - # if either phase is analytic we have to use a linkage_constraint - else: - # analytic phases use the prefix "initial" for time and distance, - # but not mass - if analytic2: - prefix = 'initial_' - else: - prefix = '' - - self.traj.add_linkage_constraint( - phase1, phase2, 'time', prefix + 'time', connected=True) - self.traj.add_linkage_constraint( - phase1, phase2, 'distance', prefix + 'distance', - connected=True) - self.traj.add_linkage_constraint( - phase1, phase2, 'mass', 'mass', connected=False, ref=1.0e5) - - # add all params and promote them to self.model level - ParamPort.promote_params( - self.model, - trajs=["traj"], - phases=[ - [*self.regular_phases, - *self.reserve_phases] - ], - ) - - self.model.promotes( - "traj", - inputs=[ - ("ascent.parameters:t_init_gear", "t_init_gear"), - ("ascent.parameters:t_init_flaps", "t_init_flaps"), - ("ascent.t_initial", Mission.Takeoff.ASCENT_T_INTIIAL), - ("ascent.t_duration", Mission.Takeoff.ASCENT_DURATION), - ], - ) - - # imitate input_initial for taxi -> groundroll - eq = self.model.add_subsystem( - "taxi_groundroll_mass_constraint", om.EQConstraintComp()) - eq.add_eq_output("mass", eq_units="lbm", normalize=False, - ref=10000., add_constraint=True) - self.model.connect( - "taxi.mass", "taxi_groundroll_mass_constraint.rhs:mass") - self.model.connect( - "traj.groundroll.states:mass", - "taxi_groundroll_mass_constraint.lhs:mass", - src_indices=[0], - flat_src_indices=True, - ) - - self.model.connect("traj.ascent.timeseries.time", "h_fit.time_cp") - self.model.connect( - "traj.ascent.timeseries.altitude", "h_fit.h_cp") - - self.model.connect(f'traj.{self.regular_phases[-1]}.states:mass', - Mission.Landing.TOUCHDOWN_MASS, src_indices=[-1]) - - connect_map = { - f"traj.{self.regular_phases[-1]}.timeseries.distance": 'actual_range', - } - - else: - connect_map = { - "taxi.mass": "traj.mass_initial", - Mission.Takeoff.ROTATION_VELOCITY: "traj.SGMGroundroll_velocity_trigger", - "traj.distance_final": 'actual_range', - "traj.mass_final": Mission.Landing.TOUCHDOWN_MASS, - } - - # promote all ParamPort inputs for analytic segments as well - param_list = list(ParamPort.param_data) - self.model.promotes("taxi", inputs=param_list) - self.model.promotes("landing", inputs=param_list) - if self.analysis_scheme is AnalysisScheme.SHOOTING: - param_list.append(Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE) - self.model.promotes("traj", inputs=param_list) - # self.model.list_inputs() - # self.model.promotes("traj", inputs=['ascent.ODE_group.eoms.'+Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE]) - - self.model.connect("taxi.mass", "vrot.mass") - - def connect_with_common_params(self, source, target): - self.model.connect( - source, - target, - src_indices=[-1], - flat_src_indices=True, - ) - - for source, target in connect_map.items(): - connect_with_common_params(self, source, target) + self.builder.link_phases(self, phases, direct_links=true_unless_mpi) def add_driver( self, optimizer=None, use_coloring=None, max_iter=50, @@ -1449,6 +1112,7 @@ def add_driver( driver.opt_settings["Major feasibility tolerance"] = 1e-7 driver.opt_settings["iSumm"] = isumm driver.opt_settings["iPrint"] = iprint + elif driver.options["optimizer"] == "IPOPT": if verbosity == Verbosity.QUIET: print_level = 3 # minimum to get exit status @@ -1469,6 +1133,7 @@ def add_driver( driver.opt_settings['nlp_scaling_method'] = 'gradient-based' driver.opt_settings['alpha_for_y'] = 'safer-min-dual-infeas' driver.opt_settings['mu_strategy'] = 'monotone' + elif driver.options["optimizer"] == "SLSQP": if verbosity == Verbosity.QUIET: disp = False @@ -1684,6 +1349,7 @@ def add_objective(self, objective_type=None, ref=None): ref = ref if ref is not None else default_ref_values.get(objective_type, 1) final_phase_name = self.regular_phases[-1] + if objective_type == 'mass': if self.analysis_scheme is AnalysisScheme.COLLOCATION: self.model.add_objective( @@ -1695,16 +1361,21 @@ def add_objective(self, objective_type=None, ref=None): last_phase = self.traj._phases.items()[final_phase_name] last_phase.add_objective( Dynamic.Vehicle.MASS, loc='final', ref=ref) + elif objective_type == 'time': self.model.add_objective( f"traj.{final_phase_name}.timeseries.time", index=-1, ref=ref) + elif objective_type == "hybrid_objective": self._add_hybrid_objective(self.phase_info) self.model.add_objective("obj_comp.obj") + elif objective_type == "fuel_burned": self.model.add_objective(Mission.Summary.FUEL_BURNED, ref=ref) + elif objective_type == "fuel": self.model.add_objective(Mission.Objectives.FUEL, ref=ref) + else: raise ValueError( f"{objective_type} is not a valid objective.\nobjective_type must" @@ -1716,10 +1387,13 @@ def add_objective(self, objective_type=None, ref=None): if self.problem_type is ProblemType.SIZING: self.model.add_objective(Mission.Objectives.FUEL, ref=ref) + elif self.problem_type is ProblemType.ALTERNATE: self.model.add_objective(Mission.Objectives.FUEL, ref=ref) + elif self.problem_type is ProblemType.FALLOUT: self.model.add_objective(Mission.Objectives.RANGE, ref=ref) + else: raise ValueError(f'{self.problem_type} is not a valid problem type.') @@ -1818,16 +1492,17 @@ def set_initial_guesses(self, parent_prob=None, parent_prefix=""): guesses for states and controls according to the information available in the 'initial_guesses' attribute of the phase. """ - setvalprob = self + target_prob = self if parent_prob is not None and parent_prefix != "": - setvalprob = parent_prob + target_prob = parent_prob + # Grab the trajectory object from the model if self.analysis_scheme is AnalysisScheme.SHOOTING: if self.problem_type is ProblemType.SIZING: - setvalprob.set_val(parent_prefix + Mission.Summary.GROSS_MASS, + target_prob.set_val(parent_prefix + Mission.Summary.GROSS_MASS, self.get_val(Mission.Design.GROSS_MASS)) - setvalprob.set_val(parent_prefix + + target_prob.set_val(parent_prefix + "traj.SGMClimb_" + Dynamic.Mission.ALTITUDE + "_trigger", val=self.cruise_alt, units="ft", @@ -1843,6 +1518,7 @@ def set_initial_guesses(self, parent_prob=None, parent_prefix=""): # Loop over each phase and set initial guesses for the state and control # variables for idx, (phase_name, phase) in enumerate(phase_items): + if self.mission_method is SOLVED_2DOF: self.phase_objects[idx].apply_initial_guesses(self, 'traj', phase) if self.phase_info[phase_name]['user_options']['ground_roll'] and self.phase_info[phase_name]['user_options']['fix_initial']: @@ -1863,12 +1539,12 @@ def set_initial_guesses(self, parent_prob=None, parent_prefix=""): if 'mass' == guess_key: # Set initial and duration mass for the analytic cruise phase. # Note we are integrating over mass, not time for this phase. - setvalprob.set_val( + target_prob.set_val( parent_prefix + f'traj.{phase_name}.t_initial', val[0], units=units) - setvalprob.set_val( + target_prob.set_val( parent_prefix + f'traj.{phase_name}.t_duration', val[1], @@ -1877,17 +1553,18 @@ def set_initial_guesses(self, parent_prob=None, parent_prefix=""): else: # Otherwise, set the value of the parameter in the trajectory # phase - setvalprob.set_val( + target_prob.set_val( parent_prefix + f'traj.{phase_name}.parameters:{guess_key}', val, units=units) continue # If not cruise and GASP, add subsystem guesses - self._add_subsystem_guesses(phase_name, phase, setvalprob, parent_prefix) + self._add_subsystem_guesses(phase_name, phase, target_prob, parent_prefix) # Set initial guesses for states and controls for each phase - self._add_guesses(phase_name, phase, guesses, setvalprob, parent_prefix) + self.builder.add_guesses(self, phase_name, phase, guesses, target_prob, + parent_prefix) def _process_guess_var(self, val, key, phase): """ @@ -1939,7 +1616,7 @@ def _process_guess_var(self, val, key, phase): # Return the processed guess value(s) return val - def _add_subsystem_guesses(self, phase_name, phase, setvalprob, parent_prefix): + def _add_subsystem_guesses(self, phase_name, phase, target_prob, parent_prefix): """ Adds the initial guesses for each subsystem of a given phase to the problem. This method first fetches all subsystems associated with the given phase. @@ -1977,210 +1654,9 @@ def _add_subsystem_guesses(self, phase_name, phase, setvalprob, parent_prefix): val['val'] = self._process_guess_var(val['val'], key, phase) # Set the initial guess in the problem - setvalprob.set_val( + target_prob.set_val( parent_prefix + f'traj.{phase_name}.{path_string}:{key}', **val) - def _add_guesses(self, phase_name, phase, guesses, setvalprob, parent_prefix): - """ - Adds the initial guesses for each variable of a given phase to the problem. - This method sets the initial guesses for time, control, state, and problem-specific - variables for a given phase. If using the GASP model, it also handles some special - cases that are not covered in the `phase_info` object. These include initial guesses - for mass, time, and distance, which are determined based on the phase name and other - mission-related variables. - Parameters - ---------- - phase_name : str - The name of the phase for which the guesses are being added. - phase : Phase - The phase object for which the guesses are being added. - guesses : dict - A dictionary containing the initial guesses for the phase. - """ - # If using the GASP model, set initial guesses for the rotation mass and - # flight duration - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - rotation_mass = self.initialization_guesses['rotation_mass'] - flight_duration = self.initialization_guesses['flight_duration'] - - if self.mission_method in (HEIGHT_ENERGY, SOLVED_2DOF): - control_keys = ["mach", "altitude"] - state_keys = ["mass", Dynamic.Mission.DISTANCE] - else: - control_keys = ["velocity_rate", "throttle"] - state_keys = [ - "altitude", - "mass", - Dynamic.Mission.DISTANCE, - Dynamic.Mission.VELOCITY, - "flight_path_angle", - "alpha", - ] - if self.mission_method is TWO_DEGREES_OF_FREEDOM and phase_name == 'ascent': - # Alpha is a control for ascent. - control_keys.append('alpha') - - prob_keys = ["tau_gear", "tau_flaps"] - - # for the simple mission method, use the provided initial and final mach - # and altitude values from phase_info - if self.mission_method in (HEIGHT_ENERGY, SOLVED_2DOF): - initial_altitude = wrapped_convert_units( - self.phase_info[phase_name]['user_options']['initial_altitude'], 'ft') - final_altitude = wrapped_convert_units( - self.phase_info[phase_name]['user_options']['final_altitude'], 'ft') - initial_mach = self.phase_info[phase_name]['user_options']['initial_mach'] - final_mach = self.phase_info[phase_name]['user_options']['final_mach'] - - guesses["mach"] = ([initial_mach[0], final_mach[0]], "unitless") - guesses["altitude"] = ([initial_altitude, final_altitude], 'ft') - - if self.mission_method is HEIGHT_ENERGY: - # if time not in initial guesses, set it to the average of the - # initial_bounds and the duration_bounds - if 'time' not in guesses: - initial_bounds = wrapped_convert_units( - self.phase_info[phase_name]['user_options']['initial_bounds'], 's') - duration_bounds = wrapped_convert_units( - self.phase_info[phase_name]['user_options']['duration_bounds'], 's') - guesses["time"] = ([np.mean(initial_bounds[0]), np.mean( - duration_bounds[0])], 's') - - # if time not in initial guesses, set it to the average of the - # initial_bounds and the duration_bounds - if 'time' not in guesses: - initial_bounds = self.phase_info[phase_name]['user_options'][ - 'initial_bounds'] - duration_bounds = self.phase_info[phase_name]['user_options'][ - 'duration_bounds'] - # Add a check for the initial and duration bounds, raise an error if they - # are not consistent - if initial_bounds[1] != duration_bounds[1]: - raise ValueError( - f"Initial and duration bounds for {phase_name} " - "are not consistent." - ) - guesses["time"] = ([np.mean(initial_bounds[0]), np.mean( - duration_bounds[0])], initial_bounds[1]) - - for guess_key, guess_data in guesses.items(): - val, units = guess_data - - # Set initial guess for time variables - if 'time' == guess_key and self.mission_method is not SOLVED_2DOF: - setvalprob.set_val(parent_prefix + f'traj.{phase_name}.t_initial', - val[0], units=units) - setvalprob.set_val(parent_prefix + f'traj.{phase_name}.t_duration', - val[1], units=units) - - else: - # Set initial guess for control variables - if guess_key in control_keys: - try: - setvalprob.set_val( - parent_prefix + f'traj.{phase_name}.controls:{guess_key}', - self._process_guess_var(val, guess_key, phase), - units=units) - except KeyError: - try: - setvalprob.set_val( - parent_prefix + - f'traj.{phase_name}.polynomial_controls:{guess_key}', - self._process_guess_var(val, guess_key, phase), - units=units) - except KeyError: - setvalprob.set_val(parent_prefix + - f'traj.{phase_name}.bspline_controls:', - {guess_key}, - self._process_guess_var( - val, guess_key, phase), - units=units) - - if self.mission_method is SOLVED_2DOF: - continue - - if guess_key in control_keys: - pass - # Set initial guess for state variables - elif guess_key in state_keys: - setvalprob.set_val(parent_prefix + - f'traj.{phase_name}.states:{guess_key}', self. - _process_guess_var(val, guess_key, phase), - units=units) - elif guess_key in prob_keys: - setvalprob.set_val(parent_prefix + guess_key, val, units=units) - elif ":" in guess_key: - setvalprob.set_val( - parent_prefix + - f'traj.{phase_name}.{guess_key}', - self._process_guess_var( - val, - guess_key, - phase), - units=units) - else: - # raise error if the guess key is not recognized - raise ValueError( - f"Initial guess key {guess_key} in {phase_name} is not " - "recognized." - ) - - if self.mission_method is SOLVED_2DOF: - return - - # We need some special logic for these following variables because GASP computes - # initial guesses using some knowledge of the mission duration and other variables - # that are only available after calling `create_vehicle`. Thus these initial guess - # values are not included in the `phase_info` object. - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - base_phase = phase_name.removeprefix('reserve_') - else: - base_phase = phase_name - if 'mass' not in guesses: - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - # Determine a mass guess depending on the phase name - if base_phase in ["groundroll", "rotation", "ascent", "accel", "climb1"]: - mass_guess = rotation_mass - elif base_phase == "climb2": - mass_guess = 0.99 * rotation_mass - elif "desc" in base_phase: - mass_guess = 0.9 * self.cruise_mass_final - else: - mass_guess = self.aviary_inputs.get_val( - Mission.Design.GROSS_MASS, units='lbm') - # Set the mass guess as the initial value for the mass state variable - setvalprob.set_val(parent_prefix + f'traj.{phase_name}.states:mass', - mass_guess, units='lbm') - - if 'time' not in guesses: - # Determine initial time and duration guesses depending on the phase name - if 'desc1' == base_phase: - t_initial = flight_duration * .9 - t_duration = flight_duration * .04 - elif 'desc2' in base_phase: - t_initial = flight_duration * .94 - t_duration = 5000 - # Set the time guesses as the initial values for the time-related - # trajectory variables - setvalprob.set_val(parent_prefix + f"traj.{phase_name}.t_initial", - t_initial, units='s') - setvalprob.set_val(parent_prefix + f"traj.{phase_name}.t_duration", - t_duration, units='s') - - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - if 'distance' not in guesses: - # Determine initial distance guesses depending on the phase name - if 'desc1' == base_phase: - ys = [self.target_range * .97, self.target_range * .99] - elif 'desc2' in base_phase: - ys = [self.target_range * .99, self.target_range] - # Set the distance guesses as the initial values for the distance state - # variable - setvalprob.set_val(parent_prefix + - f"traj.{phase_name}.states:distance", phase.interp( - Dynamic.Mission.DISTANCE, ys=ys) - ) - def run_aviary_problem(self, record_filename="problem_history.db", optimization_history_filename=None, restart_filename=None, suppress_solver_print=True, run_driver=True, simulate=False, @@ -2454,7 +1930,7 @@ def _get_all_subsystems(self, external_subsystems=None): return all_subsystems - def _add_objectives(self): + def _add_comps_for_objs_and_cons(self): "add objectives and some constraints" self.model.add_subsystem( "fuel_obj", diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 8eda9ecc8..9b6a132aa 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -2,35 +2,37 @@ from aviary.constants import GRAV_ENGLISH_LBM, RHO_SEA_LEVEL_ENGLISH from aviary.interface.default_phase_info.two_dof_fiti import add_default_sgm_args + from aviary.mission.gasp_based.idle_descent_estimation import add_descent_estimation_as_submodel from aviary.mission.gasp_based.ode.landing_ode import LandingSegment +from aviary.mission.gasp_based.ode.params import ParamPort from aviary.mission.gasp_based.ode.taxi_ode import TaxiSegment -from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.mission.gasp_based.phases.groundroll_phase import GroundrollPhase +from aviary.mission.gasp_based.phases.rotation_phase import RotationPhase +from aviary.mission.gasp_based.phases.climb_phase import ClimbPhase +from aviary.mission.gasp_based.phases.cruise_phase import CruisePhase +from aviary.mission.gasp_based.phases.accel_phase import AccelPhase +from aviary.mission.gasp_based.phases.ascent_phase import AscentPhase +from aviary.mission.gasp_based.phases.descent_phase import DescentPhase + from aviary.utils.functions import create_opts2vals, add_opts2vals, wrapped_convert_units -from aviary.utils.process_input_decks import initialization_guessing, update_GASP_options +from aviary.utils.process_input_decks import initialization_guessing from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings -class AviaryProblemBuilder_2DOF(): +class ProblemBuilder2DOF(): """ A 2DOF specific builder that customizes AviaryProblem() for use with two degree of freedom phases. """ - def initial_guesses(self, prob, engine_builders): + def initial_guesses(self, prob): # TODO: This should probably be moved to the set_initial_guesses() method in AviaryProblem class # Defines how the problem should build it's initial guesses for load_inputs() # this modifies mass_method, initialization_guesses, and aviary_values aviary_inputs = prob.aviary_inputs - prob.mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) - - if engine_builders is None: - engine_builders = build_engine_deck(aviary_inputs) - prob.engine_builders = engine_builders - - aviary_inputs = update_GASP_options(aviary_inputs) prob.initialization_guesses = initialization_guessing( aviary_inputs, prob.initialization_guesses, prob.engine_builders) @@ -187,6 +189,279 @@ def add_takeoff_systems(self, prob): def add_post_mission_takeoff_systems(self, prob): pass + def get_phase_builder(self, prob, phase_name, phase_options): + + if 'groundroll' in phase_name: + phase_builder = GroundrollPhase + elif 'rotation' in phase_name: + phase_builder = RotationPhase + elif 'accel' in phase_name: + phase_builder = AccelPhase + elif 'ascent' in phase_name: + phase_builder = AscentPhase + elif 'climb' in phase_name: + phase_builder = ClimbPhase + elif 'cruise' in phase_name: + phase_builder = CruisePhase + elif 'desc' in phase_name: + phase_builder = DescentPhase + else: + raise ValueError( + f'{phase_name} does not have an associated phase_builder \n phase_name must ' + 'include one of: groundroll, rotation, accel, ascent, climb, cruise, or desc') + + return phase_builder + + def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): + + try: + fix_initial = user_options.get_val('fix_initial') + except KeyError: + fix_initial = False + + try: + fix_duration = user_options.get_val('fix_duration') + except KeyError: + fix_duration = False + + if 'ascent' in phase_name: + phase.set_time_options( + units="s", + targets="t_curr", + input_initial=True, + input_duration=True, + ) + + elif 'cruise' in phase_name: + # Time here is really the independent variable through which we are integrating. + # In the case of the Breguet Range ODE, it's mass. + # We rely on mass being monotonically non-increasing across the phase. + phase.set_time_options( + name='mass', + fix_initial=False, + fix_duration=False, + units="lbm", + targets="mass", + initial_bounds=(0., 1.e7), + initial_ref=100.e3, + duration_bounds=(-1.e7, -1), + duration_ref=50000, + ) + + elif 'descent' in phase_name: + duration_ref = user_options.get_val("duration_ref", 's') + phase.set_time_options( + duration_bounds=duration_bounds, + fix_initial=fix_initial, + input_initial=input_initial, + units="s", + duration_ref=duration_ref, + ) + + else: + + input_initial = False + time_units = phase.time_options['units'] + + # Make a good guess for a reasonable intitial time scaler. + try: + initial_bounds = user_options.get_val('initial_bounds', units=time_units) + except KeyError: + initial_bounds = (None, None) + + if initial_bounds[0] is not None and initial_bounds[1] != 0.0: + # Upper bound is good for a ref. + user_options.set_val('initial_ref', initial_bounds[1], + units=time_units) + else: + user_options.set_val('initial_ref', 600., time_units) + + duration_bounds = user_options.get_val("duration_bounds", time_units) + user_options.set_val( + 'duration_ref', (duration_bounds[0] + duration_bounds[1]) / 2., + time_units + ) + if phase_idx > 0: + input_initial = True + + if fix_initial or input_initial: + + if prob.comm.size > 1: + # Phases are disconnected to run in parallel, so initial ref is + # valid. + initial_ref = user_options.get_val("initial_ref", time_units) + else: + # Redundant on a fixed input; raises a warning if specified. + initial_ref = None + + phase.set_time_options( + fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, + duration_bounds=user_options.get_val("duration_bounds", time_units), + duration_ref=user_options.get_val("duration_ref", time_units), + initial_ref=initial_ref, + ) + + else: # TODO: figure out how to handle this now that fix_initial is dict + phase.set_time_options( + fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, + duration_bounds=user_options.get_val("duration_bounds", time_units), + duration_ref=user_options.get_val("duration_ref", time_units), + initial_bounds=initial_bounds, + initial_ref=user_options.get_val("initial_ref", time_units), + ) + + if 'cruise' not in phase_name: + phase.add_control( + Dynamic.Vehicle.Propulsion.THROTTLE, + targets=Dynamic.Vehicle.Propulsion.THROTTLE, + units='unitless', + opt=False, + ) + + def link_phases(self, prob, phases, direct_links=True): + + if prob.analysis_scheme is AnalysisScheme.COLLOCATION: + for ii in range(len(phases) - 1): + phase1, phase2 = phases[ii:ii + 2] + analytic1 = prob.phase_info[phase1]['user_options']['analytic'] + analytic2 = prob.phase_info[phase2]['user_options']['analytic'] + + if not (analytic1 or analytic2): + # we always want time, distance, and mass to be continuous + states_to_link = { + 'time': direct_links, + Dynamic.Mission.DISTANCE: direct_links, + Dynamic.Vehicle.MASS: False, + } + + # if both phases are reserve phases or neither is a reserve phase + # (we are not on the boundary between the regular and reserve missions) + # and neither phase is ground roll or rotation (altitude isn't a state): + # we want altitude to be continous as well + if ((phase1 in prob.reserve_phases) == (phase2 in prob.reserve_phases)) and \ + not ({"groundroll", "rotation"} & {phase1, phase2}) and \ + not ('accel', 'climb1') == (phase1, phase2): # required for convergence of FwGm + states_to_link[Dynamic.Mission.ALTITUDE] = direct_links + + # if either phase is rotation, we need to connect velocity + # ascent to accel also requires velocity + if 'rotation' in ( + phase1, phase2) or ( + 'ascent', 'accel') == ( + phase1, phase2): + states_to_link[Dynamic.Mission.VELOCITY] = direct_links + # if the first phase is rotation, we also need alpha + if phase1 == 'rotation': + states_to_link['alpha'] = False + + for state, connected in states_to_link.items(): + # in initial guesses, all of the states, other than time use + # the same name + initial_guesses1 = prob.phase_info[phase1]['initial_guesses'] + initial_guesses2 = prob.phase_info[phase2]['initial_guesses'] + + # if a state is in the initial guesses, get the units of the + # initial guess + kwargs = {} + if not connected: + if state in initial_guesses1: + kwargs = {'units': initial_guesses1[state][-1]} + elif state in initial_guesses2: + kwargs = {'units': initial_guesses2[state][-1]} + + prob.traj.link_phases( + [phase1, phase2], [state], connected=connected, **kwargs) + + # if either phase is analytic we have to use a linkage_constraint + else: + # analytic phases use the prefix "initial" for time and distance, + # but not mass + if analytic2: + prefix = 'initial_' + else: + prefix = '' + + prob.traj.add_linkage_constraint( + phase1, phase2, 'time', prefix + 'time', connected=True) + prob.traj.add_linkage_constraint( + phase1, phase2, 'distance', prefix + 'distance', + connected=True) + prob.traj.add_linkage_constraint( + phase1, phase2, 'mass', 'mass', connected=False, ref=1.0e5) + + # add all params and promote them to prob.model level + ParamPort.promote_params( + prob.model, + trajs=["traj"], + phases=[ + [*prob.regular_phases, + *prob.reserve_phases] + ], + ) + + prob.model.promotes( + "traj", + inputs=[ + ("ascent.parameters:t_init_gear", "t_init_gear"), + ("ascent.parameters:t_init_flaps", "t_init_flaps"), + ("ascent.t_initial", Mission.Takeoff.ASCENT_T_INTIIAL), + ("ascent.t_duration", Mission.Takeoff.ASCENT_DURATION), + ], + ) + + # imitate input_initial for taxi -> groundroll + eq = prob.model.add_subsystem( + "taxi_groundroll_mass_constraint", om.EQConstraintComp()) + eq.add_eq_output("mass", eq_units="lbm", normalize=False, + ref=10000., add_constraint=True) + prob.model.connect( + "taxi.mass", "taxi_groundroll_mass_constraint.rhs:mass") + prob.model.connect( + "traj.groundroll.states:mass", + "taxi_groundroll_mass_constraint.lhs:mass", + src_indices=[0], + flat_src_indices=True, + ) + + prob.model.connect("traj.ascent.timeseries.time", "h_fit.time_cp") + prob.model.connect( + "traj.ascent.timeseries.altitude", "h_fit.h_cp") + + prob.model.connect(f'traj.{prob.regular_phases[-1]}.states:mass', + Mission.Landing.TOUCHDOWN_MASS, src_indices=[-1]) + + connect_map = { + f"traj.{prob.regular_phases[-1]}.timeseries.distance": 'actual_range', + } + + else: + connect_map = { + "taxi.mass": "traj.mass_initial", + Mission.Takeoff.ROTATION_VELOCITY: "traj.SGMGroundroll_velocity_trigger", + "traj.distance_final": 'actual_range', + "traj.mass_final": Mission.Landing.TOUCHDOWN_MASS, + } + + # promote all ParamPort inputs for analytic segments as well + param_list = list(ParamPort.param_data) + prob.model.promotes("taxi", inputs=param_list) + prob.model.promotes("landing", inputs=param_list) + if prob.analysis_scheme is AnalysisScheme.SHOOTING: + param_list.append(Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE) + prob.model.promotes("traj", inputs=param_list) + # prob.model.list_inputs() + # prob.model.promotes("traj", inputs=['ascent.ODE_group.eoms.'+Aircraft.Design.MAX_FUSELAGE_PITCH_ANGLE]) + + prob.model.connect("taxi.mass", "vrot.mass") + + for source, target in connect_map.items(): + prob.model.connect( + source, + target, + src_indices=[-1], + flat_src_indices=True, + ) + def add_landing_systems(self, prob): prob.model.add_subsystem( @@ -205,3 +480,166 @@ def add_landing_systems(self, prob): 'pre_mission.drag_loss_due_to_shielded_wing_area', 'landing.drag_loss_due_to_shielded_wing_area') + def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_prefix): + """ + Adds the initial guesses for each variable of a given phase to the problem. + This method sets the initial guesses for time, control, state, and problem-specific + variables for a given phase. If using the GASP model, it also handles some special + cases that are not covered in the `phase_info` object. These include initial guesses + for mass, time, and distance, which are determined based on the phase name and other + mission-related variables. + + Parameters + ---------- + phase_name : str + The name of the phase for which the guesses are being added. + phase : Phase + The phase object for which the guesses are being added. + guesses : dict + A dictionary containing the initial guesses for the phase. + target_prob : Problem + Problem instance to apply the guesses. + parent_prefix : str + Location of this trajectory in the hierarchy. + """ + # Set initial guesses for the rotation mass and flight duration + rotation_mass = prob.initialization_guesses['rotation_mass'] + flight_duration = prob.initialization_guesses['flight_duration'] + + control_keys = ["velocity_rate", "throttle"] + state_keys = [ + "altitude", + "mass", + Dynamic.Mission.DISTANCE, + Dynamic.Mission.VELOCITY, + "flight_path_angle", + "alpha", + ] + + if phase_name == 'ascent': + # Alpha is a control for ascent. + control_keys.append('alpha') + + prob_keys = ["tau_gear", "tau_flaps"] + + for guess_key, guess_data in guesses.items(): + val, units = guess_data + + # Set initial guess for time variables + if 'time' == guess_key: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.t_initial', + val[0], + units=units + ) + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.t_duration', + val[1], + units=units + ) + + else: + # Set initial guess for control variables + if guess_key in control_keys: + try: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.controls:{guess_key}', + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + except KeyError: + try: + target_prob.set_val( + parent_prefix + + f'traj.{phase_name}.polynomial_controls:{guess_key}', + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + except KeyError: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.bspline_controls:', + {guess_key}, + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + if guess_key in control_keys: + pass + + # Set initial guess for state variables + elif guess_key in state_keys: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.states:{guess_key}', + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + elif guess_key in prob_keys: + target_prob.set_val(parent_prefix + guess_key, val, units=units) + + elif ":" in guess_key: + target_prob.set_val( + parent_prefix + + f'traj.{phase_name}.{guess_key}', + prob._process_guess_var( + val, + guess_key, + phase), + units=units) + else: + # raise error if the guess key is not recognized + raise ValueError( + f"Initial guess key {guess_key} in {phase_name} is not " + "recognized." + ) + + # We need some special logic for these following variables because GASP computes + # initial guesses using some knowledge of the mission duration and other variables + # that are only available after calling `create_vehicle`. Thus these initial guess + # values are not included in the `phase_info` object. + base_phase = phase_name.removeprefix('reserve_') + + if 'mass' not in guesses: + # Determine a mass guess depending on the phase name + if base_phase in ["groundroll", "rotation", "ascent", "accel", "climb1"]: + mass_guess = rotation_mass + elif base_phase == "climb2": + mass_guess = 0.99 * rotation_mass + elif "desc" in base_phase: + mass_guess = 0.9 * prob.cruise_mass_final + + # Set the mass guess as the initial value for the mass state variable + target_prob.set_val(parent_prefix + f'traj.{phase_name}.states:mass', + mass_guess, units='lbm') + + if 'time' not in guesses: + # Determine initial time and duration guesses depending on the phase name + if 'desc1' == base_phase: + t_initial = flight_duration * .9 + t_duration = flight_duration * .04 + elif 'desc2' in base_phase: + t_initial = flight_duration * .94 + t_duration = 5000 + + # Set the time guesses as the initial values for the time-related + # trajectory variables + target_prob.set_val(parent_prefix + f"traj.{phase_name}.t_initial", + t_initial, units='s') + target_prob.set_val(parent_prefix + f"traj.{phase_name}.t_duration", + t_duration, units='s') + + if 'distance' not in guesses: + # Determine initial distance guesses depending on the phase name + if 'desc1' == base_phase: + ys = [prob.target_range * .97, prob.target_range * .99] + elif 'desc2' in base_phase: + ys = [prob.target_range * .99, prob.target_range] + # Set the distance guesses as the initial values for the distance state + # variable + target_prob.set_val( + parent_prefix + f"traj.{phase_name}.states:distance", + phase.interp(Dynamic.Mission.DISTANCE, ys=ys), + ) + diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index ac70961df..9aa65e8ca 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -1,12 +1,15 @@ +import numpy as np + import openmdao.api as om from dymos.transcriptions.transcription_base import TranscriptionBase +from aviary.mission.energy_phase import EnergyPhase from aviary.mission.flops_based.phases.build_landing import Landing from aviary.mission.flops_based.phases.build_takeoff import Takeoff -from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.mission.phase_builder_base import PhaseBuilderBase from aviary.utils.functions import wrapped_convert_units -from aviary.utils.process_input_decks import update_GASP_options, initialization_guessing +from aviary.utils.process_input_decks import initialization_guessing from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.enums import LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings @@ -17,27 +20,18 @@ use_new_dymos_syntax = True -class AviaryProblemBuilder_HE(): +class ProblemBuilderHeightEnergy(): """ A Height-Energy specific builder that customizes AviaryProblem() for use with height energy phases. """ - def initial_guesses(self, prob, engine_builders): + def initial_guesses(self, prob): # TODO: This should probably be moved to the set_initial_guesses() method in AviaryProblem class # Defines how the problem should build it's initial guesses for load_inputs() # this modifies mass_method, initialization_guesses, and aviary_values aviary_inputs = prob.aviary_inputs - prob.mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) - - if prob.mass_method is LegacyCode.GASP: - # Support for GASP mass methods with HE. - aviary_inputs = update_GASP_options(aviary_inputs) - - if engine_builders is None: - engine_builders = build_engine_deck(aviary_inputs) - prob.engine_builders = engine_builders prob.initialization_guesses = initialization_guessing( aviary_inputs, prob.initialization_guesses, prob.engine_builders) @@ -94,6 +88,130 @@ def add_takeoff_systems(self, prob): promotes_outputs=['mission:*'], ) + def get_phase_builder(self, prob, phase_name, phase_options): + + if 'phase_builder' in phase_options: + phase_builder = phase_options['phase_builder'] + if not issubclass(phase_builder, PhaseBuilderBase): + raise TypeError(f"phase_builder for the phase called " + "{phase_name} must be a PhaseBuilderBase object.") + else: + phase_builder = EnergyPhase + + return phase_builder + + def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): + + try: + fix_initial = user_options.get_val('fix_initial') + except KeyError: + fix_initial = False + + try: + fix_duration = user_options.get_val('fix_duration') + except KeyError: + fix_duration = False + + # The rest of the phases includes all Height Energy method phases + # and any 2DOF phases that don't fall into the naming patterns + # above. + input_initial = False + time_units = phase.time_options['units'] + + # Make a good guess for a reasonable intitial time scaler. + try: + initial_bounds = user_options.get_val('initial_bounds', units=time_units) + except KeyError: + initial_bounds = (None, None) + + if initial_bounds[0] is not None and initial_bounds[1] != 0.0: + # Upper bound is good for a ref. + user_options.set_val('initial_ref', initial_bounds[1], + units=time_units) + else: + user_options.set_val('initial_ref', 600., time_units) + + duration_bounds = user_options.get_val("duration_bounds", time_units) + user_options.set_val( + 'duration_ref', (duration_bounds[0] + duration_bounds[1]) / 2., + time_units + ) + if phase_idx > 0: + input_initial = True + + if fix_initial or input_initial: + + if prob.comm.size > 1: + # Phases are disconnected to run in parallel, so initial ref is + # valid. + initial_ref = user_options.get_val("initial_ref", time_units) + else: + # Redundant on a fixed input; raises a warning if specified. + initial_ref = None + + phase.set_time_options( + fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, + duration_bounds=user_options.get_val("duration_bounds", time_units), + duration_ref=user_options.get_val("duration_ref", time_units), + initial_ref=initial_ref, + ) + elif phase_name == 'descent': # TODO: generalize this logic for all phases + phase.set_time_options( + fix_initial=False, fix_duration=False, units=time_units, + duration_bounds=user_options.get_val("duration_bounds", time_units), + duration_ref=user_options.get_val("duration_ref", time_units), + initial_bounds=initial_bounds, + initial_ref=user_options.get_val("initial_ref", time_units), + ) + else: # TODO: figure out how to handle this now that fix_initial is dict + phase.set_time_options( + fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, + duration_bounds=user_options.get_val("duration_bounds", time_units), + duration_ref=user_options.get_val("duration_ref", time_units), + initial_bounds=initial_bounds, + initial_ref=user_options.get_val("initial_ref", time_units), + ) + + def link_phases(self, prob, phases, direct_links=True): + + # connect regular_phases with each other if you are optimizing alt or mach + prob._link_phases_helper_with_options( + prob.regular_phases, + 'optimize_altitude', + Dynamic.Mission.ALTITUDE, + ref=1.0e4, + ) + prob._link_phases_helper_with_options( + prob.regular_phases, 'optimize_mach', Dynamic.Atmosphere.MACH + ) + + # connect reserve phases with each other if you are optimizing alt or mach + prob._link_phases_helper_with_options( + prob.reserve_phases, + 'optimize_altitude', + Dynamic.Mission.ALTITUDE, + ref=1.0e4, + ) + prob._link_phases_helper_with_options( + prob.reserve_phases, 'optimize_mach', Dynamic.Atmosphere.MACH + ) + + # connect mass and distance between all phases regardless of reserve / + # non-reserve status + prob.traj.link_phases(phases, ["time"], + ref=None if direct_links else 1e3, + connected=direct_links) + prob.traj.link_phases(phases, [Dynamic.Vehicle.MASS], + ref=None if direct_links else 1e6, + connected=direct_links) + prob.traj.link_phases(phases, [Dynamic.Mission.DISTANCE], + ref=None if direct_links else 1e3, + connected=direct_links) + + prob.model.connect(f'traj.{prob.regular_phases[-1]}.timeseries.distance', + 'actual_range', + src_indices=[-1], flat_src_indices=True) + def add_post_mission_takeoff_systems(self, prob): first_flight_phase_name = list(prob.phase_info.keys())[0] @@ -176,3 +294,151 @@ def add_landing_systems(self, prob): Mission.Landing.INITIAL_ALTITUDE, src_indices=[0]) + def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_prefix): + """ + Adds the initial guesses for each variable of a given phase to the problem. + This method sets the initial guesses for time, control, state, and problem-specific + variables for a given phase. If using the GASP model, it also handles some special + cases that are not covered in the `phase_info` object. These include initial guesses + for mass, time, and distance, which are determined based on the phase name and other + mission-related variables. + + Parameters + ---------- + phase_name : str + The name of the phase for which the guesses are being added. + phase : Phase + The phase object for which the guesses are being added. + guesses : dict + A dictionary containing the initial guesses for the phase. + target_prob : Problem + Problem instance to apply the guesses. + parent_prefix : str + Location of this trajectory in the hierarchy. + """ + control_keys = ["mach", "altitude"] + state_keys = ["mass", Dynamic.Mission.DISTANCE] + + prob_keys = ["tau_gear", "tau_flaps"] + + # for the simple mission method, use the provided initial and final mach + # and altitude values from phase_info + initial_altitude = wrapped_convert_units( + prob.phase_info[phase_name]['user_options']['initial_altitude'], 'ft') + final_altitude = wrapped_convert_units( + prob.phase_info[phase_name]['user_options']['final_altitude'], 'ft') + initial_mach = prob.phase_info[phase_name]['user_options']['initial_mach'] + final_mach = prob.phase_info[phase_name]['user_options']['final_mach'] + + guesses["mach"] = ([initial_mach[0], final_mach[0]], "unitless") + guesses["altitude"] = ([initial_altitude, final_altitude], 'ft') + + # if time not in initial guesses, set it to the average of the + # initial_bounds and the duration_bounds + if 'time' not in guesses: + initial_bounds = wrapped_convert_units( + prob.phase_info[phase_name]['user_options']['initial_bounds'], 's') + duration_bounds = wrapped_convert_units( + prob.phase_info[phase_name]['user_options']['duration_bounds'], 's') + guesses["time"] = ([np.mean(initial_bounds[0]), np.mean( + duration_bounds[0])], 's') + + # if time not in initial guesses, set it to the average of the + # initial_bounds and the duration_bounds + if 'time' not in guesses: + initial_bounds = prob.phase_info[phase_name]['user_options'][ + 'initial_bounds'] + duration_bounds = prob.phase_info[phase_name]['user_options'][ + 'duration_bounds'] + # Add a check for the initial and duration bounds, raise an error if they + # are not consistent + if initial_bounds[1] != duration_bounds[1]: + raise ValueError( + f"Initial and duration bounds for {phase_name} " + "are not consistent." + ) + guesses["time"] = ([np.mean(initial_bounds[0]), np.mean( + duration_bounds[0])], initial_bounds[1]) + + for guess_key, guess_data in guesses.items(): + val, units = guess_data + + # Set initial guess for time variables + if 'time' == guess_key: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.t_initial', + val[0], + units=units + ) + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.t_duration', + val[1], + units=units + ) + + else: + + # Set initial guess for control variables + if guess_key in control_keys: + try: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.controls:{guess_key}', + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + except KeyError: + try: + target_prob.set_val( + parent_prefix + + f'traj.{phase_name}.polynomial_controls:{guess_key}', + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + except KeyError: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.bspline_controls:', + {guess_key}, + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + if guess_key in control_keys: + pass + + # Set initial guess for state variables + elif guess_key in state_keys: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.states:{guess_key}', + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + elif guess_key in prob_keys: + target_prob.set_val(parent_prefix + guess_key, val, units=units) + + elif ":" in guess_key: + target_prob.set_val( + parent_prefix + + f'traj.{phase_name}.{guess_key}', + prob._process_guess_var( + val, + guess_key, + phase), + units=units) + else: + # raise error if the guess key is not recognized + raise ValueError( + f"Initial guess key {guess_key} in {phase_name} is not " + "recognized." + ) + + if 'mass' not in guesses: + mass_guess = prob.aviary_inputs.get_val( + Mission.Design.GROSS_MASS, units='lbm') + + # Set the mass guess as the initial value for the mass state variable + target_prob.set_val(parent_prefix + f'traj.{phase_name}.states:mass', + mass_guess, units='lbm') + diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py new file mode 100644 index 000000000..0c14eb235 --- /dev/null +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -0,0 +1,197 @@ + +from aviary.mission.flops_based.phases.groundroll_phase import GroundrollPhase as GroundrollPhaseVelocityIntegrated +from aviary.mission.twodof_phase import TwoDOFPhase +from aviary.utils.functions import wrapped_convert_units +from aviary.variable_info.variables import Dynamic + + +class ProblemBuilderSolved2DOF(): + """ + The Solved 2DOF builder is used for detailed take-off and landing. + """ + + def initial_guesses(self, prob): + pass + + def phase_info_default_location(self, prob): + pass + + def add_takeoff_systems(self, prob): + pass + + def get_phase_builder(self, prob, phase_name, phase_options): + + if phase_options['user_options']['ground_roll'] and phase_options['user_options']['fix_initial']: + phase_builder = GroundrollPhaseVelocityIntegrated + else: + phase_builder = TwoDOFPhase + + return phase_builder + + def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): + + try: + fix_initial = user_options.get_val('fix_initial') + except KeyError: + fix_initial = False + + try: + fix_duration = user_options.get_val('fix_duration') + except KeyError: + fix_duration = False + + input_initial = False + time_units = phase.time_options['units'] + + # Make a good guess for a reasonable intitial time scaler. + try: + initial_bounds = user_options.get_val('initial_bounds', units=time_units) + except KeyError: + initial_bounds = (None, None) + + if initial_bounds[0] is not None and initial_bounds[1] != 0.0: + # Upper bound is good for a ref. + user_options.set_val('initial_ref', initial_bounds[1], + units=time_units) + else: + user_options.set_val('initial_ref', 600., time_units) + + duration_bounds = user_options.get_val("duration_bounds", time_units) + user_options.set_val( + 'duration_ref', (duration_bounds[0] + duration_bounds[1]) / 2., + time_units + ) + if phase_idx > 0: + input_initial = True + + if fix_initial or input_initial: + + if prob.comm.size > 1: + # Phases are disconnected to run in parallel, so initial ref is + # valid. + initial_ref = user_options.get_val("initial_ref", time_units) + else: + # Redundant on a fixed input; raises a warning if specified. + initial_ref = None + + phase.set_time_options( + fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, + duration_bounds=user_options.get_val("duration_bounds", time_units), + duration_ref=user_options.get_val("duration_ref", time_units), + initial_ref=initial_ref, + ) + else: # TODO: figure out how to handle this now that fix_initial is dict + phase.set_time_options( + fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, + duration_bounds=user_options.get_val("duration_bounds", time_units), + duration_ref=user_options.get_val("duration_ref", time_units), + initial_bounds=initial_bounds, + initial_ref=user_options.get_val("initial_ref", time_units), + ) + + def link_phases(self, prob, phases, direct_links=True): + + # connect regular_phases with each other if you are optimizing alt or mach + prob._link_phases_helper_with_options( + prob.regular_phases, + 'optimize_altitude', + Dynamic.Mission.ALTITUDE, + ref=1.0e4, + ) + prob._link_phases_helper_with_options( + prob.regular_phases, 'optimize_mach', Dynamic.Atmosphere.MACH + ) + + # connect reserve phases with each other if you are optimizing alt or mach + prob._link_phases_helper_with_options( + prob.reserve_phases, + 'optimize_altitude', + Dynamic.Mission.ALTITUDE, + ref=1.0e4, + ) + prob._link_phases_helper_with_options( + prob.reserve_phases, 'optimize_mach', Dynamic.Atmosphere.MACH + ) + + prob.traj.link_phases(phases, [Dynamic.Vehicle.MASS], connected=True) + prob.traj.link_phases( + phases, [Dynamic.Mission.DISTANCE], + units='ft', ref=1.e3, connected=False) + prob.traj.link_phases(phases, ["time"], connected=False) + + if len(phases) > 2: + prob.traj.link_phases( + phases[1:], ["alpha"], units='rad', connected=False) + + + def add_post_mission_takeoff_systems(self, prob): + pass + + def add_landing_systems(self, prob): + pass + + def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_prefix): + """ + Adds the initial guesses for each variable of a given phase to the problem. + This method sets the initial guesses for time, control, state, and problem-specific + variables for a given phase. If using the GASP model, it also handles some special + cases that are not covered in the `phase_info` object. These include initial guesses + for mass, time, and distance, which are determined based on the phase name and other + mission-related variables. + + Parameters + ---------- + phase_name : str + The name of the phase for which the guesses are being added. + phase : Phase + The phase object for which the guesses are being added. + guesses : dict + A dictionary containing the initial guesses for the phase. + target_prob : Problem + Problem instance to apply the guesses. + parent_prefix : str + Location of this trajectory in the hierarchy. + """ + + control_keys = ["mach", "altitude"] + + # for the simple mission method, use the provided initial and final mach + # and altitude values from phase_info + initial_altitude = wrapped_convert_units( + prob.phase_info[phase_name]['user_options']['initial_altitude'], 'ft') + final_altitude = wrapped_convert_units( + prob.phase_info[phase_name]['user_options']['final_altitude'], 'ft') + initial_mach = prob.phase_info[phase_name]['user_options']['initial_mach'] + final_mach = prob.phase_info[phase_name]['user_options']['final_mach'] + + guesses["mach"] = ([initial_mach[0], final_mach[0]], "unitless") + guesses["altitude"] = ([initial_altitude, final_altitude], 'ft') + + for guess_key, guess_data in guesses.items(): + val, units = guess_data + + # Set initial guess for control variables + if guess_key in control_keys: + try: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.controls:{guess_key}', + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + except KeyError: + try: + target_prob.set_val( + parent_prefix + + f'traj.{phase_name}.polynomial_controls:{guess_key}', + prob._process_guess_var(val, guess_key, phase), + units=units + ) + + except KeyError: + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.bspline_controls:', + {guess_key}, + prob._process_guess_var(val, guess_key, phase), + units=units + ) From ecab0d8c7b516daa8aea957fa098ad87936ad09f Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 22 Jan 2025 15:35:57 -0500 Subject: [PATCH 10/38] Most of the obvious stuff moved into the problem builders. --- aviary/interface/methods_for_level2.py | 30 +------------------------ aviary/mission/problem_builder_2DOF.py | 31 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 0dc570e17..714ef36bf 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -1531,35 +1531,7 @@ def set_initial_guesses(self, parent_prob=None, parent_prefix=""): else: guesses = {} - if self.mission_method is TWO_DEGREES_OF_FREEDOM and \ - self.phase_info[phase_name]["user_options"].get("analytic", False): - for guess_key, guess_data in guesses.items(): - val, units = guess_data - - if 'mass' == guess_key: - # Set initial and duration mass for the analytic cruise phase. - # Note we are integrating over mass, not time for this phase. - target_prob.set_val( - parent_prefix + - f'traj.{phase_name}.t_initial', - val[0], - units=units) - target_prob.set_val( - parent_prefix + - f'traj.{phase_name}.t_duration', - val[1], - units=units) - - else: - # Otherwise, set the value of the parameter in the trajectory - # phase - target_prob.set_val( - parent_prefix + f'traj.{phase_name}.parameters:{guess_key}', - val, units=units) - - continue - - # If not cruise and GASP, add subsystem guesses + # Add subsystem guesses self._add_subsystem_guesses(phase_name, phase, target_prob, parent_prefix) # Set initial guesses for states and controls for each phase diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 9b6a132aa..38b980cac 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -502,6 +502,37 @@ def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_pref parent_prefix : str Location of this trajectory in the hierarchy. """ + + # Handle Analytic Phase + if prob.phase_info[phase_name]["user_options"].get("analytic", False): + + for guess_key, guess_data in guesses.items(): + val, units = guess_data + + if 'mass' == guess_key: + # Set initial and duration mass for the analytic cruise phase. + # Note we are integrating over mass, not time for this phase. + target_prob.set_val( + parent_prefix + + f'traj.{phase_name}.t_initial', + val[0], + units=units) + target_prob.set_val( + parent_prefix + + f'traj.{phase_name}.t_duration', + val[1], + units=units) + + else: + # Otherwise, set the value of the parameter in the trajectory + # phase + target_prob.set_val( + parent_prefix + f'traj.{phase_name}.parameters:{guess_key}', + val, units=units) + + # Analytic phase should have nothing else to set. + return + # Set initial guesses for the rotation mass and flight duration rotation_mass = prob.initialization_guesses['rotation_mass'] flight_duration = prob.initialization_guesses['flight_duration'] From bf77a280b5fa38387c458407bdd7756c7896c23e Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 24 Jan 2025 13:57:00 -0500 Subject: [PATCH 11/38] Merge latest --- aviary/visualization/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/visualization/dashboard.py b/aviary/visualization/dashboard.py index 974cb4c4a..15c208bed 100644 --- a/aviary/visualization/dashboard.py +++ b/aviary/visualization/dashboard.py @@ -756,7 +756,7 @@ def create_optimization_history_plot(case_recorder, df): data_source=data_source, variable_scroll_box=variable_scroll_box, variable_checkbox_callback=variable_checkbox_callback), - code=r""" + code=""" const filter_text = cb_obj.value.toLowerCase(); const all_options = data_source.data['options']; From 079349167117b40b0d6a35b58a301112819ca695 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 24 Jan 2025 14:10:56 -0500 Subject: [PATCH 12/38] Merge latest --- aviary/core/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 aviary/core/__init__.py diff --git a/aviary/core/__init__.py b/aviary/core/__init__.py new file mode 100644 index 000000000..b336982d3 --- /dev/null +++ b/aviary/core/__init__.py @@ -0,0 +1 @@ +__version__ = "0.9.7-dev" From 2b0c143bf853f0fce2c5e434507cd486f02c7666 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 24 Jan 2025 16:05:13 -0500 Subject: [PATCH 13/38] Merge latest --- aviary/interface/methods_for_level2.py | 12 ++++++------ aviary/mission/problem_builder_2DOF.py | 3 +-- aviary/mission/problem_builder_height_energy.py | 1 - aviary/mission/problem_builder_solved_2DOF.py | 1 - 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index eddf84598..4d9a57540 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -1500,13 +1500,13 @@ def set_initial_guesses(self, parent_prob=None, parent_prefix=""): if self.analysis_scheme is AnalysisScheme.SHOOTING: if self.problem_type is ProblemType.SIZING: target_prob.set_val(parent_prefix + Mission.Summary.GROSS_MASS, - self.get_val(Mission.Design.GROSS_MASS)) + self.get_val(Mission.Design.GROSS_MASS)) - target_prob.set_val(parent_prefix + - "traj.SGMClimb_" + Dynamic.Mission.ALTITUDE + "_trigger", - val=self.cruise_alt, - units="ft", - ) + target_prob.set_val( + parent_prefix + "traj.SGMClimb_" + Dynamic.Mission.ALTITUDE + "_trigger", + val=self.cruise_alt, + units="ft", + ) return diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index a2408c47c..58b67d5b3 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -68,7 +68,7 @@ def initial_guesses(self, prob): prob.target_range = wrapped_convert_units( prob.post_mission_info['post_mission']['target_range'], 'NM') aviary_inputs.set_val(Mission.Summary.RANGE, - prob.target_range, units='NM') + prob.target_range, units='NM') else: prob.target_range = aviary_inputs.get_val( Mission.Design.RANGE, units='NM') @@ -673,4 +673,3 @@ def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_pref parent_prefix + f"traj.{phase_name}.states:distance", phase.interp(Dynamic.Mission.DISTANCE, ys=ys), ) - diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 1f48585dc..7620f6fba 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -441,4 +441,3 @@ def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_pref # Set the mass guess as the initial value for the mass state variable target_prob.set_val(parent_prefix + f'traj.{phase_name}.states:mass', mass_guess, units='lbm') - diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index 0c14eb235..fb5ef08b5 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -123,7 +123,6 @@ def link_phases(self, prob, phases, direct_links=True): prob.traj.link_phases( phases[1:], ["alpha"], units='rad', connected=False) - def add_post_mission_takeoff_systems(self, prob): pass From 3abdef5785e6730406076485e447fa6f31516228 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 24 Jan 2025 16:29:12 -0500 Subject: [PATCH 14/38] CI failure --- aviary/interface/methods_for_level2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 4d9a57540..35dbc7289 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -121,6 +121,7 @@ def load_inputs( # with default values from metadata) and generate initial guesses aviary_inputs, self.initialization_guesses = create_vehicle( aviary_inputs, meta_data=meta_data, verbosity=verbosity) + self.aviary_inputs = aviary_inputs # pull which methods will be used for subsystems and mission self.mission_method = mission_method = aviary_inputs.get_val( @@ -203,7 +204,6 @@ def load_inputs( self.engine_builders = engine_builders self.problem_type = aviary_inputs.get_val(Settings.PROBLEM_TYPE) - self.aviary_inputs = aviary_inputs self.builder.initial_guesses(self) # This function sets all the following defaults if they were not already set From 1abdfb664666fdfe8eacd7fa50eb0e55bbc62e02 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 28 Jan 2025 14:23:49 -0500 Subject: [PATCH 15/38] Some cleanup from review --- aviary/interface/methods_for_level2.py | 2 +- .../mission/problem_builder_height_energy.py | 26 +++++++------------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 35dbc7289..6e7081ff9 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -144,7 +144,7 @@ def load_inputs( f'When using "settings:equations_of_motion,custom", a problem_builder must be specified in load_inputs().') else: raise ValueError( - f'settings:equations_of_motion must be one of: height_energy, 2DOF, or custom') + f'settings:equations_of_motion must be one of: height_energy, 2DOF, solved_2DOF, or custom') # TODO this should be a preprocessor step if it is required here if mission_method is TWO_DEGREES_OF_FREEDOM or mass_method is GASP: diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 7620f6fba..a88e533e0 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -3,6 +3,7 @@ import openmdao.api as om from dymos.transcriptions.transcription_base import TranscriptionBase +from dymos.utils.misc import _unspecified from aviary.mission.energy_phase import EnergyPhase from aviary.mission.flops_based.phases.build_landing import Landing @@ -112,10 +113,6 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): except KeyError: fix_duration = False - # The rest of the phases includes all Height Energy method phases - # and any 2DOF phases that don't fall into the naming patterns - # above. - input_initial = False time_units = phase.time_options['units'] # Make a good guess for a reasonable intitial time scaler. @@ -136,8 +133,11 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): 'duration_ref', (duration_bounds[0] + duration_bounds[1]) / 2., time_units ) - if phase_idx > 0: - input_initial = True + + # The rest of the phases includes all Height Energy method phases + # and any 2DOF phases that don't fall into the naming patterns + # above. + input_initial = phase_idx > 0 if fix_initial or input_initial: @@ -147,23 +147,17 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): initial_ref = user_options.get_val("initial_ref", time_units) else: # Redundant on a fixed input; raises a warning if specified. - initial_ref = None + initial_ref = _unspecified + initial_bounds = _unspecified phase.set_time_options( fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, duration_bounds=user_options.get_val("duration_bounds", time_units), duration_ref=user_options.get_val("duration_ref", time_units), - initial_ref=initial_ref, - ) - elif phase_name == 'descent': # TODO: generalize this logic for all phases - phase.set_time_options( - fix_initial=False, fix_duration=False, units=time_units, - duration_bounds=user_options.get_val("duration_bounds", time_units), - duration_ref=user_options.get_val("duration_ref", time_units), initial_bounds=initial_bounds, - initial_ref=user_options.get_val("initial_ref", time_units), + initial_ref=initial_ref, ) - else: # TODO: figure out how to handle this now that fix_initial is dict + else: phase.set_time_options( fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, duration_bounds=user_options.get_val("duration_bounds", time_units), From bd8a2f09a02a8c0266d769c6718a5dbb071d5e7e Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 28 Jan 2025 15:09:15 -0500 Subject: [PATCH 16/38] Fixup test failure. --- aviary/mission/problem_builder_height_energy.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index a88e533e0..4ff0ce507 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -3,7 +3,6 @@ import openmdao.api as om from dymos.transcriptions.transcription_base import TranscriptionBase -from dymos.utils.misc import _unspecified from aviary.mission.energy_phase import EnergyPhase from aviary.mission.flops_based.phases.build_landing import Landing @@ -147,14 +146,13 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): initial_ref = user_options.get_val("initial_ref", time_units) else: # Redundant on a fixed input; raises a warning if specified. - initial_ref = _unspecified - initial_bounds = _unspecified + initial_ref = None + initial_bounds = (None, None) phase.set_time_options( fix_initial=fix_initial, fix_duration=fix_duration, units=time_units, duration_bounds=user_options.get_val("duration_bounds", time_units), duration_ref=user_options.get_val("duration_ref", time_units), - initial_bounds=initial_bounds, initial_ref=initial_ref, ) else: From 6c3fcc7f3b742aaf249a2a1965dd273e807aa1ed Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Wed, 29 Jan 2025 11:27:14 -0500 Subject: [PATCH 17/38] moved update_GASP_options() into 2DOF problem builder. moved build_engine_deck() when no deck is supplied into each of the 3 different builders. --- aviary/interface/methods_for_level2.py | 10 ++++------ aviary/mission/problem_builder_2DOF.py | 7 +++++++ aviary/mission/problem_builder_height_energy.py | 4 ++++ aviary/mission/problem_builder_solved_2DOF.py | 5 +++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 6e7081ff9..a1e5fb4f6 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -31,7 +31,6 @@ 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.propulsion.utils import build_engine_deck from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import wrapped_convert_units @@ -128,6 +127,9 @@ def load_inputs( Settings.EQUATIONS_OF_MOTION) self.mass_method = mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) + # Create engine_builder + self.engine_builders = engine_builders + # Determine which problem builder to use based on mission_method if mission_method is HEIGHT_ENERGY: self.builder = ProblemBuilderHeightEnergy() @@ -147,7 +149,7 @@ def load_inputs( f'settings:equations_of_motion must be one of: height_energy, 2DOF, solved_2DOF, or custom') # TODO this should be a preprocessor step if it is required here - if mission_method is TWO_DEGREES_OF_FREEDOM or mass_method is GASP: + if mass_method is GASP: aviary_inputs = update_GASP_options(aviary_inputs) ## LOAD PHASE_INFO ### @@ -199,10 +201,6 @@ def load_inputs( else: self.post_mission_info = None - if engine_builders is None: - engine_builders = build_engine_deck(aviary_inputs) - self.engine_builders = engine_builders - self.problem_type = aviary_inputs.get_val(Settings.PROBLEM_TYPE) self.builder.initial_guesses(self) diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 58b67d5b3..c70887eee 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -19,6 +19,8 @@ from aviary.utils.process_input_decks import initialization_guessing from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.utils.process_input_decks import update_GASP_options +from aviary.subsystems.propulsion.utils import build_engine_deck class ProblemBuilder2DOF(): @@ -34,6 +36,11 @@ def initial_guesses(self, prob): aviary_inputs = prob.aviary_inputs + aviary_inputs = update_GASP_options(aviary_inputs) + + if prob.engine_builders is None: + prob.engine_builders = build_engine_deck(aviary_inputs) + prob.initialization_guesses = initialization_guessing( aviary_inputs, prob.initialization_guesses, prob.engine_builders) diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 4ff0ce507..99d0eef62 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -13,6 +13,7 @@ from aviary.variable_info.enums import AnalysisScheme from aviary.variable_info.enums import LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.subsystems.propulsion.utils import build_engine_deck if hasattr(TranscriptionBase, 'setup_polynomial_controls'): use_new_dymos_syntax = False @@ -33,6 +34,9 @@ def initial_guesses(self, prob): aviary_inputs = prob.aviary_inputs + if prob.engine_builders is None: + prob.engine_builders = build_engine_deck(aviary_inputs) + prob.initialization_guesses = initialization_guessing( aviary_inputs, prob.initialization_guesses, prob.engine_builders) diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index fb5ef08b5..2facff50e 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -3,6 +3,7 @@ from aviary.mission.twodof_phase import TwoDOFPhase from aviary.utils.functions import wrapped_convert_units from aviary.variable_info.variables import Dynamic +from aviary.subsystems.propulsion.utils import build_engine_deck class ProblemBuilderSolved2DOF(): @@ -11,6 +12,10 @@ class ProblemBuilderSolved2DOF(): """ def initial_guesses(self, prob): + + if prob.engine_builders is None: + prob.engine_builders = build_engine_deck(prob.aviary_inputs) + pass def phase_info_default_location(self, prob): From 5ba7a1ced39ad51945b67a060766ef8017f121d9 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Wed, 29 Jan 2025 13:46:23 -0500 Subject: [PATCH 18/38] removed _add_comps_for_objs_and_cons() and instead broke it up into problem_builders. left all objective calcs in m4l2 because even if your running shooting, they could be useful and don\t have a large computational cost --- aviary/interface/methods_for_level2.py | 113 +++++------------- aviary/mission/problem_builder_2DOF.py | 31 +++++ .../mission/problem_builder_height_energy.py | 36 ++++++ aviary/mission/problem_builder_solved_2DOF.py | 3 + 4 files changed, 102 insertions(+), 81 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index a1e5fb4f6..566b2d848 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -908,9 +908,6 @@ def add_post_mission_systems(self, include_landing=True): self.model.add_constraint( f"{phase_name}_duration_constraint.duration_resid", equals=0.0, ref=1e2) - if self.mission_method in (TWO_DEGREES_OF_FREEDOM, HEIGHT_ENERGY): - self._add_comps_for_objs_and_cons() - ecomp = om.ExecComp( 'mass_resid = operating_empty_mass + overall_fuel + payload_mass -' ' initial_mass', @@ -934,25 +931,39 @@ def add_post_mission_systems(self, include_landing=True): ('initial_mass', Mission.Summary.GROSS_MASS)], promotes_outputs=[("mass_resid", Mission.Constraints.MASS_RESIDUAL)]) - if self.mission_method in (HEIGHT_ENERGY, TWO_DEGREES_OF_FREEDOM): - self.post_mission.add_constraint( - Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) + # Objectives should be calculated here + self.model.add_subsystem( + "fuel_obj", + om.ExecComp( + "reg_objective = overall_fuel/10000 + ascent_duration/30.", + reg_objective={"val": 0.0, "units": "unitless"}, + ascent_duration={"units": "s", "shape": 1}, + overall_fuel={"units": "lbm"}, + ), + promotes_inputs=[ + ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), + ("overall_fuel", Mission.Summary.TOTAL_FUEL_MASS), + ], + promotes_outputs=[("reg_objective", Mission.Objectives.FUEL)], + ) - if self.mission_method is HEIGHT_ENERGY: - # connect summary mass to the initial guess of mass in the first phase - if not self.pre_mission_info['include_takeoff']: - first_flight_phase_name = list(self.phase_info.keys())[0] - eq = self.model.add_subsystem( - f'link_{first_flight_phase_name}_mass', om.EQConstraintComp(), - promotes_inputs=[('rhs:mass', Mission.Summary.GROSS_MASS)]) - eq.add_eq_output('mass', eq_units='lbm', normalize=False, - ref=100000., add_constraint=True) - self.model.connect( - f'traj.{first_flight_phase_name}.states:mass', - f'link_{first_flight_phase_name}_mass.lhs:mass', - src_indices=[0], - flat_src_indices=True, - ) + self.model.add_subsystem( + "range_obj", + om.ExecComp( + "reg_objective = -actual_range/1000 + ascent_duration/30.", + reg_objective={"val": 0.0, "units": "unitless"}, + ascent_duration={"units": "s", "shape": 1}, + actual_range={ + "val": self.target_range, "units": "NM"}, + ), + promotes_inputs=[ + ("actual_range", Mission.Summary.RANGE), + ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), + ], + promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], + ) + + self.builder.add_post_mission_systems(self) def _link_phases_helper_with_options(self, phases, option_name, var, **kwargs): # Initialize a list to keep track of indices where option_name is True @@ -1900,66 +1911,6 @@ def _get_all_subsystems(self, external_subsystems=None): return all_subsystems - def _add_comps_for_objs_and_cons(self): - "add objectives and some constraints" - self.model.add_subsystem( - "fuel_obj", - om.ExecComp( - "reg_objective = overall_fuel/10000 + ascent_duration/30.", - reg_objective={"val": 0.0, "units": "unitless"}, - ascent_duration={"units": "s", "shape": 1}, - overall_fuel={"units": "lbm"}, - ), - promotes_inputs=[ - ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), - ("overall_fuel", Mission.Summary.TOTAL_FUEL_MASS), - ], - promotes_outputs=[("reg_objective", Mission.Objectives.FUEL)], - ) - - self.model.add_subsystem( - "range_obj", - om.ExecComp( - "reg_objective = -actual_range/1000 + ascent_duration/30.", - reg_objective={"val": 0.0, "units": "unitless"}, - ascent_duration={"units": "s", "shape": 1}, - actual_range={ - "val": self.target_range, "units": "NM"}, - ), - promotes_inputs=[ - ("actual_range", Mission.Summary.RANGE), - ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), - ], - promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], - ) - - if self.analysis_scheme is AnalysisScheme.COLLOCATION: - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - ascent_phase = getattr(self.traj.phases, 'ascent') - ascent_tx = ascent_phase.options["transcription"] - ascent_num_nodes = ascent_tx.grid_data.num_nodes - self.model.add_subsystem( - "h_fit", - PolynomialFit(N_cp=ascent_num_nodes), - promotes_inputs=["t_init_gear", "t_init_flaps"], - ) - - self.model.add_subsystem( - "range_constraint", - om.ExecComp( - "range_resid = target_range - actual_range", - target_range={"val": self.target_range, "units": "NM"}, - actual_range={"val": self.target_range, "units": "NM"}, - range_resid={"val": 30, "units": "NM"}, - ), - promotes_inputs=[ - ("actual_range", Mission.Summary.RANGE), - "target_range", - ], - promotes_outputs=[ - ("range_resid", Mission.Constraints.RANGE_RESIDUAL)], - ) - def _add_fuel_reserve_component(self, post_mission=True, reserves_name=Mission.Design.RESERVE_FUEL): if post_mission: diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index c70887eee..beb5f190b 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -680,3 +680,34 @@ def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_pref parent_prefix + f"traj.{phase_name}.states:distance", phase.interp(Dynamic.Mission.DISTANCE, ys=ys), ) + + def add_post_mission_systems(self, prob): + + if self.analysis_scheme is AnalysisScheme.COLLOCATION: + ascent_phase = getattr(self.traj.phases, 'ascent') + ascent_tx = ascent_phase.options["transcription"] + ascent_num_nodes = ascent_tx.grid_data.num_nodes + self.model.add_subsystem( + "h_fit", + PolynomialFit(N_cp=ascent_num_nodes), + promotes_inputs=["t_init_gear", "t_init_flaps"], + ) + + self.model.add_subsystem( + "range_constraint", + om.ExecComp( + "range_resid = target_range - actual_range", + target_range={"val": self.target_range, "units": "NM"}, + actual_range={"val": self.target_range, "units": "NM"}, + range_resid={"val": 30, "units": "NM"}, + ), + promotes_inputs=[ + ("actual_range", Mission.Summary.RANGE), + "target_range", + ], + promotes_outputs=[ + ("range_resid", Mission.Constraints.RANGE_RESIDUAL)], + ) + + self.post_mission.add_constraint( + Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 99d0eef62..d04178325 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -437,3 +437,39 @@ def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_pref # Set the mass guess as the initial value for the mass state variable target_prob.set_val(parent_prefix + f'traj.{phase_name}.states:mass', mass_guess, units='lbm') + + def add_post_mission_systems(self, prob): + + self.model.add_subsystem( + "range_constraint", + om.ExecComp( + "range_resid = target_range - actual_range", + target_range={"val": self.target_range, "units": "NM"}, + actual_range={"val": self.target_range, "units": "NM"}, + range_resid={"val": 30, "units": "NM"}, + ), + promotes_inputs=[ + ("actual_range", Mission.Summary.RANGE), + "target_range", + ], + promotes_outputs=[ + ("range_resid", Mission.Constraints.RANGE_RESIDUAL)], + ) + + self.post_mission.add_constraint( + Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) + + # connect summary mass to the initial guess of mass in the first phase + if not self.pre_mission_info['include_takeoff']: + first_flight_phase_name = list(self.phase_info.keys())[0] + eq = self.model.add_subsystem( + f'link_{first_flight_phase_name}_mass', om.EQConstraintComp(), + promotes_inputs=[('rhs:mass', Mission.Summary.GROSS_MASS)]) + eq.add_eq_output('mass', eq_units='lbm', normalize=False, + ref=100000., add_constraint=True) + self.model.connect( + f'traj.{first_flight_phase_name}.states:mass', + f'link_{first_flight_phase_name}_mass.lhs:mass', + src_indices=[0], + flat_src_indices=True, + ) diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index 2facff50e..9a30e027d 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -199,3 +199,6 @@ def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_pref prob._process_guess_var(val, guess_key, phase), units=units ) + + def add_post_mission_systems(self, prob): + pass From e08bfe9cab992d18aee7462499a630b352c15cda Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 29 Jan 2025 14:10:31 -0500 Subject: [PATCH 19/38] Some more refactor --- aviary/interface/methods_for_level2.py | 76 +------------------ aviary/mission/problem_builder_2DOF.py | 43 ++++++++++- .../mission/problem_builder_height_energy.py | 36 ++++++++- aviary/mission/problem_builder_solved_2DOF.py | 5 +- 4 files changed, 76 insertions(+), 84 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index a1e5fb4f6..e5685dac6 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -504,29 +504,6 @@ def _add_premission_external_subsystems(self): promotes_outputs=[('subsystem_mass', Aircraft.Design. EXTERNAL_SUBSYSTEMS_MASS)]) - def _add_groundroll_eq_constraint(self): - """ - Add an equality constraint to the problem to ensure that the TAS at the end of the - groundroll phase is equal to the rotation velocity at the start of the rotation phase. - """ - self.model.add_subsystem( - "groundroll_boundary", - om.EQConstraintComp( - "velocity", - eq_units="ft/s", - normalize=True, - add_constraint=True, - ), - ) - self.model.connect(Mission.Takeoff.ROTATION_VELOCITY, - "groundroll_boundary.rhs:velocity") - self.model.connect( - "traj.groundroll.states:velocity", - "groundroll_boundary.lhs:velocity", - src_indices=[-1], - flat_src_indices=True, - ) - def _get_phase(self, phase_name, phase_idx): base_phase_options = self.phase_info[phase_name] @@ -689,16 +666,6 @@ def add_subsystem_timeseries_outputs(phase, phase_name): phase_name, self._get_phase(phase_name, phase_idx)) add_subsystem_timeseries_outputs(phase, phase_name) - if self.mission_method is TWO_DEGREES_OF_FREEDOM: - - # In GASP, we still use the phase name to infer the phase type. - # We need this information to be available in the builders. - # TODO - Ultimately we should overhaul all of this. - self.phase_info[phase_name]['phase_type'] = phase_name - - if phase_name == 'ascent': - self._add_groundroll_eq_constraint() - # loop through phase_info and external subsystems external_parameters = {} for phase_name in self.phase_info: @@ -749,16 +716,12 @@ def add_post_mission_systems(self, include_landing=True): A user can override this with their own postmission systems. """ - if self.pre_mission_info['include_takeoff']: - self.builder.add_post_mission_takeoff_systems(self) - - if include_landing and self.post_mission_info['include_landing']: - self.builder.add_landing_systems(self) - self.model.add_subsystem('post_mission', self.post_mission, promotes_inputs=['*'], promotes_outputs=['*']) + self.builder.add_post_mission_systems(self, include_landing) + # Add all post-mission external subsystems. for external_subsystem in self.post_mission_info['external_subsystems']: subsystem_postmission = external_subsystem.build_post_mission( @@ -934,26 +897,6 @@ def add_post_mission_systems(self, include_landing=True): ('initial_mass', Mission.Summary.GROSS_MASS)], promotes_outputs=[("mass_resid", Mission.Constraints.MASS_RESIDUAL)]) - if self.mission_method in (HEIGHT_ENERGY, TWO_DEGREES_OF_FREEDOM): - self.post_mission.add_constraint( - Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) - - if self.mission_method is HEIGHT_ENERGY: - # connect summary mass to the initial guess of mass in the first phase - if not self.pre_mission_info['include_takeoff']: - first_flight_phase_name = list(self.phase_info.keys())[0] - eq = self.model.add_subsystem( - f'link_{first_flight_phase_name}_mass', om.EQConstraintComp(), - promotes_inputs=[('rhs:mass', Mission.Summary.GROSS_MASS)]) - eq.add_eq_output('mass', eq_units='lbm', normalize=False, - ref=100000., add_constraint=True) - self.model.connect( - f'traj.{first_flight_phase_name}.states:mass', - f'link_{first_flight_phase_name}_mass.lhs:mass', - src_indices=[0], - flat_src_indices=True, - ) - def _link_phases_helper_with_options(self, phases, option_name, var, **kwargs): # Initialize a list to keep track of indices where option_name is True true_option_indices = [] @@ -1864,21 +1807,6 @@ def _add_hybrid_objective(self, phase_info): self.model.connect(f"traj.{final_phase_name}.timeseries.time", "obj_comp.final_time", src_indices=[-1]) - def _add_vrotate_comp(self): - self.model.add_subsystem("vrot_comp", VRotateComp()) - self.model.connect('traj.groundroll.states:mass', - 'vrot_comp.mass', src_indices=om.slicer[0, ...]) - - vrot_eq_comp = self.model.add_subsystem("vrot_eq_comp", om.EQConstraintComp()) - vrot_eq_comp.add_eq_output( - "v_rotate_error", eq_units="kn", lhs_name="v_rot_computed", - rhs_name="groundroll_v_final", add_constraint=True) - - self.model.connect('vrot_comp.Vrot', 'vrot_eq_comp.v_rot_computed') - self.model.connect( - 'traj.groundroll.timeseries.velocity', 'vrot_eq_comp.groundroll_v_final', - src_indices=om.slicer[-1, ...]) - def _save_to_csv_file(self, filename): with open(filename, 'w', newline='') as csvfile: fieldnames = ['name', 'value', 'units'] diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index c70887eee..efd091d26 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -193,9 +193,6 @@ def add_takeoff_systems(self, prob): promotes_outputs=[('Vrot', Mission.Takeoff.ROTATION_VELOCITY)] ) - def add_post_mission_takeoff_systems(self, prob): - pass - def get_phase_builder(self, prob, phase_name, phase_options): if 'groundroll' in phase_name: @@ -325,6 +322,9 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): opt=False, ) + # TODO: This seems like a hack. We might want to find a better way. + prob.phase_info[phase_name]['phase_type'] = phase_name + def link_phases(self, prob, phases, direct_links=True): if prob.analysis_scheme is AnalysisScheme.COLLOCATION: @@ -469,7 +469,42 @@ def link_phases(self, prob, phases, direct_links=True): flat_src_indices=True, ) - def add_landing_systems(self, prob): + if prob.analysis_scheme is AnalysisScheme.COLLOCATION: + if 'ascent' in prob.phase_info: + self._add_groundroll_eq_constraint(prob) + + def _add_groundroll_eq_constraint(self, prob): + """ + Add an equality constraint to the problem to ensure that the TAS at the end of the + groundroll phase is equal to the rotation velocity at the start of the rotation phase. + """ + prob.model.add_subsystem( + "groundroll_boundary", + om.EQConstraintComp( + "velocity", + eq_units="ft/s", + normalize=True, + add_constraint=True, + ), + ) + prob.model.connect(Mission.Takeoff.ROTATION_VELOCITY, + "groundroll_boundary.rhs:velocity") + prob.model.connect( + "traj.groundroll.states:velocity", + "groundroll_boundary.lhs:velocity", + src_indices=[-1], + flat_src_indices=True, + ) + + def add_post_mission_systems(self, prob, include_landing=True): + + if include_landing and prob.post_mission_info['include_landing']: + self._add_landing_systems(prob) + + prob.post_mission.add_constraint( + Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) + + def _add_landing_systems(self, prob): prob.model.add_subsystem( "landing", diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 99d0eef62..c2bff31e1 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -208,7 +208,39 @@ def link_phases(self, prob, phases, direct_links=True): Mission.Summary.RANGE, src_indices=[-1], flat_src_indices=True) - def add_post_mission_takeoff_systems(self, prob): + def add_post_mission_systems(self, prob, include_landing=True): + + if prob.pre_mission_info['include_takeoff']: + self._add_post_mission_takeoff_systems(prob) + + if include_landing and prob.post_mission_info['include_landing']: + self._add_landing_systems(prob) + + prob.post_mission.add_constraint( + Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) + + # connect summary mass to the initial guess of mass in the first phase + if not prob.pre_mission_info['include_takeoff']: + + first_flight_phase_name = list(prob.phase_info.keys())[0] + + eq = prob.model.add_subsystem( + f'link_{first_flight_phase_name}_mass', + om.EQConstraintComp(), + promotes_inputs=[('rhs:mass', Mission.Summary.GROSS_MASS)], + ) + + eq.add_eq_output('mass', eq_units='lbm', normalize=False, + ref=100000., add_constraint=True) + + prob.model.connect( + f'traj.{first_flight_phase_name}.states:mass', + f'link_{first_flight_phase_name}_mass.lhs:mass', + src_indices=[0], + flat_src_indices=True, + ) + + def _add_post_mission_takeoff_systems(self, prob): first_flight_phase_name = list(prob.phase_info.keys())[0] connect_takeoff_to_climb = not prob.phase_info[first_flight_phase_name][ @@ -260,7 +292,7 @@ def add_post_mission_takeoff_systems(self, prob): prob.model.add_constraint( 'alt_diff_comp.altitude_resid_for_connecting_takeoff', equals=0.0) - def add_landing_systems(self, prob): + def _add_landing_systems(self, prob): landing_options = Landing( ref_wing_area=prob.aviary_inputs.get_val( diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index 2facff50e..ff02db532 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -128,10 +128,7 @@ def link_phases(self, prob, phases, direct_links=True): prob.traj.link_phases( phases[1:], ["alpha"], units='rad', connected=False) - def add_post_mission_takeoff_systems(self, prob): - pass - - def add_landing_systems(self, prob): + def add_post_mission_systems(self, prob, include_landing=True): pass def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_prefix): From f2595ed2e7d9e50e7d02c3d1ca7f43a96473e97b Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 29 Jan 2025 15:53:47 -0500 Subject: [PATCH 20/38] docstrings --- aviary/interface/methods_for_level2.py | 6 +- aviary/mission/problem_builder_2DOF.py | 98 +++++++++++++++-- .../mission/problem_builder_height_energy.py | 102 ++++++++++++++++-- aviary/mission/problem_builder_solved_2DOF.py | 102 +++++++++++++++++- 4 files changed, 286 insertions(+), 22 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index e5685dac6..75a0ae28c 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -159,6 +159,7 @@ def load_inputs( phase_info_module_path = Path.cwd() / 'outputted_phase_info.py' if phase_info_module_path.exists(): + spec = importlib.util.spec_from_file_location( 'outputted_phase_info', phase_info_module_path) outputted_phase_info = importlib.util.module_from_spec(spec) @@ -174,7 +175,7 @@ def load_inputs( print('Using outputted phase_info from current working directory') else: - phase_info = self.builder.phase_info_default_location(self) + phase_info = self.builder.get_default_phase_info(self) if verbosity is not None and verbosity >= Verbosity.BRIEF: print('Loaded default phase_info for ' @@ -555,6 +556,7 @@ def _get_phase(self, phase_name, phase_idx): user_options = AviaryValues(phase_options.get('user_options', ())) + # TODO: Should some of this stuff be moved into the phase builder? self.builder.set_phase_options(self, phase_name, phase_idx, phase, user_options) return phase @@ -989,7 +991,7 @@ def link_phases(self): if len(phases_to_link) > 1: # TODO: hack self.traj.link_phases(phases=phases_to_link, vars=[var], connected=True) - self.builder.link_phases(self, phases, direct_links=true_unless_mpi) + self.builder.link_phases(self, phases, connected=true_unless_mpi) def add_driver( self, optimizer=None, use_coloring=None, max_iter=50, diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index efd091d26..9aaf77351 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -30,6 +30,17 @@ class ProblemBuilder2DOF(): """ def initial_guesses(self, prob): + """ + Set any initial guesses for variables in the aviary problem. + + This is called at the end of AivaryProblem.load_inputs. + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + """ + # TODO: This should probably be moved to the set_initial_guesses() method in AviaryProblem class # Defines how the problem should build it's initial guesses for load_inputs() # this modifies mass_method, initialization_guesses, and aviary_values @@ -84,11 +95,29 @@ def initial_guesses(self, prob): prob.cruise_mach = aviary_inputs.get_val(Mission.Design.MACH) prob.require_range_residual = True - def phase_info_default_location(self, prob): - # Set the location of the default phase info for the EOM if no phase_info is specified + def get_default_phase_info(self, prob): + """ + Return a default phase_info for this type or problem. + + The default phase_info is used in the level 1 and 2 interfaces when no + phase_info is specified. + + This is called during load_inputs. + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + + Returns + ------- + AviaryValues + General default phase_info. + """ if prob.analysis_scheme is AnalysisScheme.COLLOCATION: from aviary.interface.default_phase_info.two_dof import phase_info + elif prob.analysis_scheme is AnalysisScheme.SHOOTING: from aviary.interface.default_phase_info.two_dof_fiti import phase_info, \ phase_info_parameterization @@ -194,6 +223,25 @@ def add_takeoff_systems(self, prob): ) def get_phase_builder(self, prob, phase_name, phase_options): + """ + Return a phase_builder for the requested phase. + + This is called from _get_phase in AviaryProblem.add_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phase_name : str + Name of the requested phase. + phase_options : dict + Phase options for the requested phase. + + Returns + ------- + PhaseBuilderBase + Phase builder for requested phase. + """ if 'groundroll' in phase_name: phase_builder = GroundrollPhase @@ -217,6 +265,24 @@ def get_phase_builder(self, prob, phase_name, phase_options): return phase_builder def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): + """ + Set any necessary problem-related options on the phase. + + This is called from _get_phase in AviaryProblem.add_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phase_name : str + Name of the requested phase. + phase_idx : int + Phase position in prob.phases. Can be used to identify first phase. + phase : Phase + Instantiated phase object. + user_options : dict + Subdictionary "user_options" from the phase_info. + """ try: fix_initial = user_options.get_val('fix_initial') @@ -325,7 +391,25 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): # TODO: This seems like a hack. We might want to find a better way. prob.phase_info[phase_name]['phase_type'] = phase_name - def link_phases(self, prob, phases, direct_links=True): + def link_phases(self, prob, phases, connected=True): + """ + Apply any additional phase linking. + + Note that some phase variables are handled in the AviaryProblem. Only + problem-specific ones need to be linked here. + + This is called from AviaryProblem.link_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phases : Phase + Phases to be linked. + connected : bool + When True, then connected=True. This allows the connections to be + handled by constraints if `phases` is a parallel group under MPI. + """ if prob.analysis_scheme is AnalysisScheme.COLLOCATION: for ii in range(len(phases) - 1): @@ -336,8 +420,8 @@ def link_phases(self, prob, phases, direct_links=True): if not (analytic1 or analytic2): # we always want time, distance, and mass to be continuous states_to_link = { - 'time': direct_links, - Dynamic.Mission.DISTANCE: direct_links, + 'time': connected, + Dynamic.Mission.DISTANCE: connected, Dynamic.Vehicle.MASS: False, } @@ -348,7 +432,7 @@ def link_phases(self, prob, phases, direct_links=True): if ((phase1 in prob.reserve_phases) == (phase2 in prob.reserve_phases)) and \ not ({"groundroll", "rotation"} & {phase1, phase2}) and \ not ('accel', 'climb1') == (phase1, phase2): # required for convergence of FwGm - states_to_link[Dynamic.Mission.ALTITUDE] = direct_links + states_to_link[Dynamic.Mission.ALTITUDE] = connected # if either phase is rotation, we need to connect velocity # ascent to accel also requires velocity @@ -356,7 +440,7 @@ def link_phases(self, prob, phases, direct_links=True): phase1, phase2) or ( 'ascent', 'accel') == ( phase1, phase2): - states_to_link[Dynamic.Mission.VELOCITY] = direct_links + states_to_link[Dynamic.Mission.VELOCITY] = connected # if the first phase is rotation, we also need alpha if phase1 == 'rotation': states_to_link['alpha'] = False diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index c2bff31e1..0c221464d 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -28,6 +28,17 @@ class ProblemBuilderHeightEnergy(): """ def initial_guesses(self, prob): + """ + Set any initial guesses for variables in the aviary problem. + + This is called at the end of AivaryProblem.load_inputs. + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + """ + # TODO: This should probably be moved to the set_initial_guesses() method in AviaryProblem class # Defines how the problem should build it's initial guesses for load_inputs() # this modifies mass_method, initialization_guesses, and aviary_values @@ -68,11 +79,29 @@ def initial_guesses(self, prob): prob.target_range = aviary_inputs.get_val( Mission.Design.RANGE, units='NM') - def phase_info_default_location(self, prob): - # Set the location of the default phase info for the EOM if no phase_info is specified + def get_default_phase_info(self, prob): + """ + Return a default phase_info for this type or problem. + + The default phase_info is used in the level 1 and 2 interfaces when no + phase_info is specified. + + This is called during load_inputs. + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + + Returns + ------- + AviaryValues + General default phase_info. + """ if prob.analysis_scheme is AnalysisScheme.COLLOCATION: from aviary.interface.default_phase_info.height_energy import phase_info + else: + raise RuntimeError("Height Energy requires that a phase_info is specified.") return phase_info @@ -93,6 +122,25 @@ def add_takeoff_systems(self, prob): ) def get_phase_builder(self, prob, phase_name, phase_options): + """ + Return a phase_builder for the requested phase. + + This is called from _get_phase in AviaryProblem.add_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phase_name : str + Name of the requested phase. + phase_options : dict + Phase options for the requested phase. + + Returns + ------- + PhaseBuilderBase + Phase builder for requested phase. + """ if 'phase_builder' in phase_options: phase_builder = phase_options['phase_builder'] @@ -105,6 +153,24 @@ def get_phase_builder(self, prob, phase_name, phase_options): return phase_builder def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): + """ + Set any necessary problem-related options on the phase. + + This is called from _get_phase in AviaryProblem.add_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phase_name : str + Name of the requested phase. + phase_idx : int + Phase position in prob.phases. Can be used to identify first phase. + phase : Phase + Instantiated phase object. + user_options : dict + Subdictionary "user_options" from the phase_info. + """ try: fix_initial = user_options.get_val('fix_initial') @@ -168,7 +234,25 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): initial_ref=user_options.get_val("initial_ref", time_units), ) - def link_phases(self, prob, phases, direct_links=True): + def link_phases(self, prob, phases, connected=True): + """ + Apply any additional phase linking. + + Note that some phase variables are handled in the AviaryProblem. Only + problem-specific ones need to be linked here. + + This is called from AviaryProblem.link_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phases : Phase + Phases to be linked. + connected : bool + When True, then connected=True. This allows the connections to be + handled by constraints if `phases` is a parallel group under MPI. + """ # connect regular_phases with each other if you are optimizing alt or mach prob._link_phases_helper_with_options( @@ -195,14 +279,14 @@ def link_phases(self, prob, phases, direct_links=True): # connect mass and distance between all phases regardless of reserve / # non-reserve status prob.traj.link_phases(phases, ["time"], - ref=None if direct_links else 1e3, - connected=direct_links) + ref=None if connected else 1e3, + connected=connected) prob.traj.link_phases(phases, [Dynamic.Vehicle.MASS], - ref=None if direct_links else 1e6, - connected=direct_links) + ref=None if connected else 1e6, + connected=connected) prob.traj.link_phases(phases, [Dynamic.Mission.DISTANCE], - ref=None if direct_links else 1e3, - connected=direct_links) + ref=None if connected else 1e3, + connected=connected) prob.model.connect(f'traj.{prob.regular_phases[-1]}.timeseries.distance', Mission.Summary.RANGE, diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index ff02db532..d61fef818 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -12,19 +12,63 @@ class ProblemBuilderSolved2DOF(): """ def initial_guesses(self, prob): + """ + Set any initial guesses for variables in the aviary problem. + + This is called at the end of AivaryProblem.load_inputs. + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + """ if prob.engine_builders is None: prob.engine_builders = build_engine_deck(prob.aviary_inputs) - pass + def get_default_phase_info(self, prob): + """ + Return a default phase_info for this type or problem. - def phase_info_default_location(self, prob): - pass + The default phase_info is used in the level 1 and 2 interfaces when no + phase_info is specified. + + This is called during load_inputs. + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + + Returns + ------- + AviaryValues + General default phase_info. + """ + raise RuntimeError("Solved 2DOF requires that a phase_info is specified.") def add_takeoff_systems(self, prob): pass def get_phase_builder(self, prob, phase_name, phase_options): + """ + Return a phase_builder for the requested phase. + + This is called from _get_phase in AviaryProblem.add_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phase_name : str + Name of the requested phase. + phase_options : dict + Phase options for the requested phase. + + Returns + ------- + PhaseBuilderBase + Phase builder for requested phase. + """ if phase_options['user_options']['ground_roll'] and phase_options['user_options']['fix_initial']: phase_builder = GroundrollPhaseVelocityIntegrated @@ -34,6 +78,24 @@ def get_phase_builder(self, prob, phase_name, phase_options): return phase_builder def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): + """ + Set any necessary problem-related options on the phase. + + This is called from _get_phase in AviaryProblem.add_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phase_name : str + Name of the requested phase. + phase_idx : int + Phase position in prob.phases. Can be used to identify first phase. + phase : Phase + Instantiated phase object. + user_options : dict + Subdictionary "user_options" from the phase_info. + """ try: fix_initial = user_options.get_val('fix_initial') @@ -94,7 +156,25 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): initial_ref=user_options.get_val("initial_ref", time_units), ) - def link_phases(self, prob, phases, direct_links=True): + def link_phases(self, prob, phases, connected=True): + """ + Apply any additional phase linking. + + Note that some phase variables are handled in the AviaryProblem. Only + problem-specific ones need to be linked here. + + This is called from AviaryProblem.link_phases + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + phases : Phase + Phases to be linked. + connected : bool + When True, then connected=True. This allows the connections to be + handled by constraints if `phases` is a parallel group under MPI. + """ # connect regular_phases with each other if you are optimizing alt or mach prob._link_phases_helper_with_options( @@ -129,6 +209,20 @@ def link_phases(self, prob, phases, direct_links=True): phases[1:], ["alpha"], units='rad', connected=False) def add_post_mission_systems(self, prob, include_landing=True): + """ + Apply any post mission systems. + + These may include any post-mission take off and landing systems. + + This is called from AviaryProblem.add_post_mission_systems + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + include_landing : bool + When True, include the landing systems. + """ pass def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_prefix): From da873e07ddc218242b288030c56ffbdba0d69b8b Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 29 Jan 2025 15:54:21 -0500 Subject: [PATCH 21/38] docstrings --- aviary/mission/problem_builder_2DOF.py | 14 ++++++++++++++ aviary/mission/problem_builder_height_energy.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 9aaf77351..f922fbb13 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -581,6 +581,20 @@ def _add_groundroll_eq_constraint(self, prob): ) def add_post_mission_systems(self, prob, include_landing=True): + """ + Apply any post mission systems. + + These may include any post-mission take off and landing systems. + + This is called from AviaryProblem.add_post_mission_systems + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + include_landing : bool + When True, include the landing systems. + """ if include_landing and prob.post_mission_info['include_landing']: self._add_landing_systems(prob) diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 0c221464d..8d0b5f90e 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -293,6 +293,20 @@ def link_phases(self, prob, phases, connected=True): src_indices=[-1], flat_src_indices=True) def add_post_mission_systems(self, prob, include_landing=True): + """ + Apply any post mission systems. + + These may include any post-mission take off and landing systems. + + This is called from AviaryProblem.add_post_mission_systems + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + include_landing : bool + When True, include the landing systems. + """ if prob.pre_mission_info['include_takeoff']: self._add_post_mission_takeoff_systems(prob) From 8e0b3d3da2bfc8d167992396fc8cec0ac646c0c6 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 29 Jan 2025 15:56:01 -0500 Subject: [PATCH 22/38] docstrings --- aviary/mission/problem_builder_2DOF.py | 2 +- aviary/mission/problem_builder_height_energy.py | 2 +- aviary/mission/problem_builder_solved_2DOF.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index f922fbb13..3347c277b 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -582,7 +582,7 @@ def _add_groundroll_eq_constraint(self, prob): def add_post_mission_systems(self, prob, include_landing=True): """ - Apply any post mission systems. + Add any post mission systems. These may include any post-mission take off and landing systems. diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 8d0b5f90e..a8bb37f8f 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -294,7 +294,7 @@ def link_phases(self, prob, phases, connected=True): def add_post_mission_systems(self, prob, include_landing=True): """ - Apply any post mission systems. + Add any post mission systems. These may include any post-mission take off and landing systems. diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index d61fef818..51e4fe227 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -210,7 +210,7 @@ def link_phases(self, prob, phases, connected=True): def add_post_mission_systems(self, prob, include_landing=True): """ - Apply any post mission systems. + Add any post mission systems. These may include any post-mission take off and landing systems. From 13eeed68616fd42222b41447a1330c856dc420c5 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Wed, 29 Jan 2025 16:04:59 -0500 Subject: [PATCH 23/38] updated self ref to prob in HE builder --- aviary/mission/problem_builder_height_energy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index fa7c0cb2d..04f77e304 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -339,12 +339,12 @@ def add_post_mission_systems(self, prob, include_landing=True): flat_src_indices=True, ) - self.model.add_subsystem( + prob.model.add_subsystem( "range_constraint", om.ExecComp( "range_resid = target_range - actual_range", - target_range={"val": self.target_range, "units": "NM"}, - actual_range={"val": self.target_range, "units": "NM"}, + target_range={"val": prob.target_range, "units": "NM"}, + actual_range={"val": prob.target_range, "units": "NM"}, range_resid={"val": 30, "units": "NM"}, ), promotes_inputs=[ @@ -355,7 +355,7 @@ def add_post_mission_systems(self, prob, include_landing=True): ("range_resid", Mission.Constraints.RANGE_RESIDUAL)], ) - self.post_mission.add_constraint( + prob.post_mission.add_constraint( Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) def _add_post_mission_takeoff_systems(self, prob): From 564c3fd7122d6f7feef1dabfc52518f989b186a0 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Wed, 29 Jan 2025 16:07:42 -0500 Subject: [PATCH 24/38] removed second call of add_post_mission_systems() in m4l2 --- aviary/interface/methods_for_level2.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index a83a08a61..dc8938f2a 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -927,8 +927,6 @@ def add_post_mission_systems(self, include_landing=True): promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], ) - self.builder.add_post_mission_systems(self) - def _link_phases_helper_with_options(self, phases, option_name, var, **kwargs): # Initialize a list to keep track of indices where option_name is True true_option_indices = [] From c310032414b3d0ba2dcc90587cd409f5394593b7 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 29 Jan 2025 16:49:42 -0500 Subject: [PATCH 25/38] Have a regression --- aviary/interface/methods_for_level2.py | 16 ---------------- aviary/mission/problem_builder_2DOF.py | 17 +++++++++++++++-- .../mission/problem_builder_height_energy.py | 19 ++++++++++++++++--- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index dc8938f2a..e41d378ae 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -911,22 +911,6 @@ def add_post_mission_systems(self, include_landing=True): promotes_outputs=[("reg_objective", Mission.Objectives.FUEL)], ) - self.model.add_subsystem( - "range_obj", - om.ExecComp( - "reg_objective = -actual_range/1000 + ascent_duration/30.", - reg_objective={"val": 0.0, "units": "unitless"}, - ascent_duration={"units": "s", "shape": 1}, - actual_range={ - "val": self.target_range, "units": "NM"}, - ), - promotes_inputs=[ - ("actual_range", Mission.Summary.RANGE), - ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), - ], - promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], - ) - def _link_phases_helper_with_options(self, phases, option_name, var, **kwargs): # Initialize a list to keep track of indices where option_name is True true_option_indices = [] diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 7bf7237ea..534802092 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -600,8 +600,21 @@ def add_post_mission_systems(self, prob, include_landing=True): if include_landing and prob.post_mission_info['include_landing']: self._add_landing_systems(prob) - prob.post_mission.add_constraint( - Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) + prob.model.add_subsystem( + "range_obj", + om.ExecComp( + "reg_objective = -actual_range/1000 + ascent_duration/30.", + reg_objective={"val": 0.0, "units": "unitless"}, + ascent_duration={"units": "s", "shape": 1}, + actual_range={ + "val": prob.target_range, "units": "NM"}, + ), + promotes_inputs=[ + ("actual_range", Mission.Summary.RANGE), + ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), + ], + promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], + ) if prob.analysis_scheme is AnalysisScheme.COLLOCATION: ascent_phase = getattr(prob.traj.phases, 'ascent') diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 04f77e304..3bb80ae87 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -315,9 +315,6 @@ def add_post_mission_systems(self, prob, include_landing=True): if include_landing and prob.post_mission_info['include_landing']: self._add_landing_systems(prob) - prob.post_mission.add_constraint( - Mission.Constraints.MASS_RESIDUAL, equals=0.0, ref=1.e5) - # connect summary mass to the initial guess of mass in the first phase if not prob.pre_mission_info['include_takeoff']: @@ -339,6 +336,22 @@ def add_post_mission_systems(self, prob, include_landing=True): flat_src_indices=True, ) + prob.model.add_subsystem( + "range_obj", + om.ExecComp( + "reg_objective = -actual_range/1000 + ascent_duration/30.", + reg_objective={"val": 0.0, "units": "unitless"}, + ascent_duration={"units": "s", "shape": 1}, + actual_range={ + "val": prob.target_range, "units": "NM"}, + ), + promotes_inputs=[ + ("actual_range", Mission.Summary.RANGE), + ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), + ], + promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], + ) + prob.model.add_subsystem( "range_constraint", om.ExecComp( From e413c00127fb45c7ffcbba649c23dcd8d30f45f6 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 29 Jan 2025 17:09:42 -0500 Subject: [PATCH 26/38] pep --- aviary/mission/problem_builder_2DOF.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 534802092..ae63dc755 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -331,7 +331,6 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): else: - input_initial = False time_units = phase.time_options['units'] # Make a good guess for a reasonable intitial time scaler. @@ -352,8 +351,7 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): 'duration_ref', (duration_bounds[0] + duration_bounds[1]) / 2., time_units ) - if phase_idx > 0: - input_initial = True + input_initial = phase_idx > 0 if fix_initial or input_initial: From 7cbb86f5953000937cb8e9db210dfb765bf704aa Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 29 Jan 2025 18:01:56 -0500 Subject: [PATCH 27/38] Fixed regression --- aviary/interface/methods_for_level2.py | 2 +- aviary/mission/problem_builder_2DOF.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index e41d378ae..fb0f6518d 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -1003,7 +1003,7 @@ def link_phases(self): if len(phases_to_link) > 1: # TODO: hack self.traj.link_phases(phases=phases_to_link, vars=[var], connected=True) - self.builder.link_phases(self, phases, connected=true_unless_mpi) + self.builder.link_phases(self, phases, connect_directly=true_unless_mpi) def add_driver( self, optimizer=None, use_coloring=None, max_iter=50, diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index ae63dc755..826c2f101 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -390,7 +390,7 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): # TODO: This seems like a hack. We might want to find a better way. prob.phase_info[phase_name]['phase_type'] = phase_name - def link_phases(self, prob, phases, connected=True): + def link_phases(self, prob, phases, connect_directly=True): """ Apply any additional phase linking. @@ -405,7 +405,7 @@ def link_phases(self, prob, phases, connected=True): Problem that owns this builder. phases : Phase Phases to be linked. - connected : bool + connect_directly : bool When True, then connected=True. This allows the connections to be handled by constraints if `phases` is a parallel group under MPI. """ @@ -419,8 +419,8 @@ def link_phases(self, prob, phases, connected=True): if not (analytic1 or analytic2): # we always want time, distance, and mass to be continuous states_to_link = { - 'time': connected, - Dynamic.Mission.DISTANCE: connected, + 'time': connect_directly, + Dynamic.Mission.DISTANCE: connect_directly, Dynamic.Vehicle.MASS: False, } @@ -431,7 +431,7 @@ def link_phases(self, prob, phases, connected=True): if ((phase1 in prob.reserve_phases) == (phase2 in prob.reserve_phases)) and \ not ({"groundroll", "rotation"} & {phase1, phase2}) and \ not ('accel', 'climb1') == (phase1, phase2): # required for convergence of FwGm - states_to_link[Dynamic.Mission.ALTITUDE] = connected + states_to_link[Dynamic.Mission.ALTITUDE] = connect_directly # if either phase is rotation, we need to connect velocity # ascent to accel also requires velocity @@ -439,7 +439,7 @@ def link_phases(self, prob, phases, connected=True): phase1, phase2) or ( 'ascent', 'accel') == ( phase1, phase2): - states_to_link[Dynamic.Mission.VELOCITY] = connected + states_to_link[Dynamic.Mission.VELOCITY] = connect_directly # if the first phase is rotation, we also need alpha if phase1 == 'rotation': states_to_link['alpha'] = False From c4a132a2edea612ae24ca24d2b8767248bfcf295 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Thu, 30 Jan 2025 13:50:25 -0500 Subject: [PATCH 28/38] moved fuel_obj and range_obj execomp back into m4l2 since they should be calculated no matter the problem. placed them in the add_objective() definition for eas of finding them in the future --- aviary/interface/methods_for_level2.py | 48 ++++++++++++------- aviary/mission/problem_builder_2DOF.py | 16 ------- .../mission/problem_builder_height_energy.py | 16 ------- 3 files changed, 32 insertions(+), 48 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index fb0f6518d..38fa46bbf 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -895,22 +895,6 @@ def add_post_mission_systems(self, include_landing=True): ('initial_mass', Mission.Summary.GROSS_MASS)], promotes_outputs=[("mass_resid", Mission.Constraints.MASS_RESIDUAL)]) - # Objectives should be calculated here - self.model.add_subsystem( - "fuel_obj", - om.ExecComp( - "reg_objective = overall_fuel/10000 + ascent_duration/30.", - reg_objective={"val": 0.0, "units": "unitless"}, - ascent_duration={"units": "s", "shape": 1}, - overall_fuel={"units": "lbm"}, - ), - promotes_inputs=[ - ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), - ("overall_fuel", Mission.Summary.TOTAL_FUEL_MASS), - ], - promotes_outputs=[("reg_objective", Mission.Objectives.FUEL)], - ) - def _link_phases_helper_with_options(self, phases, option_name, var, **kwargs): # Initialize a list to keep track of indices where option_name is True true_option_indices = [] @@ -1291,6 +1275,38 @@ def add_objective(self, objective_type=None, ref=None): ValueError: If an invalid problem type is provided. """ + + self.model.add_subsystem( + "fuel_obj", + om.ExecComp( + "reg_objective = overall_fuel/10000 + ascent_duration/30.", + reg_objective={"val": 0.0, "units": "unitless"}, + ascent_duration={"units": "s", "shape": 1}, + overall_fuel={"units": "lbm"}, + ), + promotes_inputs=[ + ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), + ("overall_fuel", Mission.Summary.TOTAL_FUEL_MASS), + ], + promotes_outputs=[("reg_objective", Mission.Objectives.FUEL)], + ) + + self.model.add_subsystem( + "range_obj", + om.ExecComp( + "reg_objective = -actual_range/1000 + ascent_duration/30.", + reg_objective={"val": 0.0, "units": "unitless"}, + ascent_duration={"units": "s", "shape": 1}, + actual_range={ + "val": self.target_range, "units": "NM"}, + ), + promotes_inputs=[ + ("actual_range", Mission.Summary.RANGE), + ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), + ], + promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], + ) + # Dictionary for default reference values default_ref_values = { 'mass': -5e4, diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 826c2f101..b8c88db23 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -598,22 +598,6 @@ def add_post_mission_systems(self, prob, include_landing=True): if include_landing and prob.post_mission_info['include_landing']: self._add_landing_systems(prob) - prob.model.add_subsystem( - "range_obj", - om.ExecComp( - "reg_objective = -actual_range/1000 + ascent_duration/30.", - reg_objective={"val": 0.0, "units": "unitless"}, - ascent_duration={"units": "s", "shape": 1}, - actual_range={ - "val": prob.target_range, "units": "NM"}, - ), - promotes_inputs=[ - ("actual_range", Mission.Summary.RANGE), - ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), - ], - promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], - ) - if prob.analysis_scheme is AnalysisScheme.COLLOCATION: ascent_phase = getattr(prob.traj.phases, 'ascent') ascent_tx = ascent_phase.options["transcription"] diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 3bb80ae87..60105dc8d 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -336,22 +336,6 @@ def add_post_mission_systems(self, prob, include_landing=True): flat_src_indices=True, ) - prob.model.add_subsystem( - "range_obj", - om.ExecComp( - "reg_objective = -actual_range/1000 + ascent_duration/30.", - reg_objective={"val": 0.0, "units": "unitless"}, - ascent_duration={"units": "s", "shape": 1}, - actual_range={ - "val": prob.target_range, "units": "NM"}, - ), - promotes_inputs=[ - ("actual_range", Mission.Summary.RANGE), - ("ascent_duration", Mission.Takeoff.ASCENT_DURATION), - ], - promotes_outputs=[("reg_objective", Mission.Objectives.RANGE)], - ) - prob.model.add_subsystem( "range_constraint", om.ExecComp( From ce5ce303c37ce285efc9ee0ddef2128b711c105c Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Thu, 30 Jan 2025 14:13:10 -0500 Subject: [PATCH 29/38] moved HE specific set_state_options from m4l2.add_phases() into builder.add_guesses() --- aviary/interface/methods_for_level2.py | 8 -------- aviary/mission/problem_builder_height_energy.py | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 38fa46bbf..8b3fb51c5 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -685,14 +685,6 @@ def add_subsystem_timeseries_outputs(phase, phase_name): self.model, traj, self.aviary_inputs, phases, meta_data=self.meta_data, external_parameters=external_parameters) - if self.mission_method is HEIGHT_ENERGY: - if not self.pre_mission_info['include_takeoff']: - first_flight_phase_name = list(phase_info.keys())[0] - first_flight_phase = traj._phases[first_flight_phase_name] - first_flight_phase.set_state_options( - Dynamic.Vehicle.MASS, fix_initial=False - ) - self.traj = traj return traj diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 60105dc8d..1283c3092 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -584,3 +584,10 @@ def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_pref # Set the mass guess as the initial value for the mass state variable target_prob.set_val(parent_prefix + f'traj.{phase_name}.states:mass', mass_guess, units='lbm') + + if not self.pre_mission_info['include_takeoff']: + first_flight_phase_name = list(prob.phase_info.keys())[0] + first_flight_phase = prob.traj._phases[first_flight_phase_name] + first_flight_phase.set_state_options( + Dynamic.Vehicle.MASS, fix_initial=False + ) From bd9db8d16ad5a4d84853066d9b25c48c74fda593 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Thu, 30 Jan 2025 14:26:58 -0500 Subject: [PATCH 30/38] moved default payload mass for alternative missions out of m4l2 and into builders --- aviary/interface/methods_for_level2.py | 6 ++---- aviary/mission/problem_builder_2DOF.py | 6 ++++++ aviary/mission/problem_builder_height_energy.py | 8 ++++++++ aviary/mission/problem_builder_solved_2DOF.py | 4 ++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 8b3fb51c5..eec3be83c 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -1732,11 +1732,9 @@ def fallout_mission(self, run_mission=True, phase_info = self.phase_info if mission_mass is None: mission_mass = self.get_val(Mission.Design.GROSS_MASS) + if payload_mass is None: - if self.mission_method is HEIGHT_ENERGY: - payload_mass = self.get_val(Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS) - elif self.mission_method is TWO_DEGREES_OF_FREEDOM: - payload_mass = self.get_val(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS) + payload_mass = self.builder.get_default_payload_mass(self) design_range = self.get_val(Mission.Design.RANGE) optimizer = self.driver.options["optimizer"] diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index b8c88db23..4a9dc281c 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -127,6 +127,12 @@ def get_default_phase_info(self, prob): return phase_info + def get_default_payload_mass(self, prob): + + payload_mass = self.get_val(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS) + + return payload_mass + def add_takeoff_systems(self, prob): # Create options to values OptionsToValues = create_opts2vals( diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 1283c3092..8a1f94cfe 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -106,6 +106,14 @@ def get_default_phase_info(self, prob): return phase_info + def get_default_payload_mass(self, prob): + + payload_mass = prob.get_val(Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS) + + payload_mass = self.get_val(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS) + + return payload_mass + def add_takeoff_systems(self, prob): # Initialize takeoff options takeoff_options = Takeoff( diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index 51e4fe227..96cbb69b1 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -46,6 +46,10 @@ def get_default_phase_info(self, prob): """ raise RuntimeError("Solved 2DOF requires that a phase_info is specified.") + def get_default_payload_mass(self, prob): + + return None + def add_takeoff_systems(self, prob): pass From e0a763f88ea6e0029ca74cd2ddf03caaaab6b9f7 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Thu, 30 Jan 2025 14:27:33 -0500 Subject: [PATCH 31/38] moved default payload mass for alternative missions out of m4l2 and into builders2, removed extra line --- aviary/mission/problem_builder_height_energy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 8a1f94cfe..092643473 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -110,8 +110,6 @@ def get_default_payload_mass(self, prob): payload_mass = prob.get_val(Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS) - payload_mass = self.get_val(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS) - return payload_mass def add_takeoff_systems(self, prob): From f9f4c3d59f6272400d715434e0b251115725b433 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Thu, 30 Jan 2025 14:39:31 -0500 Subject: [PATCH 32/38] moved everything_else_origin into the individual builders as get_computed_defaults() --- aviary/interface/methods_for_level2.py | 10 +--------- aviary/mission/problem_builder_2DOF.py | 9 +++++++-- aviary/mission/problem_builder_height_energy.py | 9 ++++++--- aviary/mission/problem_builder_solved_2DOF.py | 6 ++++++ 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index eec3be83c..d062f09bc 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -286,17 +286,10 @@ def check_and_preprocess_inputs(self): # Fill in anything missing in the options with computed defaults. preprocess_options(aviary_inputs, engine_models=self.engine_builders) - mission_method = aviary_inputs.get_val(Settings.EQUATIONS_OF_MOTION) mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) ## Set Up Core Subsystems ## - if mission_method in (HEIGHT_ENERGY, SOLVED_2DOF): - everything_else_origin = FLOPS - elif mission_method is TWO_DEGREES_OF_FREEDOM: - everything_else_origin = GASP - else: - # Custom - everything_else_origin = None + everything_else_origin = self.builder.get_computed_defaults(self) prop = CorePropulsionBuilder( 'core_propulsion', engine_models=self.engine_builders) @@ -1732,7 +1725,6 @@ def fallout_mission(self, run_mission=True, phase_info = self.phase_info if mission_mass is None: mission_mass = self.get_val(Mission.Design.GROSS_MASS) - if payload_mass is None: payload_mass = self.builder.get_default_payload_mass(self) diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 4a9dc281c..323caab21 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -17,8 +17,8 @@ from aviary.utils.functions import create_opts2vals, add_opts2vals, wrapped_convert_units from aviary.utils.process_input_decks import initialization_guessing -from aviary.variable_info.enums import AnalysisScheme -from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings +from aviary.variable_info.enums import AnalysisScheme, LegacyCode +from aviary.variable_info.variables import Aircraft, Mission, Dynamic from aviary.utils.process_input_decks import update_GASP_options from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.mission.gasp_based.polynomial_fit import PolynomialFit @@ -133,6 +133,11 @@ def get_default_payload_mass(self, prob): return payload_mass + def get_computed_defaults(self, prob): + # Fill in anything missing in the options with computed defaults. + + return LegacyCode.GASP + def add_takeoff_systems(self, prob): # Create options to values OptionsToValues = create_opts2vals( diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 092643473..42d55a436 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -10,11 +10,9 @@ from aviary.mission.phase_builder_base import PhaseBuilderBase from aviary.utils.functions import wrapped_convert_units from aviary.utils.process_input_decks import initialization_guessing -from aviary.variable_info.enums import AnalysisScheme -from aviary.variable_info.enums import LegacyCode +from aviary.variable_info.enums import AnalysisScheme, LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings from aviary.subsystems.propulsion.utils import build_engine_deck -from aviary.mission.gasp_based.polynomial_fit import PolynomialFit if hasattr(TranscriptionBase, 'setup_polynomial_controls'): use_new_dymos_syntax = False @@ -112,6 +110,11 @@ def get_default_payload_mass(self, prob): return payload_mass + def get_computed_defaults(self, prob): + # Fill in anything missing in the options with computed defaults. + + return LegacyCode.FLOPS + def add_takeoff_systems(self, prob): # Initialize takeoff options takeoff_options = Takeoff( diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index 96cbb69b1..733215fc8 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -4,6 +4,7 @@ from aviary.utils.functions import wrapped_convert_units from aviary.variable_info.variables import Dynamic from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.variable_info.enums import LegacyCode class ProblemBuilderSolved2DOF(): @@ -50,6 +51,11 @@ def get_default_payload_mass(self, prob): return None + def get_computed_defaults(self, prob): + # Fill in anything missing in the options with computed defaults. + + return LegacyCode.FLOPS + def add_takeoff_systems(self, prob): pass From 7a43f6423c6cad99b0fe1f25680cc322bbf7a04e Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Thu, 30 Jan 2025 15:04:17 -0500 Subject: [PATCH 33/38] simplified references to mass_method --- aviary/interface/methods_for_level2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index d062f09bc..0ef59c017 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -286,8 +286,6 @@ def check_and_preprocess_inputs(self): # Fill in anything missing in the options with computed defaults. preprocess_options(aviary_inputs, engine_models=self.engine_builders) - mass_method = aviary_inputs.get_val(Settings.MASS_METHOD) - ## Set Up Core Subsystems ## everything_else_origin = self.builder.get_computed_defaults(self) @@ -303,9 +301,9 @@ def check_and_preprocess_inputs(self): # which geometry methods should be used, or both? geom_code_origin = None - if (everything_else_origin is FLOPS) and (mass_method is FLOPS): + if (everything_else_origin is FLOPS) and (self.mass_method is FLOPS): geom_code_origin = FLOPS - elif (everything_else_origin is GASP) and (mass_method is GASP): + elif (everything_else_origin is GASP) and (self.mass_method is GASP): geom_code_origin = GASP else: both_geom = True @@ -866,6 +864,8 @@ def add_post_mission_systems(self, include_landing=True): initial_mass={'units': 'lbm'}, mass_resid={'units': 'lbm'}) + # TBD: This should be removed and updated with get_default_payload_mass() + # # HEIGHT_ENERGY if self.mass_method is GASP: payload_mass_src = Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS else: From c808953d3b14c14c6503d703e01806edef35a318 Mon Sep 17 00:00:00 2001 From: Eliot Aretskin-Hariton Date: Thu, 30 Jan 2025 16:00:24 -0500 Subject: [PATCH 34/38] tried to fix up mass_method and get_default_payload_mass() but something probably isnt quite right in there. alternative_mission was calling mass_methods based on ODE, but add_post_mission_systems was calling based on mass_method!! --- aviary/interface/methods_for_level2.py | 10 +++++----- aviary/mission/problem_builder_2DOF.py | 2 +- aviary/mission/problem_builder_height_energy.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 0ef59c017..196776f2d 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -866,6 +866,9 @@ def add_post_mission_systems(self, include_landing=True): # TBD: This should be removed and updated with get_default_payload_mass() # # HEIGHT_ENERGY + # This is very strange code. Are we saying that HE doesn't work with GASP? + # Or is there an oversite in alternative mission and fallout mission formulations + # where get_default_payload_mass was being called based on mission method??? if self.mass_method is GASP: payload_mass_src = Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS else: @@ -1667,10 +1670,7 @@ def alternate_mission(self, run_mission=True, if mission_range is None: design_range = self.get_val(Mission.Design.RANGE) if payload_mass is None: - if self.mission_method is HEIGHT_ENERGY: - payload_mass = self.get_val(Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS) - elif self.mission_method is TWO_DEGREES_OF_FREEDOM: - payload_mass = self.get_val(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS) + payload_mass = self.get_val(self.builder.get_default_payload_mass(self)) mission_mass = self.get_val(Mission.Design.GROSS_MASS) optimizer = self.driver.options["optimizer"] @@ -1726,7 +1726,7 @@ def fallout_mission(self, run_mission=True, if mission_mass is None: mission_mass = self.get_val(Mission.Design.GROSS_MASS) if payload_mass is None: - payload_mass = self.builder.get_default_payload_mass(self) + payload_mass = self.get_val(self.builder.get_default_payload_mass(self)) design_range = self.get_val(Mission.Design.RANGE) optimizer = self.driver.options["optimizer"] diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 323caab21..6ee510476 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -129,7 +129,7 @@ def get_default_phase_info(self, prob): def get_default_payload_mass(self, prob): - payload_mass = self.get_val(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS) + payload_mass = Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS return payload_mass diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 42d55a436..471dbc9c6 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -106,7 +106,7 @@ def get_default_phase_info(self, prob): def get_default_payload_mass(self, prob): - payload_mass = prob.get_val(Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS) + payload_mass = Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS return payload_mass From 3bbd3b4fae9684a358b8133aa632ad17ec8d9ce4 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 31 Jan 2025 13:40:27 -0500 Subject: [PATCH 35/38] Fixed a few things --- .../mission/problem_builder_height_energy.py | 30 +++++++++---------- aviary/mission/problem_builder_solved_2DOF.py | 4 +-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index 471dbc9c6..e0c20e77f 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -244,7 +244,7 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): initial_ref=user_options.get_val("initial_ref", time_units), ) - def link_phases(self, prob, phases, connected=True): + def link_phases(self, prob, phases, connect_directly=True): """ Apply any additional phase linking. @@ -259,7 +259,7 @@ def link_phases(self, prob, phases, connected=True): Problem that owns this builder. phases : Phase Phases to be linked. - connected : bool + connect_directly : bool When True, then connected=True. This allows the connections to be handled by constraints if `phases` is a parallel group under MPI. """ @@ -289,19 +289,26 @@ def link_phases(self, prob, phases, connected=True): # connect mass and distance between all phases regardless of reserve / # non-reserve status prob.traj.link_phases(phases, ["time"], - ref=None if connected else 1e3, - connected=connected) + ref=None if connect_directly else 1e3, + connected=connect_directly) prob.traj.link_phases(phases, [Dynamic.Vehicle.MASS], - ref=None if connected else 1e6, - connected=connected) + ref=None if connect_directly else 1e6, + connected=connect_directly) prob.traj.link_phases(phases, [Dynamic.Mission.DISTANCE], - ref=None if connected else 1e3, - connected=connected) + ref=None if connect_directly else 1e3, + connected=connect_directly) prob.model.connect(f'traj.{prob.regular_phases[-1]}.timeseries.distance', Mission.Summary.RANGE, src_indices=[-1], flat_src_indices=True) + if not prob.pre_mission_info['include_takeoff']: + first_flight_phase_name = list(prob.phase_info.keys())[0] + first_flight_phase = prob.traj._phases[first_flight_phase_name] + first_flight_phase.set_state_options( + Dynamic.Vehicle.MASS, fix_initial=False + ) + def add_post_mission_systems(self, prob, include_landing=True): """ Add any post mission systems. @@ -593,10 +600,3 @@ def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_pref # Set the mass guess as the initial value for the mass state variable target_prob.set_val(parent_prefix + f'traj.{phase_name}.states:mass', mass_guess, units='lbm') - - if not self.pre_mission_info['include_takeoff']: - first_flight_phase_name = list(prob.phase_info.keys())[0] - first_flight_phase = prob.traj._phases[first_flight_phase_name] - first_flight_phase.set_state_options( - Dynamic.Vehicle.MASS, fix_initial=False - ) diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index 733215fc8..388dcb9f3 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -166,7 +166,7 @@ def set_phase_options(self, prob, phase_name, phase_idx, phase, user_options): initial_ref=user_options.get_val("initial_ref", time_units), ) - def link_phases(self, prob, phases, connected=True): + def link_phases(self, prob, phases, connect_directly=True): """ Apply any additional phase linking. @@ -181,7 +181,7 @@ def link_phases(self, prob, phases, connected=True): Problem that owns this builder. phases : Phase Phases to be linked. - connected : bool + connect_directly : bool When True, then connected=True. This allows the connections to be handled by constraints if `phases` is a parallel group under MPI. """ From c4abe3ee309ab0df2cd8191178474ad87a7e4df7 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 31 Jan 2025 15:46:46 -0500 Subject: [PATCH 36/38] Fixed a few regressions. --- aviary/interface/methods_for_level2.py | 1 + aviary/mission/problem_builder_2DOF.py | 12 ++++++++++ .../mission/problem_builder_height_energy.py | 23 +++++++++++++------ aviary/mission/problem_builder_solved_2DOF.py | 21 +++++++++++++++-- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 196776f2d..b6cd60a64 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -1263,6 +1263,7 @@ def add_objective(self, objective_type=None, ref=None): ValueError: If an invalid problem type is provided. """ + self.builder.add_objective(self) self.model.add_subsystem( "fuel_obj", diff --git a/aviary/mission/problem_builder_2DOF.py b/aviary/mission/problem_builder_2DOF.py index 6ee510476..b6a14225d 100644 --- a/aviary/mission/problem_builder_2DOF.py +++ b/aviary/mission/problem_builder_2DOF.py @@ -93,6 +93,7 @@ def initial_guesses(self, prob): Mission.Design.RANGE, units='NM') aviary_inputs.set_val(Mission.Summary.RANGE, aviary_inputs.get_val( Mission.Design.RANGE, units='NM'), units='NM') + prob.cruise_mach = aviary_inputs.get_val(Mission.Design.MACH) prob.require_range_residual = True @@ -656,6 +657,17 @@ def _add_landing_systems(self, prob): 'pre_mission.drag_loss_due_to_shielded_wing_area', 'landing.drag_loss_due_to_shielded_wing_area') + def add_objective(self, prob): + """ + Add any additional components related to objectives. + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + """ + pass + def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_prefix): """ Adds the initial guesses for each variable of a given phase to the problem. diff --git a/aviary/mission/problem_builder_height_energy.py b/aviary/mission/problem_builder_height_energy.py index e0c20e77f..3ba6e3dd0 100644 --- a/aviary/mission/problem_builder_height_energy.py +++ b/aviary/mission/problem_builder_height_energy.py @@ -302,13 +302,6 @@ def link_phases(self, prob, phases, connect_directly=True): Mission.Summary.RANGE, src_indices=[-1], flat_src_indices=True) - if not prob.pre_mission_info['include_takeoff']: - first_flight_phase_name = list(prob.phase_info.keys())[0] - first_flight_phase = prob.traj._phases[first_flight_phase_name] - first_flight_phase.set_state_options( - Dynamic.Vehicle.MASS, fix_initial=False - ) - def add_post_mission_systems(self, prob, include_landing=True): """ Add any post mission systems. @@ -453,6 +446,22 @@ def _add_landing_systems(self, prob): Mission.Landing.INITIAL_ALTITUDE, src_indices=[0]) + def add_objective(self, prob): + """ + Add any additional components related to objectives. + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + """ + if not prob.pre_mission_info['include_takeoff']: + first_flight_phase_name = list(prob.phase_info.keys())[0] + first_flight_phase = prob.traj._phases[first_flight_phase_name] + first_flight_phase.set_state_options( + Dynamic.Vehicle.MASS, fix_initial=False + ) + def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_prefix): """ Adds the initial guesses for each variable of a given phase to the problem. diff --git a/aviary/mission/problem_builder_solved_2DOF.py b/aviary/mission/problem_builder_solved_2DOF.py index 388dcb9f3..b54406555 100644 --- a/aviary/mission/problem_builder_solved_2DOF.py +++ b/aviary/mission/problem_builder_solved_2DOF.py @@ -1,10 +1,10 @@ from aviary.mission.flops_based.phases.groundroll_phase import GroundrollPhase as GroundrollPhaseVelocityIntegrated from aviary.mission.twodof_phase import TwoDOFPhase -from aviary.utils.functions import wrapped_convert_units -from aviary.variable_info.variables import Dynamic from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.functions import wrapped_convert_units from aviary.variable_info.enums import LegacyCode +from aviary.variable_info.variables import Dynamic, Mission class ProblemBuilderSolved2DOF(): @@ -26,6 +26,12 @@ def initial_guesses(self, prob): if prob.engine_builders is None: prob.engine_builders = build_engine_deck(prob.aviary_inputs) + # This doesn't really have much value, but is needed for initializing + # an objective-related component that still lives in level 2. + prob.target_range = prob.aviary_inputs.get_val( + Mission.Design.RANGE, units='NM' + ) + def get_default_phase_info(self, prob): """ Return a default phase_info for this type or problem. @@ -235,6 +241,17 @@ def add_post_mission_systems(self, prob, include_landing=True): """ pass + def add_objective(self, prob): + """ + Add any additional components related to objectives. + + Parameters + ---------- + prob : AviaryProblem + Problem that owns this builder. + """ + pass + def add_guesses(self, prob, phase_name, phase, guesses, target_prob, parent_prefix): """ Adds the initial guesses for each variable of a given phase to the problem. From 43747806e6c8aa55489ef865ac9f78ee23b4dcb8 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 31 Jan 2025 18:51:02 -0500 Subject: [PATCH 37/38] Fix docs --- aviary/docs/examples/modified_aircraft.csv | 1 + aviary/docs/getting_started/onboarding_level2.ipynb | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/aviary/docs/examples/modified_aircraft.csv b/aviary/docs/examples/modified_aircraft.csv index 445c8e328..7d572c6c0 100644 --- a/aviary/docs/examples/modified_aircraft.csv +++ b/aviary/docs/examples/modified_aircraft.csv @@ -49,6 +49,7 @@ aircraft:engine:num_fuselage_engines,0,unitless aircraft:engine:num_wing_engines,2,unitless aircraft:engine:reference_mass,7400,lbm aircraft:engine:reference_sls_thrust,28928.1,lbf +aircraft:engine:scale_factor,1.0,unitless aircraft:engine:scale_mass,True,unitless aircraft:engine:scale_performance,True,unitless aircraft:engine:scaled_sls_thrust,28928.1,lbf diff --git a/aviary/docs/getting_started/onboarding_level2.ipynb b/aviary/docs/getting_started/onboarding_level2.ipynb index 8998c978e..ce416f6d8 100644 --- a/aviary/docs/getting_started/onboarding_level2.ipynb +++ b/aviary/docs/getting_started/onboarding_level2.ipynb @@ -46,7 +46,7 @@ "import openmdao.api as om\n", "from aviary.api import Mission\n", "from aviary.variable_info.enums import ProblemType as PT, EquationsOfMotion as EOM\n", - "from aviary.interface.methods_for_level2 import AviaryProblem\n", + "from aviary.interface.methods_for_level2 import AviaryProblem, ProblemBuilder2DOF\n", "from aviary.utils.doctape import check_contains\n", "\n", "EOM.HEIGHT_ENERGY;\n", @@ -56,6 +56,8 @@ " dummy_prob = om.Problem()\n", " dummy_prob.mission_method = EOM.TWO_DEGREES_OF_FREEDOM\n", " dummy_prob.problem_type = ptype\n", + " dummy_prob.builder = ProblemBuilder2DOF()\n", + " dummy_prob.target_range = 0\n", " AviaryProblem.add_objective(dummy_prob)\n", " dummy_prob.setup()\n", " objectives = dummy_prob.model._responses.keys()\n", @@ -1006,7 +1008,7 @@ ], "metadata": { "kernelspec": { - "display_name": "latest_env", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, From 3d652fdab35ab4bfeddf62d19f4d8e98dc92cb02 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Mon, 3 Feb 2025 13:06:09 -0500 Subject: [PATCH 38/38] More doc fix --- aviary/docs/getting_started/onboarding_level2.ipynb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aviary/docs/getting_started/onboarding_level2.ipynb b/aviary/docs/getting_started/onboarding_level2.ipynb index ce416f6d8..863b1676e 100644 --- a/aviary/docs/getting_started/onboarding_level2.ipynb +++ b/aviary/docs/getting_started/onboarding_level2.ipynb @@ -656,7 +656,7 @@ "import openmdao.api as om\n", "from aviary.api import Mission, Dynamic\n", "from aviary.variable_info.enums import EquationsOfMotion as EOM, AnalysisScheme as AS\n", - "from aviary.interface.methods_for_level2 import AviaryProblem\n", + "from aviary.interface.methods_for_level2 import AviaryProblem, ProblemBuilder2DOF\n", "from aviary.utils.aviary_values import AviaryValues\n", "from aviary.utils.doctape import check_contains\n", "\n", @@ -685,6 +685,8 @@ "\n", "for otype, obj in expected_objective.items():\n", " dummy_prob = dprob()\n", + " dummy_prob.builder = ProblemBuilder2DOF()\n", + " dummy_prob.target_range = 0\n", " AviaryProblem.add_objective(dummy_prob, otype)\n", " dummy_prob.setup()\n", " # traj timeseries values are promoted to the top in the real problem\n",