Optimizing with SciPy¶
Before we can use SciPy, we have to install it.
Thankfully, SciPy is one of the best supported python packages in the world, so to install it one can just use pip install scipy
.
SciPy has many optimizers available, but I'm going to focus on using SLSQP since it can use the gradient information from OptVL and supports constraints. To use SciPy's SLSQP, we will need to supply it with custom objective and constraint functions as well as the derivatives for each. These functions need to take in the design variables and apply them to our OptVL solver. The snippet below provides an example of an objective function.
def objective_function(x):
ovl.set_constraint("Elevator", x[0])
ovl.set_surface_params({"Wing":{"aincs":x[1:]}})
ovl.execute_run()
cd = ovl.get_total_forces()['CD']
print(x, cd)
return cd
def objective_gradient(x):
# Partial derivatives of the objective_function
ovl.set_constraint("Elevator", x[0])
ovl.set_surface_params({"Wing":{"aincs":x[1:]}})
ovl.execute_run()
sens = ovl.execute_run_sensitivities(['CD'])
dcd_dele = sens['CD']['Elevator']
dcd_daincs = sens['CD']['Wing']['aincs']
# concatinate the two and return the derivs
return np.concatenate(([dcd_dele], dcd_daincs))
ovl.execute_run_sensitivities(['CD'])
does all the necessary work to compute the derivatives for the given list of functions.
We just need to parse the sens
dictionary for the derivatives with respect to the design variables we are interested in.
One also needs to repeat the process for the constraints. This involves creating functions for both the constraint values and their corresponding gradients.
Example script¶
The script below shows the full optimization script for minimizing the drag of the example aircraft in trim, with respect to the wing twist and elevator position.
"""A scipy based optimization to trim an aircraft for an aicraft using optvl"""
import numpy as np
from scipy.optimize import minimize
from optvl import OVLSolver
ovl_solver = OVLSolver(geo_file="aircraft.avl", debug=False)
# setup OptVL
ovl_solver.set_parameter("Mach", 0.0)
# Define your custom objective function with outputs from OptVL
def objective_function(x):
ovl_solver.set_constraint("Elevator", x[0])
ovl_solver.set_surface_params({"Wing":{"aincs":x[1:]}})
ovl_solver.execute_run()
cd = ovl_solver.get_total_forces()['CD']
print(x, cd)
return cd
def objective_gradient(x):
# Partial derivatives of the objective_function
# we are trusting that the design variables have already been applied
# and propogated through by the objective_function.
ovl_solver.set_constraint("Elevator", x[0])
ovl_solver.set_surface_params({"Wing":{"aincs":x[1:]}})
ovl_solver.execute_run()
sens = ovl_solver.execute_run_sensitivities(['CD'])
dcd_dele = sens['CD']['Elevator']
dcd_daincs = sens['CD']['Wing']['aincs']
# concatinate the two and return the derivs
return np.concatenate(([dcd_dele], dcd_daincs))
# Define equality constraint: h(x) = 0
def eq_constraint(x):
ovl_solver.set_constraint("Elevator", x[0])
ovl_solver.set_surface_params({"Wing":{"aincs":x[1:]}})
ovl_solver.execute_run()
# the objective must always be run first
coeff = ovl_solver.get_total_forces()
cl_target = 1.5
cm_target = 0.0
cl_con = coeff['CL'] - cl_target
cm_con = coeff['CM'] - cm_target
return np.array([cl_con, cm_con])
# Define the gradient of the equality constraint
def eq_constraint_jac(x):
sens = ovl_solver.execute_run_sensitivities(['CL', 'CM'])
dcl_dele = sens['CL']['Elevator']
dcl_daincs = sens['CL']['Wing']['aincs']
dcl_dx = np.concatenate(([dcl_dele], dcl_daincs))
dcm_dele = sens['CM']['Elevator']
dcm_daincs = sens['CM']['Wing']['aincs']
dcm_dx = np.concatenate(([dcm_dele], dcm_daincs))
# concatinate the two and return the derivs
return np.array([dcl_dx, dcm_dx])
num_sec = 5
# Initial guess for the variables
x0 = np.zeros(1+num_sec)
# Define constraints with their gradients
constraints = [
{'type': 'eq', 'fun': eq_constraint, 'jac': eq_constraint_jac}, # Equality constraint
]
# Call the minimize function with constraints and their gradients
result = minimize(
fun=objective_function, # The objective function to minimize
jac=objective_gradient, # The gradient function of the objective
x0=x0, # Initial guess
constraints=constraints, # Constraints with gradients
method='SLSQP', # Optimization method that supports constraints
options={'disp': True}, # Display convergence messages
tol=1e-10
)
# Print the result
print("Optimization result:")
opt_x = result.x
print(f"Elevator: {opt_x[0]}")
print(f"Wing twist: {opt_x[1:]}")
print(f"Final CD: {result.fun}") # Final value of the objective function
Which should produce the output after the optimization
Optimization terminated successfully (Exit mode 0)
Current function value: 0.08551861616299436
Iterations: 56
Function evaluations: 58
Gradient evaluations: 56
Optimization result:
Elevator: 0.09925034689068805
Wing twist: [11.51424344 2.00085521 2.20301866 2.32160102 -4.79748301]
Final CD: 0.08551861616299436