Full Aircraft Configuration with Control Surfaces

Level: Advanced

This notebook demonstrates a complete aircraft configuration including:

  • Wing with ailerons and flaps

  • Fuselage

  • Horizontal tail with elevator

  • Vertical tail with rudder

We use the OpenVSP Python API (with VLM fallback) and study control surface effectiveness through deflection parameter sweeps.

Topics covered

  • Complete aircraft geometry creation in OpenVSP

  • Control surface definition (ailerons, flaps, elevator, rudder)

  • Longitudinal stability analysis (CL-alpha, Cm-alpha)

  • Control surface deflection effects on aerodynamic coefficients

  • Lateral-directional analysis

References

  • OpenVSP Python API: https://openvsp.org/pyapi_docs/latest/

  • Raymer, D.P., Aircraft Design: A Conceptual Approach, AIAA, 2018

  • Nelson, R.C., Flight Stability and Automatic Control, McGraw-Hill, 1998

[ ]:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath('.')), 'src'))

from aerodemo.openvsp_utils import (
    check_openvsp, init_vsp, add_wing, add_fuselage,
    add_horizontal_tail, add_vertical_tail, run_vspaero
)
from aerodemo.vlm import WingGeometry, VortexLatticeMethod

HAS_VSP = check_openvsp()
print(f"OpenVSP available: {HAS_VSP}")
if not HAS_VSP:
    print("NOTE: Running in VLM fallback mode (OpenVSP not installed).")
    print("      Control surface effects will be modeled analytically.")

plt.rcParams.update({'figure.dpi': 100, 'axes.grid': True, 'grid.alpha': 0.3, 'font.size': 11})

1. Complete Aircraft Configuration

We design a conventional low-wing transport/trainer aircraft.

Control surface layout

                    ╔═══ Elevator ═══╗
        ╔══ Aileron ══╗  ╔══╗        ╔══════╗
────────┤   WING      ├──┤ F├────────┤ H.T. ├──
        ╚═════════════╝  ╚══╝        ╚══════╝
                              │
                         ┌────┴────┐
                         │  V.T.   │
                         │ (Rudder)│
                         └─────────┘

Surface

Type

Chord fraction

Span fraction

Aileron

Roll control

25%

60–95% semispan

Flap

High-lift

30%

15–60% semispan

Elevator

Pitch control

35%

Full h-tail span

Rudder

Yaw control

35%

Full v-tail height

[ ]:
# Full aircraft configuration
aircraft = {
    # Wing
    'wing_span': 28.0,
    'wing_root_chord': 4.5,
    'wing_tip_chord': 1.8,
    'wing_sweep': 20.0,
    'wing_dihedral': 5.0,
    'wing_x': 10.0,
    'wing_z': -1.2,
    # Fuselage
    'fuse_length': 30.0,
    'fuse_diameter': 3.2,
    # Horizontal tail
    'htail_span': 10.0,
    'htail_root_chord': 2.0,
    'htail_tip_chord': 1.0,
    'htail_x': 26.0,
    'htail_z': 0.5,
    # Vertical tail
    'vtail_height': 4.5,
    'vtail_root_chord': 3.0,
    'vtail_tip_chord': 1.5,
    'vtail_x': 25.0,
    'vtail_z': 0.0,
}

# Reference areas and lengths
S_w = aircraft['wing_span'] * (aircraft['wing_root_chord'] + aircraft['wing_tip_chord']) / 2
b_w = aircraft['wing_span']
AR_w = b_w**2 / S_w
lam_w = aircraft['wing_tip_chord'] / aircraft['wing_root_chord']
MAC_w = (2/3) * aircraft['wing_root_chord'] * (1 + lam_w + lam_w**2) / (1 + lam_w)

S_h = aircraft['htail_span'] * (aircraft['htail_root_chord'] + aircraft['htail_tip_chord']) / 2
VH = S_h * (aircraft['htail_x'] - aircraft['wing_x']) / (S_w * MAC_w)  # horizontal tail volume

S_v = aircraft['vtail_height'] * (aircraft['vtail_root_chord'] + aircraft['vtail_tip_chord']) / 2
VV = S_v * (aircraft['vtail_x'] - aircraft['wing_x']) / (S_w * b_w)  # vertical tail volume

print("Full Aircraft Configuration:")
print(f"\nWing:")
print(f"  Span b = {b_w:.1f} m,  S_w = {S_w:.1f} m²,  AR = {AR_w:.2f},  λ = {lam_w:.3f}")
print(f"  MAC = {MAC_w:.3f} m,  Sweep = {aircraft['wing_sweep']:.1f}°")
print(f"\nHorizontal Tail:")
print(f"  S_HT = {S_h:.2f} m²,  Volume ratio V_H = {VH:.3f}")
print(f"\nVertical Tail:")
print(f"  S_VT = {S_v:.2f} m²,  Volume ratio V_V = {VV:.3f}")
print(f"\nFuselage: L={aircraft['fuse_length']:.1f} m, D={aircraft['fuse_diameter']:.1f} m")

2. Build OpenVSP Model

[ ]:
if HAS_VSP:
    init_vsp("FullAircraft")

    fuse_id = add_fuselage(
        length=aircraft['fuse_length'],
        max_diameter=aircraft['fuse_diameter'],
        name="Fuselage"
    )
    wing_id = add_wing(
        span=aircraft['wing_span'],
        root_chord=aircraft['wing_root_chord'],
        tip_chord=aircraft['wing_tip_chord'],
        sweep_deg=aircraft['wing_sweep'],
        dihedral_deg=aircraft['wing_dihedral'],
        x_offset=aircraft['wing_x'],
        z_offset=aircraft['wing_z'],
        name="Wing"
    )
    htail_id = add_horizontal_tail(
        span=aircraft['htail_span'],
        root_chord=aircraft['htail_root_chord'],
        tip_chord=aircraft['htail_tip_chord'],
        x_offset=aircraft['htail_x'],
        z_offset=aircraft['htail_z'],
        name="HTail"
    )
    vtail_id = add_vertical_tail(
        height=aircraft['vtail_height'],
        root_chord=aircraft['vtail_root_chord'],
        tip_chord=aircraft['vtail_tip_chord'],
        x_offset=aircraft['vtail_x'],
        z_offset=aircraft['vtail_z'],
        name="VTail"
    )
    print("Full aircraft VSP model created:")
    print(f"  Fuselage ID: {fuse_id}")
    print(f"  Wing ID:     {wing_id}")
    print(f"  H-Tail ID:   {htail_id}")
    print(f"  V-Tail ID:   {vtail_id}")
else:
    print("VSP model creation skipped (OpenVSP not installed).")

3. Longitudinal Stability Analysis

For longitudinal stability, the key parameter is:

\[\frac{dC_m}{d\alpha} < 0 \quad \text{(statically stable)}\]

The pitching moment coefficient about the CG depends on the tail volume ratio and the downwash factor at the tail:

\[C_{m_\alpha} = C_{L_\alpha^w}(x_{ac}^w - x_{cg}) - C_{L_\alpha^{HT}} V_H \eta_H (1 - \frac{d\varepsilon}{d\alpha})\]

where \(\varepsilon\) is the downwash angle at the horizontal tail.

[ ]:
# Analytical longitudinal stability estimate
alpha_range = np.linspace(-4, 14, 19)

# Wing aerodynamics (VLM)
wing_geom = WingGeometry(
    span=aircraft['wing_span'],
    root_chord=aircraft['wing_root_chord'],
    tip_chord=aircraft['wing_tip_chord'],
    sweep_angle=aircraft['wing_sweep'],
    dihedral=aircraft['wing_dihedral'],
    n_spanwise=14,
    n_chordwise=5,
)
vlm_wing = VortexLatticeMethod(wing_geom)
wing_sweep_result = vlm_wing.sweep_alpha(alpha_range)

# H-tail aerodynamics (VLM)
htail_geom = WingGeometry(
    span=aircraft['htail_span'],
    root_chord=aircraft['htail_root_chord'],
    tip_chord=aircraft['htail_tip_chord'],
    n_spanwise=8,
    n_chordwise=3,
)
vlm_htail = VortexLatticeMethod(htail_geom)
htail_sweep = vlm_htail.sweep_alpha(alpha_range)

# Combined aerodynamics (simplified superposition)
# Fuselage contribution (approximate destabilizing moment)
k_fuse = 0.05  # fuselage moment coefficient per radian (destabilizing)
CL_total = wing_sweep_result['CL'] + S_h / S_w * 0.9 * htail_sweep['CL']
CDi_total = wing_sweep_result['CDi'] + (S_h / S_w)**2 * htail_sweep['CDi']

