diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index b4999e7fec..c157ccdab4 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -488,7 +488,7 @@ def check_and_preprocess_inputs(self, verbosity=None): geom_code_origin = (FLOPS, GASP) # which geometry method gets prioritized in case of conflicting outputs - code_origin_to_prioritize = self.configurator.get_code_origin(self) + code_origin_to_prioritize = self.configurator.get_code_origin() geom = CoreGeometryBuilder( 'geometry', @@ -1108,9 +1108,8 @@ def link_phases(self, verbosity=None, comm=None): """ Link phases together after they've been added. - Based on which phases the user has selected, we might need special logic to do the Dymos - linkages correctly. Some of those connections for the simple GASP and FLOPS mission are - shown here. + If a variable is common to two phases, it can be linked via a direct connection or a + constraint. """ # `self.verbosity` is "true" verbosity for entire run. `verbosity` is verbosity override for # just this method @@ -1147,67 +1146,207 @@ def link_phases(self, verbosity=None, comm=None): if len(phases) <= 1: return - # In summary, the following code loops over all phases in self.mission_info, gets the linked - # variables from each external subsystem in each phase, and stores the lists of linked - # variables in lists_to_link. It then gets a list of unique variable names from - # lists_to_link and loops over them, creating a list of phase names for each variable and - # linking the phases using self.traj.link_phases(). + # Phase linking. + # If we are under mpi, and traj.phases is running in parallel, then let the optimizer handle + # the linkage constraints. Note that we can technically parallelize connected phases, but + # it requires a solver that we would like to avoid. + connect_directly = True + if comm.size > 1 and self.traj.options['parallel_phases']: + connect_directly = False - lists_to_link = [] - for idx, phase_name in enumerate(self.mission_info): + # Assemble all linkable variables in the phases. + link_vars_dict = {} + for builder in self.phase_objects: + phase_name = builder.name phase_info = self.mission_info[phase_name] all_subsystem_options = phase_info.get('subsystem_options', {}) - lists_to_link.append([]) - for subsys in self.external_subsystems: - lists_to_link[idx].extend( - subsys.get_linked_variables( - aviary_inputs=self.aviary_inputs, - user_options=self.mission_info[phase_name]['user_options'], - subsystem_options=all_subsystem_options.get(subsys.name, {}), + link_vars = set() + for subsys in self.subsystems: + sub_vars = subsys.get_linked_variables( + aviary_inputs=self.aviary_inputs, + user_options=self.mission_info[phase_name]['user_options'], + subsystem_options=all_subsystem_options.get(subsys.name, {}), + ) + link_vars = link_vars.union(sub_vars) + + phase_vars = builder.get_linked_variables() + link_vars = link_vars.union(phase_vars) + + link_vars_dict[phase_name] = link_vars + + builder2 = self.phase_objects[0] + for builder in self.phase_objects[1:]: + # Upstream phase is previous phase. + # TODO: Linking a different phase should be possible. + builder1 = builder2 + builder2 = builder + + phase1 = builder1.name + phase_info1 = self.mission_info[phase1]['user_options'] + vars1 = link_vars_dict[phase1] + + phase2 = builder2.name + phase_info2 = self.mission_info[phase2]['user_options'] + vars2 = link_vars_dict[phase2] + + if self.reserve_phases and phase2 == self.reserve_phases[0]: + # Don't link to first reserve phase. + continue + + # Find common vars across 1-2 boundary + common = vars1.intersection(vars2) + upstream_analytic = [item for item in vars1 if item.startswith('initial_')] + downstream_analytic = [item for item in vars2 if item.startswith('initial_')] + + # Sort because of MPI + for var in sorted(common): + # Controls: True or False, everything else: None + opt1 = phase_info1.get(f'{var}_optimize', None) + opt2 = phase_info2.get(f'{var}_optimize', None) + + if opt1 is False and opt2 is False: + # If both sides are static controls, don't link. + continue + + pin1 = phase_info1.get(f'{var}_final', (None, None))[0] + pin2 = phase_info2.get(f'{var}_initial', (None, None))[0] + if pin1 and pin2: + # When both ends are pinned, no need to add a duplicate constraint. + continue + + # MPI takes precedence, since we can't connect directly in the parallel group. + # Otherwise, this key allows the user to control whether the phase is connected + # or constrained on each input. + if var == 'time': + key = f'time_initial_direct_link' + else: + key = f'{var}_direct_link' + connect = connect_directly and phase_info2.get(key, connect_directly) + + if opt2 is False: + # Controls cannot connect directly. + connect = False + + elif pin2: + # Pinned front can't take input. + connect = False + + kwargs = {} + if not connect: + kwargs = self._find_scaling(var, phase1, phase_info1, phase2, phase_info2, opt2) + + self.traj.link_phases( + phases=[phase1, phase2], + connected=connect, + vars=[var], + **kwargs, + ) + + # Target analytic phases may take a single start input that needs to connect + # Sort because of MPI + for var in sorted(downstream_analytic): + source = var.removeprefix('initial_') + if source not in vars1: + continue + + if source == 'time': + key = f'time_initial_direct_link' + else: + key = f'{source}_direct_link' + connect = connect_directly and phase_info2.get(key, connect_directly) + + kwargs = {} + if not connect: + kwargs = self._find_scaling( + source, phase1, phase_info1, phase2, phase_info2, opt2 ) + + self.traj.add_linkage_constraint( + phase1, phase2, source, var, connected=connect, **kwargs ) - # get unique variable names from lists_to_link - unique_vars = list(set([var for sublist in lists_to_link for var in sublist])) + # Source analytic phases should still connect to the timeseries. + # Sort because of MPI + for var in sorted(upstream_analytic): + target = var.removeprefix('initial_') + if target not in vars2: + continue - # Phase linking. - # If we are under mpi, and traj.phases is running in parallel, then let the optimizer handle - # the linkage constraints. Note that we can technically parallelize connected phases, but - # it requires a solver that we would like to avoid. - true_unless_mpi = True - if comm.size > 1 and self.traj.options['parallel_phases']: - true_unless_mpi = False - - # loop over unique variable names - for var in unique_vars: - phases_to_link = [] - for idx, phase_name in enumerate(self.mission_info): - if var in lists_to_link[idx]: - phases_to_link.append(phase_name) - - if len(phases_to_link) > 1: # TODO: hack - # go phase by phase and either directly link if two standard phases, or use linkage - # constraint if either are analytic - # TODO need more unified way to handle this instead of splitting between AviaryGroup - # and configurators - for ii in range(len(phases) - 1): - phase1, phase2 = phases[ii : ii + 2] - opt1 = self.mission_info[phase1]['user_options'] - opt2 = self.mission_info[phase2]['user_options'] - integrates_mass1 = opt1['phase_type'] is PhaseType.BREGUET_RANGE - integrates_mass2 = opt2['phase_type'] is PhaseType.BREGUET_RANGE - - if integrates_mass1 or integrates_mass2: - # TODO need ref value for these linkage constraints - self.traj.add_linkage_constraint(phase1, phase2, var, var, connected=False) - else: - self.traj.link_phases(phases=[phase1, phase2], vars=[var], connected=True) + if var in downstream_analytic or target in downstream_analytic: + # Both phases are analytic, and we already linked this. + continue + + if target == 'time': + key = f'time_initial_direct_link' + else: + key = f'{target}_direct_link' + connect = connect_directly and phase_info2.get(key, connect_directly) + + kwargs = {} + if not connect: + kwargs = self._find_scaling( + target, phase1, phase_info1, phase2, phase_info2, opt2 + ) - self.configurator.link_phases(self, phases, connect_directly=true_unless_mpi) + self.traj.link_phases( + phases=[phase1, phase2], + connected=connect, + vars=[target], + **kwargs, + ) + + self.configurator.configure_trajectory(self, phases) self.configurator.check_trajectory(self) + def _find_scaling(self, var, phase1, phase_info1, phase2, phase_info2, opt2): + """ + Returns a dictionary of scaling keyword arguments for a dymos linkage constraint. + """ + phase = self.traj._phases[phase1] + integrated_var = phase.time_options['name'] + analytic = len(phase.state_options) < 1 + + if not analytic: + if var == 'time': + if integrated_var != 'time': + # Time is not being integrated. + return {} + elif var == integrated_var: + # This is the integration variable, and its scaling is currently stored as time. + var = 'time' + + if var == 'time': + # Time behaves a bit differently than the others. + ref0 = None + ref, units = phase_info2.get(f'{var}_initial_ref', (None, None)) + if ref is None: + ref, units = phase_info1.get(f'{var}_duration_ref', (None, None)) + else: + # First, pull from the scaling from upstream phase. + ref, units = phase_info1.get(f'{var}_ref', (None, None)) + ref0 = phase_info1.get(f'{var}_ref0', (None, None))[0] + if ref is None: + ref, units = phase_info2.get(f'{var}_ref', (None, None)) + ref0 = phase_info2.get(f'{var}_ref0', (None, None))[0] + + kwargs = {} + if ref is not None: + kwargs['ref'] = ref + if ref0 is not None: + kwargs['ref0'] = ref0 + if units is not None: + kwargs['units'] = units + + # Make sure states options are correct for this. + if opt2 is None and var != 'time': + phase = self.traj._phases[phase2] + if var in phase.state_options: + phase.set_state_options(var, input_initial=False) + + return kwargs + def _add_bus_variables_and_connect(self): all_subsystems = self.subsystems diff --git a/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb b/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb index 04dda14831..015f2daa52 100644 --- a/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb +++ b/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb @@ -155,6 +155,7 @@ " 'mach_optimize': mach_optimize,\n", " 'mach_polynomial_order': 1,\n", " 'mach_bounds': ((0.18, 0.2), 'unitless'),\n", + " 'mach_ref': (1.0, 'unitless'),\n", " 'altitude_optimize': False,\n", " 'altitude_polynomial_order': 1,\n", " 'altitude_initial': (0.0, 'ft'),\n", @@ -190,6 +191,7 @@ " 'mach_optimize': mach_optimize,\n", " 'mach_polynomial_order': 1,\n", " 'mach_bounds': ((0.2, 0.22), 'unitless'),\n", + " 'mach_ref': (1.0, 'unitless'),\n", " 'altitude_optimize': altitude_optimize,\n", " 'altitude_polynomial_order': 1,\n", " 'altitude_bounds': ((0.0, 150.0), 'ft'),\n", @@ -219,6 +221,7 @@ " 'mach_optimize': mach_optimize,\n", " 'mach_polynomial_order': 1,\n", " 'mach_bounds': ((0.22, 0.3), 'unitless'),\n", + " 'mach_ref': (1.0, 'unitless'),\n", " 'altitude_optimize': altitude_optimize,\n", " 'altitude_polynomial_order': 1,\n", " 'altitude_initial': (50.0, 'ft'),\n", @@ -247,6 +250,7 @@ " 'mach_optimize': mach_optimize,\n", " 'mach_polynomial_order': 1,\n", " 'mach_bounds': ((0.22, 0.3), 'unitless'),\n", + " 'mach_ref': (1.0, 'unitless'),\n", " 'altitude_optimize': altitude_optimize,\n", " 'altitude_polynomial_order': 1,\n", " 'altitude_bounds': ((985.0, 1100.0), 'ft'),\n", @@ -283,6 +287,7 @@ " 'mach_optimize': mach_optimize,\n", " 'mach_polynomial_order': 2,\n", " 'mach_bounds': ((0.24, 0.32), 'unitless'),\n", + " 'mach_ref': (1.0, 'unitless'),\n", " 'altitude_optimize': altitude_optimize,\n", " 'altitude_polynomial_order': 2,\n", " 'altitude_bounds': ((985.0, 1.5e3), 'ft'),\n", @@ -318,6 +323,7 @@ " 'mach_optimize': mach_optimize,\n", " 'mach_polynomial_order': 1,\n", " 'mach_bounds': ((0.24, 0.32), 'unitless'),\n", + " 'mach_ref': (1.0, 'unitless'),\n", " 'altitude_optimize': altitude_optimize,\n", " 'altitude_polynomial_order': 1,\n", " 'altitude_bounds': ((1.1e3, 1.2e3), 'ft'),\n", @@ -360,6 +366,7 @@ " 'mach_optimize': mach_optimize,\n", " 'mach_polynomial_order': 1,\n", " 'mach_bounds': ((0.24, 0.32), 'unitless'),\n", + " 'mach_ref': (1.0, 'unitless'),\n", " 'altitude_optimize': altitude_optimize,\n", " 'altitude_polynomial_order': 1,\n", " 'altitude_bounds': ((1.0e3, 3.0e3), 'ft'),\n", @@ -655,7 +662,7 @@ " 'altitude_optimize': True,\n", " 'altitude_polynomial_order': 2,\n", " #'altitude_initial': (50.0, 'ft'),\n", - " #'altitude_final': (0.0, 'ft'),\n", + " 'altitude_final': (0.0, 'ft'),\n", " 'altitude_bounds': ((0.0, 1000.0), 'ft'),\n", " 'throttle_enforcement': 'path_constraint',\n", " 'rotation': False,\n", @@ -781,7 +788,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.12" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/aviary/docs/user_guide/fundamentals/mission_definition.ipynb b/aviary/docs/user_guide/fundamentals/mission_definition.ipynb index 1ae9aa5309..c612128fe1 100644 --- a/aviary/docs/user_guide/fundamentals/mission_definition.ipynb +++ b/aviary/docs/user_guide/fundamentals/mission_definition.ipynb @@ -150,6 +150,8 @@ "\n", "- **{glue:md}`mass_solve_segments`**: When True, a solver will be used to converge the mass collocation defects within a segment. Note that the state continuity defects between segments will still be handled by the optimizer.\n", "\n", + "- **{glue:md}`mass_direct_link`**: Boolean. When `True`, connect the initial mass directly to the upstream phase. When False, connect them with a constraint instead.\n", + "- \n", "### Controls\n", "\n", "These options provide information about control variables, or variables that are directly controlled by the optimizer. Like state variables, control variables for a given phase is also dependent on what kind of phase it is and which equations of motion are being used. The naming scheme is also similar, with the control variable of interest forming the first part of the option's name.\n", @@ -168,6 +170,8 @@ "\n", "- **{glue:md}`altitude_polynomial_order`**: The order of polynomials for interpolation in the transcription. Default is None, which does not use a polynomial.\n", "\n", + "- **{glue:md}`altitude_direct_link`**: Boolean. When `True`, connect the initial altitude directly to the upstream phase. When False, connect them with a constraint instead.\n", + "- \n", "### Time\n", "- **{glue:md}`time_initial`**: Value of \"time\" at the start of the phase. When unspecified, the value is determined by the optimizer.\n", "\n", @@ -181,6 +185,8 @@ "\n", "- **{glue:md}`time_duration_ref`**: Additive scale factor \"ref\" for time_duration. Default is None.\n", "\n", + "- **{glue:md}`time_initial_direct_link`**: Boolean. When `True`, connect the initial time directly to the upstream phase. When False, connect them with a constraint instead.\n", + "\n", "### Specialized 2DOF phase settings\n", "\n", "\n", @@ -205,13 +211,21 @@ ], "metadata": { "kernelspec": { - "display_name": "aviary", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "version": "3.12.11" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" } }, "nbformat": 4, diff --git a/aviary/interface/test/test_linkage_logic.py b/aviary/interface/test/test_linkage_logic.py index 5d68c98c1f..5f414c483d 100644 --- a/aviary/interface/test/test_linkage_logic.py +++ b/aviary/interface/test/test_linkage_logic.py @@ -16,11 +16,11 @@ def setUp(self): 'num_segments': 5, 'order': 3, 'mach_optimize': False, - 'mach_initial': (0.72, 'unitless'), + #'mach_initial': (0.72, 'unitless'), 'mach_final': (0.72, 'unitless'), 'mach_bounds': ((0.7, 0.74), 'unitless'), 'altitude_optimize': False, - 'altitude_initial': (32000.0, 'ft'), + #'altitude_initial': (32000.0, 'ft'), 'altitude_final': (34000.0, 'ft'), 'altitude_bounds': ((23000.0, 38000.0), 'ft'), 'throttle_enforcement': 'boundary_constraint', @@ -79,12 +79,12 @@ def setUp(self): 'order': 3, 'mach_optimize': False, 'mach_polynomial_order': 1, - 'mach_initial': (0.72, 'unitless'), + #'mach_initial': (0.72, 'unitless'), 'mach_final': (0.36, 'unitless'), 'mach_bounds': ((0.34, 0.74), 'unitless'), 'altitude_optimize': False, 'altitude_polynomial_order': 1, - 'altitude_initial': (34000.0, 'ft'), + #'altitude_initial': (34000.0, 'ft'), 'altitude_final': (500.0, 'ft'), 'altitude_bounds': ((0.0, 38000.0), 'ft'), 'throttle_enforcement': 'path_constraint', diff --git a/aviary/mission/energy_state_problem_configurator.py b/aviary/mission/energy_state_problem_configurator.py index 09e761514a..d48c4e1097 100644 --- a/aviary/mission/energy_state_problem_configurator.py +++ b/aviary/mission/energy_state_problem_configurator.py @@ -86,15 +86,10 @@ def get_default_phase_info(self, aviary_group): return phase_info - def get_code_origin(self, aviary_group): + def get_code_origin(self): """ Return the legacy of this problem configurator. - Parameters - ---------- - aviary_group : AviaryGroup - Aviary model that owns this configurator. - Returns ------- LegacyCode @@ -228,12 +223,9 @@ def set_phase_options(self, aviary_group, phase_name, phase_idx, phase, user_opt **extra_options, ) - def link_phases(self, aviary_group, phases, connect_directly=True): + def configure_trajectory(self, aviary_group, phases): """ - Apply any additional phase linking. - - Note that some phase variables are handled in the AviaryProblem. Only - problem-specific ones need to be linked here. + Link or configure phase connections to other upstream or downstream components. This is called from AviaryProblem.link_phases @@ -241,61 +233,10 @@ def link_phases(self, aviary_group, phases, connect_directly=True): ---------- aviary_group : AviaryGroup Aviary model that owns this configurator. - phases : Phase - Phases to be linked. - 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. + phases : list[Phase] + List of all phases in the trajectory. """ - # connect regular_phases with each other if you are optimizing alt or mach - self.link_phases_helper_with_options( - aviary_group, - aviary_group.regular_phases, - 'altitude_optimize', - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self.link_phases_helper_with_options( - aviary_group, aviary_group.regular_phases, 'mach_optimize', Dynamic.Atmosphere.MACH - ) - - # connect reserve phases with each other if you are optimizing alt or mach - self.link_phases_helper_with_options( - aviary_group, - aviary_group.reserve_phases, - 'altitude_optimize', - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self.link_phases_helper_with_options( - aviary_group, aviary_group.reserve_phases, 'mach_optimize', Dynamic.Atmosphere.MACH - ) - - # connect mass and distance between all phases regardless of reserve / - # non-reserve status - aviary_group.traj.link_phases( - phases, ['time'], ref=None if connect_directly else 1e3, connected=connect_directly - ) - aviary_group.traj.link_phases( - phases, - [Dynamic.Vehicle.MASS], - ref=None if connect_directly else 1e6, - connected=connect_directly, - ) - aviary_group.traj.link_phases( - phases, - [Dynamic.Mission.DISTANCE], - ref=None if connect_directly else 1e3, - connected=connect_directly, - ) - - # Under MPI, the states aren't directly connected. - if not connect_directly: - for phase_name in phases[1:]: - phase = aviary_group.traj._phases[phase_name] - phase.set_state_options(Dynamic.Vehicle.MASS, input_initial=False) - phase.set_state_options(Dynamic.Mission.DISTANCE, input_initial=False) - + # Boundary conditions for the first phase. phase = aviary_group.traj._phases[phases[0]] # Currently expects Distance to be an input. diff --git a/aviary/mission/flight_phase_builder.py b/aviary/mission/flight_phase_builder.py index 282fc3341e..c6c986e54e 100644 --- a/aviary/mission/flight_phase_builder.py +++ b/aviary/mission/flight_phase_builder.py @@ -44,6 +44,7 @@ def declare_options(self): defaults = { 'mass_ref': 1e4, 'mass_bounds': (0.0, None), + 'mass_direct_link': True, } self.add_state_options('mass', units='kg', defaults=defaults) @@ -51,18 +52,26 @@ def declare_options(self): defaults = { 'distance_ref': 1e6, 'distance_bounds': (0.0, None), + 'distance_direct_link': True, } self.add_state_options('distance', units='m', defaults=defaults) - self.add_control_options('altitude', units='ft') + defaults = { + 'altitude_direct_link': False, + } + self.add_control_options('altitude', units='ft', defaults=defaults) # TODO: These defaults aren't great, but need to keep things the same for now. defaults = { 'mach_ref': 0.5, + 'mach_direct_link': False, } self.add_control_options('mach', units='unitless', defaults=defaults) - self.add_time_options(units='s') + defaults = { + 'initial_time_direct_link': True, + } + self.add_time_options(units='s', defaults=defaults) self.declare( name='throttle_enforcement', @@ -418,6 +427,16 @@ def make_default_transcription(self): return transcription + def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): + linked_vars = [ + Dynamic.Mission.ALTITUDE, + Dynamic.Mission.DISTANCE, + Dynamic.Atmosphere.MACH, + Dynamic.Vehicle.MASS, + 'time', + ] + return linked_vars + def _extra_ode_init_kwargs(self): """Return extra kwargs required for initializing the ODE.""" # TODO: support subsystems and meta_data in the base class diff --git a/aviary/mission/phase_builder.py b/aviary/mission/phase_builder.py index a58090b9a9..8fbe95dcb8 100644 --- a/aviary/mission/phase_builder.py +++ b/aviary/mission/phase_builder.py @@ -629,6 +629,21 @@ def get_parameters(self): """ return {} + def get_linked_variables(self): + """ + Return a list of variable names that will be linked when this phase is connected to another + phase that shares the variable. + + If you have an analytic phase, and you need to link an input parameter to the upstream + phase, prepend the name with _initial. For example, if you need to connect to mass, name + your parameter 'initial_mass'. + + Returns + ------- + linked_vars : list of variables to link between phases + """ + return [] + _registered_phase_builder_types = [] diff --git a/aviary/mission/problem_configurator.py b/aviary/mission/problem_configurator.py index fc29075bf6..bdf19eea57 100644 --- a/aviary/mission/problem_configurator.py +++ b/aviary/mission/problem_configurator.py @@ -39,15 +39,10 @@ def get_default_phase_info(self, aviary_group): msg = 'This pmethod must be defined in your problem configurator.' raise NotImplementedError(msg) - def get_code_origin(self, aviary_group): + def get_code_origin(self): """ Return the legacy of this problem configurator. - Parameters - ---------- - aviary_group : AviaryGroup - Aviary model that owns this builder. - Returns ------- LegacyCode @@ -112,24 +107,18 @@ def set_phase_options(self, aviary_group, phase_name, phase_idx, phase, user_opt """ pass - def link_phases(self, aviary_group, phases, connect_directly=True): + def configure_trajectory(self, aviary_group, phases): """ - Apply any additional phase linking. - - Note that some phase variables are handled in the AviaryProblem. Only - problem-specific ones need to be linked here. + Link or configure phase connections to other upstream or downstream components. This is called from AviaryProblem.link_phases Parameters ---------- aviary_group : AviaryGroup - Aviary model that owns this builder. - phases : Phase - Phases to be linked. - 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. + Aviary model that owns this configurator. + phases : list[Phase] + List of all phases in the trajectory. """ pass @@ -144,48 +133,6 @@ def check_trajectory(self, aviary_group): """ pass - def link_phases_helper_with_options(self, aviary_group, phases, option_name, var, **kwargs): - # Initialize a list to keep track of indices where option_name is True - true_option_indices = [] - - # Loop through phases to find where option_name is True - for idx, phase_name in enumerate(phases): - if aviary_group.mission_info[phase_name]['user_options'].get(option_name, False): - true_option_indices.append(idx) - - # Determine the groups of phases to link based on consecutive indices - groups_to_link = [] - current_group = [] - - for idx in true_option_indices: - if not current_group or idx == current_group[-1] + 1: - # If the current index is consecutive, add it to the current group - current_group.append(idx) - else: - # Otherwise, start a new group and save the previous one - groups_to_link.append(current_group) - current_group = [idx] - - # Add the last group if it exists - if current_group: - groups_to_link.append(current_group) - - # Loop through each group and determine the phases to link - for group in groups_to_link: - # Extend the group to include the phase before the first True option and - # after the last True option, if applicable - if group[0] > 0: - group.insert(0, group[0] - 1) - if group[-1] < len(phases) - 1: - group.append(group[-1] + 1) - - # Extract the phase names for the current group - phases_to_link = [phases[idx] for idx in group] - - # Link the phases for the current group - if len(phases_to_link) > 1: - aviary_group.traj.link_phases(phases=phases_to_link, vars=[var], **kwargs) - def add_post_mission_systems(self, aviary_group): """ Add any post mission systems. diff --git a/aviary/mission/solved_two_dof/phases/groundroll_phase.py b/aviary/mission/solved_two_dof/phases/groundroll_phase.py index 43bed8df27..ca4553d624 100644 --- a/aviary/mission/solved_two_dof/phases/groundroll_phase.py +++ b/aviary/mission/solved_two_dof/phases/groundroll_phase.py @@ -43,6 +43,7 @@ def declare_options(self): 'time_duration_bounds': (0.0, 3600.0), 'time_initial_ref': 100.0, 'time_duration_ref': 100.0, + 'time_initial_direct_link': False, } self.add_time_options(units='kn', defaults=defaults) @@ -199,6 +200,15 @@ def _extra_ode_init_kwargs(self): 'set_input_defaults': False, } + def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): + linked_vars = [ + Dynamic.Mission.DISTANCE, + Dynamic.Atmosphere.MACH, + Dynamic.Vehicle.MASS, + 'time', + ] + return linked_vars + def get_parameters(self): params = {} params[Aircraft.Wing.INCIDENCE] = { diff --git a/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py b/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py index ac5c1e8fb6..a9d7208d18 100644 --- a/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py +++ b/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py @@ -44,6 +44,7 @@ def declare_options(self): 'mass_ref': 1e4, 'mass_defect_ref': 1e6, 'mass_bounds': (0.0, None), + 'mass_direct_link': True, } self.add_state_options('mass', units='lbm', defaults=defaults) @@ -52,29 +53,37 @@ def declare_options(self): 'distance_ref': 1e6, 'distance_defect_ref': 1e8, 'distance_bounds': (0.0, None), - 'mass_bounds': (0.0, None), + 'distance_direct_link': False, } self.add_state_options('distance', units='m', defaults=defaults) - self.add_control_options('altitude', units='ft') + defaults = { + 'altitude_direct_link': False, + } + self.add_control_options('altitude', units='ft', defaults=defaults) # TODO: These defaults aren't great, but need to keep things the same for now. defaults = { 'mach_ref': 0.5, + 'mach_direct_link': False, } self.add_control_options('mach', units='unitless', defaults=defaults) defaults = { 'angle_of_attack_polynomial_order': 1, 'angle_of_attack_optimize': True, - 'angle_of_attack_ref': 10.0, + 'angle_of_attack_ref': 15.0, 'angle_of_attack_bounds': (0.0, 15.0), 'angle_of_attack_optimize': True, 'angle_of_attack_initial': 0.0, + 'angle_of_attack_direct_link': False, } self.add_control_options('angle_of_attack', units='deg', defaults=defaults) - self.add_time_options(units='ft') + defaults = { + 'time_initial_direct_link': False, + } + self.add_time_options(units='ft', defaults=defaults) self.declare( 'reserve', @@ -286,6 +295,17 @@ def _extra_ode_init_kwargs(self): 'throttle_enforcement': self.user_options.get_val('throttle_enforcement'), } + def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): + linked_vars = [ + Dynamic.Mission.ALTITUDE, + Dynamic.Mission.DISTANCE, + Dynamic.Vehicle.ANGLE_OF_ATTACK, + Dynamic.Atmosphere.MACH, + Dynamic.Vehicle.MASS, + 'time', + ] + return linked_vars + def get_parameters(self): params = {} params[Aircraft.Wing.INCIDENCE] = { diff --git a/aviary/mission/solved_two_dof_problem_configurator.py b/aviary/mission/solved_two_dof_problem_configurator.py index 64bd9ba8b1..b774f42606 100644 --- a/aviary/mission/solved_two_dof_problem_configurator.py +++ b/aviary/mission/solved_two_dof_problem_configurator.py @@ -59,15 +59,10 @@ def get_default_phase_info(self, aviary_group): """ raise RuntimeError('Solved 2DOF requires that a phase_info is specified.') - def get_code_origin(self, aviary_group): + def get_code_origin(self): """ Return the legacy of this problem configurator. - Parameters - ---------- - aviary_group : AviaryGroup - Aviary model that owns this configurator. - Returns ------- LegacyCode @@ -174,81 +169,6 @@ def set_phase_options(self, aviary_group, phase_name, phase_idx, phase, user_opt **extra_options, ) - def link_phases(self, aviary_group, phases, connect_directly=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 - ---------- - aviary_group : AviaryGroup - Aviary model that owns this configurator. - phases : Phase - Phases to be linked. - 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. - """ - # connect regular_phases with each other if you are optimizing alt or mach - self.link_phases_helper_with_options( - aviary_group, - aviary_group.regular_phases, - 'altitude_optimize', - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self.link_phases_helper_with_options( - aviary_group, - aviary_group.regular_phases, - 'mach_optimize', - Dynamic.Atmosphere.MACH, - ) - - # connect reserve phases with each other if you are optimizing alt or mach - self.link_phases_helper_with_options( - aviary_group, - aviary_group.reserve_phases, - 'altitude_optimize', - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self.link_phases_helper_with_options( - aviary_group, - aviary_group.reserve_phases, - 'mach_optimize', - Dynamic.Atmosphere.MACH, - ) - - aviary_group.traj.link_phases(phases, [Dynamic.Vehicle.MASS], connected=True) - aviary_group.traj.link_phases( - phases, [Dynamic.Mission.DISTANCE], units='ft', ref=1.0e3, connected=False - ) - aviary_group.traj.link_phases(phases, ['time'], connected=False) - - if len(phases) > 2: - aviary_group.traj.link_phases( - phases[1:], - [Dynamic.Vehicle.ANGLE_OF_ATTACK], - units='deg', - ref=15.0, - connected=False, - ) - - def check_trajectory(self, aviary_group): - """ - Checks the phase_info user options for any inconsistency. - - Parameters - ---------- - aviary_group : AviaryGroup - Aviary model that owns this configurator. - """ - pass - def add_post_mission_systems(self, aviary_group): """ Add any post mission systems. diff --git a/aviary/mission/two_dof/phases/accel_phase.py b/aviary/mission/two_dof/phases/accel_phase.py index 2e5062b11e..bef4b7272e 100644 --- a/aviary/mission/two_dof/phases/accel_phase.py +++ b/aviary/mission/two_dof/phases/accel_phase.py @@ -30,20 +30,26 @@ def declare_options(self): defaults = { 'mass_bounds': (0.0, None), + 'mass_direct_link': False, } self.add_state_options('mass', units='lbm', defaults=defaults) defaults = { 'distance_bounds': (0.0, None), + 'distance_direct_link': True, } self.add_state_options('distance', units='NM', defaults=defaults) defaults = { 'velocity_bounds': (0.0, None), + 'velocity_direct_link': True, } self.add_state_options('velocity', units='kn', defaults=defaults) - self.add_time_options(units='s') + defaults = { + 'initial_time_direct_link': True, + } + self.add_time_options(units='s', defaults=defaults) self.declare( 'reserve', @@ -149,6 +155,15 @@ def build_phase(self, aviary_options: AviaryValues = None): return phase + def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): + linked_vars = [ + Dynamic.Mission.DISTANCE, + Dynamic.Mission.VELOCITY, + Dynamic.Vehicle.MASS, + 'time', + ] + return linked_vars + AccelPhase._add_initial_guess_meta_data( InitialGuessIntegrationVariable(), diff --git a/aviary/mission/two_dof/phases/breguet_cruise_phase.py b/aviary/mission/two_dof/phases/breguet_cruise_phase.py index e270e0e4b3..a735b5e991 100644 --- a/aviary/mission/two_dof/phases/breguet_cruise_phase.py +++ b/aviary/mission/two_dof/phases/breguet_cruise_phase.py @@ -58,6 +58,37 @@ def declare_options(self): 'start between 25 and 45 minutes after the start of the mission.', ) + self.declare( + name='time_initial_direct_link', + default=True, + types=bool, + desc='When True, directly link the initial time parameter to the previous ' + 'phase. When False, use a constraint.', + ) + + self.declare( + name='altitude_direct_link', + default=True, + types=bool, + desc='When True, directly link the initial altitude parameter to the previous ' + 'phase. When False, use a constraint.', + ) + + self.declare( + name='distance_direct_link', + default=True, + types=bool, + desc='When True, directly link the initial distance parameter to the previous ' + 'phase. When False, use a constraint.', + ) + + self.declare( + name='mass_direct_link', + default=False, + types=bool, + desc='Because mass is output, this should always be false..', + ) + class BreguetCruisePhase(PhaseBuilder): """ @@ -154,6 +185,16 @@ def build_phase(self, aviary_options: AviaryValues = None): return phase + def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): + linked_vars = [ + 'initial_time', + 'initial_distance', + Dynamic.Mission.ALTITUDE, + Dynamic.Atmosphere.MACH, + Dynamic.Vehicle.MASS, + ] + return linked_vars + BreguetCruisePhase._add_initial_guess_meta_data( InitialGuessIntegrationVariable(), diff --git a/aviary/mission/two_dof/phases/flight_phase.py b/aviary/mission/two_dof/phases/flight_phase.py index 92847c49dd..afc0270a64 100644 --- a/aviary/mission/two_dof/phases/flight_phase.py +++ b/aviary/mission/two_dof/phases/flight_phase.py @@ -31,20 +31,26 @@ def declare_options(self): defaults = { 'mass_bounds': (0.0, None), + 'mass_direct_link': False, } self.add_state_options('mass', units='lbm', defaults=defaults) defaults = { 'distance_bounds': (0.0, None), + 'distance_direct_link': True, } self.add_state_options('distance', units='NM', defaults=defaults) defaults = { 'altitude_bounds': (0.0, None), + 'altitude_direct_link': True, } self.add_state_options('altitude', units='ft', defaults=defaults) - self.add_time_options(units='s') + defaults = { + 'initial_time_direct_link': True, + } + self.add_time_options(units='s', defaults=defaults) self.declare( 'reserve', @@ -194,6 +200,15 @@ def _extra_ode_init_kwargs(self): 'EAS_target': self.user_options.get_val('EAS_target', 'kn'), } + def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): + linked_vars = [ + Dynamic.Mission.ALTITUDE, + Dynamic.Mission.DISTANCE, + Dynamic.Vehicle.MASS, + 'time', + ] + return linked_vars + def get_parameters(self): params = {} params[Aircraft.Wing.INCIDENCE] = { diff --git a/aviary/mission/two_dof/phases/simple_cruise_phase.py b/aviary/mission/two_dof/phases/simple_cruise_phase.py index de43523035..92db1e723b 100644 --- a/aviary/mission/two_dof/phases/simple_cruise_phase.py +++ b/aviary/mission/two_dof/phases/simple_cruise_phase.py @@ -28,10 +28,16 @@ def declare_options(self): defaults = { 'mass_bounds': (0.0, None), + 'mass_direct_link': False, } self.add_state_options('mass', units='lbm', defaults=defaults) - self.add_time_options(units='s') + defaults = { + 'time_duration_bounds': (0, 3600), + 'time_initial_bounds': (0.0, 100.0), + 'initial_time_direct_link': True, + } + self.add_time_options(units='s', defaults=defaults) self.declare(name='alt_cruise', default=0.0, units='ft', desc='Cruise altitude.') @@ -56,28 +62,27 @@ def declare_options(self): ) self.declare( - 'time_duration', - default=None, - units='s', - desc='The amount of time taken by this phase added as a constraint.', + name='altitude_direct_link', + default=False, + types=bool, + desc='When True, directly link the initial altitude parameter to the previous ' + 'phase. When False, use a constraint.', ) self.declare( - name='time_duration_bounds', - default=(0, 3600), - units='s', - desc='Lower and upper bounds on the phase duration, in the form of a nested tuple: ' - 'i.e. ((20, 36), "min") This constrains the duration to be between 20 and 36 min.', + name='distance_direct_link', + default=False, + types=bool, + desc='When True, directly link the initial distance parameter to the previous ' + 'phase. When False, use a constraint.', ) self.declare( - 'time_initial_bounds', - types=tuple, - default=(0.0, 100.0), - units='s', - desc='Lower and upper bounds on the starting time for this phase relative to the ' - 'starting time of the mission, i.e., ((25, 45), "min") constrians this phase to ' - 'start between 25 and 45 minutes after the start of the mission.', + name='mach_direct_link', + default=True, + types=bool, + desc='When True, directly link the initial mach parameter to the previous ' + 'phase. When False, use a constraint.', ) @@ -185,6 +190,16 @@ def build_phase(self, aviary_options: AviaryValues = None): return phase + def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): + linked_vars = [ + 'time', + 'initial_distance', + Dynamic.Atmosphere.MACH, + Dynamic.Mission.ALTITUDE, + Dynamic.Vehicle.MASS, + ] + return linked_vars + SimpleCruisePhase._add_initial_guess_meta_data( InitialGuessIntegrationVariable(), diff --git a/aviary/mission/two_dof/phases/takeoff_phase.py b/aviary/mission/two_dof/phases/takeoff_phase.py index 284a5e9cb6..1a37f6b9c1 100644 --- a/aviary/mission/two_dof/phases/takeoff_phase.py +++ b/aviary/mission/two_dof/phases/takeoff_phase.py @@ -35,11 +35,13 @@ def declare_options(self): 'mass_bounds': (0.0, 200_000.0), 'mass_ref': 100_000.0, 'mass_defect_ref': 100.0, + 'mass_direct_link': False, } self.add_state_options('mass', units='lbm', defaults=defaults) defaults = { 'time_duration_bounds': (1.0, 100.0), + 'initial_time_direct_link': True, } self.add_time_options(units='s', defaults=defaults) @@ -47,6 +49,7 @@ def declare_options(self): defaults = { 'distance_bounds': (0.0, 10000.0), 'distance_ref': 3000.0, + 'distance_direct_link': True, } self.add_state_options('distance', units='ft', defaults=defaults) @@ -60,6 +63,7 @@ def declare_options(self): 'altitude_ref': 100.0, 'altitude_bounds': (0.0, 700.0), 'altitude_constraint_ref': 100.0, + 'altitude_direct_link': False, } self.add_state_options('altitude', units='ft', defaults=defaults) @@ -73,6 +77,7 @@ def declare_options(self): 'angle_of_attack_ref': np.deg2rad(15), 'angle_of_attack_bounds': (np.deg2rad(-30), np.deg2rad(30)), 'angle_of_attack_optimize': True, + 'angle_of_attack_direct_link': False, } # NOTE: Sometimes alpha is a control, and sometimes a state. This actually works # for making sure both sets of options are in there. @@ -283,6 +288,25 @@ def _extra_ode_init_kwargs(self): 'rotation': self.user_options.get_val('rotation'), } + def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): + ground_roll = self.user_options.get_val('ground_roll') + rotation = self.user_options.get_val('rotation') + + linked_vars = [ + Dynamic.Mission.DISTANCE, + Dynamic.Mission.VELOCITY, + Dynamic.Vehicle.MASS, + 'time', + ] + + if not (ground_roll or rotation): + linked_vars.append(Dynamic.Mission.ALTITUDE) + + if not ground_roll: + linked_vars.append(Dynamic.Vehicle.ANGLE_OF_ATTACK) + + return linked_vars + def get_parameters(self): params = {} params[Aircraft.Wing.INCIDENCE] = { diff --git a/aviary/mission/two_dof_problem_configurator.py b/aviary/mission/two_dof_problem_configurator.py index 938cc22182..1597b5e2af 100644 --- a/aviary/mission/two_dof_problem_configurator.py +++ b/aviary/mission/two_dof_problem_configurator.py @@ -97,15 +97,10 @@ def get_default_phase_info(self, aviary_group): return phase_info - def get_code_origin(self, aviary_group): + def get_code_origin(self): """ Return the legacy of this problem configurator. - Parameters - ---------- - aviary_group : AviaryGroup - Aviary model that owns this configurator. - Returns ------- LegacyCode @@ -342,12 +337,9 @@ def set_phase_options(self, aviary_group, phase_name, phase_idx, phase, user_opt 'aerodynamics', {} ).setdefault('method', 'low_speed') - def link_phases(self, aviary_group, phases, connect_directly=True): + def configure_trajectory(self, aviary_group, phases): """ - Apply any additional phase linking. - - Note that some phase variables are handled in the AviaryProblem. Only - problem-specific ones need to be linked here. + Link or configure phase connections to other upstream or downstream components. This is called from AviaryProblem.link_phases @@ -355,140 +347,9 @@ def link_phases(self, aviary_group, phases, connect_directly=True): ---------- aviary_group : AviaryGroup Aviary model that owns this configurator. - phases : Phase - Phases to be linked. - 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. + phases : list[Phase] + List of all phases in the trajectory. """ - for ii in range(len(phases) - 1): - phase1, phase2 = phases[ii : ii + 2] - info1 = aviary_group.mission_info[phase1] - info2 = aviary_group.mission_info[phase2] - phase_builder1 = info1['user_options']['phase_type'] - phase_builder2 = info2['user_options']['phase_type'] - - integrate_mass1 = phase_builder1 is PhaseType.BREGUET_RANGE - integrate_mass2 = phase_builder2 is PhaseType.BREGUET_RANGE - - if integrate_mass1 or integrate_mass2: - # if either phase integrates mass we have to use a linkage_constraint. - # This phase use the prefix "initial" for time and distance, but not mass. - if integrate_mass2: - prefix = 'initial_' - else: - prefix = '' - - aviary_group.traj.add_linkage_constraint( - phase1, phase2, 'time', prefix + 'time', connected=True - ) - aviary_group.traj.add_linkage_constraint( - phase1, phase2, 'distance', prefix + 'distance', connected=True - ) - aviary_group.traj.add_linkage_constraint( - phase1, phase2, 'mass', 'mass', connected=False, ref=1.0e5 - ) - - # This isn't computed, but is instead set in the cruise phase_info. - # We still need altitude continuity. - # Note: if both sides are Breguet Range, the user is doing something odd like a - # step cruise, so don't enforce a constraint. - if not (integrate_mass1 and integrate_mass2): - aviary_group.traj.add_linkage_constraint( - phase1, phase2, 'altitude', 'altitude', connected=False, ref=1.0e4 - ) - else: - # we always want time, distance, and mass to be continuous. - - # Time and mass are always available. - states_to_link = { - 'time': connect_directly, - Dynamic.Vehicle.MASS: False, - } - - # Distance is a constraint for SIMPLE_CRUISE - if ( - phase_builder1 is PhaseType.SIMPLE_CRUISE - or phase_builder2 is PhaseType.SIMPLE_CRUISE - ): - if phase_builder2 is PhaseType.SIMPLE_CRUISE: - prefix = 'initial_' - else: - prefix = '' - - aviary_group.traj.add_linkage_constraint( - phase1, - phase2, - 'distance', - prefix + 'distance', - connected=False, - ref=100.0, - ) - else: - # Add distance to the linked states. - states_to_link[Dynamic.Mission.DISTANCE] = connect_directly - - # Alititude is more complicated. SIMPLE_CRUISE should be a constraint. - # 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 continuous as well - if ( - phase_builder1 is PhaseType.SIMPLE_CRUISE - or phase_builder2 is PhaseType.SIMPLE_CRUISE - ): - aviary_group.traj.add_linkage_constraint( - phase1, phase2, 'altitude', 'altitude', connected=False, ref=1.0e4 - ) - - elif ( - ( - (phase1 in aviary_group.reserve_phases) - == (phase2 in aviary_group.reserve_phases) - ) - and not info1['user_options'].get('ground_roll') - and phase_builder1 is not PhaseType.TWO_DOF_TAKEOFF - ): # required for convergence of FwGm - states_to_link[Dynamic.Mission.ALTITUDE] = connect_directly - - # if either phase is rotation, we need to connect velocity - # ascent to accel also requires velocity - # TODO: This may need refined now that trajectories are more flexible. - if phase_builder1 is PhaseType.TWO_DOF_TAKEOFF: - states_to_link[Dynamic.Mission.VELOCITY] = connect_directly - # if the first phase is rotation, we also need alpha - if info1['user_options'].get('rotation'): - states_to_link[Dynamic.Vehicle.ANGLE_OF_ATTACK] = 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 = info1['initial_guesses'] - initial_guesses2 = info2['initial_guesses'] - - # if a state is in the initial guesses, get the units of the - # initial guess - kwargs = {} - if not connected: - # Some better scaling of the linkage constraint. - if state in initial_guesses1: - val, units = initial_guesses1[state] - if isinstance(val, (tuple, list)): - val = abs(val[-1]) - elif state in initial_guesses2: - val, units = initial_guesses2[state] - if isinstance(val, (tuple, list)): - val = abs(val[0]) - else: - val, units = info1['user_options'][f'{state}_ref'] - - kwargs['ref'] = val - kwargs['units'] = units - - aviary_group.traj.link_phases( - [phase1, phase2], [state], connected=connected, **kwargs - ) - aviary_group.promotes( 'traj', inputs=[ @@ -540,17 +401,6 @@ def link_phases(self, aviary_group, phases, connect_directly=True): if len(phases) > 1: self._add_groundroll_eq_constraint(aviary_group, phases) - def check_trajectory(self, aviary_group): - """ - Checks the phase_info user options for any inconsistency. - - Parameters - ---------- - aviary_group : AviaryGroup - Aviary model that owns this configurator. - """ - pass - def _add_groundroll_eq_constraint(self, aviary_group, phases): """ Add an equality constraint to the problem to ensure that the TAS at the end of the diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index 78930456ce..58e37ea647 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py @@ -234,15 +234,14 @@ 'time_duration_ref': (5000, 's'), 'altitude_final': (21_000, 'ft'), 'altitude_bounds': ((9000.0, 22_000.0), 'ft'), - 'altitude_ref': (20_000, 'ft'), + 'altitude_ref': (22_000, 'ft'), 'altitude_ref0': (0, 'ft'), 'mass_bounds': ((0, None), 'lbm'), 'mass_ref': (150_000, 'lbm'), 'mass_defect_ref': (150_000, 'lbm'), - 'distance_bounds': ((10.0, 1000.0), 'NM'), - 'distance_ref': (500, 'NM'), - 'distance_ref0': (0, 'NM'), - 'distance_defect_ref': (500, 'NM'), + 'distance_bounds': ((10.0, 100.0), 'NM'), + 'distance_ref': (100.0, 'NM'), + 'distance_defect_ref': (100.0, 'NM'), }, 'initial_guesses': { 'time': ([216.0, 1300.0], 's'), @@ -283,14 +282,16 @@ 'altitude_ref': (20_000, 'ft'), 'altitude_ref0': (0, 'ft'), 'altitude_constraint_ref': (10000, 'ft'), + 'altitude_direct_link': False, 'mass_bounds': ((0, None), 'lbm'), 'mass_ref': (140_000, 'lbm'), 'mass_ref0': (0, 'lbm'), 'mass_defect_ref': (140_000, 'lbm'), 'distance_bounds': ((1_000.0, 3_000.0), 'NM'), - 'distance_ref': (2_020, 'NM'), + 'distance_ref': (3200.0, 'NM'), 'distance_ref0': (0, 'NM'), 'distance_defect_ref': (100, 'NM'), + 'distance_direct_link': False, }, 'initial_guesses': { 'mass': (136000.0, 'lbm'), diff --git a/aviary/utils/aviary_options_dict.py b/aviary/utils/aviary_options_dict.py index 9edbad60cc..f45b20c304 100644 --- a/aviary/utils/aviary_options_dict.py +++ b/aviary/utils/aviary_options_dict.py @@ -289,6 +289,18 @@ def add_state_options(self, state_name: str, units: str = None, defaults=None): desc=desc, ) + name = f'{state_name}_direct_link' + default = defaults.get(name, True) + desc = 'When True, directly connect the initial state to the upstream phase.\n' + desc += 'When False, use a constraint.\n' + desc += f'Only valid when {state_name}_optimize is set to True.' + self.declare( + name=name, + default=default, + types=bool, + desc=desc, + ) + def add_control_options(self, ctrl_name: str, units: str = None, defaults=None): """ Adds all options needed for a control variable. @@ -399,6 +411,18 @@ def add_control_options(self, ctrl_name: str, units: str = None, defaults=None): desc=desc, ) + name = f'{ctrl_name}_direct_link' + default = defaults.get(name, True) + desc = 'When True, directly connect the first control point to the upstream phase.\n' + desc += 'When False, use a constraint.\n' + desc += f'Only valid when {ctrl_name}_optimize is set to True.' + self.declare( + name=name, + default=default, + types=bool, + desc=desc, + ) + def add_time_options(self, units: str = None, defaults=None): """ Adds all options for controlling time initial and duration. @@ -456,3 +480,14 @@ def add_time_options(self, units: str = None, defaults=None): units=units, desc=desc, ) + + name = 'time_initial_direct_link' + default = defaults.get(name, True) + desc = f'When True, directly connect the initial_time to the upstream phase.\n' + desc += 'When False, use a constraint.' + self.declare( + name=name, + default=default, + types=bool, + desc=desc, + ) diff --git a/aviary/validation_cases/benchmark_tests/test_bench_large_turboprop_freighter.py b/aviary/validation_cases/benchmark_tests/test_bench_large_turboprop_freighter.py index 049c22e412..0d13142210 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_large_turboprop_freighter.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_large_turboprop_freighter.py @@ -52,7 +52,7 @@ def build_and_run_problem(self, mission_method): prob.check_and_preprocess_inputs() prob.build_model() - prob.add_driver('SNOPT', max_iter=20, verbosity=1) + prob.add_driver('SNOPT', max_iter=25, verbosity=1) prob.add_design_variables() prob.add_objective() prob.setup()