So this is a write-up for something I implemented a while ago. If you go back over some of the earlier posts on my site, you’ll see I wrote a vectorized BEMT solver to calculate the thrust and efficiency of propellers. The same technique can actually be used to solve for the lift and drag forces acting on an aircraft, which is what I eventually did. The reasons for this are that blade element analysis is actually very simple, and — after Roskam/DATCOM techniques for estimating aircraft stability derivatives — is probably the simplest body force solver to implement. It’s also very powerful; as this paper shows, it’s fairly close to vortex lattice and other panel-based methods in terms of accuracy (especially for conventional configurations), and it’s much faster (
at worst as opposed to
for panel methods). Finally, with the right data, it’s capable of enabling full-envelope flight simulation in real-time, which — to my knowledge — no other technique can currently do. This means we can use the technique for real-time simulation of complicated aerobatic maneuvers like tail slides, which are typically fairly difficult to capture.
For this post, I’ll go over the basic mathematics of the technique, and then compare it to TornadoVLM for calculation of stability derivatives for a simple lifting body. I’m going to assume a linearized lift-curve slope since it makes the maths easier, and we can test it very quickly. This is generally in line with what other groups have done in the literature (see here, for example).
To begin with, I’m going to assume a standard axis system for the aircraft:

We’ll also be using the flat Earth assumption and use Euler rotation matrices to convert between the inertial and body axis frames. This lets us rotate the wing and subject it to a velocity in our virtual wind tunnel, so that we can calculate the stability derivatives for any direction and rotation. It’s also necessary further down the line if we want to calculate the frequency response of the vehicle, or use it in a flight simulator. I’ll briefly go over how this is done towards the end of this article.
I should also make a note on the architectural convention I tend to use for the simulations I build. First off, I like to define an environment that holds all of our basic parameters like air density, the gravity vector, the simulation time step, and the total time it will run for. This environment contains a list of vehicles, and each vehicle is built up of numerous parts — wings, propulsion, bodies, etc — and each of these sub-components have a solver attached to them. This lets us test out different solvers on the same geometry. The vehicle tends to hold parameters such as the inertia tensor, mass, and cg location, whilst a wing holds information such as the reference area, wingspan, root and tip chords, etc. The solver attached to the wing accesses these variables in order to run its calculation, and the forces and moments are stored as part of the vehicle’s state. For this write-up, I’ll keep it simple, and we’ll just be looking at the wing, though some of these architectural considerations will become apparent in the functions (which is why it’s important that I mention them).
For our wing, we want to calculate the total lift force generated in the body axis coordinate system — that is, the force components in our x, y and z axes respectively. The expression for this is similar to the expression you might have seen in my BEMT article:


For a rotating aircraft, the local velocity will change along the wing, which we can find using:

Where R is the location of the CG in [x,y,z], and R_i is the location of a particle in [x,y,z]. If we know the local velocity, we can generate an angle function and integrate along the wing:



Unfortunately, these integrals have no solution expressible using elementary functions, meaning that no analytical solution exists (if there were, aircraft simulation would be very quick and easy since you could just plug parameters into the equation). So to calculate these forces, we need to use a numerical method to estimate a solution to the integral.
Those of you who remember high school calculus might remember that we can approximate an integral using a Riemann sum of finite elements:

Which is exactly what we do with our wing! We divide it up into strips, we calculate the force in the body X,Y,Z from each strip, and then we sum them up. We assume that each strip is independent of the others, and we can see that as the number of strips approaches infinity, we’ll get a better and better approximation of the exact value (though in practice we really don’t need that many).
For each strip, we need the local chord, the local velocity, the local angle of attack (calculated using the velocity) and the local lift and drag coefficients (calculated using the angle of attack).
So in Python, we can build a vector of locations as follows:
def buildElementPositions(self):
COORDS = self.wing.COORDS
theta = self.wing.dihedral
phi = self.wing.qtrchordsweep
K = self.el/2*math.tan(phi)
if COORDS[0][1]-COORDS[1][1] == 0:
A = min(COORDS[0][2],COORDS[1][2])
B = max(COORDS[0][2],COORDS[1][2])
YS = np.linspace(COORDS[0][1], COORDS[1][1], self.elements)
ZS = np.linspace(A+self.el/2,B-self.el/2,self.elements)
if A == COORDS[0][2]:
XS = np.linspace(COORDS[0][0]-0.25*self.wing.rc+K, COORDS[1][0]-0.25*self.wing.tc-K, self.elements)
else:
XS = np.linspace(COORDS[1][0]-0.25*self.wing.tc-K, COORDS[0][0]-0.25*self.wing.rc+K, self.elements)
else:
A = min(COORDS[0][1],COORDS[1][1])
B = max(COORDS[0][1],COORDS[1][1])
C = min(COORDS[0][2],COORDS[1][2])
D = max(COORDS[0][2],COORDS[1][2])
YS = np.linspace(A+self.el/2, B-self.el/2, self.elements)
if A == COORDS[0][1]:
XS = np.linspace(COORDS[0][0]-0.25*self.wing.rc+K, COORDS[1][0]-0.25*self.wing.tc-K, self.elements)
if C == COORDS[0][2]:
ZS = np.linspace(COORDS[0][2]+self.el/2*math.tan(theta), COORDS[1][2]-self.el/2*math.tan(theta), self.elements)
else:
ZS = np.linspace(COORDS[0][2]-self.el/2*math.tan(theta), COORDS[1][2]+self.el/2*math.tan(theta), self.elements)
else:
XS = np.linspace(COORDS[1][0]-0.25*self.wing.tc-K, COORDS[0][0]-0.25*self.wing.rc+K, self.elements)
if C == COORDS[0][2]:
ZS = np.linspace(COORDS[1][2]-self.el/2*math.tan(theta), COORDS[0][2]+self.el/2*math.tan(theta), self.elements)
else:
ZS = np.linspace(COORDS[1][2]+self.el/2*math.tan(theta), COORDS[0][2]-self.el/2*math.tan(theta), self.elements)
WING = []
for i in range(0,self.elements):
WING.append([XS[i],YS[i],ZS[i]])
self.WING = np.asarray(WING)
The wing is stored as a vector of four location coordinates using the following convention:

Where we define both the left and right wing as separate entities that are added to the vehicle container.
I access the wing’s parameters, and create a vector of stations along the wing in the body [x,y,z] coordinate system. This is a little bit involved since I use the same wing object for both vertical and horizontal tail sections, as well as wings, so I need to check to see if I’m dealing with a vertical object or not. From there, I create a coordinate vector for the local chord of each wing element:
def buildElementChords(self):
COORDS = self.wing.COORDS
theta = math.atan2(COORDS[1][2]-COORDS[0][2],COORDS[1][1]-COORDS[0][1])
if COORDS[0][2]-COORDS[1][2] == 0:
STATIONS = abs(self.WING[:,1])
else:
STATIONS = abs(self.WING[:,1]/math.cos(theta))
A = 2*self.wing.Sref/((1+self.wing.lmbd)*self.wing.halfspan)
B = (1-self.wing.lmbd)/(self.wing.halfspan)
self.CHORD = A*(1-B*STATIONS)
There’s a formula to be able to do this, linked here, though it assumes a flat wing. The theta value is the dihedral angle, and I use it to get the true step distance along the wing, and calculate the chord. On this note, I actually use a magnitude for the wing span, which is technically not the way it’s normally done (wing span for a dihedral wing is normally tip-to-tip).
From here, we need to write up a function to do numerical integration of lift and drag forces and moments over the wing. We’ll take in the velocity components in the body frame (u, v, w) and calculate the local angle of attack for each element, and from there, we’ll use these to calculate the forces in X,Y,Z using the previous equations. For the moments, I’m using:



