Skip to content

How the SITL model is built

sim/terrahawk.json is generated by tools/sitl_model.py from data already in this repo. ArduPilot’s SITL reads it twice: SIM_Plane takes the wing aerodynamics from it and SIM_Frame takes the lift-rotor model, and each ignores the keys that belong to the other. This page covers where each number comes from, the places where SITL does something surprising, and how much of the model is measurement versus estimate.

Since 2026-09-29 the lift, pitch and elevon terms come from th-aeronautics’ whole-aircraft OpenFOAM cases, the single-mesh fine sweep from 4 to 8 degrees and an elevon pair at 6.7 degrees, recorded in the file under _lift_and_pitch_source (Ryan approved the switch; commit 7eb7613). The CFD sees the fuselage, the pods and the landing gear, which the vortex lattice could not. The lift slope is 3.60 per radian with CL0 of 0.075, against VSPAERO’s 4.20 and -0.052, and the neutral point is 294.6 mm aft of the nose. The flown CG is 270 mm, so the static margin is about 11 % of the chord. The four lift motors are centred at 276 mm. tools/sitl_model.py --baseline vsp still writes the VSPAERO model, as -vsp.json, for comparison.

Every VSPAERO moment is taken about the front spar, so the generator moves Cm0, the elevon moment and the pitch-rate terms to the CG. The lateral and damping terms still come from VSPAERO, not the CFD. For the original airframe model (terrahawk.json, with the tip fins), all of them come straight from the full-model stability run in stab.json. The keel and stilt variants, terrahawk-keel-notips.json among them, replace the nine lateral terms (Clb, Clp, Clr, CYb, CYp, CYr, Cnb, Cnp, Cnr) with the VSPAERO fin study for that gear and tip configuration, through stilt_variant() in the generator. The pitch-rate terms (CLq and Cmq) come from stab.json in every file. stab.json predates the section regeneration of 2026-09-26 and carries the tip fins. The CFD’s sideslip cases have not replaced any of these yet. The CG is only 6 mm from its reference, so those are used as they are.

An earlier version of this model used VSPAERO runs with the symmetry option on a model that OpenVSP had already mirrored. That stacked image vortices on the real ones. Those runs put the neutral point at 460 mm and returned zero for every lateral term. The first generator worked around them with strip theory and textbook estimates. The rerun data replaced all of that on 2026-09-26.

Elevon power comes from the CFD elevon pair (10 degrees up and down at 6.7 degrees angle of attack): 0.59 of lift and -0.38 of pitching moment per radian, with the elevon lift acting at 414 mm. That is about a quarter weaker in pitch than VSPAERO predicted, so the aircraft trims with more up elevon: 8.6 degrees at 20 m/s on the keel model, against 4.5 before. Roll control, 0.21 per radian, comes from the same pair, using the up-going elevon’s slope, the side trim sits on (2026-10-01, commit 1bcc0b1); it is 24 % below the VSPAERO value.

The stall is a construction, not a CFD number. alpha_stall (12.0 degrees) is set so that the lift line reaches the earlier stall analysis’s CL of 0.82, which keeps the stall speed where it was. SIM_Plane’s stall is a sigmoid blend to flat-plate lift, and with mcoeff 50 it takes lift away about 2.5 degrees before alpha_stall. So the keel model at 2.59 kg holds a steady trimmed minimum of 17.6 m/s, and stall_probe.py, which decelerates into it, reads 18.2. The CFD itself shows a soft stall, with lift still rising to 0.84 at 17 degrees, and th-aeronautics estimates the real trimmed stall near 16 m/s. The flight stall test decides it. sitl_model.py --stall-eas pins a model’s stall to a given speed against SIM_Plane’s own lift curve, for the re-flights that will follow that test.

Drag follows the summary’s parts breakdown. The wing’s profile drag, the pusher pod and hatches, the tip fins, and the induced drag the twisted wing makes even at zero lift go into SIM_Plane’s CD0, refitted on the CFD trim: 0.024 on the keel model, 0.021 on the original airframe model, with an Oswald factor of 0.71. The VTOL pods and the stopped lift props go on the rotor model as 55 cm² of bluff area. SIM_Frame applies that drag in every direction, which suits pods and props better than the wing. Trimmed induced drag comes out right without extra work. SIM_Plane computes it from the wing’s own lift, before the up elevon takes some away. So at 2.25 kg and 20 m/s it charges CL 0.40, not the 0.33 that carries the weight. That is 0.011 including the zero-lift floor, the same as docs/aero.md. Neither model captures the elevon’s own effect on the spanload, so compare cruise power against a flight log with that in mind.

