Quasi-steady blade-element model: conventions and equations¶
This page states the conventions FlapSim uses for wings and summarises the
quasi-steady model implemented in flapsim/fluid/aero.py. The complete
derivation with closed-form integrals is in the Chinese original,
docs/zh/derivations/quasi_steady_aero.md;
an English translation is planned.
Wing aero frame¶
Every wing has an aerodynamic frame attached to its MuJoCo body:
| Axis | Meaning |
|---|---|
+Y |
feathering axis, side * span where span points root to tip |
-Z |
chord direction from the leading-edge hinge to the trailing edge |
+X |
surface normal, X = Y x Z |
A manifest gives span and chord; the normal is derived. side is +1 for
a left wing and -1 for its mirror image, so mirrored wings share one frame
orientation at the neutral pose and the side factor inside the formulas
places the centre of pressure on each wing. Positive stroke angle psi is a
right-handed rotation about side * Z; positive feathering angle theta is a
right-handed rotation about +Y for both wings. The manifest's joint sign
fields convert MuJoCo joint coordinates into these conventions, and
flapsim.robot.load checks them at the neutral pose and warns when a sign
or the chord direction looks reversed.
Equations¶
With body velocities (u, v, w) and rates (p, q, r) expressed in the torso
frame, the relative flow at span coordinate r_w has a component U_i
along +X and U_o along +Z, both affine in r_w:
U_i(r_w) = U_i1 r_w + U_i0, U_o(r_w) = U_o1 r_w + U_o0
(the coefficients are the long expressions in wing_aero). The angle of
attack is evaluated at the fixed spanwise centre of pressure
r_cp = R r33 / r22:
alpha = theta + sign(U_i*) pi/2 - atan(U_o* / U_i*)
C_N(alpha) = 1.8 sin(2 alpha) cos(alpha) + 1.95 sin(alpha) - 1.5 cos(2 alpha) sin(alpha)
d_cp(alpha) = 0.46 - 0.332 cos(alpha) - 0.037 cos(3 alpha) - 0.013 cos(5 alpha)
C_N is odd in alpha, so the normal force carries the sign of the flow;
d_cp is even. With U^2 = a_U2 r_w^2 + a_U1 r_w + a_U0 the closed-form
blade-element integrals give
F_N = 1/2 rho c C_N (a_U2 R^3 r22 + a_U1 R^2 r11 + a_U0 R r00)
M_aero = -1/2 rho c^2 C_N d_cp (a_U2 R^3 z_cp2 + a_U1 R^2 z_cp1 + a_U0 R z_cp0)
M_rd = -1/8 rho |theta_dot| theta_dot (5 R c^4 z_rd)
where r_kk and z_cpk are the planform integrals from
flapsim.wings.planform. M_aero is not applied as a separate torque: it is
converted into the chordwise centre of pressure chord_CoP = -M_aero / F_N
(always positive), and the single force F_N along +X is applied at
[0, side * span_CoP, -chord_CoP]. The torque about the body centre of mass
is r x F plus the rotational damping M_rd about +Y. The torque on the
feathering hinge therefore always turns the trailing edge downstream, which
is what makes passive feathering work.
Hinge geometry¶
The feathering axis need not coincide with the leading edge. planform.
pitch_axis_fraction places the hinge inside the chord at that fraction from
the leading edge; planform.leading_edge_offset places the leading edge that
many metres beyond the hinge (a root spar). Both shift the chordwise centre
of pressure, chord_CoP = x_le + (d_cp - x0) c_eff, and the rotational
damping integral z_rd, which grows with the fourth power of the distance
from the hinge.
Passive feathering and the wing mass model¶
Passive feathering is a driven oscillator: the aerodynamic moment turns the
trailing edge downstream, rotational damping and the hinge spring resist,
and the wing's inertia, including the inertial torque from the stroke
acceleration acting on a centre of mass offset from the hinge, carries the
motion through reversal. When inertia dominates, the response is in
anti-phase with the moment: the trailing edge leads, the angle of attack is
inverted and the mean lift is negative. CAD meshes exported as uniform solids
often put the wing centroid far behind the hinge and produce exactly this.
A membrane wing carries most of its mass in the leading-edge spar, so
manifests should declare the mass distribution explicitly with
wings[].mass_model (spar_plate with a spar fraction, thin_plate, or
explicit values in wing coordinates). The example robot uses
spar_plate with 80 percent of 62.6 mg in the spar; with the URDF's own
centroid, 22 mm aft, it does not fly.
Correction relative to the pre-platform implementation¶
The earlier implementation multiplied M_aero by sign(alpha). Since C_N
is odd, that made M_aero = -|...| independent of the flow direction, so the
centre of pressure switched to the opposite side of the hinge at every stroke
reversal and the force and torque were no longer those of one force acting
at one point. A translational force flip in the MuJoCo adapter compensated
the resulting lift sign for the example robot, at the cost of an
inconsistent force/torque pair. On 2026-10-28 the moment was restored to
-d_cp c F_N as written in the derivation, the flip was removed, and the
wing frame was redefined through span and chord so the side of the hinge
on which the plate lies is explicit. The regression values in
tests/test_lift_sweep.py were re-measured after this change.