From 3cdb4273a361ff59de8e6540dd9a281a901ffabd Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 26 May 2026 13:38:48 -0400 Subject: [PATCH 01/34] Working on link phases" --- aviary/core/aviary_group.py | 13 ++++++------- .../energy_state_problem_configurator.py | 14 ++++++-------- aviary/mission/problem_configurator.py | 18 ++++++++++++++++-- .../solved_two_dof_problem_configurator.py | 4 ---- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index a3e49de16f..6d3e9e9d34 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1178,10 +1178,6 @@ def link_phases(self, verbosity=None, comm=None): 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'] @@ -1190,11 +1186,14 @@ def link_phases(self, verbosity=None, comm=None): 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) + connected = False else: - self.traj.link_phases(phases=[phase1, phase2], vars=[var], connected=True) + connected = true_unless_mpi + self.traj.link_phases(phases=[phase1, phase2], vars=[var], connected=connected) + + # TODO need more unified way to handle this instead of splitting between AviaryGroup + # and configurators self.configurator.link_phases(self, phases, connect_directly=true_unless_mpi) self.configurator.check_trajectory(self) diff --git a/aviary/mission/energy_state_problem_configurator.py b/aviary/mission/energy_state_problem_configurator.py index 871c12cf29..09892b379c 100644 --- a/aviary/mission/energy_state_problem_configurator.py +++ b/aviary/mission/energy_state_problem_configurator.py @@ -241,34 +241,32 @@ 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. + phases : list[Phase] + List of all phases in the trajectory. 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. + When True, phases are connected directly in openmdao. When false, the phases are not + connected, and a constraint is added to drive the difference to zero. """ # 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 + aviary_group, aviary_group.regular_phases, 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, aviary_group.reserve_phases, Dynamic.Atmosphere.MACH ) # connect mass and distance between all phases regardless of reserve / diff --git a/aviary/mission/problem_configurator.py b/aviary/mission/problem_configurator.py index fc29075bf6..1aa60b6076 100644 --- a/aviary/mission/problem_configurator.py +++ b/aviary/mission/problem_configurator.py @@ -144,13 +144,27 @@ def check_trajectory(self, aviary_group): """ pass - def link_phases_helper_with_options(self, aviary_group, phases, option_name, var, **kwargs): + def link_phases_helper_with_options(self, aviary_group, phases, var, **kwargs): + """ + Links phases based on the options in the phase_info. + + Parameters + ---------- + aviary_group : AviaryGroup + Aviary model that owns this builder. + phases : List(Phase) + All linkable phases in this trajectory. + var : str + Aivary state or control to be linked across the phase boundaries. + """ + mission = aviary_group.mission_info # 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): + option_name = f'{var}_optimize' + if mission[phase_name]['user_options'].get(option_name, False): true_option_indices.append(idx) # Determine the groups of phases to link based on consecutive indices diff --git a/aviary/mission/solved_two_dof_problem_configurator.py b/aviary/mission/solved_two_dof_problem_configurator.py index 8d63888280..75dc9e9506 100644 --- a/aviary/mission/solved_two_dof_problem_configurator.py +++ b/aviary/mission/solved_two_dof_problem_configurator.py @@ -197,14 +197,12 @@ def link_phases(self, aviary_group, phases, connect_directly=True): 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, ) @@ -212,14 +210,12 @@ def link_phases(self, aviary_group, phases, connect_directly=True): 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, ) From 9e80e329cc707ae873b8d430d3e150317e561c64 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 29 May 2026 11:55:08 -0400 Subject: [PATCH 02/34] Starting on the new link code --- aviary/core/aviary_group.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 6d3e9e9d34..920b365317 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1145,6 +1145,23 @@ def link_phases(self, verbosity=None, comm=None): if len(phases) <= 1: return + # New section starts here + + # TODO: need to update the get_linked_variables api. + external_links = set() + for subsys in self.external_subsystems: + link_vars = subsys.get_linked_variables(aviary_inputs=self.aviary_inputs) + external_links.add(link_vars) + + # TODO: We can support linking a phase with any upstream phase. A networkx graph might be + # a good choice. + phase1 = phase[0] + builder1 = self.phase_objects[0] + phase_links1 = builder1.get_linked_variables(aviary_inputs=self.aviary_inputs) + for phase, builder in zip(phases[1:], self.phase_objects[1:]): + phase2 = phase + builder2 = builder + # 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 From c09190a0c51d1161836d273deee4a4e270224e4e Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 29 May 2026 14:14:54 -0400 Subject: [PATCH 03/34] general link_phases works for basic HE --- aviary/core/aviary_group.py | 179 ++++++++++++------ .../energy_state_problem_configurator.py | 76 ++++---- aviary/mission/flight_phase_builder.py | 10 + aviary/mission/phase_builder.py | 13 ++ aviary/utils/aviary_options_dict.py | 12 ++ 5 files changed, 191 insertions(+), 99 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 262a3ed2d4..c565c2c64c 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1145,80 +1145,137 @@ def link_phases(self, verbosity=None, comm=None): if len(phases) <= 1: return - # New section starts here - - # TODO: need to update the get_linked_variables api. - external_links = set() - for subsys in self.external_subsystems: - link_vars = subsys.get_linked_variables(aviary_inputs=self.aviary_inputs) - external_links.add(link_vars) - - # TODO: We can support linking a phase with any upstream phase. A networkx graph might be - # a good choice. - phase1 = phase[0] - builder1 = self.phase_objects[0] - phase_links1 = builder1.get_linked_variables(aviary_inputs=self.aviary_inputs) - for phase, builder in zip(phases[1:], self.phase_objects[1:]): - phase2 = phase - builder2 = builder + # 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 - # 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(). + # New linking section starts here - 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([]) + link_vars = set() 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, {}), - ) + 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) - # get unique variable names from lists_to_link - unique_vars = list(set([var for sublist in lists_to_link for var in sublist])) + phase_vars = builder.get_linked_variables() + link_vars = link_vars.union(phase_vars) - # 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 - 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: - connected = False - else: - connected = true_unless_mpi + 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] + + # Find common vars across 1-2 boundary + common = vars1.union(vars2) + + for var in common: + + opt1 = phase_info1.get(f'{var}_optimize', True) + opt2 = phase_info2.get(f'{var}_optimize', True) + + # If both sides are static controls, don't link. + if not (opt1 or opt2): + continue + + # If one side is a static control, use constraint instead of connection. + connect = False if not opt1 or not opt2 else connect_directly + + # Use ref from the previous phase. + # Time behaves a bit differently than the others. + if var == 'time': + rvar = 'time_duration' + else: + rvar = var + ref, units = phase_info1.get(f'{rvar}_ref') + ref0 = phase_info1.get(f'{rvar}_ref0')[0] + + self.traj.link_phases( + phases=[phase1, phase2], + connected=connect, + vars=[var], + ref=ref, + ref0=ref0, + units=units, + ) + + # TODO: Apply any transformations of similar variables across boundary. + + + # 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(). - self.traj.link_phases(phases=[phase1, phase2], vars=[var], connected=connected) + #lists_to_link = [] + #for idx, phase_name in enumerate(self.mission_info): + #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, {}), + #) + #) + + ## get unique variable names from lists_to_link + #unique_vars = list(set([var for sublist in lists_to_link for var in sublist])) + + ## 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 + #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: + #connected = False + #else: + #connected = connect_directly + + #self.traj.link_phases(phases=[phase1, phase2], vars=[var], connected=connected) # TODO need more unified way to handle this instead of splitting between AviaryGroup # and configurators - self.configurator.link_phases(self, phases, connect_directly=true_unless_mpi) + self.configurator.link_phases(self, phases, connect_directly=connect_directly) self.configurator.check_trajectory(self) diff --git a/aviary/mission/energy_state_problem_configurator.py b/aviary/mission/energy_state_problem_configurator.py index 09892b379c..8c7264e27d 100644 --- a/aviary/mission/energy_state_problem_configurator.py +++ b/aviary/mission/energy_state_problem_configurator.py @@ -248,44 +248,44 @@ def link_phases(self, aviary_group, phases, connect_directly=True): connected, and a constraint is added to drive the difference to zero. """ # 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, - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self.link_phases_helper_with_options( - aviary_group, aviary_group.regular_phases, 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, - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self.link_phases_helper_with_options( - aviary_group, aviary_group.reserve_phases, 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, - ) + #self.link_phases_helper_with_options( + #aviary_group, + #aviary_group.regular_phases, + #Dynamic.Mission.ALTITUDE, + #ref=1.0e4, + #) + #self.link_phases_helper_with_options( + #aviary_group, aviary_group.regular_phases, 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, + #Dynamic.Mission.ALTITUDE, + #ref=1.0e4, + #) + #self.link_phases_helper_with_options( + #aviary_group, aviary_group.reserve_phases, 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: diff --git a/aviary/mission/flight_phase_builder.py b/aviary/mission/flight_phase_builder.py index be77e46164..c9bceaeca3 100644 --- a/aviary/mission/flight_phase_builder.py +++ b/aviary/mission/flight_phase_builder.py @@ -413,6 +413,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..ad8508b1a2 100644 --- a/aviary/mission/phase_builder.py +++ b/aviary/mission/phase_builder.py @@ -629,6 +629,19 @@ 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. + + Analytic phases should define a + + Returns + ------- + linked_vars : list of variables to link between phases + """ + return [] + _registered_phase_builder_types = [] diff --git a/aviary/utils/aviary_options_dict.py b/aviary/utils/aviary_options_dict.py index 9edbad60cc..367178bf57 100644 --- a/aviary/utils/aviary_options_dict.py +++ b/aviary/utils/aviary_options_dict.py @@ -456,3 +456,15 @@ def add_time_options(self, units: str = None, defaults=None): units=units, desc=desc, ) + + name = f'{stem}_ref0' + default = defaults.get(name, 0.0) + desc = f'Additive scale factor "ref0" for {stem}.\n' + desc += 'Default is 0.0' + self.declare( + name=name, + default=default, + types=float, + units=units, + desc=desc, + ) \ No newline at end of file From a1be352c0d44f776e58ba1387e27c3e36282216f Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 29 May 2026 15:02:36 -0400 Subject: [PATCH 04/34] checkpoint --- aviary/core/aviary_group.py | 46 ++++++++++++------- .../energy_state_problem_configurator.py | 40 ---------------- 2 files changed, 29 insertions(+), 57 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index c565c2c64c..9ee23554b2 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1191,37 +1191,49 @@ def link_phases(self, verbosity=None, comm=None): phase_info2 = self.mission_info[phase2]['user_options'] vars2 = link_vars_dict[phase2] + # Don't link to first reserve phase. + if self.reserve_phases and phase2 == self.reserve_phases[0]: + continue + # Find common vars across 1-2 boundary common = vars1.union(vars2) - for var in common: + # Sort because of MPI + for var in sorted(common): - opt1 = phase_info1.get(f'{var}_optimize', True) - opt2 = phase_info2.get(f'{var}_optimize', True) + # Controls: True or False, everything else: None + opt1 = phase_info1.get(f'{var}_optimize', None) + opt2 = phase_info2.get(f'{var}_optimize', None) # If both sides are static controls, don't link. - if not (opt1 or opt2): + if opt1 is False and opt2 is False: continue - # If one side is a static control, use constraint instead of connection. - connect = False if not opt1 or not opt2 else connect_directly + # Controls cannot connect directly. + connect = False if opt2 is not None else connect_directly - # Use ref from the previous phase. - # Time behaves a bit differently than the others. - if var == 'time': - rvar = 'time_duration' - else: - rvar = var - ref, units = phase_info1.get(f'{rvar}_ref') - ref0 = phase_info1.get(f'{rvar}_ref0')[0] + kwargs = {} + if not connect: + # Use ref from the previous phase. + # Time behaves a bit differently than the others. + if var == 'time': + rvar = 'time_duration' + else: + rvar = var + ref, units = phase_info1.get(f'{rvar}_ref') + ref0 = phase_info1.get(f'{rvar}_ref0')[0] + + kwargs = { + 'ref': ref, + 'ref0': ref0, + 'units': units, + } self.traj.link_phases( phases=[phase1, phase2], connected=connect, vars=[var], - ref=ref, - ref0=ref0, - units=units, + **kwargs, ) # TODO: Apply any transformations of similar variables across boundary. diff --git a/aviary/mission/energy_state_problem_configurator.py b/aviary/mission/energy_state_problem_configurator.py index 8c7264e27d..5a860515bb 100644 --- a/aviary/mission/energy_state_problem_configurator.py +++ b/aviary/mission/energy_state_problem_configurator.py @@ -247,46 +247,6 @@ def link_phases(self, aviary_group, phases, connect_directly=True): When True, phases are connected directly in openmdao. When false, the phases are not connected, and a constraint is added to drive the difference to zero. """ - # 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, - #Dynamic.Mission.ALTITUDE, - #ref=1.0e4, - #) - #self.link_phases_helper_with_options( - #aviary_group, aviary_group.regular_phases, 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, - #Dynamic.Mission.ALTITUDE, - #ref=1.0e4, - #) - #self.link_phases_helper_with_options( - #aviary_group, aviary_group.reserve_phases, 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:]: From a82344662d740da11e36fc4fbd057ddd7e35c644 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 29 May 2026 18:04:09 -0400 Subject: [PATCH 05/34] Getting some groundwork in for the 2dof link phases --- aviary/core/aviary_group.py | 88 ++++++------------- .../energy_state_problem_configurator.py | 8 +- aviary/utils/aviary_options_dict.py | 12 --- 3 files changed, 28 insertions(+), 80 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 9ee23554b2..896d092b1d 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1153,8 +1153,6 @@ def link_phases(self, verbosity=None, comm=None): if comm.size > 1 and self.traj.options['parallel_phases']: connect_directly = False - # New linking section starts here - # Assemble all linkable variables in the phases. link_vars_dict = {} for builder in self.phase_objects: @@ -1209,25 +1207,42 @@ def link_phases(self, verbosity=None, comm=None): if opt1 is False and opt2 is False: continue + # If both ends are pinned with a constraint, don't link. + pin1 = phase_info1.get(f'{var}_final', (None, None))[0] + pin2 = phase_info2.get(f'{var}_initial', (None, None))[0] + if pin1 and pin2: + continue + # Controls cannot connect directly. connect = False if opt2 is not None else connect_directly + # Pinned front can't take input. + connect = False if pin2 else connect + kwargs = {} if not connect: + # Link with a constraint. # Use ref from the previous phase. # Time behaves a bit differently than the others. if var == 'time': - rvar = 'time_duration' + ref, units = phase_info1.get(f'{var}_duration_ref') + kwargs = { + 'ref': ref, + 'units': units, + } else: - rvar = var - ref, units = phase_info1.get(f'{rvar}_ref') - ref0 = phase_info1.get(f'{rvar}_ref0')[0] - - kwargs = { - 'ref': ref, - 'ref0': ref0, - 'units': units, - } + ref, units = phase_info1.get(f'{var}_ref') + ref0 = phase_info1.get(f'{var}_ref0')[0] + kwargs = { + 'ref': ref, + 'ref0': ref0, + 'units': units, + } + + # + if opt2 is None and var is not 'time': + phase = self.traj._phases[phase2] + phase.set_state_options(var, input_initial=False) self.traj.link_phases( phases=[phase1, phase2], @@ -1238,55 +1253,6 @@ def link_phases(self, verbosity=None, comm=None): # TODO: Apply any transformations of similar variables across boundary. - - # 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(). - - #lists_to_link = [] - #for idx, phase_name in enumerate(self.mission_info): - #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, {}), - #) - #) - - ## get unique variable names from lists_to_link - #unique_vars = list(set([var for sublist in lists_to_link for var in sublist])) - - ## 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 - #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: - #connected = False - #else: - #connected = connect_directly - - #self.traj.link_phases(phases=[phase1, phase2], vars=[var], connected=connected) - - # TODO need more unified way to handle this instead of splitting between AviaryGroup - # and configurators self.configurator.link_phases(self, phases, connect_directly=connect_directly) self.configurator.check_trajectory(self) diff --git a/aviary/mission/energy_state_problem_configurator.py b/aviary/mission/energy_state_problem_configurator.py index 5a860515bb..7cf1609e6f 100644 --- a/aviary/mission/energy_state_problem_configurator.py +++ b/aviary/mission/energy_state_problem_configurator.py @@ -247,13 +247,7 @@ def link_phases(self, aviary_group, phases, connect_directly=True): When True, phases are connected directly in openmdao. When false, the phases are not connected, and a constraint is added to drive the difference to zero. """ - # 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/utils/aviary_options_dict.py b/aviary/utils/aviary_options_dict.py index 367178bf57..9edbad60cc 100644 --- a/aviary/utils/aviary_options_dict.py +++ b/aviary/utils/aviary_options_dict.py @@ -456,15 +456,3 @@ def add_time_options(self, units: str = None, defaults=None): units=units, desc=desc, ) - - name = f'{stem}_ref0' - default = defaults.get(name, 0.0) - desc = f'Additive scale factor "ref0" for {stem}.\n' - desc += 'Default is 0.0' - self.declare( - name=name, - default=default, - types=float, - units=units, - desc=desc, - ) \ No newline at end of file From d69c79e141049d195d20d1264d827160df3bfe5f Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 2 Jun 2026 09:06:06 -0400 Subject: [PATCH 06/34] A little further --- aviary/core/aviary_group.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 896d092b1d..05466f2332 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1198,6 +1198,9 @@ def link_phases(self, verbosity=None, comm=None): # Sort because of MPI for var in sorted(common): + # Analytic phase, handle this input later. + if f'{var}_input' in vars2: + continue # Controls: True or False, everything else: None opt1 = phase_info1.get(f'{var}_optimize', None) @@ -1239,7 +1242,7 @@ def link_phases(self, verbosity=None, comm=None): 'units': units, } - # + # Make sure states options are correct for this. if opt2 is None and var is not 'time': phase = self.traj._phases[phase2] phase.set_state_options(var, input_initial=False) @@ -1251,7 +1254,16 @@ def link_phases(self, verbosity=None, comm=None): **kwargs, ) - # TODO: Apply any transformations of similar variables across boundary. + + # Analytic phases may take a single start input that needs to connect + # Sort because of MPI + for var in sorted(vars2): + if not var.startswith('input_'): + continue + + + + # TODO: Apply any transformations of similar variables across boundary. self.configurator.link_phases(self, phases, connect_directly=connect_directly) From 6fc314d8fa7d59c95f83cfd95ee8a097d9ca61a2 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 10 Jun 2026 15:48:29 -0400 Subject: [PATCH 07/34] Moved 2dof over to use the new link_phases --- aviary/core/aviary_group.py | 64 ++++- aviary/mission/two_dof/phases/accel_phase.py | 9 + .../two_dof/phases/breguet_cruise_phase.py | 9 + aviary/mission/two_dof/phases/flight_phase.py | 9 + .../two_dof/phases/simple_cruise_phase.py | 10 + .../mission/two_dof/phases/takeoff_phase.py | 18 ++ .../mission/two_dof_problem_configurator.py | 254 +++++++++--------- 7 files changed, 239 insertions(+), 134 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 8944472b89..6791197bcc 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1194,7 +1194,7 @@ def link_phases(self, verbosity=None, comm=None): continue # Find common vars across 1-2 boundary - common = vars1.union(vars2) + common = vars1.intersection(vars2) # Sort because of MPI for var in sorted(common): @@ -1234,8 +1234,12 @@ def link_phases(self, verbosity=None, comm=None): 'units': units, } else: - ref, units = phase_info1.get(f'{var}_ref') - ref0 = phase_info1.get(f'{var}_ref0')[0] + 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 = { 'ref': ref, 'ref0': ref0, @@ -1243,10 +1247,11 @@ def link_phases(self, verbosity=None, comm=None): } # Make sure states options are correct for this. - if opt2 is None and var is not 'time': + if opt2 is None and var != 'time': phase = self.traj._phases[phase2] phase.set_state_options(var, input_initial=False) + print(phase1, phase2, var) self.traj.link_phases( phases=[phase1, phase2], connected=connect, @@ -1254,14 +1259,59 @@ def link_phases(self, verbosity=None, comm=None): **kwargs, ) - - # Analytic phases may take a single start input that needs to connect + # Target analytic phases may take a single start input that needs to connect # Sort because of MPI for var in sorted(vars2): - if not var.startswith('input_'): + if not var.startswith('initial_'): continue + source = var.lstrip('initial_') + if source not in vars1: + continue + + # Link with a constraint. + # Use ref from the previous phase. + # Time behaves a bit differently than the others. + if source == 'time': + ref, units = phase_info1.get(f'{source}_duration_ref') + kwargs = { + 'ref': ref, + 'units': units, + } + else: + ref, units = phase_info1.get(f'{source}_ref', (None, None)) + ref0 = phase_info1.get(f'{source}_ref0', (None, None))[0] + if ref is None: + ref, units = phase_info2.get(f'{source}_ref', (None, None)) + ref0 = phase_info2.get(f'{source}_ref0', (None, None))[0] + kwargs = { + 'ref': ref, + 'ref0': ref0, + 'units': units, + } + + print(phase1, phase2, source, var) + self.traj.add_linkage_constraint( + phase1, phase2, source, var, connected=False + ) + + # Source analytic phases should still connect to the timeseries. + # Sort because of MPI + for var in sorted(vars1): + if not var.startswith('initial_'): + continue + var = var.lstrip('initial_') + if var not in vars2: + continue + + print(phase1, phase2, source, var) + self.traj.link_phases( + phases=[phase1, phase2], + connected=connect, + vars=[var], + **kwargs, + ) # TODO: Apply any transformations of similar variables across boundary. diff --git a/aviary/mission/two_dof/phases/accel_phase.py b/aviary/mission/two_dof/phases/accel_phase.py index f0043ca1f7..f51032dad4 100644 --- a/aviary/mission/two_dof/phases/accel_phase.py +++ b/aviary/mission/two_dof/phases/accel_phase.py @@ -149,6 +149,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.ALTITUDE, + 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..8ba7187c37 100644 --- a/aviary/mission/two_dof/phases/breguet_cruise_phase.py +++ b/aviary/mission/two_dof/phases/breguet_cruise_phase.py @@ -154,6 +154,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 = [ + 'initial_distance', + 'initial_time', + Dynamic.Mission.ALTITUDE, + Dynamic.Atmosphere.MACH, + ] + 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 4de5a7a28d..91bec3b14f 100644 --- a/aviary/mission/two_dof/phases/flight_phase.py +++ b/aviary/mission/two_dof/phases/flight_phase.py @@ -194,6 +194,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 084c01af37..8bfd1ca76f 100644 --- a/aviary/mission/two_dof/phases/simple_cruise_phase.py +++ b/aviary/mission/two_dof/phases/simple_cruise_phase.py @@ -185,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_distance', + Dynamic.Atmosphere.MACH, + Dynamic.Mission.ALTITUDE, + Dynamic.Vehicle.MASS, + 'time', + ] + 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 85a57b11d7..31992fc351 100644 --- a/aviary/mission/two_dof/phases/takeoff_phase.py +++ b/aviary/mission/two_dof/phases/takeoff_phase.py @@ -283,6 +283,24 @@ 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.ANGLE_OF_ATTACK, + Dynamic.Vehicle.MASS, + 'time', + ] + + if not (ground_roll or rotation): + linked_vars.append(Dynamic.Mission.ALTITUDE) + + 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..a8f3613a08 100644 --- a/aviary/mission/two_dof_problem_configurator.py +++ b/aviary/mission/two_dof_problem_configurator.py @@ -361,133 +361,133 @@ def link_phases(self, aviary_group, phases, connect_directly=True): When True, then connected=True. This allows the connections to be handled by constraints if `phases` is a parallel group under MPI. """ - 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 - ) + #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', From 32d00af37ecc447000620ad884ac967c63047997 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 10 Jun 2026 17:08:37 -0400 Subject: [PATCH 08/34] Getting closer --- aviary/core/aviary_group.py | 47 ++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 6791197bcc..799dbcec7e 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -43,7 +43,7 @@ Verbosity, ) from aviary.variable_info.functions import setup_trajectory_params -from aviary.variable_info.variables import Aircraft, Mission, Settings +from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings TWO_DEGREES_OF_FREEDOM = EquationsOfMotion.TWO_DEGREES_OF_FREEDOM ENERGY_STATE = EquationsOfMotion.ENERGY_STATE @@ -1195,13 +1195,11 @@ def link_phases(self, verbosity=None, comm=None): # 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): - # Analytic phase, handle this input later. - if f'{var}_input' in vars2: - continue - # Controls: True or False, everything else: None opt1 = phase_info1.get(f'{var}_optimize', None) opt2 = phase_info2.get(f'{var}_optimize', None) @@ -1216,11 +1214,28 @@ def link_phases(self, verbosity=None, comm=None): if pin1 and pin2: continue - # Controls cannot connect directly. - connect = False if opt2 is not None else connect_directly - # Pinned front can't take input. - connect = False if pin2 else connect + if var == 'time': + # Always connect time. + connect = connect_directly + + elif var == Dynamic.Vehicle.ANGLE_OF_ATTACK: + # This has been troublesome if connected directly. + connect = False + + elif len(downstream_analytic) > 0 or len(upstream_analytic) > 0: + # Constraints seem to work better with the analytic phases. + connect = False + + else: + # Controls cannot connect directly. + connect = False if opt2 is not None else connect_directly + + # Pinned front can't take input. + connect = False if pin2 else connect + + if var == 'mass': + connect = False kwargs = {} if not connect: @@ -1261,10 +1276,7 @@ def link_phases(self, verbosity=None, comm=None): # Target analytic phases may take a single start input that needs to connect # Sort because of MPI - for var in sorted(vars2): - if not var.startswith('initial_'): - continue - + for var in sorted(downstream_analytic): source = var.lstrip('initial_') if source not in vars1: continue @@ -1292,15 +1304,12 @@ def link_phases(self, verbosity=None, comm=None): print(phase1, phase2, source, var) self.traj.add_linkage_constraint( - phase1, phase2, source, var, connected=False + phase1, phase2, source, var, connected=False, **kwargs ) # Source analytic phases should still connect to the timeseries. # Sort because of MPI - for var in sorted(vars1): - if not var.startswith('initial_'): - continue - + for var in sorted(upstream_analytic): var = var.lstrip('initial_') if var not in vars2: continue @@ -1308,7 +1317,7 @@ def link_phases(self, verbosity=None, comm=None): print(phase1, phase2, source, var) self.traj.link_phases( phases=[phase1, phase2], - connected=connect, + connected=False, vars=[var], **kwargs, ) From 4a7eeb20d1226a9ff71349979e289b5b73d1aeda Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Thu, 11 Jun 2026 09:30:08 -0400 Subject: [PATCH 09/34] Some odd dymos bug? --- aviary/core/aviary_group.py | 6 +++--- aviary/mission/two_dof/phases/takeoff_phase.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 799dbcec7e..895f3daae7 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1266,7 +1266,7 @@ def link_phases(self, verbosity=None, comm=None): phase = self.traj._phases[phase2] phase.set_state_options(var, input_initial=False) - print(phase1, phase2, var) + print(phase1, phase2, var, connect) self.traj.link_phases( phases=[phase1, phase2], connected=connect, @@ -1302,7 +1302,7 @@ def link_phases(self, verbosity=None, comm=None): 'units': units, } - print(phase1, phase2, source, var) + print(phase1, phase2, source, var, False) self.traj.add_linkage_constraint( phase1, phase2, source, var, connected=False, **kwargs ) @@ -1314,7 +1314,7 @@ def link_phases(self, verbosity=None, comm=None): if var not in vars2: continue - print(phase1, phase2, source, var) + print(phase1, phase2, source, var, False) self.traj.link_phases( phases=[phase1, phase2], connected=False, diff --git a/aviary/mission/two_dof/phases/takeoff_phase.py b/aviary/mission/two_dof/phases/takeoff_phase.py index 31992fc351..9d4a661215 100644 --- a/aviary/mission/two_dof/phases/takeoff_phase.py +++ b/aviary/mission/two_dof/phases/takeoff_phase.py @@ -291,7 +291,6 @@ def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_ linked_vars = [ Dynamic.Mission.DISTANCE, Dynamic.Mission.VELOCITY, - Dynamic.Vehicle.ANGLE_OF_ATTACK, Dynamic.Vehicle.MASS, 'time', ] @@ -299,6 +298,9 @@ def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_ if not (ground_roll or rotation): linked_vars.append(Dynamic.Mission.ALTITUDE) + if rotation: + linked_vars.append(Dynamic.Vehicle.ANGLE_OF_ATTACK) + return linked_vars def get_parameters(self): From 8613ca3b5b2f00dcb6079e77bf6cf5fd7fe22c58 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 12 Jun 2026 11:03:46 -0400 Subject: [PATCH 10/34] Found the problem --- aviary/core/aviary_group.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 895f3daae7..97234c1830 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1264,7 +1264,8 @@ def link_phases(self, verbosity=None, comm=None): # Make sure states options are correct for this. if opt2 is None and var != 'time': phase = self.traj._phases[phase2] - phase.set_state_options(var, input_initial=False) + if var in phase.state_options: + phase.set_state_options(var, input_initial=False) print(phase1, phase2, var, connect) self.traj.link_phases( From 0fa1294bd16f433adb9cd48915963cba35e810f3 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 12 Jun 2026 12:52:14 -0400 Subject: [PATCH 11/34] 2dof is working --- aviary/core/aviary_group.py | 96 ++++++------- .../energy_state_problem_configurator.py | 7 +- aviary/mission/problem_configurator.py | 7 +- .../solved_two_dof_problem_configurator.py | 7 +- .../mission/two_dof/phases/takeoff_phase.py | 2 +- .../mission/two_dof_problem_configurator.py | 135 +----------------- 6 files changed, 47 insertions(+), 207 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 97234c1830..afd644b189 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', @@ -1214,11 +1214,14 @@ def link_phases(self, verbosity=None, comm=None): if pin1 and pin2: continue - if var == 'time': # Always connect time. connect = connect_directly + elif self.mission_method is TWO_DEGREES_OF_FREEDOM and var == 'mass': + # In twodof, we didn't connect the mass directly. + connect = False + elif var == Dynamic.Vehicle.ANGLE_OF_ATTACK: # This has been troublesome if connected directly. connect = False @@ -1234,32 +1237,9 @@ def link_phases(self, verbosity=None, comm=None): # Pinned front can't take input. connect = False if pin2 else connect - if var == 'mass': - connect = False - kwargs = {} if not connect: - # Link with a constraint. - # Use ref from the previous phase. - # Time behaves a bit differently than the others. - if var == 'time': - ref, units = phase_info1.get(f'{var}_duration_ref') - kwargs = { - 'ref': ref, - 'units': units, - } - else: - 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 = { - 'ref': ref, - 'ref0': ref0, - 'units': units, - } + kwargs = self._find_scaling(var, phase_info1, phase_info2) # Make sure states options are correct for this. if opt2 is None and var != 'time': @@ -1267,7 +1247,6 @@ def link_phases(self, verbosity=None, comm=None): if var in phase.state_options: phase.set_state_options(var, input_initial=False) - print(phase1, phase2, var, connect) self.traj.link_phases( phases=[phase1, phase2], connected=connect, @@ -1282,31 +1261,11 @@ def link_phases(self, verbosity=None, comm=None): if source not in vars1: continue - # Link with a constraint. - # Use ref from the previous phase. - # Time behaves a bit differently than the others. - if source == 'time': - ref, units = phase_info1.get(f'{source}_duration_ref') - kwargs = { - 'ref': ref, - 'units': units, - } - else: - ref, units = phase_info1.get(f'{source}_ref', (None, None)) - ref0 = phase_info1.get(f'{source}_ref0', (None, None))[0] - if ref is None: - ref, units = phase_info2.get(f'{source}_ref', (None, None)) - ref0 = phase_info2.get(f'{source}_ref0', (None, None))[0] - kwargs = { - 'ref': ref, - 'ref0': ref0, - 'units': units, - } - - print(phase1, phase2, source, var, False) - self.traj.add_linkage_constraint( - phase1, phase2, source, var, connected=False, **kwargs - ) + kwargs = self._find_scaling(source, phase_info1, phase_info2) + + self.traj.add_linkage_constraint( + phase1, phase2, source, var, connected=False, **kwargs + ) # Source analytic phases should still connect to the timeseries. # Sort because of MPI @@ -1315,7 +1274,8 @@ def link_phases(self, verbosity=None, comm=None): if var not in vars2: continue - print(phase1, phase2, source, var, False) + kwargs = self._find_scaling(var, phase_info1, phase_info2) + self.traj.link_phases( phases=[phase1, phase2], connected=False, @@ -1323,12 +1283,40 @@ def link_phases(self, verbosity=None, comm=None): **kwargs, ) - # TODO: Apply any transformations of similar variables across boundary. + # TODO: Apply any coordinate transformations of similar variables across boundary. self.configurator.link_phases(self, phases, connect_directly=connect_directly) self.configurator.check_trajectory(self) + def _find_scaling(self, var, phase_info1, phase_info2): + """ + Returns a dictionary of scaling keyword arguments for a dymos linkage constraint. + """ + 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 ref is not None: + kwargs['units'] = units + + return kwargs + def _add_bus_variables_and_connect(self): all_subsystems = self.subsystems diff --git a/aviary/mission/energy_state_problem_configurator.py b/aviary/mission/energy_state_problem_configurator.py index 7cf1609e6f..b7fdace5d6 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 diff --git a/aviary/mission/problem_configurator.py b/aviary/mission/problem_configurator.py index 1aa60b6076..3c7cd257ee 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 diff --git a/aviary/mission/solved_two_dof_problem_configurator.py b/aviary/mission/solved_two_dof_problem_configurator.py index 092f1c7fbd..147d3006d5 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 diff --git a/aviary/mission/two_dof/phases/takeoff_phase.py b/aviary/mission/two_dof/phases/takeoff_phase.py index 9d4a661215..429eeaa17d 100644 --- a/aviary/mission/two_dof/phases/takeoff_phase.py +++ b/aviary/mission/two_dof/phases/takeoff_phase.py @@ -298,7 +298,7 @@ def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_ if not (ground_roll or rotation): linked_vars.append(Dynamic.Mission.ALTITUDE) - if rotation: + if not ground_roll: linked_vars.append(Dynamic.Vehicle.ANGLE_OF_ATTACK) return linked_vars diff --git a/aviary/mission/two_dof_problem_configurator.py b/aviary/mission/two_dof_problem_configurator.py index a8f3613a08..fd530fd600 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 @@ -361,134 +356,6 @@ def link_phases(self, aviary_group, phases, connect_directly=True): When True, then connected=True. This allows the connections to be handled by constraints if `phases` is a parallel group under MPI. """ - #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=[ From b55ad30251232e11e36f98186a9050dedf97e638 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Fri, 12 Jun 2026 13:15:06 -0400 Subject: [PATCH 12/34] Works, but some benches aren't stable yet --- aviary/interface/test/test_linkage_logic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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', From 4179ef6c1c400e572720cb68cbc869a80087bd2a Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 16 Jun 2026 11:57:12 -0400 Subject: [PATCH 13/34] a few adjustments --- aviary/mission/two_dof/phases/breguet_cruise_phase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aviary/mission/two_dof/phases/breguet_cruise_phase.py b/aviary/mission/two_dof/phases/breguet_cruise_phase.py index 8ba7187c37..d64997d8cd 100644 --- a/aviary/mission/two_dof/phases/breguet_cruise_phase.py +++ b/aviary/mission/two_dof/phases/breguet_cruise_phase.py @@ -160,6 +160,7 @@ def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_ 'initial_time', Dynamic.Mission.ALTITUDE, Dynamic.Atmosphere.MACH, + Dynamic.Vehicle.MASS, ] return linked_vars From 420e0cadfcf86a4d38ce9fd7beaa4b68ee78314a Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 16 Jun 2026 13:21:07 -0400 Subject: [PATCH 14/34] breguet link phases working --- aviary/core/aviary_group.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 0a3aa13aac..22e6a87f11 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1216,8 +1216,9 @@ def link_phases(self, verbosity=None, comm=None): if pin1 and pin2: continue - if var == 'time': - # Always connect time. + if var == 'time' or var == 'distance': + # Connecting these directly makes the problem more robust to mistakes in the + # user-assigned initial conditions. connect = connect_directly elif self.mission_method is TWO_DEGREES_OF_FREEDOM and var == 'mass': @@ -1259,28 +1260,38 @@ def link_phases(self, verbosity=None, comm=None): # 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.lstrip('initial_') + source = var.removeprefix('initial_') if source not in vars1: continue - kwargs = self._find_scaling(source, phase_info1, phase_info2) + if source == 'time' or source == 'distance': + connected = connect_directly + kwargs = {} + else: + connected = False + kwargs = self._find_scaling(source, phase_info1, phase_info2) self.traj.add_linkage_constraint( - phase1, phase2, source, var, connected=False, **kwargs + phase1, phase2, source, var, connected=connected, **kwargs ) # Source analytic phases should still connect to the timeseries. # Sort because of MPI for var in sorted(upstream_analytic): - var = var.lstrip('initial_') + var = var.removeprefix('initial_') if var not in vars2: continue - kwargs = self._find_scaling(var, phase_info1, phase_info2) + if var == 'time' or var == 'distance': + connected = connect_directly + kwargs = {} + else: + connected = False + kwargs = self._find_scaling(var, phase_info1, phase_info2) self.traj.link_phases( phases=[phase1, phase2], - connected=False, + connected=connected, vars=[var], **kwargs, ) From f39cc958806a3c1daae69e5d61fc42952cd04cb6 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 16 Jun 2026 13:23:30 -0400 Subject: [PATCH 15/34] breguet link phases working --- aviary/core/aviary_group.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 22e6a87f11..6e3e9b0d0f 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1216,6 +1216,8 @@ def link_phases(self, verbosity=None, comm=None): if pin1 and pin2: continue + connect = connect_directly + if var == 'time' or var == 'distance': # Connecting these directly makes the problem more robust to mistakes in the # user-assigned initial conditions. @@ -1233,12 +1235,13 @@ def link_phases(self, verbosity=None, comm=None): # Constraints seem to work better with the analytic phases. connect = False - else: + elif opt2 is not None : # Controls cannot connect directly. - connect = False if opt2 is not None else connect_directly + connect = False + elif pin2: # Pinned front can't take input. - connect = False if pin2 else connect + connect = False kwargs = {} if not connect: From 5d32c11cec09cadd97048e360ceae08d5c78a7c0 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 16 Jun 2026 13:42:44 -0400 Subject: [PATCH 16/34] dbugging --- aviary/core/aviary_group.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 6e3e9b0d0f..c688529076 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1231,9 +1231,9 @@ def link_phases(self, verbosity=None, comm=None): # This has been troublesome if connected directly. connect = False - elif len(downstream_analytic) > 0 or len(upstream_analytic) > 0: - # Constraints seem to work better with the analytic phases. - connect = False + #elif len(downstream_analytic) > 0 or len(upstream_analytic) > 0: + # # Constraints seem to work better with the analytic phases. + # connect = True elif opt2 is not None : # Controls cannot connect directly. @@ -1259,6 +1259,7 @@ def link_phases(self, verbosity=None, comm=None): vars=[var], **kwargs, ) + print(var, phase1, phase2, connect) # Target analytic phases may take a single start input that needs to connect # Sort because of MPI From 4f3ae159ad5323bd7a4eb0a77b1b6c4034b48b3b Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 23 Jun 2026 14:45:52 -0400 Subject: [PATCH 17/34] Implemented a new phase_info key to control the input link connection/constraint' --- aviary/core/aviary_group.py | 33 +++++++++------- aviary/mission/flight_phase_builder.py | 13 ++++++- .../two_dof/phases/breguet_cruise_phase.py | 2 +- aviary/mission/two_dof/phases/flight_phase.py | 8 +++- .../two_dof/phases/simple_cruise_phase.py | 39 +++++++++---------- .../mission/two_dof/phases/takeoff_phase.py | 5 +++ aviary/utils/aviary_options_dict.py | 35 +++++++++++++++++ 7 files changed, 96 insertions(+), 39 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index c688529076..5399249bff 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1213,29 +1213,35 @@ def link_phases(self, verbosity=None, comm=None): # If both ends are pinned with a constraint, don't link. pin1 = phase_info1.get(f'{var}_final', (None, None))[0] pin2 = phase_info2.get(f'{var}_initial', (None, None))[0] - if pin1 and pin2: + if pin2: + # Pinned front can't take input. continue - connect = connect_directly + # MPI takes precedence, since we can't connect directly in the parallel group. + 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 var == 'time' or var == 'distance': - # Connecting these directly makes the problem more robust to mistakes in the - # user-assigned initial conditions. - connect = connect_directly + #if var == 'time' or var == 'distance': + ## Connecting these directly makes the problem more robust to mistakes in the + ## user-assigned initial conditions. + #connect = connect_directly - elif self.mission_method is TWO_DEGREES_OF_FREEDOM and var == 'mass': - # In twodof, we didn't connect the mass directly. - connect = False + #elif self.mission_method is TWO_DEGREES_OF_FREEDOM and (var == 'mass' or var == 'altitude'): + ## In twodof, we didn't connect the mass directly. + #connect = False - elif var == Dynamic.Vehicle.ANGLE_OF_ATTACK: - # This has been troublesome if connected directly. - connect = False + #elif var == Dynamic.Vehicle.ANGLE_OF_ATTACK: + ## This has been troublesome if connected directly. + #connect = False #elif len(downstream_analytic) > 0 or len(upstream_analytic) > 0: # # Constraints seem to work better with the analytic phases. # connect = True - elif opt2 is not None : + if opt2 is False: # Controls cannot connect directly. connect = False @@ -1259,7 +1265,6 @@ def link_phases(self, verbosity=None, comm=None): vars=[var], **kwargs, ) - print(var, phase1, phase2, connect) # Target analytic phases may take a single start input that needs to connect # Sort because of MPI diff --git a/aviary/mission/flight_phase_builder.py b/aviary/mission/flight_phase_builder.py index ad0df66459..dbd9b81fbf 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': True, + } + 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': True, } 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', diff --git a/aviary/mission/two_dof/phases/breguet_cruise_phase.py b/aviary/mission/two_dof/phases/breguet_cruise_phase.py index d64997d8cd..a78aa190db 100644 --- a/aviary/mission/two_dof/phases/breguet_cruise_phase.py +++ b/aviary/mission/two_dof/phases/breguet_cruise_phase.py @@ -156,8 +156,8 @@ def build_phase(self, aviary_options: AviaryValues = None): def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): linked_vars = [ - 'initial_distance', 'initial_time', + 'initial_distance', Dynamic.Mission.ALTITUDE, Dynamic.Atmosphere.MACH, Dynamic.Vehicle.MASS, diff --git a/aviary/mission/two_dof/phases/flight_phase.py b/aviary/mission/two_dof/phases/flight_phase.py index 7a50704316..14e22c02ba 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': False, } 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', diff --git a/aviary/mission/two_dof/phases/simple_cruise_phase.py b/aviary/mission/two_dof/phases/simple_cruise_phase.py index 51ead6b0bd..580a058d48 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,19 @@ 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.', - ) - - 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='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( - '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.' ) @@ -187,11 +184,11 @@ def build_phase(self, aviary_options: AviaryValues = None): 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, - 'time', ] return linked_vars diff --git a/aviary/mission/two_dof/phases/takeoff_phase.py b/aviary/mission/two_dof/phases/takeoff_phase.py index 5cb3709cca..8b32250d44 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. diff --git a/aviary/utils/aviary_options_dict.py b/aviary/utils/aviary_options_dict.py index 9edbad60cc..ec0a3a91aa 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, + ) \ No newline at end of file From 6b4c28bf07c7ab1cd6f442305513ff3e4f60e955 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 23 Jun 2026 15:56:44 -0400 Subject: [PATCH 18/34] cleaning up and getting last tests to pass --- aviary/core/aviary_group.py | 49 ++++++------- aviary/mission/problem_configurator.py | 56 --------------- .../phases/solved_twodof_phase.py | 17 +++-- .../solved_two_dof_problem_configurator.py | 71 ------------------- aviary/mission/two_dof/phases/accel_phase.py | 9 ++- .../two_dof/phases/breguet_cruise_phase.py | 31 ++++++++ .../two_dof/phases/simple_cruise_phase.py | 8 +++ 7 files changed, 79 insertions(+), 162 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 5399249bff..174894d1a9 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1213,34 +1213,19 @@ def link_phases(self, verbosity=None, comm=None): # If both ends are pinned with a constraint, don't link. pin1 = phase_info1.get(f'{var}_final', (None, None))[0] pin2 = phase_info2.get(f'{var}_initial', (None, None))[0] - if pin2: - # Pinned front can't take input. + 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 var == 'time' or var == 'distance': - ## Connecting these directly makes the problem more robust to mistakes in the - ## user-assigned initial conditions. - #connect = connect_directly - - #elif self.mission_method is TWO_DEGREES_OF_FREEDOM and (var == 'mass' or var == 'altitude'): - ## In twodof, we didn't connect the mass directly. - #connect = False - - #elif var == Dynamic.Vehicle.ANGLE_OF_ATTACK: - ## This has been troublesome if connected directly. - #connect = False - - #elif len(downstream_analytic) > 0 or len(upstream_analytic) > 0: - # # Constraints seem to work better with the analytic phases. - # connect = True - if opt2 is False: # Controls cannot connect directly. connect = False @@ -1273,15 +1258,18 @@ def link_phases(self, verbosity=None, comm=None): if source not in vars1: continue - if source == 'time' or source == 'distance': - connected = connect_directly - kwargs = {} + if source == 'time': + key = f'time_initial_direct_link' else: - connected = False + key = f'{source}_direct_link' + connect = connect_directly and phase_info2.get(key, connect_directly) + + kwargs = {} + if not connect: kwargs = self._find_scaling(source, phase_info1, phase_info2) self.traj.add_linkage_constraint( - phase1, phase2, source, var, connected=connected, **kwargs + phase1, phase2, source, var, connected=connect, **kwargs ) # Source analytic phases should still connect to the timeseries. @@ -1291,16 +1279,19 @@ def link_phases(self, verbosity=None, comm=None): if var not in vars2: continue - if var == 'time' or var == 'distance': - connected = connect_directly - kwargs = {} + if var == 'time': + key = f'time_initial_direct_link' else: - connected = False + key = f'{var}_direct_link' + connect = connect_directly and phase_info2.get(key, connect_directly) + + kwargs = {} + if not connect: kwargs = self._find_scaling(var, phase_info1, phase_info2) self.traj.link_phases( phases=[phase1, phase2], - connected=connected, + connected=connect, vars=[var], **kwargs, ) diff --git a/aviary/mission/problem_configurator.py b/aviary/mission/problem_configurator.py index 3c7cd257ee..d5ac66b46a 100644 --- a/aviary/mission/problem_configurator.py +++ b/aviary/mission/problem_configurator.py @@ -139,62 +139,6 @@ def check_trajectory(self, aviary_group): """ pass - def link_phases_helper_with_options(self, aviary_group, phases, var, **kwargs): - """ - Links phases based on the options in the phase_info. - - Parameters - ---------- - aviary_group : AviaryGroup - Aviary model that owns this builder. - phases : List(Phase) - All linkable phases in this trajectory. - var : str - Aivary state or control to be linked across the phase boundaries. - """ - mission = aviary_group.mission_info - # 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): - option_name = f'{var}_optimize' - if mission[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/solved_twodof_phase.py b/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py index ac5c1e8fb6..eb9a982564 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': True, + } + 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': True, } 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 = { + 'initial_time_direct_link': False, + } + self.add_time_options(units='ft', defaults=defaults) self.declare( 'reserve', diff --git a/aviary/mission/solved_two_dof_problem_configurator.py b/aviary/mission/solved_two_dof_problem_configurator.py index 147d3006d5..b774f42606 100644 --- a/aviary/mission/solved_two_dof_problem_configurator.py +++ b/aviary/mission/solved_two_dof_problem_configurator.py @@ -169,77 +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, - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self.link_phases_helper_with_options( - aviary_group, - aviary_group.regular_phases, - 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, - Dynamic.Mission.ALTITUDE, - ref=1.0e4, - ) - self.link_phases_helper_with_options( - aviary_group, - aviary_group.reserve_phases, - 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 383521f75a..37aaa0b38a 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', @@ -151,7 +157,6 @@ def build_phase(self, aviary_options: AviaryValues = None): def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_options=None): linked_vars = [ - #Dynamic.Mission.ALTITUDE, Dynamic.Mission.DISTANCE, Dynamic.Mission.VELOCITY, Dynamic.Vehicle.MASS, diff --git a/aviary/mission/two_dof/phases/breguet_cruise_phase.py b/aviary/mission/two_dof/phases/breguet_cruise_phase.py index a78aa190db..302f983d8e 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): """ diff --git a/aviary/mission/two_dof/phases/simple_cruise_phase.py b/aviary/mission/two_dof/phases/simple_cruise_phase.py index 580a058d48..9be88fb75a 100644 --- a/aviary/mission/two_dof/phases/simple_cruise_phase.py +++ b/aviary/mission/two_dof/phases/simple_cruise_phase.py @@ -69,6 +69,14 @@ def declare_options(self): 'phase. When False, use a constraint.' ) + self.declare( + 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( name='mach_direct_link', default=True, From a9fd54dae04314707b7413622176d89ecd6c6ad5 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 23 Jun 2026 16:09:03 -0400 Subject: [PATCH 19/34] cleaning up and getting last tests to pass --- aviary/mission/two_dof/phases/flight_phase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/mission/two_dof/phases/flight_phase.py b/aviary/mission/two_dof/phases/flight_phase.py index 14e22c02ba..afc0270a64 100644 --- a/aviary/mission/two_dof/phases/flight_phase.py +++ b/aviary/mission/two_dof/phases/flight_phase.py @@ -43,7 +43,7 @@ def declare_options(self): defaults = { 'altitude_bounds': (0.0, None), - 'altitude_direct_link': False, + 'altitude_direct_link': True, } self.add_state_options('altitude', units='ft', defaults=defaults) From b7240a1972d544e700b2acdf1fcb3188f38ea6c9 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 23 Jun 2026 21:21:00 -0400 Subject: [PATCH 20/34] All tests pass --- aviary/mission/flight_phase_builder.py | 4 ++-- .../solved_two_dof/phases/solved_twodof_phase.py | 4 ++-- .../aircraft/large_turboprop_freighter/phase_info.py | 11 ++++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/aviary/mission/flight_phase_builder.py b/aviary/mission/flight_phase_builder.py index dbd9b81fbf..3f34a79223 100644 --- a/aviary/mission/flight_phase_builder.py +++ b/aviary/mission/flight_phase_builder.py @@ -57,14 +57,14 @@ def declare_options(self): self.add_state_options('distance', units='m', defaults=defaults) defaults = { - 'altitude_direct_link': True, + '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': True, + 'mach_direct_link': False, } self.add_control_options('mach', units='unitless', defaults=defaults) 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 eb9a982564..02d474bb35 100644 --- a/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py +++ b/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py @@ -58,14 +58,14 @@ def declare_options(self): self.add_state_options('distance', units='m', defaults=defaults) defaults = { - 'altitude_direct_link': True, + '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': True, + 'mach_direct_link': False, } self.add_control_options('mach', units='unitless', defaults=defaults) diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index 78930456ce..e81e4a852f 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': (32., 'NM'), + 'distance_defect_ref': (32., 'NM'), }, 'initial_guesses': { 'time': ([216.0, 1300.0], 's'), @@ -283,6 +282,7 @@ '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'), @@ -291,6 +291,7 @@ 'distance_ref': (2_020, 'NM'), 'distance_ref0': (0, 'NM'), 'distance_defect_ref': (100, 'NM'), + 'distance_direct_link': False, }, 'initial_guesses': { 'mass': (136000.0, 'lbm'), From 2477a356d0f2ee3d83cf001bd9a025ea548c17bb Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 10:27:14 -0400 Subject: [PATCH 21/34] Some cleanup --- aviary/core/aviary_group.py | 30 +++++++++---------- .../energy_state_problem_configurator.py | 10 ++----- aviary/mission/phase_builder.py | 4 ++- aviary/mission/problem_configurator.py | 16 ++++------ .../mission/two_dof_problem_configurator.py | 25 +++------------- aviary/subsystems/subsystem_builder.py | 4 +++ 6 files changed, 33 insertions(+), 56 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 174894d1a9..90e8253c38 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -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 @@ -1191,8 +1190,8 @@ def link_phases(self, verbosity=None, comm=None): phase_info2 = self.mission_info[phase2]['user_options'] vars2 = link_vars_dict[phase2] - # Don't link to first reserve phase. 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 @@ -1206,11 +1205,10 @@ def link_phases(self, verbosity=None, comm=None): opt1 = phase_info1.get(f'{var}_optimize', None) opt2 = phase_info2.get(f'{var}_optimize', None) - # If both sides are static controls, don't link. if opt1 is False and opt2 is False: + # If both sides are static controls, don't link. continue - # If both ends are pinned with a constraint, don't link. pin1 = phase_info1.get(f'{var}_final', (None, None))[0] pin2 = phase_info2.get(f'{var}_initial', (None, None))[0] if pin1 and pin2: @@ -1275,30 +1273,32 @@ def link_phases(self, verbosity=None, comm=None): # Source analytic phases should still connect to the timeseries. # Sort because of MPI for var in sorted(upstream_analytic): - var = var.removeprefix('initial_') - if var not in vars2: + target = var.removeprefix('initial_') + if target not in vars2: continue - if var == 'time': + 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'{var}_direct_link' + key = f'{target}_direct_link' connect = connect_directly and phase_info2.get(key, connect_directly) kwargs = {} if not connect: - kwargs = self._find_scaling(var, phase_info1, phase_info2) + kwargs = self._find_scaling(target, phase_info1, phase_info2) self.traj.link_phases( phases=[phase1, phase2], connected=connect, - vars=[var], + vars=[target], **kwargs, ) - # TODO: Apply any coordinate transformations of similar variables across boundary. - - self.configurator.link_phases(self, phases, connect_directly=connect_directly) + self.configurator.link_trajectory(self, phases) self.configurator.check_trajectory(self) diff --git a/aviary/mission/energy_state_problem_configurator.py b/aviary/mission/energy_state_problem_configurator.py index 646b28bc8f..5d4c4ae4bd 100644 --- a/aviary/mission/energy_state_problem_configurator.py +++ b/aviary/mission/energy_state_problem_configurator.py @@ -223,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 link_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 @@ -238,9 +235,6 @@ def link_phases(self, aviary_group, phases, connect_directly=True): Aviary model that owns this configurator. phases : list[Phase] List of all phases in the trajectory. - connect_directly : bool - When True, phases are connected directly in openmdao. When false, the phases are not - connected, and a constraint is added to drive the difference to zero. """ # Boundary conditions for the first phase. phase = aviary_group.traj._phases[phases[0]] diff --git a/aviary/mission/phase_builder.py b/aviary/mission/phase_builder.py index ad8508b1a2..8fbe95dcb8 100644 --- a/aviary/mission/phase_builder.py +++ b/aviary/mission/phase_builder.py @@ -634,7 +634,9 @@ 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. - Analytic phases should define a + 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 ------- diff --git a/aviary/mission/problem_configurator.py b/aviary/mission/problem_configurator.py index d5ac66b46a..4bdaa4a21c 100644 --- a/aviary/mission/problem_configurator.py +++ b/aviary/mission/problem_configurator.py @@ -107,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 link_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 diff --git a/aviary/mission/two_dof_problem_configurator.py b/aviary/mission/two_dof_problem_configurator.py index fd530fd600..c91b843623 100644 --- a/aviary/mission/two_dof_problem_configurator.py +++ b/aviary/mission/two_dof_problem_configurator.py @@ -337,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 link_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 @@ -350,11 +347,8 @@ 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. """ aviary_group.promotes( 'traj', @@ -407,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/subsystems/subsystem_builder.py b/aviary/subsystems/subsystem_builder.py index a65a5fcd68..65dca88ffc 100644 --- a/aviary/subsystems/subsystem_builder.py +++ b/aviary/subsystems/subsystem_builder.py @@ -197,6 +197,10 @@ def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_ 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'. + Parameters ---------- aviary_inputs : dict From 5009dfb89de009b7822d9d8cd303e99b87d74bc7 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 10:39:05 -0400 Subject: [PATCH 22/34] Update docs with new phase_info key --- .../fundamentals/mission_definition.ipynb | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) 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, From 4532e7d87d9f4b535fb1f260d39ebe4bb20197c5 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 11:15:33 -0400 Subject: [PATCH 23/34] RUFF --- aviary/mission/two_dof/phases/accel_phase.py | 1 + aviary/mission/two_dof/phases/breguet_cruise_phase.py | 8 ++++---- aviary/mission/two_dof/phases/simple_cruise_phase.py | 6 +++--- aviary/mission/two_dof/phases/takeoff_phase.py | 1 - .../aircraft/large_turboprop_freighter/phase_info.py | 4 ++-- aviary/utils/aviary_options_dict.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/aviary/mission/two_dof/phases/accel_phase.py b/aviary/mission/two_dof/phases/accel_phase.py index 37aaa0b38a..bef4b7272e 100644 --- a/aviary/mission/two_dof/phases/accel_phase.py +++ b/aviary/mission/two_dof/phases/accel_phase.py @@ -164,6 +164,7 @@ def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_ ] return linked_vars + AccelPhase._add_initial_guess_meta_data( InitialGuessIntegrationVariable(), desc='initial guess for initial time and duration specified as a tuple', diff --git a/aviary/mission/two_dof/phases/breguet_cruise_phase.py b/aviary/mission/two_dof/phases/breguet_cruise_phase.py index 302f983d8e..a735b5e991 100644 --- a/aviary/mission/two_dof/phases/breguet_cruise_phase.py +++ b/aviary/mission/two_dof/phases/breguet_cruise_phase.py @@ -63,7 +63,7 @@ def declare_options(self): default=True, types=bool, desc='When True, directly link the initial time parameter to the previous ' - 'phase. When False, use a constraint.' + 'phase. When False, use a constraint.', ) self.declare( @@ -71,7 +71,7 @@ def declare_options(self): default=True, types=bool, desc='When True, directly link the initial altitude parameter to the previous ' - 'phase. When False, use a constraint.' + 'phase. When False, use a constraint.', ) self.declare( @@ -79,14 +79,14 @@ def declare_options(self): default=True, types=bool, desc='When True, directly link the initial distance parameter to the previous ' - 'phase. When False, use a constraint.' + '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..' + desc='Because mass is output, this should always be false..', ) diff --git a/aviary/mission/two_dof/phases/simple_cruise_phase.py b/aviary/mission/two_dof/phases/simple_cruise_phase.py index 9be88fb75a..92db1e723b 100644 --- a/aviary/mission/two_dof/phases/simple_cruise_phase.py +++ b/aviary/mission/two_dof/phases/simple_cruise_phase.py @@ -66,7 +66,7 @@ def declare_options(self): default=False, types=bool, desc='When True, directly link the initial altitude parameter to the previous ' - 'phase. When False, use a constraint.' + 'phase. When False, use a constraint.', ) self.declare( @@ -74,7 +74,7 @@ def declare_options(self): default=False, types=bool, desc='When True, directly link the initial distance parameter to the previous ' - 'phase. When False, use a constraint.' + 'phase. When False, use a constraint.', ) self.declare( @@ -82,7 +82,7 @@ def declare_options(self): default=True, types=bool, desc='When True, directly link the initial mach parameter to the previous ' - 'phase. When False, use a constraint.' + 'phase. When False, use a constraint.', ) diff --git a/aviary/mission/two_dof/phases/takeoff_phase.py b/aviary/mission/two_dof/phases/takeoff_phase.py index 8b32250d44..1a37f6b9c1 100644 --- a/aviary/mission/two_dof/phases/takeoff_phase.py +++ b/aviary/mission/two_dof/phases/takeoff_phase.py @@ -288,7 +288,6 @@ 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') diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index e81e4a852f..8506beb37d 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py @@ -240,8 +240,8 @@ 'mass_ref': (150_000, 'lbm'), 'mass_defect_ref': (150_000, 'lbm'), 'distance_bounds': ((10.0, 100.0), 'NM'), - 'distance_ref': (32., 'NM'), - 'distance_defect_ref': (32., 'NM'), + 'distance_ref': (32.0, 'NM'), + 'distance_defect_ref': (32.0, 'NM'), }, 'initial_guesses': { 'time': ([216.0, 1300.0], 's'), diff --git a/aviary/utils/aviary_options_dict.py b/aviary/utils/aviary_options_dict.py index ec0a3a91aa..f45b20c304 100644 --- a/aviary/utils/aviary_options_dict.py +++ b/aviary/utils/aviary_options_dict.py @@ -490,4 +490,4 @@ def add_time_options(self, units: str = None, defaults=None): default=default, types=bool, desc=desc, - ) \ No newline at end of file + ) From 08980041e77afdeffc2241b87c924de303383224 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 11:33:44 -0400 Subject: [PATCH 24/34] cleanup --- aviary/core/aviary_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 90e8253c38..628f91a15b 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -43,7 +43,7 @@ Verbosity, ) from aviary.variable_info.functions import setup_trajectory_params -from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings +from aviary.variable_info.variables import Aircraft, Mission, Settings TWO_DEGREES_OF_FREEDOM = EquationsOfMotion.TWO_DEGREES_OF_FREEDOM ENERGY_STATE = EquationsOfMotion.ENERGY_STATE From 688f333dcab8380c76191e8edd0bd241b4d51ba8 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 12:34:04 -0400 Subject: [PATCH 25/34] Missed something in solved2dof --- .../detailed_takeoff_landing.ipynb | 2 +- .../solved_two_dof/phases/solved_twodof_phase.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb b/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb index 04dda14831..b0e419c521 100644 --- a/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb +++ b/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb @@ -781,7 +781,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.12" + "version": "3.12.3" } }, "nbformat": 4, 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 02d474bb35..71235329b9 100644 --- a/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py +++ b/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py @@ -295,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] = { From 363ac317feeb5db7f518049bfa2f1fdbe65ac7df Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 14:38:09 -0400 Subject: [PATCH 26/34] debug --- aviary/core/aviary_group.py | 2 ++ aviary/mission/solved_two_dof/phases/solved_twodof_phase.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 628f91a15b..4782b967fc 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1241,6 +1241,8 @@ def link_phases(self, verbosity=None, comm=None): phase = self.traj._phases[phase2] if var in phase.state_options: phase.set_state_options(var, input_initial=False) + if var == 'time': + kwargs = {} self.traj.link_phases( phases=[phase1, phase2], 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 71235329b9..a9d7208d18 100644 --- a/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py +++ b/aviary/mission/solved_two_dof/phases/solved_twodof_phase.py @@ -81,7 +81,7 @@ def declare_options(self): self.add_control_options('angle_of_attack', units='deg', defaults=defaults) defaults = { - 'initial_time_direct_link': False, + 'time_initial_direct_link': False, } self.add_time_options(units='ft', defaults=defaults) From 39c006e834e72f349dad7a7c0adc411b1972cafd Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 15:34:33 -0400 Subject: [PATCH 27/34] Some fixes for solved 2dof --- aviary/core/aviary_group.py | 35 ++++++++++++------- .../solved_two_dof/phases/groundroll_phase.py | 10 ++++++ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 4782b967fc..ec13c42e52 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1234,15 +1234,9 @@ def link_phases(self, verbosity=None, comm=None): kwargs = {} if not connect: - kwargs = self._find_scaling(var, phase_info1, phase_info2) - - # 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) - if var == 'time': - kwargs = {} + kwargs = self._find_scaling( + var, phase1, phase_info1, phase2, phase_info2, opt2 + ) self.traj.link_phases( phases=[phase1, phase2], @@ -1266,7 +1260,9 @@ def link_phases(self, verbosity=None, comm=None): kwargs = {} if not connect: - kwargs = self._find_scaling(source, phase_info1, phase_info2) + kwargs = self._find_scaling( + var, phase1, phase_info1, phase2, phase_info2, opt2 + ) self.traj.add_linkage_constraint( phase1, phase2, source, var, connected=connect, **kwargs @@ -1291,7 +1287,9 @@ def link_phases(self, verbosity=None, comm=None): kwargs = {} if not connect: - kwargs = self._find_scaling(target, phase_info1, phase_info2) + kwargs = self._find_scaling( + var, phase1, phase_info1, phase2, phase_info2, opt2 + ) self.traj.link_phases( phases=[phase1, phase2], @@ -1304,11 +1302,16 @@ def link_phases(self, verbosity=None, comm=None): self.configurator.check_trajectory(self) - def _find_scaling(self, var, phase_info1, phase_info2): + def _find_scaling(self, var, phase1, phase_info1, phase2, phase_info2, opt2): """ Returns a dictionary of scaling keyword arguments for a dymos linkage constraint. """ if var == 'time': + phase = self.traj._phases[phase1] + if phase.time_options['name'] != 'time': + # Time is not being integrated. + return {} + # Time behaves a bit differently than the others. ref0 = None ref, units = phase_info2.get(f'{var}_initial_ref', (None, None)) @@ -1327,9 +1330,15 @@ def _find_scaling(self, var, phase_info1, phase_info2): kwargs['ref'] = ref if ref0 is not None: kwargs['ref0'] = ref0 - if ref is not None: + 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): 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] = { From eabf929c494baa7ed7e525fedc8628f5f9cbc6fe Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 15:56:45 -0400 Subject: [PATCH 28/34] more support for non-time integration --- aviary/core/aviary_group.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index ec13c42e52..d401ced1a2 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1306,12 +1306,18 @@ 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'] + if var == 'time': - phase = self.traj._phases[phase1] - if phase.time_options['name'] != '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)) From f367aea49e18e7fc2449853599259365f101021a Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 16:04:22 -0400 Subject: [PATCH 29/34] updated doc --- .../examples_unreviewed/detailed_takeoff_landing.ipynb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb b/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb index b0e419c521..ff507c08ae 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", From 2b5a08dd220b768f64b6d2c95b25fd15209117eb Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 16:35:28 -0400 Subject: [PATCH 30/34] Fixed one mistake in the landing example --- aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb b/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb index ff507c08ae..015f2daa52 100644 --- a/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb +++ b/aviary/docs/examples_unreviewed/detailed_takeoff_landing.ipynb @@ -662,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", From 75bd7f73c6c9dae261ff692e6acb84f14501efd1 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 17:05:20 -0400 Subject: [PATCH 31/34] one last adjustment --- aviary/core/aviary_group.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index d401ced1a2..7a2930303f 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1308,14 +1308,16 @@ def _find_scaling(self, var, phase1, phase_info1, phase2, phase_info2, opt2): """ phase = self.traj._phases[phase1] integrated_var = phase.time_options['name'] - - 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' + 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. From 18aa9074ba86261860e54ff246ad3daa3a0b7fe4 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Wed, 24 Jun 2026 17:08:26 -0400 Subject: [PATCH 32/34] one last adjustment --- aviary/core/aviary_group.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index 7a2930303f..69c6523aad 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1234,9 +1234,7 @@ def link_phases(self, verbosity=None, comm=None): kwargs = {} if not connect: - kwargs = self._find_scaling( - var, phase1, phase_info1, phase2, phase_info2, opt2 - ) + kwargs = self._find_scaling(var, phase1, phase_info1, phase2, phase_info2, opt2) self.traj.link_phases( phases=[phase1, phase2], @@ -1261,7 +1259,7 @@ def link_phases(self, verbosity=None, comm=None): kwargs = {} if not connect: kwargs = self._find_scaling( - var, phase1, phase_info1, phase2, phase_info2, opt2 + source, phase1, phase_info1, phase2, phase_info2, opt2 ) self.traj.add_linkage_constraint( @@ -1288,7 +1286,7 @@ def link_phases(self, verbosity=None, comm=None): kwargs = {} if not connect: kwargs = self._find_scaling( - var, phase1, phase_info1, phase2, phase_info2, opt2 + target, phase1, phase_info1, phase2, phase_info2, opt2 ) self.traj.link_phases( From 602b5a308123c8a0c92afb532030037d873473a5 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 7 Jul 2026 10:50:09 -0400 Subject: [PATCH 33/34] Review, but one test fails. --- aviary/core/aviary_group.py | 4 ++-- aviary/mission/energy_state_problem_configurator.py | 2 +- aviary/mission/problem_configurator.py | 2 +- aviary/mission/two_dof_problem_configurator.py | 2 +- aviary/subsystems/subsystem_builder.py | 4 ---- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/aviary/core/aviary_group.py b/aviary/core/aviary_group.py index ebd288ec66..c157ccdab4 100644 --- a/aviary/core/aviary_group.py +++ b/aviary/core/aviary_group.py @@ -1162,7 +1162,7 @@ def link_phases(self, verbosity=None, comm=None): all_subsystem_options = phase_info.get('subsystem_options', {}) link_vars = set() - for subsys in self.external_subsystems: + 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'], @@ -1296,7 +1296,7 @@ def link_phases(self, verbosity=None, comm=None): **kwargs, ) - self.configurator.link_trajectory(self, phases) + self.configurator.configure_trajectory(self, phases) self.configurator.check_trajectory(self) diff --git a/aviary/mission/energy_state_problem_configurator.py b/aviary/mission/energy_state_problem_configurator.py index 5d4c4ae4bd..d48c4e1097 100644 --- a/aviary/mission/energy_state_problem_configurator.py +++ b/aviary/mission/energy_state_problem_configurator.py @@ -223,7 +223,7 @@ def set_phase_options(self, aviary_group, phase_name, phase_idx, phase, user_opt **extra_options, ) - def link_trajectory(self, aviary_group, phases): + def configure_trajectory(self, aviary_group, phases): """ Link or configure phase connections to other upstream or downstream components. diff --git a/aviary/mission/problem_configurator.py b/aviary/mission/problem_configurator.py index 4bdaa4a21c..bdf19eea57 100644 --- a/aviary/mission/problem_configurator.py +++ b/aviary/mission/problem_configurator.py @@ -107,7 +107,7 @@ def set_phase_options(self, aviary_group, phase_name, phase_idx, phase, user_opt """ pass - def link_trajectory(self, aviary_group, phases): + def configure_trajectory(self, aviary_group, phases): """ Link or configure phase connections to other upstream or downstream components. diff --git a/aviary/mission/two_dof_problem_configurator.py b/aviary/mission/two_dof_problem_configurator.py index c91b843623..1597b5e2af 100644 --- a/aviary/mission/two_dof_problem_configurator.py +++ b/aviary/mission/two_dof_problem_configurator.py @@ -337,7 +337,7 @@ def set_phase_options(self, aviary_group, phase_name, phase_idx, phase, user_opt 'aerodynamics', {} ).setdefault('method', 'low_speed') - def link_trajectory(self, aviary_group, phases): + def configure_trajectory(self, aviary_group, phases): """ Link or configure phase connections to other upstream or downstream components. diff --git a/aviary/subsystems/subsystem_builder.py b/aviary/subsystems/subsystem_builder.py index 65dca88ffc..a65a5fcd68 100644 --- a/aviary/subsystems/subsystem_builder.py +++ b/aviary/subsystems/subsystem_builder.py @@ -197,10 +197,6 @@ def get_linked_variables(self, aviary_inputs=None, user_options=None, subsystem_ 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'. - Parameters ---------- aviary_inputs : dict From 470c7c6ce2525f0e9cf6c2403faa239d7ef4a237 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 7 Jul 2026 11:27:13 -0400 Subject: [PATCH 34/34] A little tweak --- .../models/aircraft/large_turboprop_freighter/phase_info.py | 6 +++--- .../benchmark_tests/test_bench_large_turboprop_freighter.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py index 8506beb37d..58e37ea647 100644 --- a/aviary/models/aircraft/large_turboprop_freighter/phase_info.py +++ b/aviary/models/aircraft/large_turboprop_freighter/phase_info.py @@ -240,8 +240,8 @@ 'mass_ref': (150_000, 'lbm'), 'mass_defect_ref': (150_000, 'lbm'), 'distance_bounds': ((10.0, 100.0), 'NM'), - 'distance_ref': (32.0, 'NM'), - 'distance_defect_ref': (32.0, 'NM'), + 'distance_ref': (100.0, 'NM'), + 'distance_defect_ref': (100.0, 'NM'), }, 'initial_guesses': { 'time': ([216.0, 1300.0], 's'), @@ -288,7 +288,7 @@ '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, 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()