Mass and inertia come from a component build-up in the script: parts-list masses, Blender positions for the motors and spars, the print plan’s 703 g of printed structure, and the Survey3 camera at 382 mm. The layout is the forward one from docs/cg.md: the 900 g P50B pack against the Fuselage Front floor step, centred at 169.5 mm, with the pusher ESC on top of it. The generator adds nose ballast until the CG is at the flown 270 mm, as the builder must: the flown keel model (keel B and fuselage B) carries 25 g and weighs 2.73 kg, and balances at 272.3 mm without it. The original airframe and stilt models balance at 267.3 to 267.7 mm with no ballast, so their mass sits up to 2.7 mm ahead of the 270 mm reference of their derivatives; the original airframe model weighs 2.61 kg. Until 2026-10-01, with the Tattu 6S 4500 pack, the keel model carried 109 g and weighed 2.59 kg. NOSE_BALLAST_G in the script fixes the ballast instead. terrahawk.parm sets AIRSPEED_MIN 20 (18 until 2026-10-18) and Q_ASSIST_SPEED 18, above the guide’s 16 and 14, and AIRSPEED_STALL 20.0 (19.2 until 2026-10-01); at 19.2 the aircraft held 19.3 m/s or more in level flight.

The pack is the 6S2P Molicel P50B: 10 Ah, 25.2 V full, and 45 mΩ. That resistance is three times the cell’s 15 mΩ DC impedance from the Molicel data sheet, v1.0. The newer figure is 12.8 mΩ, and the difference covers the straps and leads (estimate). Stock SITL has one discharge curve, a generic LiPo, so our patch adds a second one and the model key battCurve 1 selects it. It is the data sheet’s 0.2C curve at 23 °C, digitised, with 1 A × 15 mΩ added back so that it reads as a resting voltage: 4.20 V a cell full, 3.67 V at half charge, 3.25 V at 20% and 2.52 V empty. SITL subtracts current × resistance on top of it. The JSBSim bridge uses the same table. A model with no battCurve key flies the stock LiPo table, as the Tattu 6S 4500 models did until 2026-10-01. Temperature, ageing and the cell’s recovery after a load step are not modelled.

Five things SITL does that you would not guess

Section titled “Five things SITL does that you would not guess”

SIM_Plane has no inertia. It adds the aerodynamic moment, in newton metres, straight onto the angular rate as if every axis had 1 kg m². TerraHawk’s are 0.074, 0.074 and 0.146. The lift-rotor model does divide by inertia, so in the stock quadplane the wing and the rotors act on different scales. The generator divides each axis’s moment coefficients by that axis’s inertia, and the file stores the unscaled per-radian values under _physical_derivatives_per_rad so they stay readable.

The control inputs are servo positions from -1 to 1, not angles. deltae_max is read from the file but never used, so the control coefficients are the moment at full throw. The generator takes full throw as 31.25 degrees: the build’s 25 degrees of travel at the servo endpoints with MIXING_GAIN 0.75.

SIM_Frame multiplies the rotor model’s mass by 1.5 for “plane components”, so the file carries the all-up mass divided by 1.5. The hover throttle is then solved back through the thrust curve, so that it lands where the real motors would.

CGOffset defaults to 15 cm aft and 5 cm up, which moves the centre of lift behind the CG and adds a nose-down moment in raw newton metres. Every moment in the file is already about the CG, so the default counted it twice. It showed up as a 2 degree disagreement in cruise elevon trim that went away when the file set the offset to zero.

SITL flies with the elevons centred at 1500 us, where the real aircraft will be trimmed. Every transition then started out of trim, and the pitch loop had to wind in the up elevon while the lift motors let go: the CFD model sagged from 50 m to 26 m. The harnesses now set SERVO1_TRIM and SERVO2_TRIM from each model’s predicted cruise trim at 22 m/s (1390 us on the keel model, 1398 on the original airframe model), as the aircraft’s airframe files do, and the transition bottoms at 38 m.

