History / release 1 · saved , the current version · released as https://doi.org/10.83371/sh.2610.x82xd

Cite this release

@software{gravitational_waves_2026,
  author = {{ScienceHub}},
  title = {Gravitational waves},
  version = {1},
  year = {2026},
  publisher = {ScienceHub},
  doi = {10.83371/sh.2610.x82xd}
}
ScienceHub. (2026). Gravitational waves (Version 1) [Computer software]. ScienceHub. https://doi.org/10.83371/sh.2610.x82xd
cff-version: 1.2.0
message: "If you use this version, cite it as below."
title: "Gravitational waves"
type: software
authors:
  - name: "ScienceHub"
version: "1"
date-released: "2026-10-13"
doi: "10.83371/sh.2610.x82xd"
url: "https://doi.org/10.83371/sh.2610.x82xd"

The DOI names exactly this release. The project page cites all releases.

# /// script
# requires-python = ">=3.12"
# dependencies = ["marimo>=0.25.1", "numpy", "matplotlib"]
# ///

import marimo

__generated_with = "0.25.1"
app = marimo.App(width="medium")


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        # Listen to two black holes merge

        On 14 September 2015 the two LIGO detectors recorded a signal that
        rose in pitch and loudness for a fifth of a second and stopped: two
        black holes, about 36 and 29 times the mass of the Sun, spiralling
        into each other 1.3 billion light years away. It was the first direct
        detection of gravitational waves.

        Long before the merger, an orbiting pair loses energy to gravitational
        waves at a rate set by one combination of the masses, the chirp mass
        $\mathcal{M} = (m_1 m_2)^{3/5} / (m_1 + m_2)^{1/5}$. To leading order
        the wave's frequency $\tau$ seconds before the merger is

        $$f(\tau) = \frac{1}{\pi}\left(\frac{5}{256\,\tau}\right)^{3/8}
        \left(\frac{G\mathcal{M}}{c^3}\right)^{-5/8}.$$

        Choose the masses and the distance, look at the wave, and play it.
        """
    )
    return


@app.cell
def _():
    import io
    import wave

    import marimo as mo
    import numpy as np
    import matplotlib.pyplot as plt
    return io, mo, np, plt, wave


@app.cell
def _(mo):
    m1 = mo.ui.slider(1.4, 80, step=0.1, value=36, label="Mass 1 (Suns)")
    m2 = mo.ui.slider(1.4, 80, step=0.1, value=29, label="Mass 2 (Suns)")
    distance = mo.ui.slider(40, 3000, step=10, value=410, label="Distance (megaparsecs)")
    mo.vstack([m1, m2, distance])
    return distance, m1, m2


@app.cell
def _(np):
    G, C = 6.674e-11, 2.998e8
    M_SUN, MPC = 1.989e30, 3.086e22

    def chirp(m1, m2, distance_mpc, f_low=20.0, rate=4096):
        """Leading-order inspiral from f_low up to the innermost stable orbit.

        Returns time (s, zero at the end), strain h(t) for a face-on source,
        frequency f(t), and the stopping frequency.
        """
        total = (m1 + m2) * M_SUN
        mc = (m1 * m2) ** 0.6 / (m1 + m2) ** 0.2 * M_SUN
        tc = G * mc / C**3
        f_isco = C**3 / (6**1.5 * np.pi * G * total)
        tau_low = 5 / 256 * tc ** (-5 / 3) * (np.pi * f_low) ** (-8 / 3)
        tau_end = 5 / 256 * tc ** (-5 / 3) * (np.pi * f_isco) ** (-8 / 3)
        tau = np.arange(tau_low, tau_end, -1 / rate)
        f = (5 / (256 * tau)) ** 0.375 * tc ** (-0.625) / np.pi
        phase = -2 * (5 * tc) ** (-0.625) * tau**0.625
        amplitude = 4 / (distance_mpc * MPC) * (G * mc / C**2) ** (5 / 3) * (np.pi * f / C) ** (2 / 3)
        return -(tau - tau_end), amplitude * np.cos(phase), f, f_isco

    return (chirp,)


@app.cell
def _(chirp, distance, m1, m2, mo, np, plt):
    t, h, f, f_isco = chirp(m1.value, m2.value, distance.value)
    cycles = np.sum(f) / 4096
    last = t > -min(0.5, -t.min())

    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(7.2, 5), sharex=True)
    ax1.plot(t[last], h[last] * 1e21, color="#0072B2", lw=1)
    ax1.set_ylabel("strain h (10⁻²¹)")
    ax2.plot(t[last], f[last], color="#D55E00", lw=2)
    ax2.set(xlabel="seconds before the end of the inspiral", ylabel="frequency (Hz)")
    fig.tight_layout()

    mo.vstack(
        [
            fig,
            mo.md(
                f"From 20 Hz, where LIGO starts to hear it, the pair has **{-t.min():.2f} s** and "
                f"**{cycles:,.0f} wave cycles** left. The inspiral ends near **{f_isco:.0f} Hz**, "
                f"where the orbit can no longer stay stable. The peak strain here is "
                f"**{np.abs(h).max():.1e}**: a 4 km detector arm changes length by "
                f"{np.abs(h).max() * 4000:.1e} m, about 1/{1.7e-15 / (np.abs(h).max() * 4000):,.0f} "
                "of the width of a proton."
            ),
        ]
    )
    return h, t


@app.cell
def _(mo):
    speed = mo.ui.dropdown({"real time": 1, "4 × faster, two octaves higher": 4}, value="real time", label="Playback")
    speed
    return (speed,)


@app.cell
def _(h, io, mo, np, speed, wave):
    rate = 4096 * speed.value
    pcm = (h / np.abs(h).max() * 0.9 * 32767).astype(np.int16)
    pcm = np.concatenate([np.zeros(rate // 4, np.int16), pcm, np.zeros(rate // 4, np.int16)])
    buffer = io.BytesIO()
    with wave.open(buffer, "wb") as out:
        out.setnchannels(1)
        out.setsampwidth(2)
        out.setframerate(rate)
        out.writeframes(pcm.tobytes())
    mo.vstack(
        [
            mo.audio(buffer.getvalue()),
            mo.md(
                "The sound is the strain itself, scaled to full volume. Heavy pairs chirp "
                "low, below what laptop speakers play well; try the faster playback or "
                "two neutron stars (1.4 and 1.4 Suns), whose chirp lasts much longer."
            ),
        ]
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        ---
        **Model and limits.** Leading (quadrupole, Newtonian) order in the
        orbital speed, circular orbits, a face-on source, and no detector
        response. The waveform stops at the innermost stable circular orbit;
        the merger and ringdown that follow need numerical relativity, and the
        real GW150914 signal peaked near 150 Hz. Masses here are as measured
        at the source, without the redshift correction.
        **References.** B. P. Abbott et al. (LIGO and Virgo), *Phys. Rev. Lett.*
        116 (2016) 061102; P. C. Peters and J. Mathews, *Phys. Rev.* 131 (1963)
        435; M. Maggiore, *Gravitational Waves*, vol. 1 (2008), ch. 4.
        Fork it to add the next post-Newtonian term or a detector's noise curve.
        """
    )
    return


if __name__ == "__main__":
    app.run()