The linkage report currently handles linkages between states, controls, and parameters (if defined via a linkage), but parameters are often defined at the trajectory level and shared to different phases in which they are relevant.
The linkage report should add Parameters section above the phases. This section would provide the trajectory level parameters (stored in Trajectory.parameter_options). If the option opt=False is set, these parameters should be yellow. Any connection from a Trajectory parameter to a parameter in a phase should be denoted with the connected arrow (->) that is currently used for connection-based linkages.
Each phase section should also have a parameters section above the initial and final sections, showing the parameters that exist for that particular phase (again available via Phase.parameter_options).
Since the logic behind how parameters get connect can be a bit complex, it's probably best to use the connections info for the parameter input values in those phases to determine a connection to the trajectory level parameters.
The code in the example section below is for the balanced field example, which has the following trajectory parameters: m, T_nominal, T_engine_out, T_shutdown, mu_r_nominal, mu_r_braking, and h_runway. These should flow from the trajectory.parameters section to the individual phase.parameters section of the diagram. This is the linkage report for the code below:
Note that theres a linkage between alpha as a parameter in v1_to_vr and alpha as a polynomial control in rotate. We should move alpha to the parameters section of v1_to_vr.
As a tricky corner case, theres nothing preventing the user from explicitly connecting the output parameter_vals:{param_name} from one phase to the parameters:{param_name} input in another. I can supply an example where this happens when necessary.
Example
import openmdao.api as om
import matplotlib.pyplot as plt
import numpy as np
from openmdao.utils.general_utils import set_pyoptsparse_opt
import dymos as dm
class BalancedFieldODEComp(om.ExplicitComponent):
"""
The ODE System for an aircraft takeoff climb.
Computes the rates for states v (true airspeed) gam (flight path angle) r (range) and h (altitude).
References
----------
.. [1] Raymer, Daniel. Aircraft design: a conceptual approach. American Institute of
Aeronautics and Astronautics, Inc., 2012.
"""
def initialize(self):
self.options.declare('num_nodes', types=int)
self.options.declare('g', types=(float, int), default=9.80665, desc='gravitational acceleration (m/s**2)')
self.options.declare('mode', values=('runway', 'climb'), desc='mode of operation (ground roll or flight)')
def setup(self):
nn = self.options['num_nodes']
# Scalar (constant) inputs
self.add_input('rho', val=1.225, desc='atmospheric density at runway', units='kg/m**3')
self.add_input('S', val=124.7, desc='aerodynamic reference area', units='m**2')
self.add_input('CD0', val=0.03, desc='zero-lift drag coefficient', units=None)
self.add_input('CL0', val=0.5, desc='zero-alpha lift coefficient', units=None)
self.add_input('CL_max', val=2.0, desc='maximum lift coefficient for linear fit', units=None)
self.add_input('alpha_max', val=np.radians(10), desc='angle of attack at CL_max', units='rad')
self.add_input('h_w', val=1.0, desc='height of the wing above the CG', units='m')
self.add_input('AR', val=9.45, desc='wing aspect ratio', units=None)
self.add_input('e', val=0.801, desc='Oswald span efficiency factor', units=None)
self.add_input('span', val=35.7, desc='Wingspan', units='m')
self.add_input('T', val=1.0, desc='thrust', units='N')
# Dynamic inputs (can assume a different value at every node)
self.add_input('m', shape=(nn,), desc='aircraft mass', units='kg')
self.add_input('v', shape=(nn,), desc='aircraft true airspeed', units='m/s')
self.add_input('h', shape=(nn,), desc='altitude', units='m')
self.add_input('alpha', shape=(nn,), desc='angle of attack', units='rad')
# Outputs
self.add_output('CL', shape=(nn,), desc='lift coefficient', units=None)
self.add_output('q', shape=(nn,), desc='dynamic pressure', units='Pa')
self.add_output('L', shape=(nn,), desc='lift force', units='N')
self.add_output('D', shape=(nn,), desc='drag force', units='N')
self.add_output('K', val=np.ones(nn), desc='drag-due-to-lift factor', units=None)
self.add_output('F_r', shape=(nn,), desc='runway normal force', units='N')
self.add_output('v_dot', shape=(nn,), desc='rate of change of speed', units='m/s**2',
tags=['dymos.state_rate_source:v'])
self.add_output('r_dot', shape=(nn,), desc='rate of change of range', units='m/s',
tags=['dymos.state_rate_source:r'])
self.add_output('W', shape=(nn,), desc='aircraft weight', units='N')
self.add_output('v_stall', shape=(nn,), desc='stall speed', units='m/s')
self.add_output('v_over_v_stall', shape=(nn,), desc='stall speed ratio', units=None)
# Mode-dependent IO
if self.options['mode'] == 'runway':
self.add_input('mu_r', val=0.05, desc='runway friction coefficient', units=None)
else:
self.add_input('gam', shape=(nn,), desc='flight path angle', units='rad')
self.add_output('gam_dot', shape=(nn,), desc='rate of change of flight path angle',
units='rad/s', tags=['dymos.state_rate_source:gam'])
self.add_output('h_dot', shape=(nn,), desc='rate of change of altitude', units='m/s',
tags=['dymos.state_rate_source:h'])
self.declare_coloring(wrt='*', method='cs')
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
g = self.options['g']
# Compute factor k to include ground effect on lift
rho = inputs['rho']
v = inputs['v']
S = inputs['S']
CD0 = inputs['CD0']
m = inputs['m']
T = inputs['T']
h = inputs['h']
h_w = inputs['h_w']
span = inputs['span']
AR = inputs['AR']
CL0 = inputs['CL0']
alpha = inputs['alpha']
alpha_max = inputs['alpha_max']
CL_max = inputs['CL_max']
e = inputs['e']
outputs['W'] = W = m * g
outputs['v_stall'] = v_stall = np.sqrt(2 * W / rho / S / CL_max)
outputs['v_over_v_stall'] = v / v_stall
outputs['CL'] = CL = CL0 + (alpha / alpha_max) * (CL_max - CL0)
K_nom = 1.0 / (np.pi * AR * e)
b = span / 2.0
fact = ((h + h_w) / b) ** 1.5
outputs['K'] = K = K_nom * 33 * fact / (1.0 + 33 * fact)
outputs['q'] = q = 0.5 * rho * v ** 2
outputs['L'] = L = q * S * CL
outputs['D'] = D = q * S * (CD0 + K * CL ** 2)
# Compute the downward force on the landing gear
calpha = np.cos(alpha)
salpha = np.sin(alpha)
# Runway normal force
outputs['F_r'] = F_r = m * g - L * calpha - T * salpha
# Compute the dynamics
if self.options['mode'] == 'climb':
gam = inputs['gam']
cgam = np.cos(gam)
sgam = np.sin(gam)
outputs['v_dot'] = (T * calpha - D) / m - g * sgam
outputs['gam_dot'] = (T * salpha + L) / (m * v) - (g / v) * cgam
outputs['h_dot'] = v * sgam
outputs['r_dot'] = v * cgam
else:
outputs['v_dot'] = (T * calpha - D - F_r * inputs['mu_r']) / m
outputs['r_dot'] = v
p = om.Problem()
_, optimizer = set_pyoptsparse_opt('IPOPT', fallback=True)
p.driver = om.pyOptSparseDriver()
p.driver.declare_coloring()
# Use IPOPT
p.driver.options['optimizer'] = optimizer
p.driver.options['print_results'] = False
if optimizer == 'IPOPT':
p.driver.opt_settings['print_level'] = 0
p.driver.opt_settings['mu_strategy'] = 'adaptive'
p.driver.opt_settings['bound_mult_init_method'] = 'mu-based'
p.driver.opt_settings['mu_init'] = 0.01
p.driver.opt_settings['nlp_scaling_method'] = 'gradient-based'
# Next we define our five phases and add them to a trajectory.
# In[4]:
# First Phase: Brake release to V1 - both engines operable
br_to_v1 = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=3),
ode_init_kwargs={'mode': 'runway'})
br_to_v1.set_time_options(fix_initial=True, duration_bounds=(1, 1000), duration_ref=10.0)
br_to_v1.add_state('r', fix_initial=True, lower=0, ref=1000.0, defect_ref=1000.0)
br_to_v1.add_state('v', fix_initial=True, lower=0, ref=100.0, defect_ref=100.0)
br_to_v1.add_parameter('alpha', val=0.0, opt=False, units='deg')
br_to_v1.add_timeseries_output('*')
# Second Phase: Rejected takeoff at V1 - no engines operable
rto = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=3),
ode_init_kwargs={'mode': 'runway'})
rto.set_time_options(fix_initial=False, duration_bounds=(1, 1000), duration_ref=1.0)
rto.add_state('r', fix_initial=False, lower=0, ref=1000.0, defect_ref=1000.0)
rto.add_state('v', fix_initial=False, lower=0, ref=100.0, defect_ref=100.0)
rto.add_parameter('alpha', val=0.0, opt=False, units='deg')
rto.add_timeseries_output('*')
# Third Phase: V1 to Vr - single engine operable
v1_to_vr = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=3),
ode_init_kwargs={'mode': 'runway'})
v1_to_vr.set_time_options(fix_initial=False, duration_bounds=(1, 1000), duration_ref=1.0)
v1_to_vr.add_state('r', fix_initial=False, lower=0, ref=1000.0, defect_ref=1000.0)
v1_to_vr.add_state('v', fix_initial=False, lower=0, ref=100.0, defect_ref=100.0)
v1_to_vr.add_parameter('alpha', val=0.0, opt=False, units='deg')
v1_to_vr.add_timeseries_output('*')
# Fourth Phase: Rotate - single engine operable
rotate = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=3),
ode_init_kwargs={'mode': 'runway'})
rotate.set_time_options(fix_initial=False, duration_bounds=(1.0, 5), duration_ref=1.0)
rotate.add_state('r', fix_initial=False, lower=0, ref=1000.0, defect_ref=1000.0)
rotate.add_state('v', fix_initial=False, lower=0, ref=100.0, defect_ref=100.0)
rotate.add_polynomial_control('alpha', order=1, opt=True, units='deg', lower=0, upper=10, ref=10, val=[0, 10])
rotate.add_timeseries_output('*')
# Fifth Phase: Climb to target speed and altitude at end of runway.
climb = dm.Phase(ode_class=BalancedFieldODEComp, transcription=dm.Radau(num_segments=5),
ode_init_kwargs={'mode': 'climb'})
climb.set_time_options(fix_initial=False, duration_bounds=(1, 100), duration_ref=1.0)
climb.add_state('r', fix_initial=False, lower=0, ref=1000.0, defect_ref=1000.0)
climb.add_state('h', fix_initial=True, lower=0, ref=1.0, defect_ref=1.0)
climb.add_state('v', fix_initial=False, lower=0, ref=100.0, defect_ref=100.0)
climb.add_state('gam', fix_initial=True, lower=0, ref=0.05, defect_ref=0.05)
climb.add_control('alpha', opt=True, units='deg', lower=-10, upper=15, ref=10)
climb.add_timeseries_output('*')
# Instantiate the trajectory and add phases
traj = dm.Trajectory()
p.model.add_subsystem('traj', traj)
traj.add_phase('br_to_v1', br_to_v1)
traj.add_phase('rto', rto)
traj.add_phase('v1_to_vr', v1_to_vr)
traj.add_phase('rotate', rotate)
traj.add_phase('climb', climb)
all_phases = ['br_to_v1', 'v1_to_vr', 'rto', 'rotate', 'climb']
groundroll_phases = ['br_to_v1', 'v1_to_vr', 'rto', 'rotate']
# Add parameters common to multiple phases to the trajectory
traj.add_parameter('m', val=174200., opt=False, units='lbm',
desc='aircraft mass',
targets={phase: ['m'] for phase in all_phases})
# Handle parameters which change from phase to phase.
traj.add_parameter('T_nominal', val=27000 * 2, opt=False, units='lbf', static_target=True,
desc='nominal aircraft thrust',
targets={'br_to_v1': ['T']})
traj.add_parameter('T_engine_out', val=27000, opt=False, units='lbf', static_target=True,
desc='thrust under a single engine',
targets={'v1_to_vr': ['T'], 'rotate': ['T'], 'climb': ['T']})
traj.add_parameter('T_shutdown', val=0.0, opt=False, units='lbf', static_target=True,
desc='thrust when engines are shut down for rejected takeoff',
targets={'rto': ['T']})
traj.add_parameter('mu_r_nominal', val=0.03, opt=False, units=None, static_target=True,
desc='nominal runway friction coefficient',
targets={'br_to_v1': ['mu_r'], 'v1_to_vr': ['mu_r'], 'rotate': ['mu_r']})
traj.add_parameter('mu_r_braking', val=0.3, opt=False, units=None, static_target=True,
desc='runway friction coefficient under braking',
targets={'rto': ['mu_r']})
traj.add_parameter('h_runway', val=0., opt=False, units='ft',
desc='runway altitude',
targets={phase: ['h'] for phase in groundroll_phases})
traj.link_phases(['br_to_v1', 'v1_to_vr'], vars=['time', 'r', 'v'])
traj.link_phases(['v1_to_vr', 'rotate'], vars=['time', 'r', 'v', 'alpha'])
traj.link_phases(['rotate', 'climb'], vars=['time', 'r', 'v', 'alpha'])
traj.link_phases(['br_to_v1', 'rto'], vars=['time', 'r', 'v'])
# Less common "final value of r must match at ends of two phases".
traj.add_linkage_constraint(phase_a='rto', var_a='r', loc_a='final',
phase_b='climb', var_b='r', loc_b='final',
ref=1000)
v1_to_vr.add_boundary_constraint('v_over_v_stall', loc='final', lower=1.2, ref=100)
rto.add_boundary_constraint('v', loc='final', equals=0., ref=100, linear=True)
rotate.add_boundary_constraint('F_r', loc='final', equals=0, ref=100000)
climb.add_boundary_constraint('h', loc='final', equals=35, ref=35, units='ft', linear=True)
climb.add_boundary_constraint('gam', loc='final', equals=5, ref=5, units='deg', linear=True)
climb.add_path_constraint('gam', lower=0, upper=5, ref=5, units='deg')
climb.add_boundary_constraint('v_over_v_stall', loc='final', lower=1.25, ref=1.25)
rto.add_objective('r', loc='final', ref=1000.0)
# Finally, we setup the OpenMDAO problem, set the intial guesses, and execute the problem by both running the driver and simulating the resulting trajectory.,
# In[8]:
p.setup(check=True)
p.set_val('traj.br_to_v1.t_initial', 0)
p.set_val('traj.br_to_v1.t_duration', 35)
p.set_val('traj.br_to_v1.states:r', br_to_v1.interp('r', [0, 2500.0]))
p.set_val('traj.br_to_v1.states:v', br_to_v1.interp('v', [0, 100.0]))
p.set_val('traj.br_to_v1.parameters:alpha', 0, units='deg')
p.set_val('traj.v1_to_vr.t_initial', 35)
p.set_val('traj.v1_to_vr.t_duration', 35)
p.set_val('traj.v1_to_vr.states:r', v1_to_vr.interp('r', [2500, 300.0]))
p.set_val('traj.v1_to_vr.states:v', v1_to_vr.interp('v', [100, 110.0]))
p.set_val('traj.v1_to_vr.parameters:alpha', 0.0, units='deg')
p.set_val('traj.rto.t_initial', 35)
p.set_val('traj.rto.t_duration', 35)
p.set_val('traj.rto.states:r', rto.interp('r', [2500, 5000.0]))
p.set_val('traj.rto.states:v', rto.interp('v', [110, 0]))
p.set_val('traj.rto.parameters:alpha', 0.0, units='deg')
p.set_val('traj.rotate.t_initial', 70)
p.set_val('traj.rotate.t_duration', 5)
p.set_val('traj.rotate.states:r', rotate.interp('r', [1750, 1800.0]))
p.set_val('traj.rotate.states:v', rotate.interp('v', [80, 85.0]))
p.set_val('traj.rotate.polynomial_controls:alpha', 0.0, units='deg')
p.set_val('traj.climb.t_initial', 75)
p.set_val('traj.climb.t_duration', 15)
p.set_val('traj.climb.states:r', climb.interp('r', [5000, 5500.0]), units='ft')
p.set_val('traj.climb.states:v', climb.interp('v', [160, 170.0]), units='kn')
p.set_val('traj.climb.states:h', climb.interp('h', [0, 35.0]), units='ft')
p.set_val('traj.climb.states:gam', climb.interp('gam', [0, 5.0]), units='deg')
p.set_val('traj.climb.controls:alpha', 5.0, units='deg')
dm.run_problem(p, run_driver=True, simulate=True)
Yellow for trajectory parameters for now. I'm starting to wonder if we should just color the variables everywhere by whether or not they're design variables.
Green is fine for the cell where the connection occurs, but can you put the arrow (denoting connection, not constraint) in that off-diagonal cell? I think this is a detail I missed in the last pass at this. Sometimes something can be linked by connection with one variable and linked by constraint with something else. If we put that connection arrow on the diagonal we miss that detail.
Proposed feature.
The linkage report currently handles linkages between states, controls, and parameters (if defined via a linkage), but parameters are often defined at the trajectory level and shared to different phases in which they are relevant.
The linkage report should add
Parameters
section above the phases. This section would provide the trajectory level parameters (stored inTrajectory.parameter_options
). If the optionopt=False
is set, these parameters should be yellow. Any connection from a Trajectory parameter to a parameter in a phase should be denoted with theconnected
arrow (->) that is currently used for connection-based linkages.Each phase section should also have a
parameters
section above theinitial
andfinal
sections, showing the parameters that exist for that particular phase (again available viaPhase.parameter_options
).Since the logic behind how parameters get connect can be a bit complex, it's probably best to use the connections info for the parameter input values in those phases to determine a connection to the trajectory level parameters.
The code in the example section below is for the balanced field example, which has the following trajectory parameters:
m
,T_nominal
,T_engine_out
,T_shutdown
,mu_r_nominal
,mu_r_braking
, andh_runway
. These should flow from the trajectory.parameters section to the individual phase.parameters section of the diagram. This is the linkage report for the code below:Note that theres a linkage between alpha as a parameter in
v1_to_vr
and alpha as a polynomial control inrotate
. We should movealpha
to the parameters section ofv1_to_vr
.As a tricky corner case, theres nothing preventing the user from explicitly connecting the output
parameter_vals:{param_name}
from one phase to theparameters:{param_name}
input in another. I can supply an example where this happens when necessary.Example