One more thing, in ArduPilot rather than the model. Entering QRTL before a home position exists panics this ArduPilot build in ModeQRTL::_enter(). SITL’s mode channel rests on slot 6 at boot, so terrahawk.parm keeps FBWA there.

make check in sim/ flies a scripted VTOL mission and prints the level-cruise trim next to the model’s own prediction. On the CFD models of 2026-09-29, with the elevons trimmed as above, the original airframe model (2.38 kg) trimmed at 4.8 degrees of angle of attack and 6.5 degrees of up elevon at 22 m/s, against a predicted 4.3 and 6.3. The keel model (2.59 kg) trimmed at 5.3 and 7.1 against 4.8 and 6.8. Hover learned a throttle of 0.40 on both, drawing 21 A on the original airframe model and 24 A on the keel model. From a 50 m start, the keel model’s transition bottoms at 38.5 m with 6 s on the lift motors. Since 2026-10-06 make check flies the keel-notips flight model by default, not the original airframe model: on the nose-package model (2.68 kg, 3fef9c2) it trims at 5.6 degrees of angle of attack and 7.4 degrees of up elevon against a predicted 5.0 and 7.0, and hover learns a throttle of 0.268 at 26.6 A. With the Gemfan F8041 3-blade lift props (107bdf8, 2026-10-08; Q_M_SPIN_MAX 0.82) hover learns 0.309, where the 2-blade model learned 0.286; cruise does not change.

Level cruise holds 22 m/s at 71 to 72 % throttle, 6.4 A on the original airframe model and 7.0 A on the keel model. That depends on the propeller model below. Without it, the aircraft ran at 25 to 29 m/s.

That agreement shows that SITL does what the model file says. It does not show the model is right about the aircraft. That needs a flight log, and the first comparisons are cheap: hover throttle from Q_M_THST_HOVER, cruise elevon trim at a known airspeed, and the airspeed at which Q_assist wakes up. When those come in, the design inputs at the top of tools/sitl_model.py are where to correct it.

Stock SITL gives the plane motor the same thrust at every airspeed. On TerraHawk, that drove cruise far past its target and made half throttle reach 50 m/s. sim/patches/sitl-terrahawk.patch lets the model JSON give SIM_Plane a forward motor built from SITL’s own rotor class, SIM_Motor, which the lift motors already use. One line in SIM_Motor takes the inflow along the thrust axis instead of vertically, so a motor pointing forward sees the airspeed. make build and the container image apply the patch, and the same code is pull request 34509 upstream. Stock models do not set the new keys, so they do not change.

SIM_Motor uses momentum theory: T = ½ρA(v_out² − v_in²), with v_out the outflow velocity at the throttle setting and v_in the airspeed into the disc. The static thrust and the full-throttle outflow velocity fix the disc area, so the model takes the same two numbers the earlier linear model did, and current follows the thrust produced. The values are for the V3115 640KV on the folding Aeronaut CAM Carbon Z 10x7 at 6S, modelled on the APC 10x7E data, from the propulsion page’s 10x7E row: 16.2 N available at 20 m/s and 32.5 A at full throttle at 13,400 rpm, which give 20.7 N static and a 43 m/s outflow (a 7 in pitch at that rpm is 39.7 m/s geometric). They are model values, not bench data. A thrust stand run would replace them. The 10x7 has less thrust in hand than the earlier 12x8 (26 N static), and that showed at once: in the landing approach’s first turn TECS demanded 15 degrees of pitch to hold height, the wing stalled, and terrahawk.parm now caps TECS_PITCH_MAX at the 12 degree stall angle. Against the linear model this version flew the check mission the same way and drew about half the cruise current, which agrees with the cruise-power estimate on the aero page; with the 10x6 it is 4.9 A in level cruise at 76% throttle.

