Skip to content

Kerr spin, camera aberration, image-order tinting, an infalling beacon, and a live light curve - #6

Merged
algometrix merged 7 commits into
mainfrom
feat/physics-suite
Jul 31, 2026
Merged

Kerr spin, camera aberration, image-order tinting, an infalling beacon, and a live light curve#6
algometrix merged 7 commits into
mainfrom
feat/physics-suite

Conversation

@algometrix

Copy link
Copy Markdown
Owner

Five features, built by a fanned-out workflow of 11 agents (5 design, 3
implementation in isolated worktrees, 3 adversarial verification), then
integrated, corrected and re-verified here.

401 tests, up from 51. Typecheck clean, build clean, both new presets
driven in a real browser.

What landed

  • Kerr. One spin slider, a/M in [0, 0.998]. The outer horizon, the
    asymmetric shadow, the prograde ISCO and the split photon radii all follow
    from the spin rather than being faked, and a = 0 reproduces the shipped
    Schwarzschild image. At 0.95 the readout gives horizon 0.66 r_s, ISCO 0.97
    r_s, photon rings 0.69 and 1.98 r_s, which match the closed forms by hand.
  • Relativistic aberration on camera flights: the ray direction is
    transformed out of the moving camera's rest frame before the march, with
    the matching Doppler shift on what comes back. Exactly zero when the camera
    is at rest, so ordinary orbiting is untouched.
  • Photon ring order tinting, off by default: the disc is recoloured by
    how many times the ray wound around the hole.
  • An infalling beacon that reddens, dims and freezes at the horizon,
    with a readout contrasting the distant observer's clock against the
    probe's own, which crosses in finite time.
  • A live tidal disruption light curve on log-log axes with the
    t^(-5/3) law drawn beside it.

The most valuable thing the workflow produced was a negative result

The light curve was commissioned to "show the t^(-5/3) law emerging from the
simulation". It does not, and the agent measured that rather than shipping
the claim: the recorded decay is far steeper, and its index tracks the
disruption clock (about -12 at the slowest setting, -3 at the fastest). The
cause is in our own machinery, not the debris: the plotted series is the
absorbed-particle rate smeared by the disc's boost decay time, fed by debris
that a drag term circularises on a fixed timescale instead of letting it
return on its own orbits.

So the reference line is anchored and never fitted, the caption prints the
live fit next to the law, and both the tooltip and THEORY.md say plainly
that the app does not reproduce it and why. Making the simulation actually
produce the law is a real follow-up, and a good one.

What verification caught after the fact

All three implementations reported passing tests and a clean build, and all
three still came back needs-work. The headline was a blocker: the beacon's
frozen image was placed at an impact parameter along a three-dimensional
direction, so whenever the probe swung behind the hole its image was drawn
inside the shadow. The test meant to catch that compared a scalar against
b_crit instead of measuring where the image landed, so it passed the whole
time. Both are fixed, and the replacement test measures the sky plane offset
from six directions.

Also fixed: a preset quoting an inner-edge radius for a spin it does not
ship, three stale constants in kerr.ts doc comments, a recorder that never
stopped and slowly ate its own resolution, a transient note that claimed to
set a value it never set, and a boolean parameter in a public interface.

🤖 Generated with Claude Code

algometrix and others added 7 commits July 31, 2026 15:53
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) <noreply@anthropic.com>
Every other object in this app falls all the way in. That hides the one
thing about a black hole that nothing else demonstrates: the horizon is a
statement about clocks, not a surface. A probe released from rest reddens,
dims by nine decades and stalls just outside the shadow for us, while on
its own clock it crosses in a finite 28 r_s/c and notices nothing. Both
readings are shown side by side, because either one alone is a lie.

The probe is the only moving object here on exact Schwarzschild motion
rather than Paczynski-Wiita. PW has no coordinate-time divergence at all
and its matter crosses r_s in finite time, so reusing sim/gravity.ts would
have made the headline effect impossible to show. The integrated state is
the horizon gap rather than the radius, because an exponential decay stored
as a difference of two order-one numbers stops being physics and starts
being rounding at 1e-16.

Two things the plan for this did not survive contact with:

The coordinate-time ODE has a square-root fixed point at the release
radius, where dr/dt is exactly zero, so a midpoint scheme on the radius
alone parks the probe at r0 forever. The local speed is advanced alongside
it, which leaves zero at a finite rate and reproduces the analytic
short-time law exactly on the first step.

The redshift is stated as sqrt(1 - r_s/r) times the recession Doppler and a
test holds it to that, but it cannot be evaluated that way: the local speed
reaches 1.0 in float64 at a gap of 1e-17 and the Doppler factor then
collapses to zero, taking the last decade of the fade with it. The
algebraically identical f / (E + sqrt(E^2 - f)) is what runs.

The probe is drawn in the overlay pass rather than inside the raymarch, so
it is not truly lensed. It is placed at the apparent image radius instead,
floored at b_crit, which keeps it hugging the photon ring rather than
sinking into the shadow where the horizon mask would swallow it. Honest
about that in beacon.ts and in the new THEORY.md section.

