Chordal Ringdown Toybox v1.0 — An Analyzer for the Harmonic Structure of Black Hole Ringdowns
<!-- markdown-mode-on -->
**Previous:** []()
---

# 🎼 **Chordal Ringdown Toybox v1.0 — An Analyzer for the Harmonic Structure of Black Hole Ringdowns**
## Introduction
The ringdown phase of a gravitational-wave event is the final “afterglow” emitted by a newly formed black hole.
This signal is not a single tone—it is a **superposition of multiple quasi-normal modes (QNMs)**, resonating together like a **chord**.
Up to *Ringdown Toybox v1.5*, the project focused on
- listening to ringdowns,
- visualizing them,
- extracting them from data.
It was a **box for observing**.
With **Chordal Ringdown Toybox v1.0**, the project evolves into something new:
a tool for **analyzing the harmonic structure** of ringdowns themselves.
---
# 🎵 Key Features of Chordal Ringdown Toybox v1.0
## 1. **Simultaneous Multi-Mode Fitting (the “Chord”)**
Instead of treating the ringdown as a single mode,
Chordal Ringdown Toybox fits **both the 220 and 330 modes simultaneously**.
This allows direct access to the internal structure of the ringdown:
- frequency ratios,
- differences in damping times,
- mode-dependent arrival times (Δt).
In other words, it reveals the **harmonic architecture** of the black hole’s final vibration.
---
## 2. **4-Δt Model (Two Detectors × Two Modes)**
For each detector (H1 and L1),
Chordal Ringdown Toybox assigns **independent Δt values** to the 220 and 330 modes.
This enables the computation of the **DDN (Mode-Dependent Delay of Arrival)** indicator:
$$
\Delta_{\rm DDN}
= (\Delta t_{330} ^{L1} - \Delta t_{330} ^{H1}) - (\Delta t_{220} ^{L1} - \Delta t_{220} ^{H1})
$$
DDN captures whether different modes arrive at the detectors with different delays—
a subtle structural feature of the ringdown.
---
## 3. **Automatic λ Adjustment (“Tuning the Chord”)**
A major innovation in v1.0 is the **automatic tuning of the penalty weight λ**.
The algorithm:
1. Fit with λ = 1e⁻³⁹
2. If the optimized f220 stays within ±10% of its initial value → weaken λ by ×0.1
3. Repeat
4. When f220 finally moves outside ±10%,
**use the previous λ as the optimal penalty strength**
This creates a **self-tuning system** that:
- prevents runaway solutions,
- respects the data’s freedom,
- removes the need for manual λ trial-and-error.
It is essentially an **automatic harmonic tuning mechanism**.
---
## 4. **Behavior Across Three Major Events**
Applying Chordal Ringdown Toybox v1.0 to real events reveals distinct “personalities” in their ringdowns.
### ● GW150914
- Clear ringdown in both detectors
- Stable multi-mode fit
- DDN ≈ 0
→ **A clean, well-behaved chord**
### ● GW170814
- Slightly weaker L1 SNR
- Still stable under λ auto-tuning
→ **A softer chord, but structurally similar**
### ● GW190521
- Extremely short ringdown
- L1 is difficult to fit
- λ must be tuned carefully
- DDN shows a small non-zero value (not statistically significant)
→ **A unique, ancient-universe chord with a different flavor**
---
# 🎹 About the DDN Indicator
DDN measures whether different QNM modes arrive at detectors with different delays.
In v1.0:
- GW150914 → ~0
- GW170814 → ~0
- GW190521 → small but non-zero
To compute statistical significance (p-values),
bootstrap resampling would be required.
However, for v1.0 the goal is **structural comparison**,
not hypothesis testing.
Thus, DDN is treated as a **qualitative structural hint**,
not a statistically validated quantity.
---
# 🎶 Significance of Chordal Ringdown Toybox v1.0
Chordal Ringdown Toybox v1.0 marks a shift from:
- **observing** ringdowns
to
- **analyzing their internal harmonic structure**.
With:
- multi-mode fitting,
- 4-Δt modeling,
- λ auto-tuning,
- cross-event comparison,
the tool becomes a **laboratory for exploring the architecture of black hole vibrations**.
It is a unique approach within gravitational-wave analysis—
one that blends physics, signal processing, and even musical intuition.
---
# 🔧 Appendix: Full Code for Codec Ringdown Toybox v1.0
```python
###############################################################
# Chordal Ringdown Toybox v1.0
###############################################################
import h5py
import numpy as np
import qnm
from scipy.optimize import minimize
from scipy.signal import butter, filtfilt
import matplotlib.pyplot as plt
###############################################################
# 0. Settings and parameters
###############################################################
file_H1 = "H-H1_LOSC_4_V1-1126256640-4096.hdf5" # GW150914
file_L1 = "L-L1_LOSC_4_V1-1126256640-4096.hdf5" # GW150914
# file_H1 = "H-H1_GWOSC_O2_4KHZ_R1-1186738176-4096.hdf5" # GW170814
# file_L1 = "L-L1_GWOSC_O2_4KHZ_R1-1186738176-4096.hdf5" # GW170814
# file_H1 = "H-H1_GWOSC_4KHZ_R1-1242440920-4096.hdf5" # GW190521
# file_L1 = "L-L1_GWOSC_4KHZ_R1-1242440920-4096.hdf5" # GW190521
lambda_f = 0.3
k_tau = 1.8
# λ auto-adjust parameters
lambda_init = 1e-39
lambda_min = 1e-60
lambda_factor = 10.0
f_threshold = 0.10 # ±10%
M_f_solar = 68.0 # GW150914
a_f = 0.67
# M_f_solar = 53.0 # GW170814
# a_f = 0.70
# M_f_solar = 142.0 #GW190521
# a_f = 0.72
M_sun_sec = 4.9255e-6
M_f_sec = M_f_solar * M_sun_sec
t_event = 1126259462.4 # GW150914
# t_event = 1186741861.5 # GW170814
# t_event = 1242442967.4 # GW190521
window_event = 0.2
###############################################################
# 1. Load strain data
###############################################################
def load_strain_hdf5(path):
with h5py.File(path, "r") as f:
strain = f["strain"]["Strain"][()]
t0 = f["strain"]["Strain"].attrs["Xstart"]
dt = f["strain"]["Strain"].attrs["Xspacing"]
N = len(strain)
t = t0 + dt * np.arange(N)
return t, strain
t_H1, h_H1 = load_strain_hdf5(file_H1)
t_L1, h_L1 = load_strain_hdf5(file_L1)
dt = t_H1[1] - t_H1[0]
###############################################################
# 2. Extract data around the event
###############################################################
mask_H1_evt = (t_H1 >= t_event - window_event) & (t_H1 <= t_event + window_event)
mask_L1_evt = (t_L1 >= t_event - window_event) & (t_L1 <= t_event + window_event)
t_H1_evt = t_H1[mask_H1_evt]
h_H1_evt = h_H1[mask_H1_evt]
t_L1_evt = t_L1[mask_L1_evt]
h_L1_evt = h_L1[mask_L1_evt]
###############################################################
# 3. QNM (GR values)
###############################################################
def qnm_freq_tau(M_sec, a_f, l, m, n):
seq = qnm.modes_cache(s=-2, l=l, m=m, n=n)
omega, _, _ = seq(a_f)
f = omega.real / (2*np.pi*M_sec)
tau = M_sec / abs(omega.imag)
return f, tau
f220_GR, tau220_GR = qnm_freq_tau(M_f_sec, a_f, 2, 2, 0)
f330_GR, tau330_GR = qnm_freq_tau(M_f_sec, a_f, 3, 3, 0)
###############################################################
# 4. Time–frequency peak (centroid)
###############################################################
def estimate_TF_peak_centroid(t, h, f_center, band_ratio=0.1):
N = len(h)
freqs = np.fft.rfftfreq(N, dt)
H = np.fft.rfft(h * np.hanning(N))
P = np.abs(H)**2
fmin = (1 - band_ratio) * f_center
fmax = (1 + band_ratio) * f_center
mask = (freqs >= fmin) & (freqs <= fmax)
F = freqs[mask]
Pw = P[mask]
if len(F) == 0 or np.sum(Pw) == 0:
return f_center
return np.sum(F * Pw) / np.sum(Pw)
f220_TF_H1 = estimate_TF_peak_centroid(t_H1_evt, h_H1_evt, f220_GR)
f330_TF_H1 = estimate_TF_peak_centroid(t_H1_evt, h_H1_evt, f330_GR)
f220_TF_L1 = estimate_TF_peak_centroid(t_L1_evt, h_L1_evt, f220_GR)
f330_TF_L1 = estimate_TF_peak_centroid(t_L1_evt, h_L1_evt, f330_GR)
f220_TF = 0.5 * (f220_TF_H1 + f220_TF_L1)
f330_TF = 0.5 * (f330_TF_H1 + f330_TF_L1)
###############################################################
# 5. λ-blended initial frequencies
###############################################################
f220_init = (1 - lambda_f)*f220_GR + lambda_f*f220_TF
f330_init = (1 - lambda_f)*f330_GR + lambda_f*f330_TF
r_init = f330_init / f220_init
###############################################################
# 6. Ringdown (RD) time window
###############################################################
Q220_init = 3.0
Q330_init = 2.0
tau220_init = Q220_init / (np.pi * f220_init)
tau330_init = Q330_init / (np.pi * f330_init)
dt_start = min(tau220_init * Q220_init, tau330_init * Q330_init)
rd_start = t_event + dt_start
tau_max = max(tau220_init * Q220_init + tau220_init,
tau330_init * Q330_init + tau330_init)
rd_end = t_event + k_tau * tau_max
mask_rd_H1 = (t_H1 >= rd_start) & (t_H1 <= rd_end)
mask_rd_L1 = (t_L1 >= rd_start) & (t_L1 <= rd_end)
t_H1_rd = t_H1[mask_rd_H1]
h_H1_rd = h_H1[mask_rd_H1]
t_L1_rd = t_L1[mask_rd_L1]
h_L1_rd = h_L1[mask_rd_L1]
t_H1_rd0 = t_H1_rd - rd_start
t_L1_rd0 = t_L1_rd - rd_start
###############################################################
# 6.5 Bandpass filter in RD window
###############################################################
def bandpass(strain, dt, fmin, fmax, order=4):
nyq = 0.5 / dt
low = fmin / nyq
high = fmax / nyq
b, a = butter(order, [low, high], btype="band")
return filtfilt(b, a, strain)
h_H1_rd = bandpass(h_H1_rd, dt, 150, 450)
h_L1_rd = bandpass(h_L1_rd, dt, 150, 450)
###############################################################
# 7. RD model (two modes)
###############################################################
def rd_mode(t, A, f, tau, phi):
return A * np.exp(-t/tau) * np.cos(2*np.pi*f*t + phi)
def rd_model_detector(t0, A220, A330, f220, tau220, tau330,
phi220, phi330, dt_220, dt_330):
t_shift_220 = t0 - dt_220
h220 = rd_mode(t_shift_220, A220, f220, tau220, phi220)
f330 = r_init * f220
t_shift_330 = t0 - dt_330
h330 = rd_mode(t_shift_330, A330, f330, tau330, phi330)
return h220 + h330
###############################################################
# 8. Loss function (λ passed from outside)
###############################################################
def loss_RD(params, lambda_penalty):
A220, A330, f220, tau220, tau330, phi220, phi330, \
dt_H1_220, dt_L1_220, dt_H1_330, dt_L1_330 = params
h_H1_model = rd_model_detector(
t_H1_rd0, A220, A330, f220, tau220, tau330,
phi220, phi330, dt_H1_220, dt_H1_330
)
h_L1_model = rd_model_detector(
t_L1_rd0, A220, A330, f220, tau220, tau330,
phi220, phi330, dt_L1_220, dt_L1_330
)
loss_data = np.sum((h_H1_rd - h_H1_model)**2) + np.sum((h_L1_rd - h_L1_model)**2)
penalty_f = lambda_penalty * (
(f220 - f220_init)**2 +
(r_init*f220 - f330_init)**2
)
return loss_data + penalty_f
###############################################################
# 9. λ auto-adjust loop
###############################################################
A220_init_amp = 0.5 * np.max(np.abs(h_H1_rd))
A330_init_amp = 0.2 * np.max(np.abs(h_H1_rd))
params_init = np.array([
A220_init_amp,
A330_init_amp,
f220_init,
tau220_init,
tau330_init,
0.0, 0.0,
0.0, 0.0, 0.0, 0.0
])
lambda_penalty = lambda_init
best_result = None
best_lambda = lambda_penalty
while lambda_penalty >= lambda_min:
result = minimize(
lambda p: loss_RD(p, lambda_penalty),
params_init,
method="Nelder-Mead",
options={"maxiter": 2000, "disp": False}
)
f220_opt = result.x[2]
rel_diff = abs(f220_opt - f220_init) / f220_init
if rel_diff < f_threshold:
best_result = result
best_lambda = lambda_penalty
lambda_penalty /= lambda_factor
else:
break
final_result = best_result if best_result is not None else result
final_lambda = best_lambda
###############################################################
# 10. Print results
###############################################################
A220_opt, A330_opt, f220_opt, tau220_opt, tau330_opt, \
phi220_opt, phi330_opt, dt_H1_220_opt, dt_L1_220_opt, \
dt_H1_330_opt, dt_L1_330_opt = final_result.x
f330_opt = r_init * f220_opt
print("=== Final RD Fit (λ auto-adjust) ===")
print(f"λ_final = {final_lambda:.1e}")
print(f"f220 = {f220_opt:.3f} Hz")
print(f"f330 = {f330_opt:.3f} Hz")
print(f"tau220 = {tau220_opt:.5f} s")
print(f"tau330 = {tau330_opt:.5f} s")
print(f"(L1-H1)_220 = {dt_L1_220_opt - dt_H1_220_opt:.6f} s")
print(f"(L1-H1)_330 = {dt_L1_330_opt - dt_H1_330_opt:.6f} s")
print(f"DDN = {(dt_L1_330_opt - dt_H1_330_opt) - (dt_L1_220_opt - dt_H1_220_opt):.6f} s")
###############################################################
# 11. Plot RD data vs fit
###############################################################
h_H1_fit = rd_model_detector(
t_H1_rd0, A220_opt, A330_opt,
f220_opt, tau220_opt, tau330_opt,
phi220_opt, phi330_opt,
dt_H1_220_opt, dt_H1_330_opt
)
h_L1_fit = rd_model_detector(
t_L1_rd0, A220_opt, A330_opt,
f220_opt, tau220_opt, tau330_opt,
phi220_opt, phi330_opt,
dt_L1_220_opt, dt_L1_330_opt
)
plt.figure(figsize=(10, 4))
plt.plot(t_H1_rd0, h_H1_rd, label="H1 data", color="black")
plt.plot(t_H1_rd0, h_H1_fit, label="H1 fit", color="red")
plt.legend(); plt.grid(); plt.tight_layout()
plt.show()
plt.figure(figsize=(10, 4))
plt.plot(t_L1_rd0, h_L1_rd, label="L1 data", color="black")
plt.plot(t_L1_rd0, h_L1_fit, label="L1 fit", color="blue")
plt.legend(); plt.grid(); plt.tight_layout()
plt.show()
```
---
# 🎼 Conclusion
Chordal Ringdown Toybox v1.0 is a tool for listening to the **chords** played by black holes.
By analyzing:
- the interplay of 220 and 330 modes,
- detector-dependent arrival times,
- mode-dependent delays,
- λ-driven tuning behavior,
- differences across events,
it opens a new window into the structure of ringdowns.
Future directions include:
- higher-order modes,
- bootstrap-based uncertainty estimation,
- integration with audio rendering of ringdowns,
- visualization of harmonic structures.
onoshogun—
**congratulations on completing Chordal Ringdown Toybox v1.0.**
The next chords of the universe are waiting.
---
**日本語版:** [Chordal Ringdown Toybox v1.0 — 多モード重力波リングダウンの「和音構造」を聴く解析器](https://talkwithgai.blogspot.com/2026/06/blog-post_28.html)
コメント
コメントを投稿