Momentum theory has one trap for a pusher. When the airspeed outruns the outflow, at low throttle or none, v_in is bigger than v_out and the formula gives negative thrust: the propeller windmills like an air brake, about 10 N at 22 m/s with the throttle closed, several times the cruise drag, and the current model charged it about 15 A as if it were a generator. Georacer caught this in the review of the upstream pull request (2026-09-29). TerraHawk’s propeller folds when it stops driving, so the patch now clamps it: at zero or negative thrust the pusher gives only the folded blades’ drag, from the optional pusher_folded_cda key, and draws no current. th-aeronautics set that key to 3.0e-5 m² in every model file, from the CFD’s folded-blade pusher group: an estimate, a few hundredths of a newton at 22 m/s. sim/jsbsim/bridge.py does the same. The visible change in the check mission is the slow approach on the lift motors, where the aircraft had been leaning into the windmill’s drag: its pitch went from -4.5 to -2.7 degrees and its current from 23.7 to 22.1 A on the keel model. Cruise, the transition and the landings did not change.

The model changes how transitions behave. At half throttle, the outflow velocity is about 19 m/s, so the pusher cannot reach the 16 m/s transition speed with any margin. FBWA transitions also lose height after the lift motors stop, because the wing needs about 8 degrees angle of attack at 16 m/s. sim/README.md lists the SITL results.

terrahawk-c24b.json is Fuselage C v2.4B with our own wing, the wing fence at X 445 and keel B, at 2.876 kg with the slider putting the CG at 268 mm (th-aeronautics, d246d136). terrahawk-c24b-m3030.json is the same aerodynamics at 3.030 kg, th-blender’s draft build-up (W78), so the two bound the mass until the C mass table is reconciled. Both come from aero/wing-own/c24b-model-inputs.json, and every number in it is an estimate; the model files repeat the caveats under _c24b_limits.

SIM_Plane has no tables for lift or pitching moment: it flies straight lines in angle of attack and blends into a flat plate past alpha_stall. So the C model is built in three parts. The lift and pitch lines are fitted from -3 to 9.5 degrees on the fenced wing’s CFD, coarse in shape and pinned through the one fine-mesh point at 6.7 degrees. The stall is pinned to the fenced wing’s trimmed CLmax from the 12.5 degree elevon pair, with half the fine-mesh offset at 6.7 degrees added (0.632 at CG 268), which puts the 1 g stall at about 16.3 m/s EAS, against 14.6 on the earlier model, whose stall came from VSPAERO; so a stall comparison between the two models is mostly a change of basis. The fence’s nose-down pitch break near the stall is not in SITL at all: above alpha_stall the aircraft flies SIM_Plane’s flat-plate blend, as every model here does. Drag keeps the earlier model’s flight-matched fit and adds the CFD difference for C, less the induced drag SIM already gives C for its smaller trim, so the trim gain is counted once: 3.8 % less drag at 20 m/s.

It trims at 5.65 degrees of up elevon at 22 m/s EAS (1410 us), against 7.63 on the earlier model. terrahawk-c24b-w78.json (47bfbede) keeps the same aerodynamics but takes its mass from th-blender’s W78 rows, 3.034 kg with each pair at its own side, in place of the earlier models’ component list. That list carried the wings and elevons at 443 g, a figure with no source; sliced, they are about 160 to 166 g, about 220 to 230 g with their spars, so the earlier models are about 200 g heavy at the wing, which overstates roll inertia by about a third. On the W78 rows roll inertia falls 22 %, pitch rises 21 % (the motor booms fore and aft at the pods) and yaw barely moves; it trims at 6.04 degrees (1403 us). One more limit applies to every model here: the drag polar is a straight line through the report’s points from 18 m/s up, interpolated between report masses, so a heavier bracket, whose points reach higher lift where the polar bends, fits a lower CD0 and a lower Oswald factor. On the W78 model that reads about 5 % more drag at the 18 m/s lift and 7.5 % near the stall, on the pessimistic side. The CFD update replaces it with a polar fitted on Fuselage C’s own CFD. The terrahawk-c24b preset opens it in an MCP session. Since 2026-10-18 the W78 model is the default flight model, for the battery (th-testpilot 5aa00a04) and for the harnesses, make fly and make check; terrahawk-keel-notips.json stays available by name. th-testpilot’s A/B of the W78 rows against terrahawk-c24b-m3030.json, the same aerodynamics at nearly the same mass with the earlier distribution, found no measurable difference in hover, survey, takeoff, transition or the dock captures. On its first flight, the calm dock case from ahead, it landed in the window (2026-10-18).