stepWorld's trailing clock arguments became a named SimClocks object rather
than growing a sixth positional number nobody could read at the call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Spin is the largest realism gap this app had: real holes turn, and every
consequence of that is visible. Rather than bolt a gravitomagnetic term onto
the existing deflection, the shader and the CPU integrator now solve the exact
Kerr metric in Cartesian Kerr-Schild form as a Hamiltonian system on (x, p).
Kerr-Schild is the coordinate system that survives the horizon and its
Cartesian layout matches the march the app already had, so the disc-crossing
test, the body intersection and the uniforms are untouched.

Everything the brief asks to follow from the spin does follow from it: the
outer horizon, the frame dragging that makes the shadow lopsided, the prograde
ISCO moving in so the disc's inner edge moves with it, and the photon sphere
splitting into prograde and retrograde radii. Nothing is faked. The closed
forms live in physics/kerr.ts and are pinned against the shipped Schwarzschild
constants at a = 0, against the known extremal limits at a/M = 1, and against
the integrator's own turning points, which is what stops the two from drifting.

Spin 0 keeps the shipped image bit for bit. The a = 0 expressions are branched
rather than unified because the Kerr forms are analytically identical there and
not identical in floating point, and the old frame is a promise. That branch
carries a comment saying exactly this, and a test asserts spin 0 equals the
no-spin call.

Camera flights now aberrate. The boost is applied once per pixel to the initial
ray direction, so the shadow, the ring, the disc and the sky move together;
aberrating only the sky sample would slide the star field while the shadow
stayed put. The speed is not the derivative of the camera path, because the
flights run on a compressed clock that is superluminal in these units. Each
move publishes the physical speed of the trajectory it stands for instead: the
circular-orbit speed for the orbit, free fall from infinity for the pass and
the plunge. Both are already derived in THEORY.md. At rest the transformation
is the identity by construction rather than by a threshold.

Image-order tinting is a diagnostic overlay, off by default, that classifies
the disc by accumulated winding angle rather than by counting plane crossings:
a straight unlensed ray from a camera above the plane crosses the plane once,
so a crossing counter would call it a first-order image.

Two corrections to what was planned. The plan quoted b_crit, the ISCO and the
photon radius at a/M = 0.998 from a rougher evaluation; the tooltip and the
docs now carry the values the shipped closed forms actually produce, asserted
by a test so they cannot go stale. The plan also asserted a ZAMO speed of 0.5
at the 0.998 ISCO; 0.5 is the extremal limit and the value at 0.998 is 0.565,
so the test takes the limit instead.

Debris now reads its absorb and kill radii from the environment rather than
multiplying art constants by r_s. That is a correctness fix, not polish: at
high spin the old kill radius of 1.05 r_s sits outside a disc that reaches
0.62 r_s, so debris would vanish before it could be credited to the feed.

Kerr is a one-hole solution, so a second hole forces the spin to 0 in three
places that have to agree: the shader marches one center, the CPU integrator
throws, and the slider disables itself with a note. THEORY.md owns the
consequence, that a merger currently leaves a non-spinning remnant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three implementations came back needs-work. The findings, and what they cost:

A blocker in the beacon. `apparentImageRadius` returns an impact parameter,
which is a distance in the sky plane, and main.ts placed the sprite that far
along the probe's own direction in three dimensions. That only agrees when
the probe happens to sit side on to the camera; once it swings behind the
hole the image landed inside the shadow, the one place an escaping photon
cannot come from. Placement now projects into the sky plane first, so the
offset from the hole's centre is the impact parameter in every geometry, and
the image is lifted toward the camera so the horizon mask cannot eat it.

The test that should have caught it asserted the scalar against b_crit
rather than measuring where the image lands, so it passed throughout. The
replacement measures the sky plane offset from six directions including
directly behind the hole, which is the case that was broken.

The Kerr preset claimed an inner disc edge of 0.62 r_s while shipping spin
0.95, which puts it at 0.97. That is the ISCO for 0.998. Three doc comments
in kerr.ts carried stale values that the shipped closed forms contradict,
including the file a reader goes to for exactly those numbers.

The light curve recorded forever. Nothing cleared `recording` after a flare,
so the ring filled with post-flare zeros and then decimated the event itself
to make room for them: the longer a scene sat idle, the coarser its record
of the disruption became.

Two claims were true only at one slider position. The light curve's decay
index moves with the disruption clock, so the tooltip and the cheat sheet now
give the range and point at the live fit the caption already prints. The
"spin was set to 0" note fired while `settings.spin` kept the user's value,
so the slider disagreed with the note; it now sets what it says it sets.

Also: `setSpinAvailable(boolean)` was a boolean parameter in a public
interface, which the project standard forbids, and the beacon preset
inherited whatever spin was left over, demonstrating a Schwarzschild
solution against a dragged horizon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@algometrix
algometrix merged commit 0ee5541 into main Jul 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant