From f2e42f2e9fff67ba60a291cfad04df7100a0cecc Mon Sep 17 00:00:00 2001 From: Ashish Nagar Date: Fri, 31 Jul 2026 15:53:06 -0400 Subject: [PATCH 1/4] Add a live tidal-disruption light curve overlay A small 2D-canvas chart that plots the disc's feeding boost against time since the star came apart, on log-log axes, with the t^(-5/3) fallback law drawn through the recorded peak. Off by default, one folder in the panel, and it works on the touch layout. It was asked for on the expectation that the fallback law would emerge from the simulation. It does not, and the feature is built to show that rather than to imply it. A realistic-mode star at the shipped clock decays like t^(-7.3), and the fitted index tracks the Disruption speed slider (about -12.5 at compression 4, -3.3 at 30) while barely moving with the seed or the placement radius. That is the app's own machinery talking: the plotted series is the absorbed-debris rate smeared by DISC_TUNING.boostDecayTau, fed by debris that DEBRIS_TUNING drag circularizes on a fixed timescale and maxAge truncates. So the reference line is anchored at the peak and never fitted, the caption prints the fitted index next to the law's, and the measurement is pinned by fallbackLaw.test.ts, documented in part 11 of docs/THEORY.md and listed in its "What we cheat on" table. The recorder is a fixed-capacity ring with no DOM and no WebGL, so all of its maths is unit tested directly. The frame loop only gains one recordFeeding call per fixed tick (an add, a compare, two typed-array writes about once in 120 ticks); the chart repaints on its own 10 Hz timer behind a dirty check, so no chart work happens inside frame(). Co-Authored-By: Claude Opus 5 (1M context) --- docs/THEORY.md | 67 +++++ index.html | 39 +++ src/config.ts | 35 +++ src/main.ts | 41 +++- src/settings.ts | 12 + src/ui/__tests__/fallbackLaw.test.ts | 159 ++++++++++++ src/ui/__tests__/lightCurve.test.ts | 325 +++++++++++++++++++++++++ src/ui/lightCurve.ts | 352 +++++++++++++++++++++++++++ src/ui/lightCurveChart.ts | 278 +++++++++++++++++++++ src/ui/panel.ts | 14 ++ 10 files changed, 1320 insertions(+), 2 deletions(-) create mode 100644 src/ui/__tests__/fallbackLaw.test.ts create mode 100644 src/ui/__tests__/lightCurve.test.ts create mode 100644 src/ui/lightCurve.ts create mode 100644 src/ui/lightCurveChart.ts diff --git a/docs/THEORY.md b/docs/THEORY.md index aa35bcd..fe0ea7b 100644 --- a/docs/THEORY.md +++ b/docs/THEORY.md @@ -445,6 +445,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,9 +486,44 @@ 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. --- @@ -606,6 +670,7 @@ the other column. | **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. | +| **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 curve decays like $t^{-7.3}$, so the app draws the law next to it and says which is which. | | **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. | | **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. | @@ -630,6 +695,8 @@ 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` | 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; + }
+