Antipodal

A globe that shows the weather at any point and at its antipode, over 85 years of ERA5 reanalysis.

Screenshot of Antipodal showing a 3D globe with weather data points
Pick a point, see the weather at its antipode, and scan a date for the pair where temperature and pressure both come closest to matching.

The Borsuk-Ulam theorem, proved by Karol Borsuk in 1933, says that any continuous map from the sphere to the plane sends some pair of antipodal points to the same value. For \(f: S^2 \to \mathbb{R}^2\) there is always an \(\mathbf{x}\) with \(f(\mathbf{x}) = f(-\mathbf{x})\). Take temperature and sea-level pressure as the two coordinates and the theorem says that at every instant, somewhere on Earth, two diametrically opposite points have the same temperature and the same pressure. Antipodal looks at how close real weather data gets to that.

The antipode of latitude \(\varphi\) and longitude \(\lambda\) is \(-\varphi\) and \(\lambda + 180°\), wrapped back into range. Almost all land is opposite ocean. Australia faces the Atlantic, North America faces the Indian Ocean, and the land-to-land pairs are things like Spain and New Zealand or Argentina and China. Weather stations are sparse over oceans, so station data was never going to work. I used ERA5, ECMWF's reanalysis, through the Open-Meteo archive API, which covers 1940 onward on a regular grid.

Sampling the sphere

I couldn't fetch every ERA5 cell, so I needed a few hundred points spread evenly over the globe. A latitude-longitude grid bunches up at the poles, so I used a Fibonacci lattice. For the \(i\)-th of \(n\) points, step the height linearly from pole to pole and rotate the azimuth by the golden angle \(\phi = \pi(3 - \sqrt{5})\):

\[y_i = 1 - \frac{2i}{n - 1}, \qquad r_i = \sqrt{1 - y_i^2}, \qquad \theta_i = \phi \cdot i\]

Latitude is \(\arcsin y_i\) and longitude is \(\theta_i\) mod 360, both converted to degrees. Because the golden angle is irrational, consecutive points never line up in longitude and the spiral doesn't clump. I used \(n = 500\). Earth's surface is about 510 million km\(^2\), so that's about 1.02 million km\(^2\) per point, or a mean spacing of roughly 1,000 km. That's coarse, but temperature and pressure are smooth at that scale and the whole dataset stays small enough to keep in object storage for free.

Fetching was the slow part. For the 500 points and their 500 antipodes I pulled daily mean 2 m temperature and mean sea-level pressure from 1940 through 2024, in 5-year chunks. Open-Meteo's free tier takes 10 locations per request, so the fetcher waits 6 seconds between requests and backs off when it trips the per-minute limit. It writes a checkpoint every 5 batches because I killed it more than once. The output is one JSON file per month in Cloudflare R2, with the grid coordinates stored alongside so the Worker that serves queries never has to regenerate the lattice.

Interpolating between grid points

Great-circle distance on a sphere
The haversine formula gives the length of this arc. Wikimedia Commons

A click lands between grid points, so the Worker takes the 3 nearest and blends them by inverse distance weighting. Distances are haversine on a sphere of radius \(R = 6{,}371\) km:

\[a = \sin^2\!\left(\frac{\Delta\varphi}{2}\right) + \cos\varphi_1 \cos\varphi_2 \sin^2\!\left(\frac{\Delta\lambda}{2}\right), \qquad d = 2R \arctan\!\left(\frac{\sqrt{a}}{\sqrt{1 - a}}\right)\]

The spherical law of cosines loses precision for nearby points. Each neighbor then gets weight \(1/d^2\):

\[\hat{v} = \frac{\sum_{i=1}^3 v_i / d_i^2}{\sum_{i=1}^3 1 / d_i^2}\]

A query that lands exactly on a grid node gets that node's value. IDW won't reproduce a front passing between two nodes, but it gives a plausible number everywhere without a geostatistical model.

The scanner

The part I actually wanted was the scanner. For a chosen date it looks at all 500 pairs and finds the one where temperature and pressure both come closest to matching. Temperature and pressure have different units, so I min-max normalize each difference over that day's 500 pairs and add them:

\[\text{score}_i = \frac{|\Delta T_i| - \min|\Delta T|}{\max|\Delta T| - \min|\Delta T|} + \frac{|\Delta P_i| - \min|\Delta P|}{\max|\Delta P| - \min|\Delta P|}\]

where \(\Delta T_i\) and \(\Delta P_i\) are the differences between point \(i\) and its antipode. The lowest score wins. A winning pair usually has a temperature gap under 1 degree C and a pressure gap under 2 hPa, while the spread across all pairs runs to tens of degrees and tens of hectopascals. The globe draws all 500 grid points with the winning pair highlighted and an arc between them.