# Ephemeris Lens II: Reconnection — build notes

The sequel to [MMS4 Ephemeris Lens](../../mms4.html). The first game turned one MMS4 ephemeris product into a guided scene. This one starts at the launch pad, then hands you four physics labs. Each lab runs a real model and checks it against the NASA archive.

## Data (all public, all fetched through NASA CDAWeb HAPI)

| What | Dataset | Slice | Used for |
|---|---|---|---|
| The data.gov "mms4" trove | `MMS1–4_FGM_SRVY_L2`, `MMS1–4_FGM_BRST_L2` (8 datasets) | see below | magnetometer field |
| Survey field, MMS4 | `MMS4_FGM_SRVY_L2` (8–16 S/s) | 16 Oct 2015 00:00 → 17 Oct 06:00, 2-min means | orbit colour, the magnetopause-crossing objective, the Bow Wave verdict |
| Burst field, all four | `MMSn_FGM_BRST_L2` (128 S/s, resampled to 32 S/s) | 16 Oct 2015 13:06:40–13:07:30 and 11 Jul 2017 22:33:40–22:34:30 | the curlometer |
| Ephemeris | `MMSn_MEC_SRVY_L2_EPHT89D` | the same windows | positions (GSM + ECI), T89 field, Kp, Dst, field-line type, GMST |
| Solar wind | `OMNI_HRO_1MIN` | both event days, 5-min means | the Bow Wave replay |

`tools/data/fetch.py` → `process.py` (burst + curlometer reference) → `orbit.py` (orbit + OMNI). The FGM CSV positions are printed to only three significant figures, which is far too coarse for a 10 km tetrahedron. The curlometer therefore takes positions from MEC, which keeps six decimals.

Checks that came out of the data, not out of the code:

* The curlometer peaks at **1.26 µA/m² at 13:07:02.4 UTC**, with |B| down to 1.4 nT. Burch et al. (2016) put the electron diffusion region at 13:07:02.2.
* With the OMNI wind at 13:07 (n = 4.77 cm⁻³, v = 382 km/s, Bz = −2.9 nT), Shue (1998) places the magnetopause at **11.75 RE** at MMS's angle. MMS was at 11.93 RE.
* Near apogee on 16 Oct the FGM measured about 13 nT where the T89 field model predicts about 38 nT. T89 has no magnetosheath in its field model, so it keeps predicting magnetospheric field. The magnetometer shows MMS4 had crossed into the sheath.

## Physics (src/physics, tested in tests/)

* **orbit.js** — RK4 with an adaptive step: two-body + J2 + lunisolar third-body (low-precision Almanac Moon/Sun). Started from the real MEC state at apogee, it tracks the definitive ephemeris to **0.6 km over 18 h** (2015) and **2.7 km over 30 h** (2017). Two-body alone drifts 117 km and 178 km. Exponential drag is there for the "perigee into the air" what-if.
* **particles.js** — relativistic Boris pusher, a guiding-centre integrator (mirror force + grad/curvature + E×B, RK4), and a drift-shell average. Bounce and drift periods match the analytic dipole values to better than 1%. The loss cone at L = 4 is 5.47°.
* **magnetopause.js** — Chapman–Ferraro pressure balance, Shue et al. 1998, and the Farris & Russell 1994 bow shock with a 0.81-eccentricity conic. The dipole what-if scales the stand-off as M^(1/3).
* **curlometer.js** — reciprocal-vector curlometer, ∇·B quality, and tetrahedron elongation/planarity. The JS version matches the Python reference on the real data to within 0.1%.
* **reconnection.js** — a Harris sheet plus an X-line perturbation written as a flux function, with an exact Laplacian for the true current and marching-squares field lines.

## Models (Blender 5.2, `tools/blender/mms4v2_models.py`)

Built from dimensions, not by hand. Runs headless (`pip install bpy==5.2.1`) or inside Blender through the same bridge pattern as Black Budget Bird.

* **MMS observatory**: an octagonal deck 3.5 m across corners and 1.2 m tall, eight solar panels, four FPI head pairs, HPCA/EIS/FEEPS, star trackers, separation rings and omnis. The booms are rig nodes: two hinged 5 m magnetometer booms, four SDP wire exits (the 60 m wires are drawn in-engine) and two telescoping ADP booms.
* **Atlas V 421**: 3.81 m core, two AJ-60A SRBs, interstage, Centaur and RL10, and a 4 m fairing split into two halves. Every stage is its own node so the cutscene can drop it.
* **SLC-41-style pad**: deck, mobile launch platform, umbilical tower, four lightning masts and a water tower.
* `make_textures.py` bakes the solar cells, gold/silver MLI, the orange core foam, the fairing, the SRBs and the concrete. `tools/textures/bake_earth.py` bakes the Earth from Natural Earth (public domain) vectors.

## Planet, aura and shield (v2.1)

