Technical reference

Flight model tuning guide

docs/FDE_TUNING.md

The generated flight_model.cfg / engines.cfg are first-pass values. Masses, CG, gear, engine positions and geometry come from the LevelUp .acf; aerodynamic and engine tables are estimates (see comments in the files). Tune in this order; change the generator (tools/msfs/generate_configs.py) and rebuild, never hand-edit the generated .cfg files.

All targets are approximate public 737-800 figures (65 t / 143,300 lb unless stated); treat them as plausibility targets, not certified data.

# Check Target Main knobs
1 Static attitude / CG height on ground ~0° pitch, no bouncing contact point static compression/damping
2 Idle thrust taxi creeps at idle when light, breakaway ~30–35 % N1 engine schedule (below); ground friction
3 Idle N1/N2/FF (ground) ~20 % / ~59 % / ~700 lb/h per engine engine schedule (below)
4 Takeoff N1 at SL ISA ~97–98 % for 26K, FF ~9–10 klb/h/eng engine schedule (below)
5 Rotation / liftoff VR ≈ 146 kt (flaps 5, 69 t), 2–3 °/s to liftoff at ~8.2°, no self-rotation before VR flap stage scalars, htail_incidence, trim limits / elevator_trim_effectiveness
6 Initial climb V2+20 at ~15° pitch, 3,000+ fpm at 69 t thrust table, CD0, induced drag
7 Climb 280 kt FL100–FL250 ~2,000–2,500 fpm at 65 t CLB N1 (autothrottle table in afds.ts), drag_coef_zero_lift
8 Cruise FL350 M0.785 N1 ~85 %, FF ~2,600 lb/h/eng, pitch ~2° drag_coef_zero_lift, drag_coef_zero_lift_mach_tab, induced_drag_scalar, wing_incidence
9 Clean 1 g stall ~155 KCAS at 65 t lift_coef_aoa_table CLmax
10 Flaps 40 stall / VREF40 ~118 KCAS / ~145 kt at 65 t lift_coef_flaps, slat scalars
11 Approach flaps 30 VREF+5 pitch 2–3°, N1 ~55–60 % with gear down drag_coef_flaps, flap drag scalars, drag_coef_gear
12 Pitch stability / trim zero elevator and trim in cruise (65 t, ~21 % MAC, FL350 M.785); take-off trim 5–5.5 units at 60–70 t htail_incidence, aero_center_lift vs CG, tail geometry; pitch_stability only adds damping
13 Roll rate ~15–20°/s full wheel at approach speed aileron_effectiveness, spoileron gain
14 Engine-out (Vmca-ish) full rudder holds heading at V2 rudder_effectiveness, yaw_moment_delta_rudder
15 Spool-up time idle→takeoff ~6–7 s n1_normal_tc, fuel flow controller gains

Procedure: fly each point with the autothrottle/autopilot off where possible, note the measured value, adjust one knob at a time, rebuild. Record results in PROGRESS_LOG.md so the tuning stays reproducible.

The modern flight model ignores the legacy pitch_moment_* and lift_coef_pitch_rate values (only the sign of pitch_moment_delta_elevator is used, and its magnitude by the autopilot): pitch balance comes from where aero_center_lift sits relative to the CG, the tail geometry and htail_incidence (SDK).

Known approximations to revisit:

  • pitch_moment_*, roll_moment_*, yaw_moment_* magnitudes follow MSFS airliner practice for this weight class scaled by the .acf tail volumes — they are not derived from wind-tunnel data.
  • The engine maps come from Zibo's engine model (section below); the reverse range, spool times and the fuel flow at altitude are still estimates.
  • MSFS computes %MAC from the reference wing (MAC 13.04 ft, LEMAC at acf z = 57.48 ft); verify the CG % shown in the MSFS weight & balance screen against the .acf value (empty CG 21 % MAC).

Engine schedule from Zibo#

MSFS's own turbine model makes the thrust; the systems module only starts and stops it (Systems::coupleMsfsEngines), and the cockpit shows Zibo's N1 / N2 / EGT / FF. So that both agree, engines.cfg's N1 schedule, thrust and fuel-flow tables are generated from Zibo's engine model:

  1. wasm/tools/engine_sweep.sh > data/msfs/zibo_engine_sweep.json runs the rewrite's Zibo engine (engines.cpp, bit-exact tables of cfm56_7b.cpp) to steady state in ISA for thrust levers 0 … 1 (step 0.05), 19 pressure altitudes (0 … 43,000 ft) and Mach 0 … 0.9, on ground (≤ 14,000 ft, ≤ M 0.3) and airborne (flaps up): N1, N2, fuel flow, thrust, idle N1. It also writes Zibo's net thrust table cfm56_7b_thrust (scaled 27,300 / 27,000 as Engine_model) and its sea-level cN1 → cN2.
  2. tools/msfs/engine_schedule.py turns it into the tables; generate_configs.py writes them; python3 tools/msfs/engine_schedule.py prints the match below.

