feat(globe): port to CeliumJS
This commit is contained in:
parent
ec03425067
commit
eb7698e034
51 changed files with 3521 additions and 1992 deletions
157
docs/POLAR_FINDINGS.md
Normal file
157
docs/POLAR_FINDINGS.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# Predictor behaviour at the poles
|
||||
|
||||
Measured 2026-08-03 against the full local stack (`./run-stack.py`): Go predictor
|
||||
on :8080 with GFS dataset `2026-08-03T00:00:00Z` (`gfs-0p50-3h`, 130/130 units,
|
||||
2.05 GB), Django on :8000, the Cesium build of this SPA on :5173.
|
||||
|
||||
Reproduce the browser half with:
|
||||
|
||||
```bash
|
||||
npx playwright test tests/e2e/polar.spec.ts --reporter=list
|
||||
```
|
||||
|
||||
That spec requires the real stack (it overrides `baseURL` to `http://localhost:5173`
|
||||
— see "CSRF" below).
|
||||
|
||||
## 1. What the predictor returns near the poles
|
||||
|
||||
Direct `GET /api/v1/prediction`, launch longitude 0.1, 2026-08-03T12:00:00Z,
|
||||
ascent 5 m/s, burst 30 km, descent 5 m/s:
|
||||
|
||||
| launch lat | points | latitude range | longitude range | NaN/Inf | above 85.051129° | frozen |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 62.1234 | 148 | 62.1219 … 62.3342 | 0.100 … 1.737 | 0 | no | no |
|
||||
| 85.0 | 148 | 85.0000 … 85.1535 | 0.100 … 5.560 | 0 | **yes** | no |
|
||||
| 89.5 | 148 | 88.8092 … 89.5000 | 0.100 … 55.922 | 0 | **yes** | no |
|
||||
| 89.99 | 148 | 88.9926 … 89.9900 | 0.100 … 79.279 | 0 | **yes** | no |
|
||||
|
||||
**No NaN, no Inf, no frozen tracks.** Today's polar wind blows southward, so
|
||||
every balloon drifted away from the pole and never reached latitude 90°, where
|
||||
the defects below would trigger. The failure modes are latent, not constant.
|
||||
|
||||
Note the longitude amplification: at 89.99°N the track sweeps 79° of longitude
|
||||
while moving 1° of latitude. That is **physically correct**, not a bug — the
|
||||
parallel at 89.99°N is only ~7 km long, so a few km of drift is a large change
|
||||
in longitude.
|
||||
|
||||
## 2. What the globe now renders
|
||||
|
||||
`tests/e2e/polar.spec.ts` reads back the coordinates Cesium actually holds in
|
||||
the rendered workspace polyline:
|
||||
|
||||
```
|
||||
[89.5N] vertices=148 max=89.50000 min=88.80919
|
||||
[89.99N] vertices=148 max=89.99000 min=88.99262
|
||||
```
|
||||
|
||||
These match the predictor's API output exactly. Under the previous
|
||||
MapLibre renderer every one of these vertices would have been clamped to
|
||||
85.051129° by `@maplibre/geojson-vt`'s `projectY()` and the track would have
|
||||
rendered as a straight line along that parallel.
|
||||
|
||||
Screenshots: `test-results/polar-89.5N.png`, `test-results/polar-89.99N.png`.
|
||||
|
||||
## 3. Known limitation: no basemap above 85.051129°
|
||||
|
||||
The polar cap renders as a featureless surface. Both configured basemaps —
|
||||
OpenStreetMap raster and Esri World Imagery — are Web Mercator tile pyramids,
|
||||
which do not extend past ±85.051129°. The trajectory, markers and bounding
|
||||
boxes draw correctly there (they are geographic vector data, not tiles), but
|
||||
there is no map detail underneath. The same limitation means **Antarctica can
|
||||
never be drawn in full** from these sources; its true extent reaches −90°.
|
||||
|
||||
Fixing this needs a polar-projection basemap (NASA GIBS serves EPSG:3413 for
|
||||
the Arctic and EPSG:3031 for the Antarctic), which is a separate piece of work.
|
||||
|
||||
## 4. Predictor defects found by reading the source
|
||||
|
||||
These live in the `predictor` repo and are **not fixed**. Verified by source
|
||||
reading plus a unit probe run against the real `Axis` geometry (GFS 0p50:
|
||||
lat −90…90 step 0.5, N=361).
|
||||
|
||||
### 4.1 The wind error is swallowed into a zero derivative — worst of the three
|
||||
|
||||
`internal/engine/models.go:85-88`
|
||||
|
||||
```go
|
||||
sample, err := field.Wind(t, s.Lat, s.Lng, s.Altitude)
|
||||
if err != nil {
|
||||
return State{} // zero derivative; error discarded
|
||||
}
|
||||
```
|
||||
|
||||
`internal/numerics/grid.go:40` (`Axis.Locate`) is correct — it returns an
|
||||
explicit error rather than reading out of bounds:
|
||||
|
||||
```
|
||||
lat=89 OK lat=90 ERROR: lat=90 out of range
|
||||
lat=89.5 OK lat=90.05 ERROR: lat=90.05 out of range
|
||||
lat=89.99 OK lat=91 ERROR: lat=91 out of range
|
||||
```
|
||||
|
||||
But the caller drops it, so the propagator returns a zero derivative and the
|
||||
balloon **silently freezes horizontally** with no error and no event in
|
||||
`events[]`:
|
||||
|
||||
```
|
||||
lat=89.99 dLat=8.95e-05 dLng=0.5128576395811134
|
||||
lat=90 dLat=0 dLng=0 <-- silently zeroed
|
||||
lat=90.05 dLat=0 dLng=0 <-- silently zeroed
|
||||
lat=91 dLat=0 dLng=0 <-- silently zeroed
|
||||
```
|
||||
|
||||
A plausible-looking wrong answer is more dangerous than a crash.
|
||||
|
||||
### 4.2 Latitude is never bounded, so there is no pole crossing
|
||||
|
||||
`internal/numerics/vec.go:71-73` — `GeoAdd` wraps longitude through `PyMod` but
|
||||
adds latitude unguarded:
|
||||
|
||||
```
|
||||
step 1: lat=89.9500 step 3: lat=90.0500 step 5: lat=90.1500
|
||||
step 2: lat=90.0000 step 4: lat=90.1000 step 6: lat=90.2000
|
||||
```
|
||||
|
||||
The state leaves the sphere. Correct pole crossing is `lat → 180 − lat`,
|
||||
`lng → lng + 180`; that logic does not exist.
|
||||
|
||||
### 4.3 1/cos(lat) divergence against a fixed integration step
|
||||
|
||||
`internal/numerics/vec.go:87`
|
||||
|
||||
```go
|
||||
dLng = degPerRad * u / (r * math.Cos(lat*piOver180))
|
||||
```
|
||||
|
||||
For a 10 m/s eastward wind at 30 km:
|
||||
|
||||
| lat | cos(lat) | dLng °/s | ° per 60 s step |
|
||||
|---|---|---|---|
|
||||
| 0 | 1 | 8.951054359255286e-05 | 0.0054 |
|
||||
| 60 | 0.5000000000000001 | 0.00017902108718510567 | 0.0107 |
|
||||
| 85.051129 | 0.08626673450528127 | 0.0010376020850432562 | 0.062 |
|
||||
| 89.99 | 1.745329251907294e-05 | 5.128576370030803 | 307.7 |
|
||||
| 90 | 6.123233995736757e-17 | 1.461818112044611e+12 | 8.77e+13 |
|
||||
| 90.0000001 | −1.7453291532541063e-09 | −51285.766599190494 | −3.08e+06 |
|
||||
|
||||
At exactly 90° the result is **large but finite, not NaN** — `math.Cos(π/2)` in
|
||||
float64 is `6.123233995736757e-17`, not zero. Past 90° the cosine goes negative
|
||||
and the drift direction inverts.
|
||||
|
||||
`internal/numerics/ode.go:15` integrates with classical fixed-step RK4 — no
|
||||
adaptive step, no error control — so truncation error grows without bound as the
|
||||
derivative stiffens. At 89.999° a single 60 s step advances longitude by more
|
||||
than a full revolution. Scaling the step by `cos(lat)` would bound it.
|
||||
|
||||
## 5. Environment notes
|
||||
|
||||
**CSRF.** `run-stack.py` binds Vite to `127.0.0.1` but never sets
|
||||
`CSRF_TRUSTED_ORIGINS`, whose default (`stratoflights/settings.py:195`) is
|
||||
`http://localhost:5173, http://localhost:8000`. Reaching the app as
|
||||
`127.0.0.1:5173` therefore gets **403 on every POST**, including
|
||||
`/api/predictions/`. Use `localhost:5173`, or add the origin to the env block in
|
||||
`run-stack.py`. (The default value also has a leading space in its second entry
|
||||
after `split(',')`.)
|
||||
|
||||
`ALLOWED_HOSTS` has the same shape: it defaults to `localhost` only, so
|
||||
`curl 127.0.0.1:8000` returns 400 while `curl localhost:8000` returns 200.
|
||||
Loading…
Add table
Add a link
Reference in a new issue