# Pitching moment (simplified): Cm = Cm_ac + CL * (x_ac - x_cg) / MAC
x_ac_wing = aircraft['wing_x'] + 0.25 * MAC_w  # aerodynamic center
x_cg = aircraft['wing_x'] + 0.30 * MAC_w       # CG at 30% MAC
l_ht = aircraft['htail_x'] - x_cg               # moment arm

# Static margin (positive = stable)
x_np_frac = (x_ac_wing + S_h / S_w * l_ht) / MAC_w - aircraft['wing_x'] / MAC_w
x_cg_frac = 0.30  # 30% MAC

SM = x_np_frac - x_cg_frac
print(f"Neutral point:    x_NP/MAC = {x_np_frac:.3f}")
print(f"CG location:      x_CG/MAC = {x_cg_frac:.3f}")
print(f"Static margin:    SM = {SM*100:.1f}% MAC  ({'Stable' if SM>0 else 'UNSTABLE'})")

alpha_rad = np.deg2rad(alpha_range)
Cm_alpha = (CL_total[-1] - CL_total[0]) / (alpha_rad[-1] - alpha_rad[0]) * (x_cg_frac - x_np_frac)
Cm = Cm_alpha * alpha_rad + 0.05  # offset for trim at ~3 deg
print(f"\nCm_alpha = {Cm_alpha:.4f} /rad  ({'Stable' if Cm_alpha < 0 else 'UNSTABLE'})")
[ ]:
fig = plt.figure(figsize=(15, 10))
gs = gridspec.GridSpec(2, 3, figure=fig)

ax1 = fig.add_subplot(gs[0, 0])
ax1.plot(alpha_range, CL_total, 'bo-', linewidth=2, markersize=6, label='Total (wing+tail)')
ax1.plot(alpha_range, wing_sweep_result['CL'], 'g--', linewidth=1.5, label='Wing only')
ax1.plot(alpha_range, S_h/S_w * 0.9 * htail_sweep['CL'], 'r-.', linewidth=1.5, label='HT contribution')
ax1.axhline(0, color='k', linewidth=0.5)
ax1.set_xlabel('α [deg]')
ax1.set_ylabel('$C_L$')
ax1.set_title('Lift Contributions', fontweight='bold')
ax1.legend(fontsize=9)

ax2 = fig.add_subplot(gs[0, 1])
ax2.plot(alpha_range, Cm, 'mo-', linewidth=2, markersize=6)
ax2.axhline(0, color='k', linewidth=1)
ax2.set_xlabel('α [deg]')
ax2.set_ylabel('$C_m$')
ax2.set_title(f'Pitching Moment (SM={SM*100:.1f}%)', fontweight='bold')
color_sm = 'green' if SM > 0 else 'red'
ax2.text(0.05, 0.95, f'Statically {"Stable" if SM>0 else "Unstable"}',
         transform=ax2.transAxes, color=color_sm, fontsize=10, fontweight='bold',
         va='top')

ax3 = fig.add_subplot(gs[0, 2])
ax3.plot(CDi_total, CL_total, 'rs-', linewidth=2, markersize=6, label='Total')
ax3.plot(wing_sweep_result['CDi'], wing_sweep_result['CL'], 'g--',
         linewidth=1.5, label='Wing only')
ax3.set_xlabel('$C_{Di}$')
ax3.set_ylabel('$C_L$')
ax3.set_title('Drag Polar', fontweight='bold')
ax3.legend()

# Control surface effects (analytical model)
ax4 = fig.add_subplot(gs[1, 0])
delta_e_range = np.linspace(-25, 25, 11)  # elevator deflection [deg]
# Elevator effectiveness: dCm/d(delta_e) = -tau * C_L_alpha_HT * V_H
tau_e = 0.6  # flap effectiveness factor (depends on chord ratio)
a_ht = 2 * np.pi / (1 + 2 * np.pi / (np.pi * 8.0))  # h-tail lift slope
dCm_de = -tau_e * a_ht * VH * np.deg2rad(1)  # per degree
Cm_delta_e = dCm_de * delta_e_range
ax4.plot(delta_e_range, Cm_delta_e, 'co-', linewidth=2, markersize=6)
ax4.axhline(0, color='k', linewidth=0.5)
ax4.set_xlabel('Elevator deflection δ_e [deg]')
ax4.set_ylabel('ΔCm')
ax4.set_title('Elevator Effectiveness', fontweight='bold')
ax4.text(0.05, 0.05, f'dCm/dδ_e = {dCm_de*180/np.pi:.4f}/deg',
         transform=ax4.transAxes, fontsize=9)