MSFS formulas used (SDK, engines.cfg and "engines.cfg additional information"; subsonic, no supersonic inlet, so inlet efficiency and Mach losses are 1). P = 1 + 0.2 M²:

Quantity MSFS
total ratios θ_total = θ P, δ_total = δ P^3.5
corrected N1 / N2 N1 / √θ_total, N2 / √θ_total
gross thrust static_thrust × n1_and_mach_on_thrust_table(cN1, M) × δ_total
ram drag TAS × airflow / g, airflow = corrected_airflow_table(cN1, M) × inlet_area × δ_total / √θ_total, i.e. M a₀ × table × area × δ P³ / g
net thrust gross − ram drag
commanded Ne (use_commanded_Ne_table) corrected N2 from mach_0_ / mach_hi_corrected_commanded_ne_table (effective throttle × 1/δ), linear in inlet Mach between the two; the fuel controller drives corrected N2 to it; n2_to_n1_table → corrected N1

Zibo: net thrust = δ × ((27,300 / 27,000)(T + 3,700) − 3,700), T = cfm56_7b_thrust(N1 / θ_total^k, M), k = 0.5 … 0.4175 by N1.

  • Thrust table: Zibo's net thrust plus the ram drag MSFS will subtract, over static thrust × P^3.5, on Zibo's own N1 breakpoints (0, 20, 25 … 105, 115 %) and Mach 0 … 0.9. MSFS's √θ correction and Zibo's θ^k agree at the altitude typically flown at each Mach (TYPICAL_ALT_FOR_MACH). The previous table (commit aa913df) left out δ_total's P^3.5 and the ram drag's P³: about 1.8 × the intended net thrust at M 0.8.
  • Airflow: CFM56-7B estimate, ~800 lb/s at take-off, per ft² of inlet (the ram drag shown in the debug panel; net thrust does not depend on it).
  • N1 schedule: for each lever row (0, .08, .17, .25, .45, .65, .85, 1) one weighted least-squares fit of the 14 table values (7 altitude columns × low / high Mach) to Zibo's steady N1 at every sweep point in the flown envelope (130–260 kt below 10,000 ft, 210 kt – 340 kt / M .82 above, ground points ≤ M 0.2). n2_to_n1_table is Zibo's relation at and below ground idle and straight above it (MSFS's N2 is not shown; Zibo's own relation is steep below 30 % and flat above, which made the tables hard to fit). SDK: where both commanded-Ne tables give the same value, "the engine may incorrectly switch to idle". The high-Mach table (labelled Mach 1) is kept ≥ 0.3 % above the low-Mach one at every node, so never equal. (Build 23's tables crossed near lever 0.9.)
  • Fuel flow: corrected_n2_from_ff_table from Zibo's sea-level fuel flow, corrected with δ_total √θ_total (MSFS's exact correction is not documented).

Match (ISA, python3 tools/msfs/engine_schedule.py):

Result
N1, lever ≥ 0.3 rms 0.9 %, max 3.5 % (10,000–14,000 ft above M 0.6; the gap constraint)
N1, lever ≤ 0.25 rms 1.0 %, max 6.8 %: MSFS cannot tell ground idle (19 % at sea level) from flight idle (28–30 %) at the same Mach
Net thrust at Zibo's N1, airborne within 185 lbf (1.5 %)
Fuel flow cruise ±2 %, climb at altitude −12 %, idle at altitude +15–20 %

Examples per engine (lever → Zibo N1 / MSFS N1, MSFS gross / ram drag / net, Zibo net):

Condition Zibo N1 MSFS N1 Gross Ram drag Net Zibo net
SL static, idle 19.3 19.7 900 0 900 880
SL static, lever 0.5 74.4 73.8 12,100 0 12,100 12,350
SL static, full 104.0 103.4 26,750 0 26,750 26,980
SL M 0.2 (132 kt), full 104.0 103.9 30,240 5,880 24,370 24,430
FL350 M 0.8, lever 0.7 86.8 87.0 11,560 7,290 4,270 4,240
FL350 M 0.8, idle 40.3 40.5 3,440 3,480 −45 −50

