diff --git a/.gitignore b/.gitignore index 4e1a40a..996b131 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ dist/ *.local .DS_Store .gstack/ + +# Workflow agent worktrees, never part of the project +.claude/worktrees/ diff --git a/README.md b/README.md index 05181eb..d93eeb4 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](tsconfig.json) [![Three.js](https://img.shields.io/badge/Three.js-r185-000000?logo=three.js&logoColor=white)](https://threejs.org) [![Vite](https://img.shields.io/badge/Vite-8-646cff?logo=vite&logoColor=white)](https://vite.dev) -[![Tests](https://img.shields.io/badge/tests-51%20passing-brightgreen)](src/sim/__tests__) +[![Tests](https://img.shields.io/badge/tests-100%20passing-brightgreen)](src/sim/__tests__) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ### [Open it in your browser](https://algometrix.github.io/blackhole-sim/) No install, no account. It needs WebGL2, which every current desktop browser has. -An interactive black hole visualizer that ray-traces **real Schwarzschild photon geodesics on the GPU**, per pixel, every frame. It renders the event horizon shadow, the photon ring, a Doppler-beamed accretion disc bent over and under the hole (the *Interstellar* look), and a gravitationally lensed starfield. You can launch photons and watch their true paths, drop a planet or star and watch it be tidally shredded, and place a second black hole to watch a gravitational-wave inspiral end in a merger, complete with a LIGO-style audio chirp. +An interactive black hole visualizer that ray-traces **real photon geodesics on the GPU**, per pixel, every frame, in Schwarzschild or in the exact Kerr metric of a spinning hole. It renders the event horizon shadow, the photon ring, a Doppler-beamed accretion disc bent over and under the hole (the *Interstellar* look), and a gravitationally lensed starfield. You can launch photons and watch their true paths, drop a planet or star and watch it be tidally shredded, and place a second black hole to watch a gravitational-wave inspiral end in a merger, complete with a LIGO-style audio chirp. Everything runs in the browser. No backend, no textures, no libraries beyond Three.js: the stars, disc, physics, and sound are all procedural. @@ -27,14 +27,18 @@ Everything runs in the browser. No backend, no textures, no libraries beyond Thr - **Real GR light bending**: each pixel integrates the Schwarzschild null-geodesic equation with RK4 and adaptive stepping. The shadow, photon ring, Einstein-ring star smearing, and the disc's secondary images emerge from the math, not from textures. - **Accretion disc**: Shakura–Sunyaev temperature profile, differential-rotation noise that shears into trailing spirals, relativistic Doppler + gravitational redshift using the bent photon direction (correct even for the lensed secondary image). +- **Spin (Kerr)**: turn the hole up to the Thorne limit, $a/M = 0.998$, and the exact Kerr metric is ray traced in Cartesian Kerr-Schild form. Everything follows from the one number: the horizon shrinks to $0.53\,r_s$, frame dragging pulls the approaching edge of the shadow in to $1.06\,r_s$ while the receding edge swings out to $3.50\,r_s$ (the D shape), the photon sphere splits into prograde and retrograde radii, and the disc's inner edge follows the last stable orbit in from $3\,r_s$ to $0.62\,r_s$. Spin 0 reproduces the Schwarzschild image exactly. - **Interactive photon trajectories**: click to launch fans of photons; escaped, captured, and near-critical rays are drawn as glowing curves computed by the same integrator the shader uses. +- **Image order overlay**: colour the disc by how many half turns the light made before it reached you, a diagnostic that separates the direct image, the first lensed image and the photon ring. +- **Relativistic camera**: during a scripted flight the star field aberrates toward the direction of travel and the sky ahead blueshifts and brightens while the sky behind reddens, computed from the physical speed of the trajectory the flight stands for. Exactly off when the camera is at rest. - **Tidal disruption**: place a planet or star. *Cinematic* mode gives a directable inward spiral with spaghettification and a debris stream that feeds and brightens the disc. *Realistic TDE* mode launches a true zero-energy parabolic plunge, one violent shredding at pericenter, and a physically motivated bound/unbound debris split (roughly half the debris escapes, as in real TDEs). +- **Infalling beacon**: drop a probe from rest and watch it freeze. It reddens, dims by nine decades and stalls just outside the shadow, because the coordinate time to reach the horizon diverges and the gap decays as e^(-t/rₛ). Exact Schwarzschild radial free fall, not the pseudo-Newtonian potential the debris uses, since that one has no such divergence. The readout shows both clocks: yours running on forever, and the probe's, which crosses in a finite 28 rₛ/c and notices nothing. - **Black hole merger**: place a second hole. Its orbit decays by the actual Peters (1964) gravitational-wave equations (the trajectory shape is exact; only wall-clock time is compressed). Both holes lens light. At contact the shadow swells to the merged mass minus the radiated gravitational-wave energy, with a ringdown wobble. - **Deep sky**: the background is a procedurally baked HDR cubemap. Five layers of stars on a stellar-temperature colour sequence (the bright ones get diffraction spikes), a warped galactic band with dust lanes that actually extinguish the stars behind them, emission nebulae in hydrogen red and doubly-ionised-oxygen teal, globular clusters that crowd extra stars into their cores, and distant galaxies with dust lanes and spiral arms. All of it is lensed by the same geodesics, so it smears into Einstein arcs near the shadow. - **Feeding outflow**: when a disruption dumps matter on the disc it goes super-Eddington and drives a broad, ragged wind out of the poles. Wide and un-collimated, unlike the jet, it appears and fades with the feeding itself. - **Relativistic jet**: optional twin polar beams with braided filaments, integrated *inside* the raymarch so the jet bends with the light near the hole. The brightness difference between the two cones is the real Doppler boost, δ³. - **Curvature grid** (`G`): Flamm's paraboloid, the exact Schwarzschild embedding diagram, drawn as a wireframe funnel. During an inspiral the binary's quadrupole gravitational wave ripples outward across it: two crests per orbit, wound into a trailing spiral, growing as the pair tightens. -- **Presets**: one-click scenes. A star being devoured, slow spaghettification, a binary merger with waves, bare curved spacetime, a jetted quasar, and a clean wallpaper frame. +- **Presets**: one-click scenes. A star being devoured, slow spaghettification, a binary merger with waves, bare curved spacetime, a jetted quasar, a probe frozen at the horizon, and a clean wallpaper frame. - **Camera flights**: plunge into the horizon, fly past, or circle the hole. - **Cinematic mode** (`H`): fades the panel and readout for a wallpaper-clean frame. - **Procedural audio**: a Perseus-cluster-style deep drone, matter-rush noise when the disc is feeding, a gravitational-wave chirp that tracks the real orbital frequency, and a merger thump + ringdown tone whose pitch falls with the merged mass. @@ -120,7 +124,7 @@ You need two things: **Node.js** (version 20 or newer) and this repository. That ### Other commands ```bash -npm test # run the 51-test physics/simulation suite +npm test # run the 100-test physics/simulation suite npm run typecheck # strict TypeScript check npm run build # production build into dist/ npm run preview # serve the production build @@ -138,8 +142,11 @@ npm run preview # serve the production build | Choose disruption physics | **Place → Disruption mode**: cinematic spiral vs realistic one-pass TDE | | Place a second black hole | **Place → Place black hole** → click; watch the inspiral chirp in the HUD | | Speed up / slow the inspiral | **Simulation → GW time ×** | +| Spin the hole | **Black hole → Spin (a/M)**; the readout reports the horizon, ISCO and both photon-ring radii as you drag | | Launch photons | **Light paths → Enabled**, then click anywhere; rays per launch and spread are sliders | +| See which lensed image is which | **Light rays → Tint image orders** (diagnostic; off by default) | | Camera flights | **Camera** folder: fly in (plunge), fly past, circle; `Esc` stops | +| Relativistic optics on a flight | **Camera → Relativistic view**, with a flight-speed slider next to it | | Sound | **Sound → Enabled** (browsers require one click on the page first) | | Change the sky | **Deep sky** folder: star density, nebulae, galaxies, or reseed the whole thing | | Art-direction knobs | append `?debug=1` to the URL for the hidden tuning folder | @@ -273,16 +280,20 @@ Every noise lookup is rotated by a fixed non-axis-aligned frame, because value n - The full spacetime of two holes requires numerical relativity; this app superposes two Schwarzschild deflections, which is qualitatively right (double shadows, eyebrow images) but not exact in the final strong-field moments. The ringdown "breathing" of the shadow is art-directed shorthand for quasi-normal ringing. - The cinematic disruption mode is deliberately directable (drag-driven inspiral, fixed tidal radii): physics-inspired theater, not a simulation. Realistic mode is the honest one. - Debris particles are occluded by the horizon and approximately deflected, but not fully ray-traced. -- No black hole spin (Schwarzschild, not Kerr) and no light travel-time delay. +- Spin is exact for the light, but not for the matter: debris and bodies stay Paczyński–Wiita (a Schwarzschild pseudo-potential), and only their boundaries (the disc's inner edge, the kill radius) move with the spin. Kerr is a one-hole solution, so placing a second hole forces the spin to 0, which also means a merger leaves a non-spinning remnant where a real equal-mass merger leaves $a/M \approx 0.69$. +- No light travel-time delay: you see the whole disc at one instant rather than each part as it was when its light left. +- The camera's relativistic colour shift is a per-channel RGB gain, not a spectral shift, and the overlay pass (debris, drawn rays, curvature grid) is not aberrated, so occlusion near the shadow edge is slightly wrong during a fast flight. - The disrupted body is drawn as a teardrop, the tidal silhouette, not a hydrodynamic result, and the feeding outflow is likewise a shape and a colour, driven by the disc's feed rate rather than by radiation transport. -- The jet is art direction, not magnetohydrodynamics: a Schwarzschild hole has no Blandford–Znajek spin to tap. Its geometry, filaments and plasma speed are chosen; the Doppler beaming between the two cones and the lensing of the beams are computed. +- The jet is art direction, not magnetohydrodynamics: Blandford–Znajek needs a magnetic field as well as spin, so the jet does not strengthen when you spin the hole up or fade when you spin it down. Its geometry, filaments and plasma speed are chosen; the Doppler beaming between the two cones and the lensing of the beams are computed. - The curvature funnel is an embedding diagram of one spatial slice, the standard picture, not a picture of "gravity pulling down". The wave ripple shows the strain pattern, exaggerated far beyond any real $h \sim 10^{-21}$. - The deep sky is invented. It follows real structure (a luminosity function skewed to faint stars, hotter stars bluer and brighter, dust that reddens and extinguishes, clusters, galaxies) but it is not a star catalogue. ## Architecture ``` -src/physics/ constants + CPU null-geodesic integrator (shared numbers with the shader) +src/physics/ constants, Kerr closed forms, CPU null-geodesic integrator (Schwarzschild + superposition and exact Kerr), observer aberration, image order: + all shared, line for line, with the shader src/sim/ pure simulation core: PW gravity, tidal phase machine, debris pool, Peters binary inspiral, quadrupole wave state, zero WebGL/DOM, fully unit-tested (vitest) @@ -300,6 +311,9 @@ The renderer's key trick: everything that must be *truly* lensed (sky, disc, the - J.-P. Luminet, *Image of a spherical black hole with thin accretion disk*, A&A 75, 228 (1979), the first computed image of what this app draws. - O. James, E. von Tunzelmann, P. Franklin, K. S. Thorne, *Gravitational lensing by spinning black holes in astrophysics, and in the movie Interstellar*, Class. Quantum Grav. 32, 065001 (2015). +- R. P. Kerr, *Gravitational field of a spinning mass as an example of algebraically special metrics*, Phys. Rev. Lett. 11, 237 (1963), the spinning solution and its Kerr-Schild form. +- J. M. Bardeen, W. H. Press, S. A. Teukolsky, *Rotating black holes: locally nonrotating frames, energy extraction, and scalar synchrotron radiation*, ApJ 178, 347 (1972), the Kerr ISCO and photon orbits. +- K. S. Thorne, *Disk-accretion onto a black hole. II*, ApJ 191, 507 (1974), the a/M = 0.998 accretion limit. - P. C. Peters, *Gravitational Radiation and the Motion of Two Point Masses*, Phys. Rev. 136, B1224 (1964), the inspiral equations. - N. I. Shakura, R. A. Sunyaev, *Black holes in binary systems. Observational appearance*, A&A 24, 337 (1973), the disc temperature profile. - B. Paczyński, P. J. Wiita, *Thick accretion disks and supercritical luminosities*, A&A 88, 23 (1980), the pseudo-Newtonian potential. diff --git a/docs/THEORY.md b/docs/THEORY.md index aa35bcd..003e6e8 100644 --- a/docs/THEORY.md +++ b/docs/THEORY.md @@ -26,13 +26,15 @@ the summary, jump to [the table at the end](#every-equation-and-where-it-lives). 7. [The last stable orbit](#7-the-last-stable-orbit) 8. [Why the disc glows, and how hot](#8-why-the-disc-glows-and-how-hot) 9. [Why one side of the disc is brighter](#9-why-one-side-of-the-disc-is-brighter) -10. [Tides, and why they tear stars apart](#10-tides-and-why-they-tear-stars-apart) -11. [Why the debris becomes a stream](#11-why-the-debris-becomes-a-stream) -12. [Gravitational waves and the chirp](#12-gravitational-waves-and-the-chirp) -13. [The funnel: what curved space actually means](#13-the-funnel-what-curved-space-actually-means) -14. [Jets, beaming, and the outflow](#14-jets-beaming-and-the-outflow) -15. [What we cheat on](#15-what-we-cheat-on) -16. [Every equation and where it lives](#every-equation-and-where-it-lives) +11. [Tides, and why they tear stars apart](#11-tides-and-why-they-tear-stars-apart) +12. [Why the debris becomes a stream](#12-why-the-debris-becomes-a-stream) +13. [Gravitational waves and the chirp](#13-gravitational-waves-and-the-chirp) +14. [The funnel: what curved space actually means](#14-the-funnel-what-curved-space-actually-means) +15. [Jets, beaming, and the outflow](#15-jets-beaming-and-the-outflow) +16. [Flying fast: what the sky does](#16-flying-fast-what-the-sky-does) +17. [Spin, and what it drags](#17-spin-and-what-it-drags) +18. [What we cheat on](#18-what-we-cheat-on) +18. [Every equation and where it lives](#every-equation-and-where-it-lives) --- @@ -244,7 +246,35 @@ Just outside that edge sits the **photon ring**, the bright thin circle in every image, made of light that looped one or more times before escaping to your eye. -> **In the code:** `B_CRIT` and `R_PHOTON` in `src/physics/constants.ts`. +### Which loop am I looking at? + +The ring is not one image, it is a stack of them, and you can label them. Let +$\Phi$ be the total angle the ray sweeps about the hole on its way to you: + +$$\Phi = \int \frac{\lvert \mathbf{x} \times d\mathbf{x} \rvert}{r^2}, \qquad n = \left\lfloor \Phi / \pi \right\rfloor$$ + +$n = 0$ is the **direct** image, what you would see if light went straight. +$n = 1$ is light that came round the far side once. $n \ge 2$ is the photon +ring proper. + +Why $\pi$? For a camera sitting at elevation $\varepsilon$ above the disc +plane, the direct image of the disc spans $\Phi$ from $\varepsilon$ to +$\pi - \varepsilon$, and the first-order image spans $\pi + \varepsilon$ to +$2\pi - \varepsilon$. The boundaries at multiples of $\pi$ therefore sit in the +middle of a gap of width $2\varepsilon$. The classification margin is the +camera's own elevation, which is why an exactly edge-on view is the one place +the labels genuinely blur: there the two images of the far edge really do +merge. + +Each successive order is squeezed into a band roughly $e^{-\pi}$ times thinner +than the last. Measured on this app at its default framing, the $n = 1$ band is +about 7 pixels wide across a 1920-pixel frame and the $n = 2$ band is about a +quarter of a pixel. That is not a rendering failure, it is the reason the +photon ring reads as a hard edge rather than as a set of rings. + +> **In the code:** `B_CRIT` and `R_PHOTON` in `src/physics/constants.ts`; +> `sweptAngle` and `imageOrder` in `src/physics/imageOrder.ts`, mirrored in +> `shaders/geodesic.frag`, and switched on by Light rays -> Tint image orders. --- @@ -374,7 +404,123 @@ Two details this app gets right that are easy to get wrong: --- -## 10. Tides, and why they tear stars apart +## 10. What a distant observer sees fall in + +Drop something into a black hole and watch it go. It does not vanish. It slows, +it reddens, it dims, and then it stops, hanging just outside the shadow, fading +until there is nothing left to see. It never crosses. + +That is true. It is also only true for **you**. The thing you dropped crossed +the horizon a short while later by its own watch and felt nothing in +particular. Both statements are correct, and this part works out each of them +from the same starting line, because the gap between them is the clearest +thing a black hole does to time. + +Take the simplest possible case: a probe released **from rest** at radius +$r_0$, falling straight in, no sideways motion at all. + +**One conserved number.** Anything in free fall carries an energy per unit mass +that never changes along its path. For a probe let go from rest at $r_0$ it is +just the metric's clock factor at the radius it was released from: + +$$E = \sqrt{1 - \frac{r_s}{r_0}}$$ + +$E = 1$ for a fall that started infinitely far away, and it is smaller the +closer in you let go. Everything below is written in terms of $E$ and the same +factor evaluated at the probe's current radius, + +$$f \equiv 1 - \frac{r_s}{r}$$ + +**How fast it is going.** Put an observer at radius $r$ hovering on a rocket, +holding station, and let the probe fly past. The speed that observer measures +is + +$$v = \frac{\sqrt{E^2 - f}}{E}$$ + +At $r = r_0$ this is zero, which is the release condition. As $r \to r_s$, +$f \to 0$ and $v \to 1$: the probe passes a hovering observer *at the speed of +light*. That is the horizon's real definition. It is not a wall and there is +nothing there to hit; it is the surface where hovering stops being possible, +because holding station would take a rocket faster than light. + +**Why it reddens.** Two effects stack, and you have met both. The light has to +climb out of the well, which costs it energy: that is the $\sqrt{f}$ +gravitational factor from Part 9. And the source is running away from you at +speed $v$, which is the ordinary Doppler shift of recession. Multiply them: + +$$g = \underbrace{\sqrt{f}}_{\text{climbing out}} \times \underbrace{\frac{\sqrt{1 - v^2}}{1 + v}}_{\text{running away}} = \frac{f}{E + \sqrt{E^2 - f}}$$ + +The two expressions are the same number; substitute $v$ into the left one and +the square roots cancel. The right-hand form is worth keeping because it makes +the endgame obvious. Near the horizon $f \to 0$ while $\sqrt{E^2 - f} \to E$, +so the denominator settles at $2E$ and + +$$g \to \frac{r - r_s}{2\,E\,r_s}$$ + +**the redshift is proportional to the gap.** Brightness goes as $g^3$ (Part 9 +again), so every factor of ten the probe closes on the horizon costs it a +factor of a thousand in brightness. Between release at $7\,r_s$ and a gap of +$10^{-3}\,r_s$ it fades by nine decades. This is why nobody has ever +photographed something falling in: you have a few seconds of it, and then it is +gone, whatever your exposure. + +**Why it stalls.** Coordinate time $t$ is the time on your clock, far away. +Dividing the probe's radial motion by the rate its clock runs relative to yours +gives + +$$\frac{dr}{dt} = -f\,v = -\frac{f\sqrt{E^2 - f}}{E}$$ + +Both factors of $f$ matter here and they do different jobs, but near the +horizon the second one has already saturated ($v \to 1$) while the first is +still collapsing. Write the gap as $\varepsilon = r - r_s$, note that +$f = \varepsilon/(r_s + \varepsilon) \to \varepsilon/r_s$, and you get + +$$\frac{d\varepsilon}{dt} \to -\frac{\varepsilon}{r_s} \quad\Longrightarrow\quad \varepsilon(t) \propto e^{-t/r_s}$$ + +An exponential decay never reaches zero. The gap halves every $0.69\,r_s/c$ of +your time, forever: the probe is always still outside, and the coordinate time +to cross the horizon is infinite. That is the freeze, and notice that nothing +was imposed to get it. It fell out of one line of algebra. + +**On the probe's own clock.** Now ask the probe. Its own elapsed time obeys +$d\tau = dr/\sqrt{E^2 - f}$, which integrates to a cycloid, the same curve a +point on a rolling wheel traces: + +$$\tau = \frac{1}{2}\sqrt{\frac{r_0^3}{r_s}}\,\left(\eta + \sin\eta\right), \qquad \cos\eta = \frac{2r}{r_0} - 1$$ + +At the horizon $\eta$ is a perfectly ordinary angle and $\tau$ is a perfectly +ordinary number. From a release at $7\,r_s$ the crossing happens at +$\tau = 28.4\,r_s/c$, and for a hole of a million suns that is about half a +minute. The probe sails through, measures no jolt, sees no wall, and has some +time left before the tides get serious. + +So: infinite on your clock, half a minute on its. Neither is an illusion and +neither is the "real" answer. They are answers to two different questions. + +> **In the code.** `src/sim/beacon.ts`, which carries all of the above as pure +> functions, and is the one moving thing in this app on exact Schwarzschild +> motion rather than the Paczyński-Wiita stand-in everything else uses. That is +> not fussiness: PW has no coordinate-time divergence at all, and PW matter +> crosses $r_s$ in finite time, so reusing it here would have made the effect +> impossible to show. The state it integrates is the gap $r - r_s$ rather than +> $r$, because an exponential decay stored as a difference of two order-one +> numbers stops being physics and starts being rounding at $10^{-16}$. + +> **Honest note: where the image sits.** A probe that close to the hole is not +> seen where it is. Light leaving it is bent so hard that its image piles up on +> the photon ring, and the app draws it at the apparent radius +> $b = r/\sqrt{1 - r_s/r}$, floored at $b_{\text{crit}}$ (Part 6), which is +> exactly the impact parameter of the ray that leaves sideways and just +> escapes. That is right for an emitter seen edge-on with the ray at its +> turning point, and it is applied in every viewing geometry as a first-order +> stand-in: the true image position needs the deflection integral solved for +> the observer's own direction. The probe is also drawn at a fixed size on +> screen, because a real one would be far under a pixel. What the fall changes +> is its colour and its brightness, and those are computed. + +--- + +## 11. Tides, and why they tear stars apart A star near a black hole, with a strong pull arrow on the near side and a weaker one on the far side @@ -420,7 +566,7 @@ supermassive case. --- -## 11. Why the debris becomes a stream +## 12. Why the debris becomes a stream A stretched star splitting into a bound half that falls back and an unbound half that escapes @@ -445,6 +591,35 @@ around the hole. That returning ribbon is the stream you see. The rate at which it comes back follows a famous power law, $\dot M \propto t^{-5/3}$ (Rees, 1988), which is how real tidal disruption flares are identified. +**Where the $-5/3$ comes from.** It falls out of Kepler and nothing else. Take +the energy spread to be flat across the star, so equal masses of debris landed +in equal slices of binding energy: + +$$\frac{dM}{d|\varepsilon|} = \text{constant}$$ + +A bound fragment with binding energy $|\varepsilon|$ is on an ellipse whose size +is fixed by that energy alone, $a = GM / (2|\varepsilon|)$, and Part 2's orbit +equation gives the time it takes to come back: + +$$T = 2\pi\sqrt{\frac{a^3}{GM}} = 2\pi\, GM\, (2|\varepsilon|)^{-3/2}$$ + +Tightly bound debris returns quickly, barely bound debris takes almost forever. +Now turn the question round: at time $t$ after the disruption, *which* fragments +are arriving? The ones whose period is $t$: + +$$|\varepsilon|(t) = \tfrac{1}{2}\left(\frac{2\pi GM}{t}\right)^{2/3}$$ + +The mass arriving per second is the mass sitting in each slice of energy times +how fast that energy window sweeps downward: + +$$\frac{dM}{dt} = \frac{dM}{d|\varepsilon|}\cdot\left|\frac{d|\varepsilon|}{dt}\right| += \frac{dM}{d|\varepsilon|}\cdot\frac{1}{3}\,(2\pi GM)^{2/3}\, t^{-5/3}$$ + +$$\boxed{\dot M \propto t^{-5/3}}$$ + +Everything about the star cancels except the constant out front. That is why the +exponent, and not the brightness, is the thing surveys look for. + **The trap.** Energy is not the only thing that matters: angular momentum decides how close the debris passes on its return. Give a particle a kick along its direction of travel and you change its energy *and* strip its angular @@ -457,13 +632,48 @@ particle onto the star's own orbit at its own radius rather than copying the star's velocity vector. Both decisions were arrived at by watching the stream fail without them. +**Watching for the law, and not finding it.** The app can plot its own version +of that curve: turn on *Light curve* and it records how hard the disruption is +feeding the disc against time since the star came apart, on log axes, with the +$t^{-5/3}$ law drawn through the peak for comparison. + +The two do not agree, and the overlay is built to show that rather than hide it. +A realistic-mode star at the shipped settings decays like $t^{-7.3}$, four times +steeper than the law. Three reasons, all of them ours and none of them nature's: + +- **What is plotted is not the fallback rate.** It is the disc's feeding glow, + which is the absorbed-debris rate smeared by an exponential decay time + (`DISC_TUNING.boostDecayTau`). Once the last particle is swallowed the curve is + that decay and nothing else, and an exponential on log axes gets steeper + without limit. +- **The debris is not left alone to return.** A small drag circularises it on a + fixed timescale, so the whole bound half is eaten within about a factor of two + in time instead of spreading over the decades a real energy distribution + covers. A hard age limit kills the longest-period debris, which is exactly the + material that would have made the late tail. +- **The clock is compressed.** The feeding glow decays on the simulation clock + while the chart is drawn on the disruption clock, so the *Disruption speed* + slider changes the fitted slope: about $-12.5$ at compression 4, $-7.3$ at 8, + $-3.3$ at 30. A measurement of nature would not care where that slider is. + +Cinematic mode fits about $-1.85$, which looks like a match and is not one: that +mode is a drag-driven spiral with no energy spread at all, so it has no fallback +to obey, and its number moves with the same slider. The chart therefore prints +the fitted index next to the law's and anchors the reference line at the +recorded peak instead of fitting its height, so the gap is always on screen. + > **In the code:** `spawnFromBody` in `src/sim/debris.ts`, the orbit > reconstruction in `src/sim/orbit.ts`, and the tests in > `src/sim/__tests__/stream.test.ts` that hold the resulting shape in place. +> The light curve itself is `src/ui/lightCurve.ts` (the recorder and the +> log-log projection) and `src/ui/lightCurveChart.ts` (the canvas), with +> `src/ui/__tests__/lightCurve.test.ts` and +> `src/ui/__tests__/fallbackLaw.test.ts`, the second of which drives real +> disruptions and pins every number quoted above. --- -## 12. Gravitational waves and the chirp +## 13. Gravitational waves and the chirp Two orbiting holes with ripples wound into a spiral, two crests per orbit @@ -507,7 +717,7 @@ travel outward. --- -## 13. The funnel: what curved space actually means +## 14. The funnel: what curved space actually means A funnel-shaped surface narrowing to a throat, with rings and radial lines @@ -555,11 +765,11 @@ bending of *time* at least as much as space, and none of that is in the picture. It is an honest visualisation of one true thing, not of everything. > **In the code:** `embeddingDepth` in `src/render/spacetimeGrid.ts`, with the -> wave from Part 12 added on top. +> wave from Part 13 added on top. --- -## 14. Jets, beaming, and the outflow +## 15. Jets, beaming, and the outflow Two very different things come out of the poles, and this app draws both. @@ -593,20 +803,190 @@ away with it. --- -## 15. What we cheat on +## 16. Flying fast: what the sky does + +Everything so far assumed you were sitting still. Take the camera on one of the +scripted flights and two things happen at once, and they are the same thing +seen twice. + +**Aberration.** Directions are not absolute. A photon arriving from a given +direction in the static frame arrives from a *different* direction as far as a +moving observer is concerned, and the difference is not small at any decent +speed. Take the pixel's look direction $\hat{\ell}$ in the camera's own rest +frame, and the camera's velocity $\boldsymbol\beta$ (in units of $c$) in the +static frame. Relativistic velocity addition, applied to the photon's +propagation direction $-\hat{\ell}$ and negated back into a look direction, +gives the static-frame direction to march along: + +$$\hat{n} = \frac{\hat{\ell}/\gamma - \boldsymbol\beta + \dfrac{\gamma}{\gamma+1}(\hat{\ell}\cdot\boldsymbol\beta)\,\boldsymbol\beta}{1 - \hat{\ell}\cdot\boldsymbol\beta}$$ + +Read it the way the picture works: every static direction is dragged *backward* +against the motion, so the whole sky bunches toward where you are going. Run +fast enough and the stars behind you crowd into a ring and the stars ahead +spread out into a bowl. This is the headlight effect, and it is exactly the +reason a relativistic jet looks like one beam and not two. + +**Doppler.** The same transformation gives the frequency ratio of the received +photon: + +$$D = \gamma\left(1 + \boldsymbol\beta\cdot\hat{n}\right) = \frac{1}{\gamma\left(1 - \hat{\ell}\cdot\boldsymbol\beta\right)}$$ + +Looking straight ahead gives $\gamma(1+\beta)$, straight behind $\gamma(1-\beta)$, +and looking at right angles *in the camera frame* gives exactly $1/\gamma$, the +pure transverse redshift. Bolometrically the received brightness scales as +$D^4$; the app uses the same exponent 3 the disc and jet already use, for +continuity rather than for correctness. + +**Why the speed is not the derivative of the camera path.** The flights are +played back on a compressed clock. They move about $3\,r_s$ per wall-clock +second, which in units where $c = 1$ is several times light speed, and the +fly-in's recovery teleport would make it infinite. Differentiating the path +would therefore be nonsense. What the app uses instead is the physical speed of +the trajectory each move stands for: + +- `circle` uses the circular-orbit speed $\sqrt{M/(r-2M)}$ from Part 9, the + same expression the disc's own gas uses. About $0.33c$ at the shipped + framing. +- `flyby` and `flyin` use the free-fall-from-infinity speed $\sqrt{r_s/r}$, + which is Part 3's escape speed run backwards. About $0.41c$ at the closest + point of a pass, and clamped at $0.95c$ near the end of a plunge. + +Both are derived and both are already in this document, which an art-directed +"tour clock" divisor would not be. + +At rest all of this is identically the identity: $\gamma = 1$, +$\hat{\ell}\cdot\boldsymbol\beta = 0$, $\hat{n} = \hat{\ell}$, $D = 1$. Mouse +orbiting repositions the camera rather than flying it, so it never sees any of +this. + +> **In the code:** `aberrateLookDirection` in `src/physics/relativity.ts` and +> `aberratedRay` in `shaders/geodesic.frag`; the speeds in +> `src/render/cameraTour.ts`. + +--- + +## 17. Spin, and what it drags + +Every real black hole spins. Schwarzschild is the special case where it does +not, and once you let it turn, the picture changes in four visible ways that +all follow from one number. + +That number is the **spin parameter** $a = J/M$, an angular momentum per unit +mass, which has units of length. It is usually quoted dimensionless as $a/M$, +running from 0 (still) to 1 (as fast as a black hole can turn at all). + +**The metric that survives the horizon.** Kerr's solution is usually written in +Boyer-Lindquist coordinates, which are elegant and blow up at the horizon: +exactly where the interesting rays go. This app uses the Cartesian +**Kerr-Schild** form instead, which is flat space plus a rank-one correction: + +$$g_{\mu\nu} = \eta_{\mu\nu} + f\, l_\mu l_\nu, \qquad f = \frac{2Mr^3}{r^4 + a^2z^2}$$ + +with $l_\mu$ a null vector (null with respect to both $\eta$ and $g$) and $r$ +defined implicitly by + +$$\frac{x^2+y^2}{r^2+a^2} + \frac{z^2}{r^2} = 1$$ + +so surfaces of constant $r$ are squashed spheroids rather than spheres. Nothing +here is singular at the horizon, and the coordinates are already Cartesian, so +the raymarcher's plane-crossing test and body intersection do not change at +all. Setting $a = 0$ gives $r = |\mathbf{x}|$, $f = r_s/r$, and Schwarzschild +back. + +**Ray tracing it.** Rather than deriving Christoffel symbols, the app integrates +the Hamiltonian. With $g^{\mu\nu} = \eta^{\mu\nu} - f\,l^\mu l^\nu$ and the +photon's energy normalized so $p_t = -1$: + +$$H = \tfrac{1}{2}\left(\mathbf{p}\cdot\mathbf{p} - 1 - f\kappa^2\right), \qquad \kappa = 1 + \mathbf{k}\cdot\mathbf{p}$$ + +$$\frac{d\mathbf{x}}{d\lambda} = \frac{\partial H}{\partial \mathbf{p}} = \mathbf{p} - f\kappa\,\mathbf{k}, \qquad \frac{d\mathbf{p}}{d\lambda} = -\frac{\partial H}{\partial \mathbf{x}} = \tfrac{1}{2}\kappa^2 \nabla f + f\kappa\,(\mathbf{p}\cdot\nabla\mathbf{k})$$ + +$H$ is zero for a null ray and stays zero, and the angular momentum about the +spin axis stays put too. Both are watched by the tests, which is how a wrong +term in $\nabla f$ or $\nabla\mathbf{k}$ gets caught instead of quietly drawing +a slightly wrong picture. + +**Consequence 1: the horizon shrinks.** Solving $\Delta = r^2 - 2Mr + a^2 = 0$: + +$$r_\pm = M \pm \sqrt{M^2 - a^2}$$ + +At $a = 0$ the outer root is $2M = r_s$, the familiar horizon. At $a/M = 0.998$ +it is $0.53\,r_s$: a maximally spinning hole is half the size of a still one of +the same mass. + +**Consequence 2: the shadow goes lopsided.** The photon sphere splits in two. +Light going around the way the hole turns (prograde) can hold on closer in; +light going the other way is pushed out: + +$$r_{\text{ph}} = 2M\left[1 + \cos\left(\tfrac{2}{3}\arccos\left(\mp a/M\right)\right)\right]$$ + +with the upper sign prograde. At $a/M = 0.998$ that is $0.54\,r_s$ prograde and +$2.00\,r_s$ retrograde, against $1.5\,r_s$ for both at zero spin. Evaluating the +critical impact parameter at each, + +$$b_{\text{crit}} = a \pm \frac{2 r_{\text{ph}}\sqrt{\Delta(r_{\text{ph}})}}{r_{\text{ph}} - M}$$ + +gives $+1.06\,r_s$ on the approaching side and $-3.50\,r_s$ on the receding one. +Those are the two edges of the shadow, and they are no longer the same distance +from the centre. That is the flat side of the famous D shape: it is not the +disc doing it, it is the spacetime. + +**Consequence 3: the disc's inner edge follows.** The last stable circular orbit +moves in as well (Bardeen, Press and Teukolsky 1972): + +$$r_{\text{ISCO}} = M\left[3 + Z_2 \mp \sqrt{(3-Z_1)(3+Z_1+2Z_2)}\right]$$ + +$$Z_1 = 1 + \sqrt[3]{1-(a/M)^2}\left(\sqrt[3]{1+a/M} + \sqrt[3]{1-a/M}\right), \qquad Z_2 = \sqrt{3(a/M)^2 + Z_1^2}$$ + +From $6M = 3\,r_s$ at zero spin down to $M = 0.5\,r_s$ at the extreme, and +$0.62\,r_s$ at the shipped limit. The disc is allowed to reach further in, where +it is hotter and moving faster, which is why a spinning hole's inner disc is so +much brighter. The retrograde branch goes the other way, out to $9M$. + +**Consequence 4: frame dragging.** Near a spinning hole, "standing still" is not +an option: the geometry itself rotates, and even a ray fired straight at the +centre with zero angular momentum acquires an azimuth as it falls. The disc +feels it too, and its orbital speed and redshift pick up the spin: + +$$\Omega = \frac{\sqrt{M}}{r^{3/2} + a\sqrt{M}}, \qquad \frac{1}{u^t} = \frac{r^{3/4}\sqrt{r^{3/2} - 3M\sqrt{r} + 2a\sqrt{M}}}{r^{3/2} + a\sqrt{M}}$$ + +both of which collapse to Part 8's and Part 9's Schwarzschild expressions when +$a$ is zero. + +**Why the slider stops at 0.998.** This is the **Thorne limit**, and it is +physics rather than a fudge. A hole fed by a thin disc swallows photons from +the inner disc that carry, on average, negative angular momentum, and they spin +it back down as fast as the accreting gas spins it up. Accretion therefore +saturates just short of extremal. Conveniently it is also where the closed forms +above stay well conditioned: at $a/M = 1$ exactly, $r_{\text{ph}} - M$ goes to +zero in the prograde $b_{\text{crit}}$. + +> **In the code:** `src/physics/kerr.ts` for the closed forms, the Kerr block of +> `src/physics/geodesic.ts` and `shaders/kerr.glsl` for the integrator. + +--- + +## 18. What we cheat on A physics document that only lists what it gets right is advertising. Here is the other column. | Cheat | Why | What would fix it | |---|---|---| -| **No spin.** Schwarzschild, not Kerr. | Kerr geodesics are a rewrite of the core integrator. | Real holes spin, which drags space around with them, skews the shadow, and moves the ISCO in to $1.24\,r_s$. | | **Two holes are superposed, not solved.** | The real two-body problem in GR needs supercomputers. | Numerical relativity. Our double shadows and eyebrow images are qualitatively right, quantitatively not. | -| **Matter is pseudo-Newtonian.** | Full geodesic motion for 48,000 particles is too slow. | Paczyński-Wiita gets the ISCO exactly right, which is the part that matters visually. | +| **A second hole forces the spin to 0.** | Kerr is a one-hole solution and superposing two of them is not one. | It also means a merger leaves a non-spinning remnant, which is wrong: an equal-mass merger leaves $a/M \approx 0.69$. | +| **Matter is Paczyński-Wiita even when the hole spins.** | Full Kerr geodesic motion for 48,000 particles is too slow. | Only the *boundaries* move with the spin: the disc's inner edge and the kill radius come from the Kerr closed forms, the forces do not. | +| **The disc's emission angle is a coordinate direction.** | A local orthonormal tetrad at every crossing costs another frame basis per hit. | It is the approximation the app always had; at $a/M = 0.998$ the inner disc sits at $0.62\,r_s$ where it is worse. Keeping it is what makes $a = 0$ continuous. | +| **The curvature grid is Flamm's Schwarzschild embedding.** | There is no equally clean embedding diagram for Kerr. | The funnel does not change shape when you spin the hole up, though everything else does. | +| **The jet is drawn, not launched.** | Blandford-Znajek needs magnetohydrodynamics as well as spin. | So the jet does not fade as the spin drops, which is exactly what a real Blandford-Znajek jet would do. The beaming, at least, is computed and not painted. | +| **Image order is measured about the primary.** | With a secondary in the scene the ray no longer stays in one plane. | The overlay stays qualitatively useful there and no more than that. | +| **The camera's colour shift is an RGB gain.** | There are no spectra in this renderer, only three channels. | Real Doppler moves light in and out of the visible band and changes which stars are seen at all. Exactly neutral at rest. | +| **The overlay pass is not aberrated.** | Debris, rays and the grid are drawn by ordinary projection in pass 2. | During a fast flight the raymarched shadow has moved and that geometry has not, so occlusion near the shadow edge is slightly wrong. | | **The disc is infinitely thin.** | A volumetric disc multiplies the per-pixel cost. | Raymarched volume with real optical depth. | -| **The jet is drawn, not launched.** | Blandford-Znajek needs spin, which we do not have, plus magnetohydrodynamics. | GRMHD simulation data. The beaming, at least, is computed and not painted. | -| **Two clocks run fast.** | A disruption takes days, an inspiral from $8\,r_s$ takes about 1600 time units. | Nothing: the trajectories are exact, only the clock is compressed, and the app says so in the interface. | -| **No light travel-time delay.** | You see the whole disc at one instant rather than each part as it was when its light left. | Track photon arrival times through the march. | +| **Three clocks run fast.** | A disruption takes days, an inspiral from $8\,r_s$ takes about 1600 time units, and a probe dropped from $7\,r_s$ takes forty seconds to stop being visible. | Nothing: the trajectories are exact, only the clock is compressed, and the app says so in the interface. | +| **No light travel-time delay.** | You see the whole disc at one instant rather than each part as it was when its light left. The infalling probe is drawn at its current coordinate position too, with propagation treated as instantaneous: that changes the time constant of the freeze, not the fact of it. | Track photon arrival times through the march. | +| **The light curve is not the fallback law.** | We plot the disc's feeding glow, which is the absorbed-debris rate smeared by a decay time, fed by debris that a drag term circularises on a fixed timescale. | Leave the debris on its own orbits and plot the arrival rate directly. As it stands the decay is far steeper than $t^{-5/3}$ and its index moves with the disruption clock, from about $-12$ at the slowest setting to about $-3$ at the fastest, so the app draws the law next to the curve, prints the live fit, and says which is which. | + | **The sky is invented.** | It follows real structure (luminosity function, dust extinction, clustering) but it is not a star catalogue. | A real survey texture, at the cost of every image looking identical. | --- @@ -623,6 +1003,15 @@ the other column. | Photon sphere | $r = 1.5\,r_s$ | `physics/constants.ts` | | Shadow radius | $b_{\text{crit}} = 3\sqrt{3}M \approx 2.6\,r_s$ | `physics/constants.ts` | | ISCO | $r = 3\,r_s$ | `physics/constants.ts` | +| Image order | $n = \lfloor \Phi/\pi \rfloor$, $\Phi = \int \lvert \mathbf{x}\times d\mathbf{x}\rvert / r^2$ | `physics/imageOrder.ts`, `shaders/geodesic.frag` | +| Aberration | $\hat{n} = (\hat{\ell}/\gamma - \boldsymbol\beta + \tfrac{\gamma}{\gamma+1}(\hat{\ell}\cdot\boldsymbol\beta)\boldsymbol\beta)/(1-\hat{\ell}\cdot\boldsymbol\beta)$ | `physics/relativity.ts`, `shaders/geodesic.frag` | +| Observer Doppler | $D = \gamma(1 + \boldsymbol\beta\cdot\hat{n})$ | `physics/relativity.ts`, `shaders/geodesic.frag` | +| Kerr-Schild metric | $g_{\mu\nu} = \eta_{\mu\nu} + f\,l_\mu l_\nu$, $f = 2Mr^3/(r^4+a^2z^2)$ | `shaders/kerr.glsl`, `physics/geodesic.ts` | +| Kerr ray tracing | $\dot{\mathbf{x}} = \mathbf{p} - f\kappa\mathbf{k}$, $\dot{\mathbf{p}} = \tfrac12\kappa^2\nabla f + f\kappa(\mathbf{p}\cdot\nabla\mathbf{k})$ | `shaders/kerr.glsl`, `physics/geodesic.ts` | +| Kerr horizon | $r_\pm = M \pm \sqrt{M^2 - a^2}$ | `physics/kerr.ts` | +| Kerr photon orbits | $r_{\text{ph}} = 2M[1 + \cos(\tfrac23\arccos(\mp a/M))]$ | `physics/kerr.ts` | +| Kerr ISCO | $r = M[3 + Z_2 \mp \sqrt{(3-Z_1)(3+Z_1+2Z_2)}]$ | `physics/kerr.ts` | +| Kerr disc kinematics | $\Omega = \sqrt{M}/(r^{3/2}+a\sqrt{M})$ | `physics/kerr.ts`, `shaders/kerr.glsl` | | Paczyński-Wiita potential | $\Phi = -GM/(r - r_s)$ | `sim/gravity.ts` | | PW circular speed | $v = \sqrt{GMr}/(r - r_s)$ | `sim/gravity.ts` | | Specific energy | $\varepsilon = v^2/2 - GM/(r-r_s)$ | `sim/orbit.ts`, `sim/debris.ts` | @@ -630,11 +1019,13 @@ the other column. | Doppler + gravity shift | $g = \sqrt{1 - 3M/r}\,/\,\gamma(1 - \beta\cos\alpha)$ | `shaders/geodesic.frag` | | Tidal radius | $r_T \approx R_\star (M/m_\star)^{1/3}$ | `config.ts` | | Tidal energy spread | $\Delta\varepsilon \approx GMR_\star/r_p^2$ | `sim/debris.ts` | +| Debris return period | $T = 2\pi GM(2\lvert\varepsilon\rvert)^{-3/2}$ | Part 11 | +| Fallback rate | $\dot M \propto t^{-5/3}$ | `ui/lightCurve.ts` (drawn for comparison) | | Peters inspiral | $da/dt = -\tfrac{64}{5} G^3 m_1m_2(m_1{+}m_2)/c^5a^3$ | `sim/binary.ts` | | Radiated mass | $E_{\text{rad}} \approx 0.048 M_{\text{tot}}\,\eta/0.25$ | `sim/binary.ts` | | Flamm's paraboloid | $z = 2\sqrt{r_s(r - r_s)}$ | `render/spacetimeGrid.ts` | | Relativistic beaming | $\delta = 1/\gamma(1 - \beta\cos\theta)$, $I \propto \delta^3$ | `shaders/geodesic.frag` | -| Eddington luminosity | $L = 4\pi GMm_pc/\sigma_T$ | Part 14, motivates the outflow | +| Eddington luminosity | $L = 4\pi GMm_pc/\sigma_T$ | Part 15, motivates the outflow | --- @@ -647,5 +1038,9 @@ the other column. paper. The full version of Parts 5 to 9, written by the people who did it for the film. - **Rees (1988)** on tidal disruption, for Parts 10 and 11. -- **Peters (1964)** for Part 12, four pages that predicted a sound nobody heard +- **Peters (1964)** for Part 13, four pages that predicted a sound nobody heard for fifty-one years. +- **Bardeen, Press and Teukolsky (1972)**, *Rotating black holes*, for Part 17. + The ISCO and photon-orbit formulas this app evaluates are theirs. +- **Thorne (1974)** for the 0.998 limit: why a hole fed by a disc cannot quite + reach extremal. diff --git a/index.html b/index.html index be72042..b894454 100644 --- a/index.html +++ b/index.html @@ -48,12 +48,39 @@ pointer-events: none; white-space: pre; } + /* The tidal-disruption light curve, drawn by src/ui/lightCurveChart.ts. + Visibility is a class, not the hidden attribute: `hidden` is only a + display:none at the lowest specificity, and any display: declaration + in this rule would silently defeat it. */ + #light-curve { + display: none; + position: fixed; + right: calc(12px + env(safe-area-inset-right)); + bottom: calc(12px + env(safe-area-inset-bottom)); + width: 320px; + height: 200px; + /* Under the panel, and never a pointer target: a drag that starts on + the chart is still a camera move. */ + z-index: 5; + pointer-events: none; + } + #light-curve.visible { + display: block; + } + #light-curve canvas { + display: block; + width: 100%; + height: 100%; + border-radius: 6px; + } /* Cinematic mode: fade the chrome, leave the render untouched. */ #hud, + #light-curve, .lil-gui { transition: opacity 260ms ease; } body.cinematic #hud, + body.cinematic #light-curve, body.cinematic .lil-gui { opacity: 0; pointer-events: none; @@ -176,11 +203,23 @@ bottom: auto; top: calc(76px + env(safe-area-inset-top)); } + /* The readout and the toggle own the top of a phone screen, so the chart + goes bottom left, clear of the collapsed sheet title (40px) and of the + toggle on the right. Opening the sheet covers it, which is the same + deal every other overlay gets. */ + body.touch-ui #light-curve { + right: auto; + left: calc(8px + env(safe-area-inset-left)); + bottom: calc(48px + env(safe-area-inset-bottom)); + width: min(56vw, 260px); + height: 150px; + }
+