ax5 = fig.add_subplot(gs[1, 1])
delta_a_range = np.linspace(-25, 25, 11)  # aileron deflection [deg]
# Rolling moment coefficient: Cl_delta_a
tau_a = 0.55  # aileron effectiveness
eta1, eta2 = 0.60, 0.95  # span fraction limits of aileron
Cl_da = 2 * np.pi / (1 + 2*np.pi/(np.pi*AR_w)) * tau_a * (eta2 - eta1) / (2*AR_w)
dCl_da_deg = Cl_da * np.deg2rad(1)
Cl_roll = dCl_da_deg * delta_a_range
ax5.plot(delta_a_range, Cl_roll, 'ro-', linewidth=2, markersize=6)
ax5.axhline(0, color='k', linewidth=0.5)
ax5.set_xlabel('Aileron deflection δ_a [deg]')
ax5.set_ylabel('$C_l$ (rolling moment)')
ax5.set_title('Aileron Effectiveness', fontweight='bold')
ax5.text(0.05, 0.95, f'dCl/dδ_a = {dCl_da_deg:.5f}/deg',
         transform=ax5.transAxes, fontsize=9, va='top')

ax6 = fig.add_subplot(gs[1, 2])
delta_r_range = np.linspace(-25, 25, 11)  # rudder deflection [deg]
tau_r = 0.55
a_vt = 2 * np.pi / (1 + 2*np.pi/(np.pi * aircraft['vtail_height']**2/S_v * 2))
dCn_dr = -tau_r * a_vt * VV * np.deg2rad(1)
Cn_yaw = dCn_dr * delta_r_range
ax6.plot(delta_r_range, Cn_yaw, 'yo-', linewidth=2, markersize=6, color='darkorange')
ax6.axhline(0, color='k', linewidth=0.5)
ax6.set_xlabel('Rudder deflection δ_r [deg]')
ax6.set_ylabel('$C_n$ (yawing moment)')
ax6.set_title('Rudder Effectiveness', fontweight='bold')
ax6.text(0.05, 0.05, f'dCn/dδ_r = {dCn_dr*180/np.pi:.4f}/deg',
         transform=ax6.transAxes, fontsize=9)