Not covered: non-ISA temperatures (MSFS's commanded N2 is corrected, Zibo's lever sets physical N1: about ±1.7 % N1 per ±10 °C at mid lever), the reverse range (reversers are not modelled yet), spool dynamics (MSFS's fuel controller vs. Zibo's rates), Zibo's ground taxi-thrust term (an X-Plane tuning) and approach idle.

Reference frame and CG#

Owner: the EFB weight & balance page showed the CG %MAC far aft. Research report: reports/MSFS 2024 737 flight model CG.md (notes in research_notes/MSFS 2024 737 flight model CG/).

  • MSFS 2024 measures empty_weight_CG_position, stations, tanks, gear, engines, tail positions and the eyepoint from reference_datum_position, but aero_center_lift from the model origin, and its weight & balance tutorial measures stations from the model centre. The generator wrote everything from the X-Plane origin (60.22 ft ahead of the model origin), including aero_center_lift = -60.73: read as documented, the wing sat 60 ft behind the CG. Now every position is written from the model origin (the empty CG, where the glTF has its origin) and the datum is 0, 0, 0, so both frames agree; aero_center_lift = -0.51.
  • wing_sweep is the leading-edge sweep in MSFS (27.59° for the 737-800's 25° quarter-chord sweep); 25 put the apex-derived leading edge of the MAC 1.31 ft too far forward, +10 % MAC on the EFB. wing_cg_refchord = 13.04 (Boeing MAC 155.81 in = 12.98 ft) fixes the chord the EFB divides by.
  • Expected EFB %MAC: empty 21.1 %, with crew 19.5 %, with 15 t of passengers spread over the zones + 3 t cargo + 13 t fuel 22.1 %. LEMAC = aero_center_lift + MAC / 4 = +2.75 ft (apex-derived +2.74 ft).
  • Passenger zones are station type 0: type 3 (Passenger) is limited to 220 lb per station in the UI.
  • CG limits 6 % / 32 % MAC (reference_b738): Zibo's FMS ZFW range starts at 6 %, the .acf limits are 6.0 / 31.9 % on X-Plane's own MAC; the old 3.0 % came from mapping them onto the reference wing.
  • Also from the research: dihedral 6° (was 10.26°, the .acf area average including the winglets), empty inertia 2.6 / 1.0 / 3.3 M slug-ft² pitch / roll / yaw (FBW A32NX scaled; was 30–57 % low), StallDef ratios back to the SDK defaults 0.9 / 1.1 (2 / 3 moved flow separation to twice the stall AoA).

Flaps in the modern flight model#

SDK: flaps-position.i = angle, speed limit, drag scalar, lift scalar, … (the generator had lift and drag swapped), and the added flap lift is lift_coef_flaps × Σ(system lift_scalar × deflection in rad × stage scalar) at the stall AoA (drag likewise). System scalars are 1 / max deflection in rad (TE 1.432, LE 2.387); the stage scalars carry the target share. Targets (ΔCLmax over a clean 1.30, TE + LE): flaps 1 0.40, 2 0.47, 5 0.59, 10 0.63, 15 0.66, 25 0.78, 30 0.88, 40 1.10, from the 737-800 VREF ratios (65 t: VREF40 ~142, VREF30 ~149, VREF15 ~157 kt) and typical V2 for flaps 1 / 5 (estimates). Flap drag keeps the earlier shares of drag_coef_flaps (estimates).

Still to tune in the sim#

  • htail_incidence (now 0): fly 65 t, CG ~21 % MAC, FL350 M.785, read the trimmed elevator + trim deflection, fold it into htail_incidence, repeat until cruise needs neither (SDK recommendation; all normalisation uses it).
  • Trim limits and elevator_trim_effectiveness: the real stabiliser travels ~12.9° nose up / 4.2° nose down (1 unit ≈ 1°, 4 units ≈ 0°); check the take-off trim (5–5.5 units at 60–70 t, 21 % MAC, flaps 5).
  • Drag polar: OpenAP's B738 polar fitted at CL0 0.15 gives CD0 ≈ 0.022 with induced_drag_scalar ≈ 1.4 (SDK: the scalar should exceed 1 when CL0 ≠ 0). Not applied: the present drag already gives Zibo's cruise N1 (~87 %) at FL350 M.785; tune against the measured cruise N1 and the idle descent gradient instead.
  • drag_coef_zero_lift_mach_tab is steeper than the Gur wave-drag model between M .78 and .84: check with a level-speed run at maximum cruise thrust.