Technical reference

CFM56-7B conversion tables (zibomod) — specification

docs/specs/cfm56_7b_tables.md

Status 2026-10-31, milestone M2. Source: the Linux build of zibomod (plugins/zibomod/lin_x64/zibomod.xpl, read with tools/re/decompile.py and its disassembly). Rewrite: wasm/src/core/engine/cfm56_7b.h/.cpp (namespace lu737::engine::cfm56). Proof: wasm/test/test_cfm56_7b_zibo.cpp calls every original function directly in the plugin and requires the same float bits from the rewrite; all 25 functions match bit for bit on every tested input, including NaN, ±inf, negative and out-of-range inputs.

zibomod rewrite computes
Rescale rescale clamped linear map
Rescale_c rescaleLinear unclamped linear map (extrapolation)
Limit limit bound a value
calc_tamb_by_alt isaTemperature ISA static temperature from altitude
calc_tat_by_mach totalTemperature TAT from static temperature and Mach
corrected_cn1_to_n1 correctedToN1 physical N1 from corrected N1
corrected_n1_to_cn1 n1ToCorrected corrected N1 using the ISA TAT at altitude/Mach
corrected_n1_to_cn1_tat n1ToCorrectedTat corrected N1 using a measured TAT
n1_thruse_norm / thruse_n1_norm n1ToThrustUse / thrustUseToN1 N1 ↔ normalised thrust
cfm56_7b_cn2_to_cn1 / cfm56_7b_cn1_to_cn2 cn2ToCn1 / cn1ToCn2 corrected N2 (rpm) ↔ corrected N1
cfm56_7b_cegt_to_cn1 / cfm56_7b_cn1_to_cegt cegtToCn1 / cn1ToCegt corrected EGT (K) ↔ corrected N1
cfm56_7b_thrust / cfm56_7b_cn1 cn1ToThrust / thrustToCn1 corrected N1 ↔ net thrust (lbf)
cfm56_7b_fuel_flow / cfm56_7b_ff_thr thrustToFuelFlow / fuelFlowToThrust net thrust ↔ fuel flow
cfm56_7b_min_idle_ff minIdleFuelFlow fuel flow at minimum idle
min_idle_thrust minIdleThrust net thrust at minimum idle

All of them are pure functions of their arguments plus, for the table functions, the global plane_on_ground (an explicit onGround argument in the rewrite). zibomod is compiled without optimisation, so its float32 and double operations happen exactly in source order; the rewrite keeps that order, which is why the results are identical.

1. Conventions#

Inputs as Zibo passes them. Altitude is pressure altitude in feet (zibomod's simDR_pressure_altitude), Mach is simDR_mach_no, TAT is simDR_TAT in °C. N1 is in %, N2 in rpm (Zibo's gauges show N2 % = rpm / 14,460 × 100), EGT in kelvin (the gauge subtracts 273.15), thrust in lbf. "Corrected" values are referred to ISA sea level (15 °C); Zibo converts with the corrected_* functions before and after the tables.

Ground Mach. Every table function first does if (plane_on_ground == 1 && Mach < 0.08) Mach = 0 (comparison in double).

Sub-idle floor. cfm56_7b_cn2_to_cn1 and cfm56_7b_cegt_to_cn1 return 19.1 (float 19.1f) when the result is below 20 and plane_on_ground == 0; the original also formats a message ("Zibomod plugin: cn2 to cn1 limited = …", "… cegt to cn1 limited = …" with the inputs and simDR_TAT) and sends it to XPLMDebugString. The rewrite applies the floor without logging.

Band cascades. The tables are split into Mach (and altitude) bands by nested tests written if (m <= 0.2) … else if (m <= 0.3) …. The test against 0.5 compares floats (m <= 0.5f); all other Mach thresholds promote the Mach number to double ((double)m <= 0.3), so for example Mach 0.3f (0.30000001) is above the 0.3 threshold. Altitude thresholds are floats. Because every source test is an ordinary C comparison, a NaN fails every test and ends in the last band; the rewrite writes each test in the same direction (x < xs[0], m <= t, alt < 20000, …) so NaN takes the same path.