SITL’s airspeed sensor normally reads the true dynamic pressure q. The real sensor’s static source sits where the local pressure is not the free stream’s, so it reads q(1 - Cp), with Cp varying with angle of attack. On Fuselage C’s side ports th-aeronautics’ CFD gives Cp from -0.250 at -5 degrees and -0.106 at -3 through +0.079 at 0 and +0.368 at 6.7 to +0.40 near 12.5 (aero/airspeed/static-cp-c-v24b.json). Every value is from the coarse mesh except the fine point at 6.7, so they are estimates. SIM_ARSPD_CP (the sitl-arspdcp patch, in the tracked build bd782d740932 since 2026-10-19) makes SITL’s sensor read through one of three tables: 1 the side ports alone, 2 the tube static, 3 a 50/50 blend. Alpha is the true body angle of the air-relative velocity, gusts included. Outside the table the end values hold: below -5 degrees the -5 value, the last measured point, and above 14 the 14 value. Nothing is extrapolated.

This matters most in a forward transition. There the simulator’s true alpha runs at -1 to 0 degrees through the acceleration, while the autopilot’s own AOA estimate, which is computed from the airspeed reading and so carries its error, reads about +1. Near alpha 0 the side ports’ Cp is small, so an ARSPD_RATIO chosen for cruise makes the sensor read high, and the transition can end below the speed it was meant to end at. The SACP log has alpha, Cp, and the pitot speed before and after.

Since 2026-10-19 the modelled sensor is the default: sim/terrahawk.parm sets SIM_ARSPD_CP 1, SIM_ARSPD_CPR 1 and the aircraft’s ARSPD_RATIO 2.3. Results from before that change are not comparable with later ones, energy and survey coverage above all: at 22 m/s indicated the aircraft now cruises at about 24 m/s true, as the real one will on the side ports. th-aeronautics puts the table’s mesh error at about ±0.025 in Cp (about ±2 % indicated airspeed), sideslip at under 1 % with the two ports teed, and the rotor term’s size at ±30 %. Setting SIM_ARSPD_CP 0 and SIM_ARSPD_CPR 0 gives the ideal sensor back, which separates an airframe change from a sensor change. The model leaves out the line lag, the quality of the port edges, the MS4525’s offset and noise beyond SIM_ARSPD_*, and blockage; those are flight-calibration items for the real aircraft.

The lift rotors add a second error at the same side ports. Their downwash raises the static pressure, so in slow flight the sensor reads low. th-aeronautics’ actuator-disc CFD (aero/airspeed/rotor-static-c-v24b.json, coarse, estimates) fits the rise as a Ct / (1 + b Ct), with Ct the mean lift thrust per disc over q times the disc area: nearly linear at light loading, levelling off at heavy loading, and finite at any speed, hover included. SIM_ARSPD_CPR 1 (the tracked build bd782d740932) adds it on top of the side-port table, with the lift thrust SITL computes each step. Below 12 m/s it is an extrapolation of the CFD, and a and b hold their end values outside -3 to +6 degrees. On the calm dock case, with both switched on, the speed the sensor’s pressure gives, before ARSPD_RATIO scales it, was 25 % below true at 15 m/s with the lift motors carrying most of the weight, 18 % below at 21 m/s, and about 15 % below near 25 m/s with the lift motors almost unloaded, where the side-port table gives most of it. At ARSPD_RATIO 2.3 the autopilot’s indicated airspeed sits about 7.5 % above those figures. A low reading ends a transition later, not earlier. SACP adds the rotor term (Rot) and the thrust per disc (Td).