plt.suptitle('Full Aircraft — Stability and Control Derivatives', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('full_aircraft_stability.png', bbox_inches='tight', dpi=100)
plt.show()

4. Flap Deflection — High-Lift Analysis

Flaps increase the effective camber, shifting the lift curve up (increasing \(C_{L_{max}}\) and zero-lift CL) without changing the slope significantly.

\[\Delta C_L = C_{L_\alpha} \cdot \tau_f \cdot \delta_f \cdot \frac{S_{flap}}{S_{ref}}\]
[ ]:
from aerodemo.naca_airfoil import NACAFourDigit

# Flap effect on airfoil: equivalent to increased camber
# Model: flap deflection shifts zero-lift angle
flap_deflections = [0, 10, 20, 30, 40]
alpha_range_flap = np.linspace(-5, 25, 100)

S_flap_ratio = 0.30  # flap area as fraction of wing area
tau_flap = 0.60      # flap effectiveness factor

fig, axes = plt.subplots(1, 2, figsize=(13, 5))
colors_flap = plt.cm.cool(np.linspace(0, 1, len(flap_deflections)))

ax = axes[0]
for delta_f, col in zip(flap_deflections, colors_flap):
    # Simulate flap as increased camber (effective zero-lift shift)
    delta_alpha_L0 = -tau_flap * S_flap_ratio * np.deg2rad(delta_f)
    CL_flap = 2 * np.pi * (np.deg2rad(alpha_range_flap) - delta_alpha_L0)
    alpha_L0_deg = np.rad2deg(delta_alpha_L0)
    ax.plot(alpha_range_flap, CL_flap, color=col, linewidth=2,
            label=f'δ_f = {delta_f}° (Δα_L0={alpha_L0_deg:.1f}°)')
ax.axhline(0, color='k', linewidth=0.5)
ax.set_xlabel('α [deg]')
ax.set_ylabel('$C_L$')
ax.set_title('Effect of Flap Deflection on Lift Curve', fontweight='bold')
ax.legend(fontsize=9)
ax.set_ylim(-1, 4)

# CL at landing alpha (say 8 deg)
ax = axes[1]
alpha_land = 8.0
CL_landing = [2 * np.pi * (np.deg2rad(alpha_land) + tau_flap * S_flap_ratio * np.deg2rad(df))
              for df in flap_deflections]
delta_CL = [cl - CL_landing[0] for cl in CL_landing]
ax.bar(flap_deflections, CL_landing, color=colors_flap, alpha=0.8, width=6)
for i, (df, cl, dcl) in enumerate(zip(flap_deflections, CL_landing, delta_CL)):
    ax.text(df, cl + 0.03, f'{cl:.2f}\n(+{dcl:.2f})', ha='center', fontsize=9)
ax.set_xlabel('Flap deflection δ_f [deg]')
ax.set_ylabel(f'$C_L$ at α={alpha_land}°')
ax.set_title(f'Lift Increment from Flaps (α={alpha_land}°)', fontweight='bold')

plt.tight_layout()
plt.savefig('flap_effects.png', bbox_inches='tight', dpi=100)
plt.show()

print("\nFlap Effectiveness Summary:")
print(f"  Flap area ratio: S_f/S_w = {S_flap_ratio*100:.0f}%")
print(f"  Flap effectiveness: τ_f = {tau_flap:.2f}")
print(f"\n  δ_f [°]   CL @ α=8°   ΔCL")
print("  " + "-" * 30)
for df, cl, dcl in zip(flap_deflections, CL_landing, delta_CL):
    print(f"  {df:5.0f}°    {cl:.4f}      {dcl:+.4f}")

5. VSPAero Analysis with OpenVSP (if available)

If OpenVSP is installed, we run a full VSPAero sweep on the complete 3D model.

[ ]:
if HAS_VSP:
    print("Running VSPAero sweep on full aircraft model...")
    vsp_full = run_vspaero(
        alpha_start=-4.0,
        alpha_end=14.0,
        alpha_npts=10,
        mach=0.2,
        ref_area=S_w,
        ref_span=b_w,
        ref_chord=MAC_w,
    )
    if vsp_full:
        print("\nVSPAero Results:")
        print(f"  {'Alpha':>8} {'CL':>8} {'CDi':>8} {'Cm':>8}")
        print("  " + "-" * 36)
        for i in range(len(vsp_full['Alpha'])):
            cm_val = vsp_full.get('Cm', [0]*10)[i]
            print(f"  {vsp_full['Alpha'][i]:8.2f} {vsp_full['CL'][i]:8.4f} "
                  f"{vsp_full['CDi'][i]:8.4f} {cm_val:8.4f}")
    else:
        print("VSPAero analysis returned no results.")
else:
    print("OpenVSP not available. Skipping VSPAero analysis.")
    print("Install OpenVSP with: pip install openvsp")

6. Summary

In this advanced notebook we have:

  1. Defined a complete aircraft configuration: wing + fuselage + horizontal tail + vertical tail

  2. Analyzed longitudinal stability: static margin, pitching moment curve

  3. Computed control surface effectiveness:

    • Elevator: pitch control (dCm/dδ_e)

    • Aileron: roll control (dCl/dδ_a)

    • Rudder: yaw control (dCn/dδ_r)

  4. Studied flap deflection effects on lift augmentation

  5. (Optional) Run VSPAero on the full 3D OpenVSP model

Key takeaways

  • Horizontal tail provides pitch stability — requires negative Cm_alpha

  • Static margin SM = x_NP - x_CG (positive = stable, typically 5–15% MAC)

  • Control surfaces change moments by shifting the zero-lift condition

  • Flaps are primarily a high-lift device — critical for takeoff and landing

Configuration hierarchy

This notebook completes the progression:

  1. 01_airfoil → 2D airfoil aerodynamics

  2. 02_finite_wing → 3D wing effects (induced drag, span loading)

  3. 03_wing_fuselage → fuselage-wing interaction

  4. 04_complete_aircraft → full aircraft stability and control