|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: Mission Solver Structure |
| 4 | +date: 2017-01-07 13:25:00 |
| 5 | +categories: blog |
| 6 | +description: Understanding the mission solver |
| 7 | + |
| 8 | +permalink: /guides/mission.html |
| 9 | +--- |
| 10 | + |
| 11 | +## Mission Solver Code Structure |
| 12 | + |
| 13 | +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. |
| 14 | + |
| 15 | +### File Structure |
| 16 | + |
| 17 | +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. |
| 18 | + |
| 19 | +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. |
| 20 | + |
| 21 | +### Initialization |
| 22 | + |
| 23 | +For this tutorial, we will be considering the constant speed constant altitude cruise segment. The files are available [here (Analysis)](https://github.com/suavecode/SUAVE/blob/develop/trunk/SUAVE/Analyses/Mission/Segments/Cruise/Constant_Speed_Constant_Altitude.py) and [here (Method)](https://github.com/suavecode/SUAVE/blob/develop/trunk/SUAVE/Methods/Missions/Segments/Cruise/Constant_Speed_Constant_Altitude.py). 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: |
| 24 | + |
| 25 | + self.altitude = None |
| 26 | + self.air_speed = 10. * Units['km/hr'] |
| 27 | + self.distance = 10. * Units.km |
| 28 | + |
| 29 | +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. |
| 30 | + |
| 31 | + self.state.unknowns.throttle = ones_row(1) * 0.5 |
| 32 | + self.state.unknowns.body_angle = ones_row(1) * 0.0 |
| 33 | + self.state.residuals.forces = ones_row(2) * 0.0 |
| 34 | + |
| 35 | +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. |
| 36 | + |
| 37 | +### Evaluation Details |
| 38 | + |
| 39 | +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. |
| 40 | + |
| 41 | +<img src="http://suave.stanford.edu/images/drag_components_2.png" width="800" height="234" /> |
| 42 | + |
| 43 | +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. |
| 44 | + |
| 45 | +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](https://github.com/suavecode/SUAVE/blob/develop/trunk/SUAVE/Methods/Missions/Segments/converge_root.py). 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. |
| 46 | + |
| 47 | +### Using Multiple Segments |
| 48 | + |
| 49 | +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](/forum). |
| 50 | + |
| 51 | +#### Process Summary |
| 52 | + |
| 53 | +Mission Setup |
| 54 | + |
| 55 | +* Initializes default values for unknowns |
| 56 | +* Initializes set of functions used to determine residuals |
| 57 | +* Reads user input for segment parameters |
| 58 | +* Adds the analysis group to be used (including the vehicle and items like atmosphere) |
| 59 | +* Appends segments in order |
| 60 | + |
| 61 | +Evaluate |
| 62 | + |
| 63 | +* Varies unknowns until residual convergence is reached using scipy's fsolve |
| 64 | +* Repeats process for each segment until full mission is complete |
| 65 | + |
| 66 | +### Adding New Mission Segments |
| 67 | + |
| 68 | +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. |
| 69 | + |
| 70 | +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. |
| 71 | + |
| 72 | +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]() |
| 73 | + |
| 74 | + def initialize_conditions(segment,state): |
| 75 | + |
| 76 | + # unpack |
| 77 | + climb_rate = segment.climb_rate |
| 78 | + air_speed = segment.air_speed |
| 79 | + alt0 = segment.altitude_start |
| 80 | + altf = segment.altitude_end |
| 81 | + t_nondim = state.numerics.dimensionless.control_points |
| 82 | + conditions = state.conditions |
| 83 | + |
| 84 | + # check for initial altitude |
| 85 | + if alt0 is None: |
| 86 | + if not state.initials: raise AttributeError('initial altitude not set') |
| 87 | + alt0 = -1.0 * state.initials.conditions.frames.inertial.position_vector[-1,2] |
| 88 | + |
| 89 | + # discretize on altitude |
| 90 | + alt = t_nondim * (altf-alt0) + alt0 |
| 91 | + |
| 92 | + # process velocity vector |
| 93 | + v_mag = air_speed |
| 94 | + v_z = -climb_rate # z points down |
| 95 | + v_x = np.sqrt( v_mag**2 - v_z**2 ) |
| 96 | + |
| 97 | + # pack conditions |
| 98 | + conditions.frames.inertial.velocity_vector[:,0] = v_x |
| 99 | + conditions.frames.inertial.velocity_vector[:,2] = v_z |
| 100 | + conditions.frames.inertial.position_vector[:,2] = -alt[:,0] # z points down |
| 101 | + conditions.freestream.altitude[:,0] = alt[:,0] # positive altitude in this context |
| 102 | + |
| 103 | +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: |
| 104 | + |
| 105 | + def initialize_conditions(segment,state): |
| 106 | + |
| 107 | + # unpack |
| 108 | + throttle = segment.throttle |
| 109 | + air_speed = segment.air_speed |
| 110 | + alt0 = segment.altitude_start |
| 111 | + altf = segment.altitude_end |
| 112 | + t_nondim = state.numerics.dimensionless.control_points |
| 113 | + conditions = state.conditions |
| 114 | + |
| 115 | + # check for initial altitude |
| 116 | + if alt0 is None: |
| 117 | + if not state.initials: raise AttributeError('initial altitude not set') |
| 118 | + alt0 = -1.0 * state.initials.conditions.frames.inertial.position_vector[-1,2] |
| 119 | + |
| 120 | + # pack conditions |
| 121 | + conditions.propulsion.throttle[:,0] = throttle |
| 122 | + conditions.frames.inertial.velocity_vector[:,0] = air_speed # start up value |
| 123 | + |
| 124 | +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. |
| 125 | + |
| 126 | + def update_differentials_altitude(segment,state): |
| 127 | + |
| 128 | + # unpack |
| 129 | + t = state.numerics.dimensionless.control_points |
| 130 | + D = state.numerics.dimensionless.differentiate |
| 131 | + I = state.numerics.dimensionless.integrate |
| 132 | + |
| 133 | + |
| 134 | + # Unpack segment initials |
| 135 | + alt0 = segment.altitude_start |
| 136 | + altf = segment.altitude_end |
| 137 | + conditions = state.conditions |
| 138 | + |
| 139 | + r = state.conditions.frames.inertial.position_vector |
| 140 | + v = state.conditions.frames.inertial.velocity_vector |
| 141 | + |
| 142 | + # check for initial altitude |
| 143 | + if alt0 is None: |
| 144 | + if not state.initials: raise AttributeError('initial altitude not set') |
| 145 | + alt0 = -1.0 * state.initials.conditions.frames.inertial.position_vector[-1,2] |
| 146 | + |
| 147 | + # get overall time step |
| 148 | + vz = -v[:,2,None] # Inertial velocity is z down |
| 149 | + dz = altf- alt0 |
| 150 | + dt = dz / np.dot(I[-1,:],vz)[-1] # maintain column array |
| 151 | + |
| 152 | + # Integrate vz to get altitudes |
| 153 | + alt = alt0 + np.dot(I*dt,vz) |
| 154 | + |
| 155 | + # rescale operators |
| 156 | + t = t * dt |
| 157 | + |
| 158 | + # pack |
| 159 | + t_initial = state.conditions.frames.inertial.time[0,0] |
| 160 | + state.conditions.frames.inertial.time[:,0] = t_initial + t[:,0] |
| 161 | + conditions.frames.inertial.position_vector[:,2] = -alt[:,0] # z points down |
| 162 | + conditions.freestream.altitude[:,0] = alt[:,0] # positive altitude in this context |
| 163 | + |
| 164 | + return |
| 165 | + |
| 166 | +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. |
| 167 | + |
| 168 | +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. |
| 169 | + |
| 170 | + def update_velocity_vector_from_wind_angle(segment,state): |
| 171 | + |
| 172 | + # unpack |
| 173 | + conditions = state.conditions |
| 174 | + v_mag = segment.air_speed |
| 175 | + alpha = state.unknowns.wind_angle[:,0][:,None] |
| 176 | + theta = state.unknowns.body_angle[:,0][:,None] |
| 177 | + |
| 178 | + # Flight path angle |
| 179 | + gamma = theta-alpha |
| 180 | + |
| 181 | + # process |
| 182 | + v_x = v_mag * np.cos(gamma) |
| 183 | + v_z = -v_mag * np.sin(gamma) # z points down |
| 184 | + |
| 185 | + # pack |
| 186 | + conditions.frames.inertial.velocity_vector[:,0] = v_x[:,0] |
| 187 | + conditions.frames.inertial.velocity_vector[:,2] = v_z[:,0] |
| 188 | + |
| 189 | + return conditions |
| 190 | + |
| 191 | +This uses our new set of unknowns to determine the velocities. |
| 192 | + |
| 193 | +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. |
| 194 | + |
| 195 | + def unpack_body_angle(segment,state): |
| 196 | + |
| 197 | + # unpack unknowns |
| 198 | + theta = state.unknowns.body_angle |
| 199 | + |
| 200 | + # apply unknowns |
| 201 | + state.conditions.frames.body.inertial_rotations[:,1] = theta[:,0] |
| 202 | + |
| 203 | +We now add these functions to the segment process list. |
| 204 | + |
| 205 | + # -------------------------------------------------------------- |
| 206 | + # Initialize - before iteration |
| 207 | + # -------------------------------------------------------------- |
| 208 | + initialize = self.process.initialize |
| 209 | + |
| 210 | + initialize.expand_state = Methods.expand_state |
| 211 | + initialize.differentials = Methods.Common.Numerics.initialize_differentials_dimensionless |
| 212 | + initialize.conditions = Methods.Climb.Constant_Throttle_Constant_Speed.initialize_conditions |
| 213 | + initialize.velocities = Methods.Climb.Constant_Throttle_Constant_Speed.update_velocity_vector_from_wind_angle |
| 214 | + initialize.differentials_altitude = Methods.Climb.Constant_Throttle_Constant_Speed.update_differentials_altitude |
| 215 | + |
| 216 | +and |
| 217 | + |
| 218 | + # Unpack Unknowns |
| 219 | + iterate.unknowns = Process() |
| 220 | + iterate.unknowns.mission = Methods.Climb.Constant_Throttle_Constant_Speed.unpack_body_angle |
| 221 | + |
| 222 | + # Update Conditions |
| 223 | + iterate.conditions = Process() |
| 224 | + iterate.conditions.velocities = Methods.Climb.Constant_Throttle_Constant_Speed.update_velocity_vector_from_wind_angle |
| 225 | + iterate.conditions.differentials_a = Methods.Climb.Constant_Throttle_Constant_Speed.update_differentials_altitude |
| 226 | + iterate.conditions.differentials_b = Methods.Common.Numerics.update_differentials_time |
| 227 | + iterate.conditions.acceleration = Methods.Common.Frames.update_acceleration |
| 228 | + iterate.conditions.altitude = Methods.Common.Aerodynamics.update_altitude |
| 229 | + iterate.conditions.atmosphere = Methods.Common.Aerodynamics.update_atmosphere |
| 230 | + iterate.conditions.gravity = Methods.Common.Weights.update_gravity |
| 231 | + iterate.conditions.freestream = Methods.Common.Aerodynamics.update_freestream |
| 232 | + iterate.conditions.orientations = Methods.Common.Frames.update_orientations |
| 233 | + iterate.conditions.aerodynamics = Methods.Common.Aerodynamics.update_aerodynamics |
| 234 | + iterate.conditions.stability = Methods.Common.Aerodynamics.update_stability |
| 235 | + iterate.conditions.propulsion = Methods.Common.Energy.update_thrust |
| 236 | + iterate.conditions.weights = Methods.Common.Weights.update_weights |
| 237 | + iterate.conditions.forces = Methods.Common.Frames.update_forces |
| 238 | + iterate.conditions.planet_position = Methods.Common.Frames.update_planet_position |
| 239 | + |
| 240 | +If you have any questions that are not answered in other tutorials or the FAQ please ask on our [forum](/forum) page. This is also the place to go if you want help building a more elaborate evaluation, such as one that includes moments. |
0 commit comments