Lookups. A breakpoint lookup finds the first segment whose upper breakpoint is ≥ x (searching up from index 1). Inside a segment the value is interpolated with Rescale (clamped) or Rescale_c (linear), depending on the function (stated below). Above the last breakpoint (and for NaN) the last segment is extrapolated with Rescale_c. Below the first breakpoint the functions either extrapolate the first segment with Rescale_c, hold the first output value, or do no test at all (the first segment's Rescale then holds its first value). Tables whose rows are not monotonic (EGT rows dip in places) are searched the same way: the first segment whose upper breakpoint is ≥ x wins.

2. Primitives#

  • Rescale(x0, y0, x1, y1, x): if x1 >= x0: x0 > x → y0, x > x1 → y1; otherwise (also for NaN in x0/x1): x > x0 → y0, x1 > x → y1. Then x1 == x0 → y0, else (x - x0) * (y1 - y0) / (x1 - x0) + y0 in float.
  • Rescale_c(x0, y0, x1, y1, x): d = x1 - x0; d != 0 → (x - x0) * ((y1 - y0) / d) + y0, else 0 (also 0 for d == 0 with any x).
  • Limit(v, lo, hi): lo > hi && v > hi → hi; lo > v → lo; v > hi → hi; else v.
  • calc_tamb_by_alt(alt) = Rescale(0, 15, 37000, -56.5, alt) (°C).
  • calc_tat_by_mach(t, M) = (float)(((double)t + 273.15) * (1 + 0.2·M·M) - 273.15), computed in double.

Callers: Rescale/Rescale_c/Limit everywhere; calc_tamb_by_alt in corrected_n1_to_cn1, corrected_ff2, ff_corr_coef, Thr_shift; calc_tat_by_mach in corrected_n1_to_cn1, Thr_shift.

3. Temperature corrections of N1#

Exponent k of the temperature ratio θ: 0.5 below 30 % N1, then linear (Rescale) through 90 → 0.485, 95 → 0.475, 98 → 0.45, 100 → 0.42, 101 → 0.4175, and 0.4175 above 101 %. The exponents are float constants (0.485f …) in the interpolation and double constants (0.485 …) where corrected_cn1_to_n1 moves the breakpoints.

  • corrected_cn1_to_n1(cN1, θ) → N1 (%). θ ≤ 0 → 0. The breakpoints are moved to corrected N1: b = (float)(N / pow((double)θ, e)) for (N, e) = (30, 0.5), (90, 0.485), (95, 0.475), (98, 0.45), (100, 0.42), (101, 0.4175) (libm pow); k is interpolated between the moved breakpoints (tests b > cN1), result powf(θ, k) * cN1. Zibo passes θ = (TAT + 273.15) / 288.15. Callers: Engine_n1_n2, Starter, Thr_shift (6).
  • corrected_n1_to_cn1(N1, alt, M, ref) → corrected N1. θ = (float)((ISA-TAT(alt, M) + 273.15) / (ref + 273.15)) with ISA-TAT = calc_tat_by_mach(calc_tamb_by_alt(alt), M); θ ≤ 0 → 0; k from N1; result N1 / powf(θ, k) if powf(θ, k) > 0, else N1. Zibo passes ref = 15. (The original also computes an unused pow(θ, 0.5).) Callers: Engine_n1_n2, EFBRender_opengl (16, the EFB performance pages).
  • corrected_n1_to_cn1_tat(N1, TAT, ref) → the same with θ = (TAT + 273.15) / (ref + 273.15). Callers: Engine_model (6), Gauge_N1_N2_EGT (2).

4. Thrust use (n1_thruse_norm, thruse_n1_norm)#

One curve of 58 points: normalised thrust 0 … 1.12 (steps 0.001 to 0.02, 0.01 to 0.2, then 0.05; last 1.12) against N1 19.019285 … 104.741905 % (kThrustUse, kThrustUseN1). The first four N1 values are equal. n1_thruse_norm(N1): first i with N1 < N1[i]: i = 0 → 0, else Rescale of segment i-1..i; past the end → 1.12. thruse_n1_norm(T): the inverse with T < T[i], i = 0 → 19.019285, past the end → 104.741905. Callers: Calc_request_n1, Calc_request_thrust (autothrottle N1/thrust requests).

5. N2 tables (cfm56_7b_cn2_to_cn1, cfm56_7b_cn1_to_cn2)#

Arguments (x, pressure altitude ft, Mach); cn2_to_cn1 takes corrected N2 in rpm and returns corrected N1 %, cn1_to_cn2 the inverse. x is limited to 0 … 16,000 rpm resp. 0 … 108 %, Mach to 0 … 0.9f (after the ground rule).

Tables (rpm). Zibo adds a trim row to the base rows at start-up (a trim of exactly 0 would count as 1); the values after the trim are:

Below 20,000 ft — corrected N1 19, 20, 30, 40, 50, 60 % (kN2LowN1), trim +290, +290, +500, +450, +320, +200:

Mach 19 20 30 40 50 60
0.2 8290 8490 10700 11750 12220 12600
0.3 8140 8340 10600 11750 12220 12600
0.4 8140 8340 10350 11650 12170 12600
0.5 8090 8290 10150 11650 12120 12600
0.6 7340 7540 9900 11400 12120 12600

From 20,000 ft — corrected N1 30, 40, 50, 60 % (kN2HighN1), trim +500, +450, +320, +200:

Mach 30 40 50 60
≤ 0.6 9600 11250 12095 12500
0.7 9000 10950 12170 12500
0.8 8750 10825 11820 12500
0.9 8300 10650 11670 12500

Mach bands (m <= 0.2, <= 0.3, <= 0.4, <= 0.5f, <= 0.6, <= 0.7, <= 0.8, else): two low-altitude rows and two high-altitude rows with the Mach of the band edges. Below Mach 0.2 the low rows are a Mach 0 row extrapolated (Rescale_c) from the 0.2/0.3 rows and the 0.2 row; above 0.6 both low rows are the 0.6 row; up to 0.6 both high rows are the "≤ 0.6" row.

cn2_to_cn1:

  • below 20,000 ft: threshold thr = Rescale(0, 12220, 20000, 12500, alt). N2 ≥ thr (or NaN): above the tables, linear (Rescale_c) to 108 % at 14,755 rpm from (12600 rpm, 60 %) at sea level and to 108 % at 15,050 rpm from (12500, 60 %) at 20,000 ft, blended in altitude with Rescale. Otherwise by Mach: ≤ 0.5f: low tables (segment Rescale, below and above extrapolated); ≤ 0.6: the upper-Mach low row is first blended between 19,000 and 20,000 ft towards the high table read at the low breakpoints (Rescale in altitude, no effect below 19,000 ft), then the low tables with 19 % held below the first breakpoint; < 0.7: the lower-Mach high row is blended the same way towards the low table, then the high tables (extrapolated below/above); ≥ 0.7: high tables.
  • from 20,000 ft: N2 ≥ 12,500 → linear to 108 % at 15,050 rpm. Otherwise Mach ≤ 0.6: the same blend as above, which at this altitude replaces the upper-Mach low row by the high table, then the low tables (hold below); ≤ 0.7 or above: high tables. The final value is Rescale between the two Mach rows' results.

cn1_to_cn2 mirrors this with N1 as the input axis (segments Rescale, extrapolated below/above): below 20,000 ft N1 ≥ 60 → Rescale(60, 12600, 108, 14755) at sea level and Rescale(60, 12500, 108, 15050) at 20,000 ft, blended with Rescale; from 20,000 ft N1 ≥ 60 → Rescale(60, 12500, 108, 15050, N1).

Callers: cn2_to_cn1 in Engine_n1_n2, Starter, Thr_shift (3); cn1_to_cn2 in Engine_n1_n2 (3), Gauge_N1_N2_EGT (2, N2 gauge).

6. EGT tables (cfm56_7b_cegt_to_cn1, cfm56_7b_cn1_to_cegt)#

Arguments (x, pressure altitude ft, Mach); cn1_to_cegt takes corrected N1 % and returns corrected EGT in K, cegt_to_cn1 the inverse. Mach limited to 0 … 0.9f, x not limited.

  • Up to 25,000 ft (alt <= 25000): N1 axis of 19 points (20, 30, 40, 42.5, … 55, 60 … 105) × rows for Mach 0.2, 0.3, 0.4, 0.5, 0.6 (kEgtLowN1, kEgtLow, 635 … 1155 K). Bands <= 0.3, <= 0.4, <= 0.5f, else; the 0.2-0.3 band extrapolates down to Mach 0, the 0.5-0.6 band up.
  • From 19,000 ft (alt >= 19000): N1 axis of 23 points (40 … 90 in 2.5 % steps, 95, 100, 105, 110) × rows for Mach 0.6, 0.7, 0.8, 0.9 (kEgtHighN1, kEgtHigh, 638 … 1260 K). Bands <= 0.7, <= 0.8, else; read only when (double)m <= 0.9 (always, after the limit).
  • Every step uses Rescale_c: segments, below the first and above the last breakpoint, and the Mach interpolation (so Mach 0 extrapolates the 0.2/0.3 rows). The result is the low table below 20,000 ft, Rescale between the two from 20,000 to 25,000 ft, and the high table from 25,000 ft.

cegt_to_cn1 reads the same rows backwards (EGT row as the input axis) and then applies the in-flight 19.1 % floor. Callers: cn1_to_cegt in Gauge_N1_N2_EGT (EGT gauge: Rescale(0, TAT, 58, cEGT - 273.15, p) with a per-engine parameter p); cegt_to_cn1 in Thr_shift (3).

7. Thrust tables (cfm56_7b_thrust, cfm56_7b_cn1)#

Arguments (x, Mach), no altitude; Mach is not limited. cfm56_7b_thrust takes corrected N1 % and returns net thrust in lbf; cfm56_7b_cn1 the inverse. N1 axis of 20 points: 0, 20, 25, 30 … 105, 107.5 (kThrustN1); rows for Mach 0.0 … 0.9 (kThrust, −3150 … 27,800 lbf; 27,600 lbf at 107.5 % and Mach 0).

Bands <= 0.1 … <= 0.9 (0.5 as float) pick two adjacent rows. Above Mach 0.9: if m <= 1.0f, the band 0.9-1.0 with the Mach 0.9 row and, for cfm56_7b_thrust, a Mach 1.0 row built per point as (float)(T − ((double)(2850f + T) · 0.9 − 2850)); cfm56_7b_cn1 leaves that row at 0. Above Mach 1.0 (and NaN) there are no rows and both functions return 0. Lookup: no test below the first breakpoint, segments Rescale, extrapolation above; final Rescale between the Mach rows.

Callers: cfm56_7b_thrust in Engine_model (3, thrust of each engine from corrected_n1_to_cn1_tat(N1, TAT, 15), scaled by engine-on and 27,300/27,000) and EFBRender_opengl (8); cfm56_7b_cn1 in Engine_n1_n2 (2).

8. Fuel-flow tables (cfm56_7b_fuel_flow, cfm56_7b_ff_thr)#

cfm56_7b_fuel_flow(thrust lbf, alt ft, Mach) → fuel flow; cfm56_7b_ff_thr(fuel flow, alt ft, Mach) → net thrust lbf. Altitude limited to 0 … 41,000 ft, Mach not limited. Thrust axis 0, 1, 16, 18, 20, 22, 24, 25 (1000 lbf; kFfThrust; fuel_flow divides its input by 1000f, ff_thr multiplies its result by 1000f). 70 rows of 8 fuel flows (kFf), in blocks by altitude:

block rows Mach block rows Mach
0 ft 0-6 0.0-0.6 25,000 ft 31-35 0.4-0.8
5,000 ft 7-13 0.0-0.6 29,000 ft 36-39 0.5-0.8
10,000 ft 14-19 0.2-0.7 31,000 … 41,000 ft (2,000 ft steps) 40-69 0.5-0.9 each
15,000 ft 20-25 0.2-0.7
20,000 ft 26-30 0.4-0.8

Altitude bands alt <= 5000, <= 10000, … <= 39000, else (39-41,000). In each band a Mach cascade picks four rows: two at the band's lower altitude (Mach a0, a1) and two at the upper (Mach b0, b1). Where an altitude block has no row for the Mach, the nearest pair is used and extrapolated (e.g. 15-20,000 ft below Mach 0.3 reads the 20,000 ft rows at Mach 0.4/0.5). The full selection is the kBand* data in the rewrite.

fuel_flow: lookup on the thrust axis (below the first breakpoint Rescale_c of the first segment, segments Rescale, above Rescale_c of the last); at the found segment each of the four rows is interpolated, then Rescale_c in Mach at each altitude and Rescale_c in altitude. ff_thr: first the two altitude rows are interpolated in Mach with Rescale_c, then each is searched as the input axis (below/above extrapolated, segments Rescale), then Rescale_c in altitude, × 1000.

The two functions differ in the 5,000-15,000 ft bands (section 10). Callers: fuel_flow in Engine_model (2, then corrected_ff2); ff_thr in Engine_n1_n2 (2, input divided by ff_corr_coef(alt), result fed to cfm56_7b_cn1).

9. Minimum idle (cfm56_7b_min_idle_ff, min_idle_thrust)#

Arguments (pressure altitude ft, Mach). Mach axis 0 … 0.9 in 0.1 steps (kMinIdleMach), rows at 0, 5,000, 10,000, 15,000, 20,000, 25,000, 29,000, 31,000 … 41,000 ft (kMinIdleAlt). Fuel flow 810 … 330 (kMinIdleFf), net thrust 2,625 … −1,675 lbf (kMinIdleThrust).

  • min_idle_ff: altitude not limited, bands alt < 5000 … alt < 39000, else; Mach not limited: m < 0 → first column; Mach segments Rescale; above 0.9 nothing is found and both rows read 0; final Rescale in altitude (holds outside 0 … 41,000).
  • min_idle_thrust: altitude limited to 0 … 41,000, bands alt <= 5000 …; Mach limited to 0 … 0.9f; (double)m >= 0.9 → last column; final Rescale in altitude.

Neither function has a caller in this zibomod build.

10. Zibo quirks reproduced#

Kept on purpose (the rewrite must behave like Zibo); each is marked "Zibo:" in the code.

  1. Swapped axes in synthesised fuel-flow rows. Where Zibo builds a missing low-Mach row of the 10,000 ft or 15,000 ft block it calls Rescale_c(T[r], 0.2, T[r+1], 0.3, M) — the fuel flows as the x axis and the Mach numbers as y — so the "row" holds numbers around 0.2-0.3 instead of fuel flows. Effect: fuel_flow at 10,000-15,000 ft below Mach 0.2 returns meaningless small or negative values (5,000 lbf at 12,000 ft, Mach 0.05 → −0.41); fuel_flow and ff_thr at 5,000-10,000 ft below Mach 0.1 (and ff_thr below 0.2) interpolate towards such a row at 10,000 ft. In flight these Mach numbers do not occur at those altitudes; on the ground at high airports they do.
  2. Dead synthesis in ff_thr at 10,000-15,000 ft: the Mach 0-0.2 rows are built and then always overwritten by the Mach 0.2-0.3 rows (missing else); the rewrite does not build them.
  3. Zero idle fuel flow of synthesised rows: forced to 0 at 10,000-15,000 ft below Mach 0.1 in fuel_flow only when not on the ground, at 5,000-10,000 ft (both functions) only on the ground.
  4. cn1_to_cn2 above 20,000 ft below Mach 0.2 blends towards high-altitude rows that were never filled (all 0): N2 is roughly halved (40 % N1 at 30,000 ft: Mach 0.3 → 11,750 rpm, Mach 0.1 → 5,875 rpm).
  5. cn2_to_cn1 above 20,000 ft, Mach 0.6-0.7: the blend of the high row searches only 5 of the 6 low breakpoints, so the 60 % point reuses the 50 % value. Without effect: this path only runs from 20,000 ft, where the blend returns the high-row value unchanged.
  6. Mach 1.0 thrust rows: built from the Mach 0.9 row in cfm56_7b_thrust, left at 0 in cfm56_7b_cn1; above Mach 1.0 both return 0. min_idle_ff returns 0 above Mach 0.9.
  7. Mixed float/double thresholds (section 1) and the in-flight 19.1 % floor of cn2_to_cn1/cegt_to_cn1.

11. Tests and results#

wasm/build.sh test builds one runner (ASan/UBSan) with all native tests; the differential tests dlopen the original plugin (harness wasm/test/zibo_direct.h/.cpp: ELF .symtab lookup by plain, mangled or demangled name, plane_on_ground set before each batch, XPLMDebugString stub). Each function is compared over the Cartesian product of input axes (a uniform grid plus every table breakpoint and band threshold with both float neighbours, ±0, ±1e30, ±inf, NaN, negatives and out-of-range values), plus 300,000 random points per function (per ground state for the table functions, which run with plane_on_ground 0 and 1). Equality is bit for bit; NaN must meet NaN.

function calls result
Rescale / Rescale_c / Limit 2.6 M / 2.6 M / 1.2 M bit-exact
calc_tamb_by_alt / calc_tat_by_mach 0.30 M / 0.35 M bit-exact
corrected_cn1_to_n1 / corrected_n1_to_cn1 / corrected_n1_to_cn1_tat 0.67 M / 1.7 M / 0.64 M bit-exact
n1_thruse_norm / thruse_n1_norm 0.15 M / 0.20 M bit-exact
cfm56_7b_cn2_to_cn1 / cn1_to_cn2 6.6 M / 6.2 M bit-exact
cfm56_7b_cn1_to_cegt / cegt_to_cn1 7.5 M / 8.4 M bit-exact
cfm56_7b_thrust / cfm56_7b_cn1 1.4 M / 2.9 M bit-exact
cfm56_7b_fuel_flow / ff_thr 6.5 M / 6.7 M bit-exact
cfm56_7b_min_idle_ff / min_idle_thrust 0.9 M / 0.9 M bit-exact

The results are also bit-exact with clang 18 at -O2 instead of g++ -O1. wasm/test/test_cfm56_7b.cpp pins original values (and the quirks above) without the plugin.

WASM. The module builds the same source with clang for wasm32 and the SDK's wasi-libc. A one-off check (not in CI) ran the engine code compiled for wasm32 under Node's WASI against the native build: 9.6 M table evaluations and 1.6 M correction evaluations gave identical bits. The only library calls are pow/powf in the corrections; wasi-libc (musl) and glibc both derive these from Arm's optimized-routines (error below 1 ulp, not correctly rounded), so a last-bit difference between the two libms remains possible in rare cases; Zibo itself runs with each platform's libm (MSVC's on Windows).

12. Open questions#

  • Fuel-flow units: the tables' magnitudes fit lb/h at take-off and kg/h in cruise; corrected_ff2 and ff_corr_coef (temperature/altitude corrections, specified in the engine spec §10.1) do no unit conversion; Engine_model treats the table value as lb/h (× 0.45359237 / 3600 → kg/s), which reproduces the oracle's 0.082 kg/s ground-idle fuel flow.
  • min_idle_ff and min_idle_thrust are unused in this build; keep them for completeness or drop them when the engine model is specified.