* **Earth** (`render/earth.js`): baked by `tools/textures/bake_earth_nasa.py` from NASA Visible Earth (Blue Marble NG Oct 2004, Black Marble 2016, GEBCO 2008 relief ×14, cloud composite). The surface shader does tangent-space relief normals, cloud shadows offset along the sun direction, and Fresnel ocean glint. Aerial perspective uses the same ray-marched Rayleigh (β = 5.8/13.5/33.1 ×10⁻⁶ m⁻¹, H = 8 km) and Mie (21 ×10⁻⁶ m⁻¹, H = 1.2 km, g = 0.76) integral that draws the sky shell. The ground intersection is analytic, so the faceted mesh doesn't print a grid in the haze.
* **Aurora** (`render/aurora.js`): curtains and a diffuse band centred on the IGRF-2015 geomagnetic pole (80.37° N, 72.62° W). The oval sits about 67° at midnight and about 76° at noon for Kp ≈ 2, and moves equatorward with Kp.
* **Field model** (`physics/fieldmodel.js`, tested in `tests/fieldmodel.test.mjs`): with a quiet wind the traced open/closed boundary is 75.5° at noon (the cusp) and 76° at midnight. A 72° footpoint reaches −19 RE at midnight but stays inside the 10.7 RE nose at noon. In the strong-CME case the cusp moves equatorward to 68.5°.
* **Plasmasphere** (`render/plasmasphere.js`): L_pp = 5.6 − 0.46 Kp. Kp in the Bow Wave lab is a rough estimate from v·Bs and is labelled as one.

## Launch-site terrain (v2.2, `src/render/terrain/`)

Ported from the gamelab's own terrain work. FIELDWORK supplied the tiling detail textures, the multi-scale detail sampling, the patched-MeshStandardMaterial look layer, the instanced grass and the water normals. island-v4 supplied the instanced scatter of trees and shrubs over a height-coloured surface.

* **site.js** is the site plan in pad metres (+x east, −z north). It has one `sample(x, z)` that returns the height and the masks: sand, scrub, wet, marsh, mowed and gravel. The terrain mesh, the grass map, the scatter, the props and the tests all read it. The coastline formula has a GLSL twin, and the tests check the two agree.
* **padTerrain.js**:
  * The terrain is a polar mesh with 2.5 m rings out to 420 m, then geometric rings out to 32 km: 167k vertices on high, 42k on low.
  * The terrain shader does concrete joints, scorch, the deluge, roads with lamp pools, ballast and ties, and flood pools.
  * The water plane uses a Reflector-style planar mirror with an oblique near plane.
  * The grass is FIELDWORK's camera-following instanced blades, reading a baked float map.
  * Also built here: the shrub and palm scatter, the pad props, and the distant landmarks with their beacons.
* The opening camera path has a sight corridor. Within it only low palmetto mounds are allowed, and `tests/terrain.test.mjs` traces the line from the camera to the top of the MLP at every half second of the shot.

## The Sun (v2.3, `src/render/sun.js`)

What the Sun looks like from 1 AU depends on how you look:
* **The eye, or any unfiltered camera**: a white disc 0.53° across (5772 K is white, not yellow, outside the atmosphere). It is darker and redder toward the limb, down to about 20% of the centre's brightness at the edge at 550 nm. A few sunspots are visible, and the glare is blinding.
* **The corona**: about a millionth of the disc's brightness, so it only shows when the disc is blocked, during an eclipse or behind a coronagraph. The pink prominences are hydrogen-alpha and He lines from the chromosphere.
* **The swirling and the flares**: these show in extreme-UV, which is why SDO watches at 304, 171, 193 and 131 Å.

The game shows all four of these looks, from real data for one minute: 16 Oct 2015 13:07 UTC.

* **Data**: `tools/sun/fetch_sun.sh` pulls six images from the Helioviewer API, all Sun-centred at 2.54″/px (24″/px for C2): HMI continuum, AIA 304/171/193/131 and LASCO C2. It also pulls the HEK flare list for the day.
* **Bake** (`bake_sun.py`):
  * The HMI limb-darkening profile is divided out, leaving a contrast map. The shader re-applies limb darkening per colour channel.
  * The EUV channels are packed into two RGB WebPs.
  * C2 is flattened by r^2.5, with the occulter filled by radially extending each position angle's 2.45 R value.
  * AIA's field ends at a square of ±1.28 R, so the shader fades the EUV data inside that.
* **Flares**: positions are from SSW Latest Events. SWPC-only events are placed at their active region's last position. Amplitude is set by log GOES flux.
* **Scale**: the true disc is about 10 px across at a 50° field, too small to show anything. The default draws it 10× larger, and the setting offers 4× or true size. The Sun view zooms the field so the disc fills the free part of the screen.

## Audio (src/audio/synth.js)

No samples at all. Every sound is a Web Audio voice: a brown-noise and crackle rocket that loses its highs as the air thins, pyro bangs with structural ring modes, a spring release with inharmonic plate partials, a boom-reel ratchet, a whistler that follows the Eckersley t ∝ 1/√f dispersion law, a type III solar radio burst (falling plasma-frequency drift), chorus chirps, an aurora pad, data sonification of |J|, and a slow generative score.

`tools/audio/analyze.py` renders every voice offline in headless Chromium and reports peak, RMS, crest, clipping, DC, click and band energy, plus a waveform and spectrogram sheet (`tools/audio/waveforms.png`). The first pass caught four problems, all now fixed: the pyro clipped at +3.5 dBFS, the rocket carried −0.05 DC and peaked at −0.2 dBFS, the whistler clicked at its stop, and the whoosh and burn-up sat 20 dB too quiet.

## Layout & controls

Landscape and desktop put the panel on the side. Portrait phones get a bottom sheet that collapses. Short landscape phones get a compact side panel. The layout respects safe-area insets. Controls work by touch (orbit, pinch, double-tap to recentre), mouse, keyboard and gamepad (D-pad focus navigation, sliders nudged left/right).

## Run the checks

```
node tests/orbit.test.mjs && node tests/particles.test.mjs && node tests/physics-misc.test.mjs && node tests/fieldmodel.test.mjs && node tests/terrain.test.mjs && node tests/sun.test.mjs && node tests/data.test.mjs && node tests/static-contract.test.mjs
python3 tools/audio/analyze.py
```
