This note describes the two engines behind the Stratolink flight path system: a forward forecast that predicts where a balloon is going, and a reconstruction that infers where it has been between sparse position fixes. I document the full method, including the wind advection model, the in-flight bias correction, the Monte Carlo ensemble and its confidence ellipses, the particle smoother used for reconstruction, the role of altitude, and the handling of long gaps where the tracker falls silent. Throughout, the goal is to draw a firm path only when the data supports one, and to report an honest region when it does not.
There are two things I want to know about a balloon in flight. Where is it going, and where has it been. These sound like the same question pointed in opposite directions, but they are genuinely different problems, and I solve them with two different engines.
Predicting the future is a forecasting problem. I stand at the last known position and reason forward, and my uncertainty grows the further out I look, because I am extrapolating into weather I have not seen yet. Reconstructing the past is a smoothing problem. I have a confirmed position before a gap and another after it, so the truth is pinned at both ends and I am filling in the middle. The math rewards me for that: a reconstruction between two known points is far tighter than a forecast over the same span.
Both engines rest on one idea, so I will start there.
A pico balloon floating in the stratosphere has no propulsion and no steering. It is a leaf on a river of air. So if I know the wind at the balloon's position, I know how it moves. Everything in this system is built on that.
I represent the wind as a grid of vectors. At each grid point I store two numbers: u, the eastward wind speed, and v, the northward wind speed, both in metres per second. The balloon is almost never sitting exactly on a grid point, so to get the wind where it actually is I blend the four surrounding points using bilinear interpolation, weighting each by how close it is. That gives a smooth wind value anywhere inside the grid.
Once I have the local wind, moving the balloon one time step is just arithmetic. Latitude and longitude change in proportion to the wind, scaled by how many metres there are in a degree:
The cos φ term in the longitude line matters. Lines of longitude crowd together as you move away from the equator, so the same eastward wind moves you through more degrees of longitude at high latitude than at low latitude. I take a step every ten minutes, which is fine and smooth for the timescales a balloon flies on. Repeat this thousands of times and the balloon traces a path. That loop is the heart of both engines.
If I trust the wind, I can move the balloon. Everything else in this system is about getting the wind right, being honest about how wrong it might be, and using the balloon's own reports to correct it.
The forecast is the simpler of the two engines, so I will explain it first. I take the balloon's last confirmed position and I integrate forward through the forecast wind field, one ten minute step at a time, out to twenty four hours. That produces a single predicted path, ending at a single predicted point.
If the wind forecast were perfect, that would be the end of it. It is not perfect, and pretending otherwise would make the whole thing useless. So the rest of the forecast engine is about two things: correcting the wind using what the balloon has already told me, and being honest about the uncertainty that remains.
The balloon is the best wind sensor I have, because it is physically riding the wind I am trying to model. Every time it reports two positions in a row, I can work out the wind it actually experienced between them. I just divide the distance it moved by the time it took:
Then I compare that to what the forecast claimed the wind was at the same place and time. The difference is a bias, and it has two parts. The forecast might have the wind speed wrong, and it might have the direction wrong. I measure both across every pair of fixes I have, and average them into a single speed multiplier k and a single direction rotation θ:
To apply the correction I scale every forecast wind vector by k and rotate it by θ before I step the balloon:
This is the same idea professional trajectory models use, and it is the single biggest accuracy gain in the forecast. I do put guard rails on it. I clamp the speed multiplier to between 0.75 and 1.25 and the rotation to plus or minus 25 degrees. If the raw numbers come out larger than that, it usually means the balloon briefly hit a small local feature that will not persist into the rest of the forecast, so I cap the correction and record both the raw and the capped value for inspection.
Even after the bias correction, I do not know the future wind exactly. So instead of pretending I have one answer, I run two hundred. This is the Monte Carlo ensemble.
Each of the two hundred members is a complete forecast run, but with the wind nudged a little differently. For each member I draw a random speed multiplier, a random direction offset, and a random altitude offset from normal distributions whose widths match how wrong forecasts of this kind typically are:
The altitude offset matters because a balloon at a slightly different pressure level rides slightly different winds. I integrate all two hundred members forward. Where they bunch together, I am confident. Where they fan apart, I am not. The spread of the two hundred endpoints is not a guess at the uncertainty; it is the uncertainty, measured directly from the physics. This is exactly how hurricane forecasters and weather centres produce their cones.
Here is a detail I care about, because getting it wrong makes the whole forecast either dishonest or useless. Balloon trajectory uncertainty is not a circle. It is an ellipse, and the ellipse points along the direction of travel (Figure 1).
The reason is that the two kinds of wind error push in two different directions. An error in wind speed makes the balloon arrive too early or too late, which spreads the possibilities along the track. An error in wind direction pushes the balloon to one side, which spreads them across the track. These two spreads are almost never equal, so the honest shape is a stretched ellipse, not a round blob.
To find that ellipse I take the cloud of ensemble positions at a given time, project them into local kilometres, and compute their covariance matrix. The covariance captures how the points spread and how the two directions are correlated:
The eigenvectors of this matrix give the directions of the ellipse's two axes, and the eigenvalues give their lengths. To turn that into a confidence region I scale the axes by a value from the chi squared distribution with two degrees of freedom, which is the number that says how far out I need to draw the boundary to enclose a given fraction of the cloud:
For the fiftieth percentile the chi squared value is 1.386, and for the ninetieth it is 4.605. So I draw two ellipses: a tight inner one that the balloon is in half the time, and a looser outer one that holds it ninety percent of the time. That pair is what you see on the map.
Now the second engine, which I find more interesting. Sometimes the tracker goes quiet. It drifts out of range, or it browns out at night when the solar panel loses the sun. When it comes back, I have a confirmed position before the silence and another after it, with hours of unknown path in between. I want to reconstruct where it went.
The key insight is that this is not the same as forecasting. A forecast only knows the past, so it fans outward. A reconstruction is anchored at both ends, so the unknown path is a bridge, not a cone (Figure 2). The uncertainty is zero at each confirmed fix and bulges in the middle, like a rope held at both ends and sagging between them. Mathematically this is the difference between a filter, which uses only past information, and a smoother, which uses information from both sides of the gap.
That bridge shape is not something I impose by hand. It falls out of the math. If you model the unknown path as a process anchored at both endpoints, the variance in the middle follows a simple curve that is zero at both ends and peaks at the midpoint:
One more important choice. For reconstruction I do not use the forecast winds. I use reanalysis winds, which are the best reconstruction of what the atmosphere actually did, assembled after the fact from all available observations. For predicting the future I have to use a forecast, but for reconstructing the past the real analysed winds are available and far more accurate. Using them is most of why reconstruction can be so much tighter than forecasting.
To build the bridge I use a particle smoother. The procedure has three moves.
First I solve a simple targeting problem. I want to find the wind bias that, when I integrate from fix A, lands me exactly on fix B. I integrate, see where I land, measure how far off I am, nudge the speed and heading toward the target, and repeat. I damp each nudge so it converges smoothly instead of overshooting back and forth. After a handful of iterations the path connects the two fixes. This gives me the centre of my search.
Then I launch many particles from fix A, each with the connecting bias plus a small random perturbation, and integrate them all to the far side of the gap. Most land near fix B, some land off to the side. I score each one by how close it landed, using a smooth weight that falls off with the square of the miss distance:
I then resample the particles in proportion to their weights, so trajectories that threaded both fixes survive and multiply, while ones that wandered off are dropped. The survivors, taken together, are the bridge. Their average is the reconstructed path, and their spread at each moment gives the confidence ellipses, which now pinch to nothing at both fixes exactly as the bridge picture promised.
I keep one number to tell me whether the result can be trusted: the effective sample size. If almost all the weight piles onto a few lucky particles, the answer rests on too little, and this number catches it:
A healthy bridge has hundreds of effective particles. A handful means the data cannot really support a precise path, and as I will explain later, that is exactly when I stop drawing a line.
A balloon's float altitude is one of the most useful things I can know, because the wind is different at different heights. If I know the balloon's altitude during a gap, I know which layer of wind to push it through, and the reconstruction tightens dramatically.
Often I do know it, even when GPS is gone, because the pressure sensor keeps reporting. Pressure converts directly to altitude through the barometric formula:
On one of our flights the GPS froze for several hours, but the pressure sensor kept transmitting the whole time. Because I knew the altitude throughout, I knew the wind layer, and the reconstruction of that stretch came out far tighter than the length of the gap would suggest. When there is no pressure during a gap, I treat altitude as an unknown to be solved for, and the surviving particles reveal which altitude best connects the two fixes. So even in the dark, the path tells me roughly how high the balloon was.
Any scrap of telemetry during a gap helps, not just position. Pressure pins the altitude, which pins the wind layer, which pins the path. I feed the reconstruction everything the balloon managed to send, not only its GPS fixes.
Short gaps are easy. The real challenge is a gap of many hours, where the tracker went silent overnight and the balloon crossed into a different weather pattern while I was not watching. One of our flights had a gap of about thirty six hours. The balloon sent four readings in the first thirty five minutes and then nothing at all for a day and a half.
A gap this long breaks the simple approach in three ways, and I had to harden the engine for each.
Over thirty six hours the weather itself moves. A single snapshot of the wind is just wrong. So I pull the reanalysis wind for every hour of the gap and interpolate between those hourly fields as the balloon flies, so each leg of the journey sees the wind that was actually present at that hour.
Pico balloons rise during the day as the sun warms the gas, and sink at night as it cools. Over a multi day gap that cycle is large and it changes which wind layer the balloon rides. I model it as a daily rhythm in the float altitude, anchored to the recorded sunlight and temperature where I have them, swinging between a daytime and a nighttime height:
Over so many hours, a particle launched from fix A almost never happens to land near fix B on its own, so nearly all of them get thrown away and the estimate collapses onto too few survivors. To fix this I give each particle a gentle pull toward the target that strengthens as it nears the end of the gap, like a guide rope. Because that pull is my own invention and not real physics, I keep a running penalty on how hard I had to pull, and use it to weight the particle down, so steering a particle into place does not let it pretend to be more likely than it really is.
There is a subtle trap in long gaps that I only caught by looking carefully at the data. On that thirty six hour leg, the balloon ended up only about 580 kilometres from where it started. At its float altitude the winds were strong enough to carry it several times that distance. The only way to cover so little net ground in so much time is for the path to have curled back on itself. The balloon almost certainly looped or stalled around a slow weather system before moving on.
My first version could not represent that, because each particle held a single fixed heading error for the whole gap, which only ever produces smooth, mostly straight arcs. A particle could not curl back. So I changed the model. Now each particle's heading drifts over the gap as a slow random walk, picking up a fresh nudge every few hours, the way real winds veer over days:
But I did not want to make every long gap loopy, because some long legs really were fast and straight. So I let the data decide how much wandering to allow. I measure how directly the balloon got from A to B by comparing its net ground speed to a typical wind speed at that altitude. I call this the directness:
When the directness is high, the wandering switches off and the leg stays a tight line. When it is low, the wandering turns on fully and the cloud of particles explores looping paths, so the uncertainty region honestly includes the possibility that the balloon circled for a while. On that thirty six hour gap the engine correctly opens up into a wide region; on a fast eight hour leg the same day it stays a clean line.
I want to be honest that this is a stand in. The wandering is random because I have not yet wired in the real hour by hour winds for that gap. Once I do, a genuine loop will come out of the actual swirling wind field rather than from injected randomness, which I trust far more. The directness measure stays useful either way, as a simple check on how much the path could have meandered.
The most important feature of the whole system is that it knows when not to draw a line. A confident looking path that is actually a guess is worse than useless, because someone will believe it.
After reconstructing each gap I check two numbers. If the effective sample size falls too low, or the uncertainty in the middle of the gap grows past a sensible threshold, I flag the gap as under determined. When that happens I stop pretending I have a path. Instead I output a region, but I am careful about what shape that region takes, because it is easy to get this wrong in a way that quietly overstates what I know.
The tempting shortcut is to wrap a convex hull around all the places the particles went. I do not do that, because a convex hull fills in every dent and curve. If the real set of places the balloon could have been is a curved, possibly looping band, a hull paints over the empty middle of that curve and claims it as reachable, when in truth the balloon could not have swung out there and still made it to the closing fix in time. So instead I build an occupancy footprint. I lay a fine grid over the area, march every surviving trajectory through it, and shade each cell by how many of those trajectories actually passed through it. Cells that no trajectory ever entered stay blank, even if they sit geometrically in the middle of the cloud.
This is more honest in two ways. It follows the true shape, including concavities and separate lobes, rather than a smoothed outer wrapper. And it respects the return trip for free, because a trajectory only shades a cell if that same trajectory also reached the closing fix; the ones that wandered too far to get back were already discarded by the endpoint weighting, so they never contribute. The map then shows a graded shaded footprint, dark where many possible paths agree and faint at the edges, with a note explaining why, so the picture matches what I actually know (Figure 4).
When the data can support a path, I draw a path with an honest band around it. When it cannot, I draw a region and say so. The engine never upgrades a guess into a line.
This is also why the thirty six hour gap stays a region for now. The synthetic winds I am using as a placeholder cannot resolve a day and a half of real overnight drift, and the engine says so out loud rather than inventing a route. When the real hourly winds go in, that gap should sharpen into a path if the physics allows, and if it does not, the region is the correct and honest answer.
None of this math runs while you are looking at the map, and that is on purpose. Heavy computation on every page load would be slow and wasteful. Instead I compute everything ahead of time on a schedule, and serve the result as a small file.
A scheduled job runs the forecast every half hour and the reconstruction every hour. Each job pulls the latest winds and telemetry, runs the ensemble or the smoother, and writes a compact file of around a hundred kilobytes containing the paths, the bundles of sample trajectories, the confidence ellipses, and the wind grid for the animation. That file sits on a fast content network at the edge, close to wherever you are. When you open the page, it fetches that one small file and draws it, which takes a few tens of milliseconds anywhere in the world. The expensive thinking happens once, in the background, and everyone who visits simply reads the result.
I would rather be clear about the edges than oversell the middle. The forecast is only as good as the wind forecast it rests on, and its honest uncertainty grows the further ahead it looks. The reconstruction is far stronger, but it has a hard floor: if the balloon goes completely silent for a very long time and crosses changing weather, there may simply not be enough information to recover the exact path, no matter how clever the method. In those cases the right answer is a region, not a line, and the engine is built to say that rather than guess.
The biggest single improvement ahead is feeding in real hourly reanalysis winds for the long gaps, in place of the synthetic stand in I use today. With the actual winds, the loops and stalls that I currently approximate with randomness will emerge from the real moving air, and the longest gaps should tighten on their own. Everything in the system is built to accept that upgrade without changing how any of it is presented to you.
That is the whole engine. A balloon rides the wind, I model the wind as carefully and as honestly as I can, I let the balloon correct me with its own measurements, and I am careful to draw a firm line only when the data has earned it.