We aren’t going to iteratively calculate downwash or wake effects in this case, which greatly simplifies matters. We’re also going to do something a bit clever — we’ll do the lift calculation in an axis system in which the z-direction is aligned with the wing normal vector, and the x-axis travels in the same direction as the body x-axis (i.e. the wing is never yawed relative to the body, but it could have dihedral or incidence). The reason for this is that we can set our integral in the y direction to zero, and then rotate our forces back to the body frame to get the X,Y,Z components. In code, the process for this is:
def solveForces(self, U, P):
V = U+np.cross(P, self.WING-self.CG) # calculate element velocities (body frame)
projN = np.einsum("ij,ij->i",V,self.WINGNORM) # project velocity vector to wing norm axis
projX = np.einsum("ij,ij->i",V,self.bodX) # project velocity vector to body X axis
PNsq = np.einsum("i,i->i",projN,projN) # squared magnitude of proj N
PXsq = np.einsum("i,i->i",projX,projX) # squared magnitude of proj X
Vsq = PNsq+PXsq # calculate squared magnitude of local V
ALPHA = np.arctan2(projN,projX) # calculate local angle of attack
cl = self.liftfunction.CL(ALPHA) # calculate lift coefficiencts using aoa
cm = self.liftfunction.CM(ALPHA) # calculate moment coefficients using aoa
cd = cl**2/self.K1 # calculate drag coefficients using cl
LIFT = cl*self.K2*Vsq*self.dA # calculate elemental lift vector
DRAG = cd*self.K2*Vsq*self.dA # calculate elemental drag vector
PITCH = cm*self.K2*Vsq*self.dA*self.CHORD # calculate elemental pitching moment vector
locXS = DRAG*np.cos(ALPHA)-LIFT*np.sin(ALPHA) # local X component
locYS = np.zeros(np.size(locXS)) # local Y component (always zero)
locZS = LIFT*np.cos(ALPHA)+DRAG*np.sin(ALPHA) # local Z component
locF = np.asarray([locXS, locYS, locZS]).T # build local elemental force vector array
projF = np.einsum("ij,ij->i",self.WINGNORM,locF) # project local force vector to wing norm
F = np.einsum("ij,i->ij",self.WINGNORM,projF) # calculate wing norm force vector
XS = np.einsum("ij,ij->i",self.bodX,locF) # project into body Xs for each element
YS = np.einsum("ij,ij->i",self.bodY,F) # project into body Ys for each element
ZS = np.einsum("ij,ij->i",self.bodZ,F) # project into body Zs for each element
X, Y, Z = np.sum(XS), np.sum(YS), np.sum(ZS) # sum linear forces in body X, Y, Z
L = np.sum(ZS*self.dY)+np.sum(YS*self.dZ) # sum moments about X axis (roll)
M = -np.sum(ZS*self.dX)-np.sum(XS*self.dZ) # sum moments about Y axis (pitch)
N = np.sum(XS*self.dY)+np.sum(YS*self.dX) # sum moments about Z axis (yaw)
return np.asarray([[X, Y, Z], [L, M, N]])
The first step is to take the cross product to determine the velocity of each element control point. I then project the velocity vector onto the wing normal vector, and the body x-axis using Einstein summation to take the dot product (this is convenient for dealing with large arrays of vectors). From there, I find the squared magnitude of this velocity vector, the angle of attack, and the magnitude of the lift/drag forces and the pitching moment. I find the components in the local X,Y,Z axis using conventional strip theory (where, as mentioned, the y component is always zero), and then project it back onto the wing norm vector (which is in the body frame). From there, it can be projected back into the body axis system, after which it’s a fairly straightforward process of finding the forces and moments.
The reason for doing it this way is that if we wanted to create a wing with dihedral, the dihedral reduces the local angle of attack. I wanted to code in such a way that if I wanted to, I could play around with these types of configuration (though that’s still mostly untested and unvalidated).
Now to test it out. I built a wing with the following shape and dimensions:
CG = [0,0,0]
Span = 1.8m
Root Chord = 0.25
Lambda = 0.44
LE Sweep = 15 degrees
Dihedral = 0 degrees
Plot it, along with the element locations: 
Now, lets pass it a velocity vector (body frame), and calculate the forces:
V = [15, 0, 1]
F = [-1.16360633, 0., 19.56530196]
M = [0.00000000e+00, -2.90892967e+00, -2.77555756e-17]
Seems reasonable. Roll is zero, and yaw is very close to zero (potentially a numerical error) which is what you would expect. We have a Z component of 19.5N, or roughly 2kg, which certainly seems ballpark.
For reference, the dimensions for a wing I built whilst on exchange to NUAA in China gives a Z force component of 26N for a velocity vector of [15,0,1]. That aircraft cruised at around 15m/s, with a total weight of 2.85kg. The value we’re getting is pretty close to that, which is a good sign.
Now let’s calculate our stability derivatives using:




And compare our calculation to one done in TornadoVLM, which uses vortex lattice method to calculate the lifting force of a wing divided into quadrilateral panels. We’ll use a velocity vector of roughly 10m/s magnitude, with a 5 degree angle of attack:
V = [9.961, 0, 0.872]
= [0, 0, 0]
Strip theory gives us:
F = [-0.88130927, 0., 11.31456245]
M = [0., -1.68222634, 0.]
CX = [-0.04302242, 0., 0.55233719]
CL = [ 0., -0.15258542, 0.]
And Tornado gives us:
CX = [-0.0357, 0., 0.5690]
CL = [0., -0.1871, 0.]
They’re actually pretty close, and this is without doing any kind of 3D correction (I’m using the ideal lift-curve slope of
). Let’s put in some angular velocity and see how they compare:
V = [9.961, 0, 0.872]
= [
,
, 0]
Strip theory:
CX = [-0.08657472, 0., 0.62970771]
CL = [-0.11854038, -0.17696761, 0.02177258]
And VLM:
CX = [-0.0169, 0., 0.6116]
CL = [-0.1074, -0.1682, -0.0272]
And a bit of crosswind:
V = [9.807, 1.754, 0.858]
= [0, 0, 0]
Strip theory:
CX = [-0.04317599, 0., 0.55331712]
CL = [0., -0.15285613, 0.]
VLM:
CX = [-0.0298, 0, 0.4653]
CL = [-0.0413, -0.1428, -0.0015]
We seem to be overestimating the force in the x-direction and z-directions, but otherwise it looks ballpark. It’s still in the process of being validated, so it’s possible that there are still some errors or oversights in the code. That said, I’m always happy to take feedback and suggestions to improve it, so if you have any thoughts let me know.
Further down the line, I’ll tidy up the environment and do a few more experiments to verify the code. From there, the plan is to start playing around with a full rigid-body dynamics simulation; the code for this already exists, but it’s very much a rough draft. Each vehicle belongs to an environment, and has its own state update function that calculates the forces acting on the body. These forces are used to determine the linear and angular acceleration in the body axis system, and a rotation matrix is used to find components in the inertial frame. A numerical integrator is used to step the simulation forward by some timestep dt (usually using a semi-inplicit Euler method).
As always, the code for this is available on my GitHub, and I’m always happy to take feedback, corrections, and/or improvements to my code. Until next time!
Ciao