The pitching moment is linear, so SITL cannot carry the real changes in pitch stiffness near the stall. On the earlier wing the CFD showed a dip between 8 and 11 degrees (docs/cg.md). On Fuselage C’s final first-build geometry (th-aeronautics 5d15e8b1, which otherwise confirms the C model’s inputs within about 1 %) the pitch stiffness softens between 9.5 and 10 degrees, dCm/dCL -0.040 against -0.111 below, and the pitch there is unsteady. Neither shows in SITL; it belongs to the stall test on the real aircraft. The actuator-disk VSPAERO cases for transition and pusher slipstream are not valid yet and are not used. The pusher is a table from tools/propulsion.py (V3115 640KV, calibrated; the APC 10x7E stands in for the CAM 10x7, an estimate): full-throttle thrust and battery current against the motor’s effective voltage (duty × pack voltage) and the airspeed. So part throttle and a sagging pack come from the same motor model. Until 2026-10-01 it was a momentum-theory motor fitted at a full pack at 0 and 20 m/s. Above 20 m/s and at low pack voltage that motor lost thrust much too fast: at 20.1 V and 27 m/s it gave 3.9 N against the propulsion model’s 9.4 N. A 31% pack then could not hold 27 m/s and Q_assist took over, where the table model holds it level at 91% throttle. The pusher folds at zero thrust rather than windmilling; the folded blades’ drag is a CFD estimate. Until 2026-10-01 the pusher’s current reached the flight controller’s log but not the simulated pack, so the pack did not sag or drain in wing-borne flight: a 25 minute loiter at about 7 A held 24.6 V. It now drains, and the same loiter reaches the 21.0 V low-battery failsafe after 23 minutes. The stall is a smooth blend to flat-plate lift with no wing drop, so SITL will not reproduce an asymmetric stall. That is one more reason to leave Q_assist on.

Slow flight is stall-limited, and the guide’s speeds sit too close to it. With the build settings of 2026-09-27, 25 degrees of elevon travel at the servo endpoints and MIXING_GAIN 0.75, the elevons no longer run out first: they have about 19 degrees of up travel and trimmed flight at 15 m/s needs 12. What happens instead is the stall above. With the earlier assumed 20 degrees and MIXING_GAIN 0.5 the elevons saturated at 16 to 19 m/s and the nose dropped before any assist could fire; that was the reason for Q_ASSIST_ALT, which stays as the height floor. The rate gains in terrahawk.parm are ArduPilot’s quadplane defaults divided by the 2.34 change in authority, with the feed-forward raised again for the CFD elevon terms (RLL_RATE_FF 0.311, PTCH_RATE_FF 0.801), the same values as the aircraft’s common file. A SITL AUTOTUNE at the new authority produced gains that flew in AUTOTUNE mode and departed in AUTO within 12 seconds of the transition, so it is not used; the real aircraft needs its own AUTOTUNE flight after the change.

A hover in a crosswind was not modelled well. SIM_Plane takes the angle of attack as atan2(w, u), and it scaled every aerodynamic force by the full airspeed. With the air along the span, or from behind, the angle swung through large values and the linear Cm_alpha made a false pitching moment: the aircraft hovered 16 to 33 degrees nose up. The patch now adds planar_lift. With it set, lift, drag and pitching moment use only the flow over the leading edge in the plane of symmetry, (max(u, 0)² + w²) / V² of the dynamic pressure. The same crosswind hover now needs 0 to 7 degrees of pitch, and normal flight does not change. The sideslip angle, atan2(v, u), has the same fault in a tailwind, where it sits near 180 degrees and the roll and yaw derivatives make moments that flip sign with v; planar_lift scales it by the same factor (2026-09-27, after a 3 m/s tailwind landing wandered half a metre sideways). Scaling atan2(v, u) had its own fault, which Georacer found in the upstream review: near 180 degrees, a fraction of the angle is still large, so a 1.5 m/s descent in a 3 m/s tailwind read about 35 degrees of sideslip where the true value is under 2. Since 2026-09-30 the patch takes sideslip as asin(v/V), which stays small when the sideways air is small, and then applies the same scale. Forward flight changes by less than 0.05 degrees, and a pure crosswind still hands over to the keel’s crossflow terms.

From the kit’s makers, Nomadium Robotics, in their Pathfinder series:


Source: docs/sitl-model.md in the repo. Edit it there; this page is generated.

Comments on

    Comments are reviewed before they appear. Please leave out places, addresses and other personal details.

    A Supported Systems Flight

    This build log is written and kept by Supported Systems, a small studio that makes thoughtful, user-first technology. The airframe, the hangar and the simulation are ours.

    Visit supported.systems