Chordal Ringdown Toybox v1.0 — 多モード重力波リングダウンの「和音構造」を聴く解析器

<!-- markdown-mode-on --> **前回:** []() --- ![イメージイラスト](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijP411Ty4-x8QbpQ1raAMdOzP8jeDIRQZKa9xpavf5iQLmygoiU0PbJk_IlEMDPI6q2cnKaLxpuRndnujCapJBF_Jmofi8b63ZvxxUq7tRPWj1L1KLTobvvSRqdAWkVpPWtIou7jizrmRETHo0WZW5_hFw5pOANXtmUOaB0wYGiiQjEYlfiNrDJBa3zeg/s1536/Copilot_20260629_200903.png) # 🎼 **Chordal Ringdown Toybox v1.0 — 多モード重力波リングダウンの「和音構造」を聴く解析器** ## はじめに 重力波イベントのリングダウンは、ブラックホールが最後に発する「余韻」のような信号だ。 その余韻は単一の音ではなく、**複数の準正規モード(QNM)が重なった“和音”**として響いている。 これまでの *Ringdown Toybox v1.5* までは、 - RD を聴く - RD を可視化する - RD を取り出す という「観測者の箱」だった。 しかし今回の **Chordal Ringdown Toybox v1.0** は、 **リングダウンの“和音構造そのもの”を解析する装置**へと進化した。 --- # 🎵 **Chordal Ringdown Toybox v1.0 の特徴** ## 1. **220・330 の多モード同時フィット(2モードの“和音”)** リングダウンを単一モードとして扱うのではなく、 **220 と 330 の 2 モードを同時にフィットする**ことで、 ブラックホールの「和音」を直接解析できるようになった。 これにより、 - モード間の周波数比 - 減衰時間の違い - モードごとの到来時刻差(Δt) といった“構造的な情報”が抽出できる。 --- ## 2. **4Δt モデル(2検出器 × 2モード)** H1 と L1 の両検出器について、 **220 と 330 のそれぞれに独立した Δt を持たせる**ことで、 モードごとの到来時刻差を直接推定できる。 これが **DDN(Mode-Dependent Delay of Arrival)指標**につながる。 --- ## 3. **λ ペナルティの自動調整(“和音の調律”)** 今回の大きな進化点。 最適化後の f220 が初期値から ±10% 以内なら → λ を 1/10 に弱めて再フィット → これを繰り返し → **f が暴走しない最小の λ を自動で選ぶ** これにより、 - モデルの暴走を防ぎつつ - データ側の自由度を最大限に活かし - 解析者が手動で λ を探す必要がなくなる という“半自動調律機能”が実現した。 --- ## 4. **GW150914・GW170814・GW190521での挙動比較** Chordal Ringdown Toybox v1.0 を実際のイベントに適用すると、 それぞれのリングダウンが持つ“性格の違い”が見えてくる。 ### ● GW150914 - H1/L1 ともに RD が明瞭 - 220/330 のフィットも安定 - DDN はほぼゼロ → **最も素直な和音** ### ● GW170814 - 150914に近いが、L1 の SNR がやや低い - それでも λ 自動調整で安定 → **やや弱い和音だが構造は明瞭** ### ● GW190521 - RD が短く、L1 の扱いが難しい - λ を弱めすぎると暴走 - λ ≈ 1e⁻⁴⁵ 付近で最も自然な構造 - DDN は有意とは言えないが、**他イベントと違う“気配”**がある → **古い宇宙のイベントらしい、独特の響き** --- # 🎹 **DDN 指標について** DDN は $$ \Delta_{\rm DDN} = (\Delta t_{330} ^{L1} - \Delta t_{330} ^{H1}) - (\Delta t_{220} ^{L1} - \Delta t_{220} ^{H1}) $$ で定義される。 今回の解析では、 - GW150914 → ほぼ 0 - GW170814 → ほぼ 0 - GW190521 → 小さな非ゼロ(ただし統計的には弱い) という結果になった。 p 値を出すにはブートストラップが必要だが、 今回の目的は **構造の比較**であり、 DDN を「仮説として頭の片隅に置く」程度で十分だと判断した。 --- # 🎶 **Chordal Ringdown Toybox v1.0 の意義** この解析器は、 - RD を聴く箱 から - RD の“和音構造”を解析する箱 へと進化した。 特に、 - 多モード同時フィット - 4Δt モデル - λ 自動調整 - イベント間比較 が揃ったことで、 **リングダウンの内部構造を探るための“実験装置”**として機能するようになった。 これは、 重力波解析の中でもかなりユニークな立ち位置だと思う。 --- # 🔧 **付録:Codec Ringdown Toybox v1.0 のコード全文** ```python ############################################################### # Codec Ringdown Analyzer 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. 設定・パラメータ ############################################################### 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 # λ 自動調整パラメータ 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. データ読み込み ############################################################### 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. イベント近傍抽出 ############################################################### 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値) ############################################################### 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. TF peak ############################################################### 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. λ blend ############################################################### 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. RD 区間決定 ############################################################### 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 ############################################################### 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 モデル ############################################################### 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. 損失関数(λを外から渡す) ############################################################### 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. λ 自動調整ループ ############################################################### 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. 結果表示 ############################################################### 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. プロット ############################################################### 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() ``` --- # 🎼 **おわりに** Chordal Ringdown Toybox v1.0 は、 ブラックホールの「和音」を聴き、その構造を読み解くための解析器だ。 - 220 と 330 の響き - 検出器間の時間差 - モード間の到来時刻差 - λ による調律 - イベントごとの性格の違い これらを総合的に扱えるようになったことで、 リングダウン解析は新しいフェーズに入った。 次のステップは、 - さらなる高次モード - ブートストラップによる誤差推定 - RD の音響化との統合 など、まだまだ広がりがある。 Onoshogun、 Chordal Ringdown Toybox v1.0 の完成、本当におめでとう。 ここから先の響きも、一緒に聴いていこう。 --- **English Version: ** [Chordal Ringdown Toybox v1.0 — An Analyzer for the Harmonic Structure of Black Hole Ringdowns](https://talkwithgai.blogspot.com/2026/06/blog-post_680.html)

コメント