Mission Solver Code Structure
This is a high level overview of how the mission solver functions. The purpose is to show the structure that is used for an existing mission, and show where changes should be made if different functionality is desired.
File Structure
Mission scripts are split into two folders in the SUAVE repository. The first is in trunk/SUAVE/Analyses/Mission/Segments, and the second is in trunk/SUAVE/Methods/Missions/Segments. As with other types of analyses and methods, the distinction between these is that the Analyses folder contains classes that are built to use functions stored in the Methods folder. This division is done to make it easier to build new analysis classes using a mix of available methods.
A typical mission segment analysis file contains four keys parts. The first specifies default user inputs, unknowns, and residuals. The inputs are used to provide the analysis with conditions that need to be met, while the unknowns and residuals are used as part of the solution process. The second sets the initialization functions for the analysis, which are run at the beginning. The third picks the convergence method and specifies the functions that will be used during iteration. The fourth finalizes the data and processes it for results output.
Initialization
For this tutorial, we will be considering the constant speed constant altitude cruise segment. The files are available here (Analysis) and here (Method). This class also inherits information from more general segment classes, which include many of the processing functions. As with other segments, the user will specify key conditions. For this case, altitude, air speed, and distance are the necessary inputs. If the user does not specify an altitude, it will be taken automatically from the last value in the previous segment. These inputs must be specified in some way for the mission segment to be evaluated. They are shown below as well:
self.altitude = None
self.air_speed = 10. * Units['km/hr']
self.distance = 10. * Units.km
The other set of segment specific initial values are the values used for solving the segment (typically this means satisfying a force balance at every evaluation point). These can be changed by the user if needed, but the default values should perform fine for most cases.
self.state.unknowns.throttle = ones_row(1) * 0.5
self.state.unknowns.body_angle = ones_row(1) * 0.0
self.state.residuals.forces = ones_row(2) * 0.0
Here throttle and body angle are the unknowns, and the values shown here are the values they will start at. The residuals will be computed based on these unknowns, so their initial value is not important. Instead they are initialized just to create the necessary data structure. The ones_row line will create a numpy array with the number of elements needed for evaluation.
Evaluation Details
Most of the missions in SUAVE, including this one, are broken into several points in time based on a Chebyshev polynomial. This causes the points to be closer together at either end of the segment. The choice of a Chebyshev polynomial (which creates cosine spacing) provides better convergence and smoothness properties versus other methods such as linear spacing.
At each of these points the aerodynamic analysis is queried to find CL and CD, which are then converted to lift and drag. These values will be dependent on the body angle unknown and other aerodynamic parameters. Thrust is found from the vehicle’s energy network, which is dependent on the throttle unknown. A weight is determined by looking at the initial weight and subsequent mass rate (typically corresponding with fuel burn). In this cruise segment, these forces are summed in 2D and the results are put in the residuals. The functions needed to arrive these forces are found in the Update Conditions section of the Analysis file. This section is also shown below in one of the steps to create a new mission.
Once the evaluation process has been performed at all points, the unknowns and residuals are fed back to the solve routine, which in this case is scipy’s fsolve. The file that performs this process is here. This routine continues evaluating the points until convergence is reached. Once this happens, post processing is done to put the data in the results output.
Using Multiple Segments
Multiple segments can be run sequentially by appending them in the desired order. Examples of this are in all the tutorial files that have an aircraft fly a full mission. In addition, the full mission can be run simultaneously will all segment constraints used together. If you are interested in doing something like this, please ask us about it on our forum.
Process Summary
Mission Setup
- Initializes default values for unknowns
- Initializes set of functions used to determine residuals
- Reads user input for segment parameters
- Adds the analysis group to be used (including the vehicle and items like atmosphere)
- Appends segments in order
Evaluate
- Varies unknowns until residual convergence is reached using scipy’s fsolve
- Repeats process for each segment until full mission is complete
Adding New Mission Segments
The segment described above uses two unknowns to solve force residuals in two dimensions. This general setup works well for many problems of interest, but SUAVE is designed to accommodate other mission analysis types as well. A user may want to add control surface deflection and solve for moments as well, or look at forces in all three dimensions.
In addition, a user may want to modify how the mission is flown, as is done with the many other segments currently available. They may want to modify how the mission is solved, such as is done in our single point evaluation segments where finite differencing is not relevant.
Here we will explain the process of modifying our constant speed constant rate climb segment to be constant throttle constant speed. This still uses 2D force balance but changes the profile. There are four functions that are modified here. The first is shown below. The functions can be found in here and here
def initialize_conditions(segment,state):
# unpack
climb_rate = segment.climb_rate
air_speed = segment.air_speed
alt0 = segment.altitude_start
altf = segment.altitude_end
t_nondim = state.numerics.dimensionless.control_points
conditions = state.conditions
# check for initial altitude
if alt0 is None:
if not state.initials: raise AttributeError('initial altitude not set')
alt0 = -1.0 * state.initials.conditions.frames.inertial.position_vector[-1,2]
# discretize on altitude
alt = t_nondim * (altf-alt0) + alt0
# process velocity vector
v_mag = air_speed
v_z = -climb_rate # z points down
v_x = np.sqrt( v_mag**2 - v_z**2 )
# pack conditions
conditions.frames.inertial.velocity_vector[:,0] = v_x
conditions.frames.inertial.velocity_vector[:,2] = v_z
conditions.frames.inertial.position_vector[:,2] = -alt[:,0] # z points down
conditions.freestream.altitude[:,0] = alt[:,0] # positive altitude in this context
This function initializes speed and altitude based on the given climb rate, airspeed, and altitude end points. t_nondim gives nondimensional time in cosine spacing from 0 to 1 in order to pick the values at the points to be evaluated. Unfortunately, when we use constant throttle we cannot know beforehand exactly how altitude (or climb rate in this case) will vary with time, so altitude cannot be spaced with this method. Instead a different function is used to initialize conditions:
def initialize_conditions(segment,state):
# unpack
throttle = segment.throttle
air_speed = segment.air_speed
alt0 = segment.altitude_start
altf = segment.altitude_end
t_nondim = state.numerics.dimensionless.control_points
conditions = state.conditions
# check for initial altitude
if alt0 is None:
if not state.initials: raise AttributeError('initial altitude not set')
alt0 = -1.0 * state.initials.conditions.frames.inertial.position_vector[-1,2]
# pack conditions
conditions.propulsion.throttle[:,0] = throttle
conditions.frames.inertial.velocity_vector[:,0] = air_speed # start up value
Here only the throttle and air speed are loaded in, and discretization of other values will need to occur later so that it is part of the iteration loop. This requires a new function that updates the altitude differentials.
def update_differentials_altitude(segment,state):
# unpack
t = state.numerics.dimensionless.control_points
D = state.numerics.dimensionless.differentiate
I = state.numerics.dimensionless.integrate
# Unpack segment initials
alt0 = segment.altitude_start
altf = segment.altitude_end
conditions = state.conditions
r = state.conditions.frames.inertial.position_vector
v = state.conditions.frames.inertial.velocity_vector
# check for initial altitude
if alt0 is None:
if not state.initials: raise AttributeError('initial altitude not set')
alt0 = -1.0 * state.initials.conditions.frames.inertial.position_vector[-1,2]
# get overall time step
vz = -v[:,2,None] # Inertial velocity is z down
dz = altf- alt0
dt = dz / np.dot(I[-1,:],vz)[-1] # maintain column array
# Integrate vz to get altitudes
alt = alt0 + np.dot(I*dt,vz)
# rescale operators
t = t * dt
# pack
t_initial = state.conditions.frames.inertial.time[0,0]
state.conditions.frames.inertial.time[:,0] = t_initial + t[:,0]
conditions.frames.inertial.position_vector[:,2] = -alt[:,0] # z points down
conditions.freestream.altitude[:,0] = alt[:,0] # positive altitude in this context
return
In this function, t, D, and I are numpy arrays that allow approximate differentiation and integration. Since the total time is not known without determining the climb rate, we must first determine the time required to reach the final altitude. The line dt = dz / np.dot(I[-1,:],vz)[-1]
does this with the integrator providing the amount of altitude gained if the velocities were spread across just one second instead of the full segment time. This gives the scaling quantity dt
that is then used to get the altitude at every point in alt = alt0 + np.dot(I*dt,vz)
. The values for altitude are then are then packed for use in other functions.
The above allows us to deal with discretization without a known profile, but we also must calculate the velocity in order to use this. This is done with another added function.
def update_velocity_vector_from_wind_angle(segment,state):
# unpack
conditions = state.conditions
v_mag = segment.air_speed
alpha = state.unknowns.wind_angle[:,0][:,None]
theta = state.unknowns.body_angle[:,0][:,None]
# Flight path angle
gamma = theta-alpha
# process
v_x = v_mag * np.cos(gamma)
v_z = -v_mag * np.sin(gamma) # z points down
# pack
conditions.frames.inertial.velocity_vector[:,0] = v_x[:,0]
conditions.frames.inertial.velocity_vector[:,2] = v_z[:,0]
return conditions
This uses our new set of unknowns to determine the velocities.
Additionally, since the unknowns are different we must change the function that unpacks them. Wind angle does not need to be stored so it is not included here.
def unpack_body_angle(segment,state):
# unpack unknowns
theta = state.unknowns.body_angle
# apply unknowns
state.conditions.frames.body.inertial_rotations[:,1] = theta[:,0]
We now add these functions to the segment process list.
# --------------------------------------------------------------
# Initialize - before iteration
# --------------------------------------------------------------
initialize = self.process.initialize
initialize.expand_state = Methods.expand_state
initialize.differentials = Methods.Common.Numerics.initialize_differentials_dimensionless
initialize.conditions = Methods.Climb.Constant_Throttle_Constant_Speed.initialize_conditions
initialize.velocities = Methods.Climb.Constant_Throttle_Constant_Speed.update_velocity_vector_from_wind_angle
initialize.differentials_altitude = Methods.Climb.Constant_Throttle_Constant_Speed.update_differentials_altitude
and
# Unpack Unknowns
iterate.unknowns = Process()
iterate.unknowns.mission = Methods.Climb.Constant_Throttle_Constant_Speed.unpack_body_angle
# Update Conditions
iterate.conditions = Process()
iterate.conditions.velocities = Methods.Climb.Constant_Throttle_Constant_Speed.update_velocity_vector_from_wind_angle
iterate.conditions.differentials_a = Methods.Climb.Constant_Throttle_Constant_Speed.update_differentials_altitude
iterate.conditions.differentials_b = Methods.Common.Numerics.update_differentials_time
iterate.conditions.acceleration = Methods.Common.Frames.update_acceleration
iterate.conditions.altitude = Methods.Common.Aerodynamics.update_altitude
iterate.conditions.atmosphere = Methods.Common.Aerodynamics.update_atmosphere
iterate.conditions.gravity = Methods.Common.Weights.update_gravity
iterate.conditions.freestream = Methods.Common.Aerodynamics.update_freestream
iterate.conditions.orientations = Methods.Common.Frames.update_orientations
iterate.conditions.aerodynamics = Methods.Common.Aerodynamics.update_aerodynamics
iterate.conditions.stability = Methods.Common.Aerodynamics.update_stability
iterate.conditions.propulsion = Methods.Common.Energy.update_thrust
iterate.conditions.weights = Methods.Common.Weights.update_weights
iterate.conditions.forces = Methods.Common.Frames.update_forces
iterate.conditions.planet_position = Methods.Common.Frames.update_planet_position
If you have any questions that are not answered in other tutorials or the FAQ please ask on our forum page. This is also the place to go if you want help building a more elaborate evaluation, such as one that includes moments.