Flight Simulation Tools
The aeronautics tools say how the TerraHawk’s air pushes on it. The flight simulation tools say what the autopilot does with that: whether it holds a heading across the wind, how far a landing misses the pad, where a transition sags, and what each phase of a survey costs in energy. This page is the tour. It covers how SITL is set up and changed for this aircraft, the scripts that fly it, the gusts it flies in, the test battery that runs against every change, and the JSBSim model that now sits beside it. The model itself and its limits are on the SITL model page, and every parameter with its evidence is on parameters beyond the guide.
How the pieces fit together
Section titled “How the pieces fit together”| Piece | What it is | What it answers here |
|---|---|---|
| ArduPilot SITL | The real ArduPlane code, compiled for a PC, flying ArduPilot’s built-in plane and multirotor models | Everything the autopilot decides: trim, transitions, failsafes, landings |
| Our SITL patch | Changes to those built-in models, carried in sim/patches/ |
A pusher that loses thrust with airspeed, a wing that behaves in a hover, a keel that resists sideslip |
| The model file | sim/terrahawk-c24b-w78.json since 2026-10-18 (sim/terrahawk-keel-notips.json before), written by tools/sitl_model.py from the aero data and the mass build-up |
The numbers the models fly with |
| The parameter files | The aircraft’s params/terrahawk-common.parm, with sim/terrahawk.parm on top |
The same settings the real aircraft will fly |
| The harnesses | Python scripts under sim/ that fly one question each, many times |
Dock landings, gusts, stalls, transitions, failsafes |
| Dryden gusts | A gust model in the harness, fed to SITL as its wind | Realistic turbulence, in place of SITL’s own |
| The test battery | th-testpilot’s standard set of flights under sim/battery/ |
Whether a change still flies, flight by flight |
| JSBSim | An open flight dynamics engine, flown by the same autopilot through a bridge | A real rigid body, landing gear on the pad, and in time table-driven aerodynamics |
SITL answers most questions. JSBSim exists for the ones SITL cannot see, above all what happens after the soles touch the pad.
ArduPilot SITL and our patch
Section titled “ArduPilot SITL and our patch”SITL (software in the loop) runs the same ArduPlane code that flies the aircraft, against a simulated airframe. The autopilot cannot tell the difference, so its modes, its failsafes, its Lua scripts and its parameter quirks all behave as they will in the air. That makes it the right place to test the flight scripts, the failsafe design and the landing logic, even where its aerodynamics are coarse.
The airframe it flies is ArduPilot’s quadplane model: a plane model for the wing and a multirotor model for the lift motors. Out of the box both were too simple for this aircraft in ways that changed answers, so we patched them. The patch is in sim/patches/sitl-terrahawk.patch and offered upstream as a draft pull request (ArduPilot #34509).
- The pusher. The stock plane model gives the same thrust at any airspeed. The patched pusher is a motor and propeller in momentum theory, whose thrust falls as the airspeed approaches the propeller’s pitch speed, and whose current comes from a watts-per-newton fit to the propeller data. Cruise current came out right only after this.
- Planar lift. In a hover with a crosswind, the stock wing model reads the sideways air as a huge angle of attack and sideslip and makes lift and rolling moments a real wing does not. The patch counts only the air that arrives over the leading edge.
- The keel. With planar lift, the wing’s sideslip terms fade out in a hover. The keels still catch a crosswind, so the patch adds their crossflow drag, which takes over where the wing’s terms stop. This is what makes the hover yaw loop work against the wind the way the real aircraft will.
- Two fixes. The lift motors now see the air flowing into them along their axis, and the stall curve is computed in double precision, because a steep stall model overflowed.
The binary the harness flies is built from three recorded inputs: an ArduPilot commit, that patch, and ArduPilot’s build image pinned by its digest (sim/build-spec.json). make sitl-build builds it in that image from a fresh clone, twice, and keeps it only when the two binaries are identical; the record of each build is committed in sim/builds/, and every battery result names the build that flew it. The harness refuses to fly when the patch has changed and no build matches it, so a result can always be traced to the exact code. The firmware for the charge node and our flight controller builds in the same image.
Four stock behaviours are worth knowing, and the SITL model page explains them: the plane model has no inertia, the multirotor model counts the aircraft’s mass one and a half times, a CG offset must be zeroed, and the airframe is levelled and held still the moment it touches the ground. The last one is why JSBSim came in.
The model file and the parameters
Section titled “The model file and the parameters”tools/sitl_model.py, shared with th-aeronautics, turns the aero data into one JSON file. The stability derivatives come from the vortex lattice and CFD work, the lift curve and stall from the section polars, the mass and inertia from the parts list and Blender positions, the lift motors from their measured hover power and the pusher from the propeller data. The file keeps the plain derivatives next to the converted values, so a reader can check the one against the other.
The parameters are the aircraft’s. Every SITL flight loads th-avionics’ params/terrahawk-common.parm first and sim/terrahawk.parm on top. The SITL file holds only what the simulated aircraft needs in addition: its servo map, its sensors, and where its GPS sits. make params fails if a setting in both files ever differs, so the aircraft that flies in SITL is the one that will fly for real.
That check found one thing worth a paragraph. On Fuselage B the GPS antenna sat 134 mm aft of the CG point (the Q43 dome at Y 404, the CG point at 270), and the common file told the autopilot so. SITL’s GPS sat at the CG, so the autopilot corrected for a lever arm that was not there. In a 5 m/s crosswind, dock landings then ended 189 and 323 mm forward of the aim point, against 33 and -65 mm with SITL’s GPS moved to the antenna. A wrong antenna position costs a quarter of a metre in exactly the landings with the least margin. That finding dates from 2026-09-29. On Fuselage C the Q43 moved forward over the spars, to 30 mm aft of the CG point, and the common file and SITL’s SIM_GPS1_POS moved together (7894390d, 2026-10-15), which is exactly the pairing this check enforces.
The harnesses
Section titled “The harnesses”Each question gets a script that flies it many times, with fresh SITL instances in parallel, and writes one row per flight.
| Script | The question |
|---|---|
flight_check.py (make check) |
One scripted mission: hover, transition, a box at 60 m, transition back, landing. The standing check that a model or parameter change did what it should |
dock_approach.py |
The hangar landing: return from far out, yaw onto the box axis, land, score the touchdown against the pad’s centring capture |
hangar_envelope.py |
The wind matrix for launches and landings in the box, and the shared Sitl class every harness uses |
lateral_tests.py |
A side-gust step, removing the crab, and the spiral mode |
stall_probe.py |
Where the wing stalls, from the angle of attack |
transition_probe.py |
Whether a transition settles on the wing or stays on the lift motors |
lua_yaw_test.py, lua_failsafe_test.py |
The dock yaw script and what it does to the RC failsafe |
pitot_kinematics.py |
Whether a log’s pitot agrees with the aircraft’s own acceleration along its air path. With --lua-equivalent it replays the stuck-pitot guard’s exact rule on any log, SITL or real, and stops at the guard’s own fire, after which the logged airspeed is synthetic. This is the acceptance check before the guard may act on the real aircraft |
This is the kind of result they produce. A 5 m/s gust from the side, flown with the keels and with the original tip fins:

And the check mission, which is also the quickest way to see energy by phase:
| Phase | Airspeed, m/s | Pusher throttle | Lift motors | Current, A | Pack, V |
|---|---|---|---|---|---|
| VTOL takeoff to 40 m | 0.7 | 49% | 27.7 | 23.8 | |
| Transition | 14.7 | 83% | 35% | 34.5 | 23.3 |
| Level cruise | 22.1 | 58% | 0% | 5.7 | 24.4 |
| Approach, on the lift motors | 5.1 | 46% | 24.6 | 23.4 | |
| VTOL landing | 0.7 | 42% | 21.1 | 23.4 |
These are the flown keel model at 2.73 kg on the P50B pack, with the pusher from the propulsion table and the CFD drag basis (2026-10-01, model fadf08c). SITL under-reads drag below 18.5 m/s, so take no minimum-power or loiter figure from it there. Level cruise trims at 5.8 degrees angle of attack and 7.6 degrees of up elevon, against the model’s prediction of 5.2 and 7.2. JSBSim flies the same mission at 59% pusher throttle, 5.6 A and the same pack voltage in level cruise. The hover and landing cost about four times the current of cruise, which is why the mission plans keep them short.
Wind: gusts and the box
Section titled “Wind: gusts and the box”SITL has its own turbulence setting, and we stopped using it. Its source code picks a new random direction for the gust on every physics step, 1200 times a second, so what it makes is close to white noise. The wing feels that as a violent jitter in angle of attack, which inflated stalls and drag, while the position of the aircraft hardly moves because the noise averages out.
The harness now makes its own gusts, to the low-altitude Dryden model in MIL-F-8785C, and feeds them to SITL as its wind. Their strength is a tenth of the mean wind vertically and more along it, and their length grows with height, so a gust near the ground is short and one at altitude is long. At landing height they are about 1 m/s and last about three seconds: enough to push the aircraft off the pad before the controller can bring it back.
The box around the pad is modelled as changes to that wind by height: the lid’s shelter when the wind comes from its end, a pull toward the walls, a small loss of thrust on the front lift motors near the front wall, and, as an option, the lee of the front and side walls. These are estimates from th-hangar until a CFD section of the box says otherwise.
Flying the dock approach on the final configuration, 54 times, gave this:
Steady wind from ahead or across lands inside the capture. Gusts from ahead or across spread the landings by a few hundred millimetres, and wind from the lid end moves them aft. No flight departed from controlled flight. The spread from gusts is why the dock now holds launches and landings in gusty air, and why the pad centres the aircraft mechanically rather than asking the controller to land it exactly.
The test battery
Section titled “The test battery”The harnesses answer design questions. The battery answers “does this change still fly”. It is th-testpilot’s standard set of flights, in tiers (smoke, standard and full) plus experiments, each with pass and fail criteria agreed with th-aeronautics and th-avionics: a 100 ha survey in calm air, across Dryden gusts and along a steady 8 m/s wind; a trim and power sweep at four airspeeds; rehearsals of the first flight test’s stall, wings level and banked; RC loss near the pad and far out; the assist trap, where a sagging aircraft sits on its lift motors; and dock captures from ahead and across. Every flight writes a result file and the runner prints one table. It runs on every aircraft change, and the flight reports are in docs/agent-threads/test-battery/.
JSBSim: the aircraft on its landing gear
Section titled “JSBSim: the aircraft on its landing gear”SITL stops being honest at the ground: when the aircraft touches, SITL sets it level and holds it still. That hides the part of a dock landing that matters most to the pad: the rock from the toes onto the keels, and whether the aircraft slides before the push bars close.
JSBSim is an open flight dynamics engine, used by FlightGear and many research groups. It carries a real rigid body with its inertia, gear contacts with springs, dampers and friction, and its own atmosphere and turbulence. The same ArduPlane flies it through a bridge, sim/jsbsim/bridge.py, over SITL’s JSON interface: each step, the autopilot’s servo outputs go out and the aircraft’s state comes back. The bridge models the lift motors, pusher, servos and battery as SITL does, so the two simulators start from the same aircraft. On the check mission they agree on trim, angle of attack, elevon and hover power.
The airframe model is generated by tools/jsbsim_model.py, owned by jbsim-06, our JSBSim specialist. It has six gear contacts: the two front stilt soles and the front and rear of each keel sole, modelled as skids.
This landing touches the front soles first at 11 degrees nose-down, and the aircraft rocks level onto the keels over about 1.5 seconds. The finding SITL could not make is the bottom plot. For the first second or so the lift motors still carry most of the weight, so the soles grip weakly. The aircraft is rocking level, so the lift motors stop leaning into the wind, and the wind pushes it aft about 0.7 m before its weight settles. How far it goes depends on the sole friction on the pad, and on how much the front wall shelters the pad: with th-hangar’s estimate of that shelter, the slide turns forward instead. Both are being measured, by a tilt test of a real sole and a CFD section of the wall.
jbsim-06 is now building table-driven aerodynamics for the JSBSim model from th-aeronautics’ data, so it can use the CFD’s stall, each elevon’s own effect and the rotor wake. The stall test and the transition flights will then have two independent models to agree or disagree.
What stays unmeasured
Section titled “What stays unmeasured”Both simulators fly models, and the models inherit every estimate in the aero data, the mass build-up and the propulsion model. SITL’s aerodynamics are linear with a blended stall; JSBSim’s gear stiffness and friction are guesses until the tilt test; the box’s wind is an estimate until the CFD. What the tools are good for is comparison: a change that makes things worse in both simulators almost certainly makes them worse in the air. When the aircraft flies, its logs go through the same scripts, and the models get corrected against it.
Source: docs/simulator-tools.md in the repo. Edit it there; this page is generated.
Comments
Comment on the whole page here, or on one section or step with the button beside it.