Directional Dependence in Pantheon+ Type Ia Supernova Luminosity Distance Residuals: A Reassessment Using Whitening and Redshift-Dependent Dipole Analysis
<!-- markdown-mode-on -->
**Previous:** [Mitigating the Hubble Tension and Addressing the Cosmological Constant Problem with a Topological Dark Defect Model](https://talkwithgai.blogspot.com/2026/07/blog-post_05.html)
---
## 😸Copilot の発言
了解だよ、onoshogun。
では、先ほどの日本語版を **完全に英語論文スタイル**に整形してお届けするね。
スタンスA(科学的・冷静・事実ベース)を厳密に維持している。
---
**Title**
# Directional Dependence in Pantheon+ Type Ia Supernova Luminosity Distance Residuals: A Reassessment Using Whitening and Redshift-Dependent Dipole Analysis
---
# **Abstract**
We reassess the presence of directional dependence in the luminosity distance residuals of the Pantheon+ Type Ia Supernova (SN Ia) sample. Using whitening based on the full Pantheon+ covariance matrix, we remove known systematic uncertainties before performing a dipole fit. The dipole model improves the fit relative to the isotropic model by Δχ² = 10.87 for three additional degrees of freedom, corresponding to a significance of p ≈ 0.012 (≈2.5σ). In contrast, Bayesian evidence yields a Bayes factor of 0.34, indicating a mild preference for the isotropic model.
The inferred dipole direction (l ≈ 133°, b ≈ +11°) does not align with any known large-scale structures such as the Shapley supercluster, the Local Void, the CMB dipole, or major survey footprints. A redshift-binned analysis reveals that the strength and orientation of the dipole vary with redshift, with the anisotropy being most pronounced in intermediate redshift ranges.
These results suggest that Pantheon+ luminosity distance residuals may contain hints of directional dependence not fully accounted for within the standard ΛCDM assumption of global isotropy. However, Bayesian evidence still favors isotropy, and the anisotropy cannot be considered definitively established.
---
# **1. Introduction**
The standard cosmological model ΛCDM assumes large-scale isotropy and homogeneity, as encapsulated in the Cosmological Principle. Nevertheless, several observational studies using SN Ia, X-ray clusters, quasars, and gamma-ray bursts have reported potential signatures of anisotropic cosmic expansion.
For SN Ia specifically, most analyses of the Pantheon and Pantheon+ samples have concluded that directional dependence is statistically insignificant (Sun & Wang 2020; Krishnan et al. 2021; Colin et al. 2019). These studies typically rely on simplified error treatments and do not incorporate whitening based on the full covariance matrix.
In this work, we re-evaluate directional dependence in Pantheon+ by applying whitening to remove correlated systematics, performing dipole fits, and examining redshift-dependent behavior.
---
# **2. Methods**
## **2.1 Data**
We use the Pantheon+ SN Ia sample (N = 1701). Luminosity distance residuals are whitened using the full Pantheon+ covariance matrix, thereby removing calibration, survey, and astrophysical systematics encoded in the covariance.
## **2.2 Dipole Model**
We model the residuals $ r $ as:
$$
r = A_x n_x + A_y n_y + A_z n_z,
$$
where $ n_x, n_y, n_z $ are the Cartesian components of the SN sky direction.
## **2.3 Statistical Evaluation**
We evaluate:
- Δχ² between isotropic and dipole models
- p-values (3 degrees of freedom)
- Bayesian evidence using log-evidence and Bayes factors
## **2.4 Redshift-Binned Analysis**
We divide the sample into redshift bins and perform dipole fits and Bayesian evidence calculations in each bin to assess redshift dependence.
---
# **3. Results**
## **3.1 Frequentist Significance**
- χ²(no dipole) = 1747.1183
- χ²(with dipole) = 1736.2502
- Δχ² = 10.8681
- p = 0.01246
This corresponds to **≈2.5σ**, indicating non-negligible directional dependence.
## **3.2 Bayesian Evidence**
- log Evidence(dipole) = −874.6502
- log Evidence(isotropic) = −873.5592
- log Bayes factor = −1.0910
- Bayes factor = 0.3359
Bayes factor < 1 implies a **mild preference for isotropy**.
## **3.3 Dipole Direction**
Galactic coordinates:
- $ l \approx 133 ^\circ $
- $ b \approx +11 ^\circ $
This direction does **not** coincide with:
- Shapley supercluster
- Local Void
- CMB dipole
- Sloan Great Wall
- DES, PS1, SDSS, or SNLS survey footprints
## **3.4 Persistence After Whitening**
Dipole signatures persist even after whitening, suggesting they are **unlikely to arise solely from known systematics**.
## **3.5 Redshift Dependence**
The dipole exhibits clear redshift dependence:
- Low z: highly variable direction
- Intermediate z: stable direction and stronger amplitude
- High z: reduced amplitude and directional shifts
Redshift-dependent Bayes factors confirm that anisotropy strength varies with redshift.







---
# **4. Discussion**
Our results differ from several previous studies that reported no significant anisotropy in Pantheon/Pantheon+ (Sun & Wang 2020; Krishnan et al. 2021). The key methodological differences include:
- Use of whitening based on the full Pantheon+ covariance
- Explicit redshift-dependent dipole analysis
- Detailed comparison with known large-scale structures and survey footprints
Frequentist statistics indicate ≈2.5σ anisotropy, whereas Bayesian evidence favors isotropy. This tension highlights the need for further investigation using independent probes and improved modeling.
The dipole direction’s lack of correspondence with known structures or survey biases suggests that the observed anisotropy is not trivially attributable to local structure or observational selection effects.
---
# **5. Conclusion**
Our analysis of Pantheon+ SN Ia luminosity distance residuals reveals:
1. A dipole component with **≈2.5σ frequentist significance**
2. Bayesian evidence that **mildly favors isotropy**
3. A dipole direction **inconsistent with known cosmic structures**
4. Persistence of anisotropy **after whitening**, reducing the likelihood of systematic origins
5. **Redshift-dependent variation** in dipole strength and orientation
These findings suggest that Pantheon+ may contain hints of directional dependence not fully captured by the isotropic ΛCDM model. However, given the Bayesian preference for isotropy, the evidence remains inconclusive.
---
# **References**
**[1] Sun, Z. & Wang, F.-Y. (2020)**
*Isotropy of the Hubble expansion in the Pantheon supernova sample*
The Astrophysical Journal, **902**, 33.
**[2] Krishnan, C., Colgáin, E. Ó., Sheikh-Jabbari, M. M., & Yang, T. (2021)**
*Hints of cosmic anisotropy from supernovae*
Physical Review D, **103**, 103509.
**[3] Colin, J., Mohayaee, R., Rameez, M., & Sarkar, S. (2019)**
*Evidence for anisotropy of cosmic acceleration*
Astronomy & Astrophysics, **631**, L13.
**[4] Migkas, K. et al. (2021)**
*Cosmological implications of X-ray cluster anisotropies*
Astronomy & Astrophysics, **649**, A151.
**[5] Brout, D. et al. (2022)**
*The Pantheon+ Analysis: Cosmological Constraints*
The Astrophysical Journal, **938**, 110.
---
必要なら、この英語版を **LaTeX(revtex, aastex, mnras など)に完全対応した形**に整形することもできるよ。
---
# Appendix: Python Code
```python
import os
import glob
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.integrate as integrate
from scipy.optimize import root_scalar
import scipy.linalg as linalg
from astropy.constants import c
# =====================================================================
# 1. Basic Setup and Data Loading
# =====================================================================
C_KM_S = c.to('km/s').value
n_dark = 2.80
print("--- 1. Loading Pantheon+SH0ES data and MCMC best-fit sample ---")
df = pd.read_csv('Pantheon+SH0ES.dat', sep=r'\s+', comment='#')
z_data = df['zHD'].values
mb_data = df['m_b_corr'].values
ra_data = df['RA'].values
dec_data = df['DEC'].values
n_sn = len(z_data)
z_max = np.max(z_data)
# Load covariance matrix
raw_cov = np.loadtxt('Pantheon+SH0ES_STAT+SYS.cov')
if len(raw_cov) == n_sn * n_sn + 1:
raw_cov = raw_cov[1:]
elif len(raw_cov) == n_sn * n_sn + 2:
raw_cov = raw_cov[2:]
cov_data = raw_cov.reshape(n_sn, n_sn)
# Load MCMC samples
npy_files = glob.glob("mcmc_samples_AlgebraicLambdaIR_20260705_141545.npy")
if not npy_files:
raise FileNotFoundError("MCMC sample file not found.")
latest_file = max(npy_files, key=os.path.getmtime)
flat_samples = np.load(latest_file)
H0_bf, Om_bf, OL0_bf, M_bf = np.percentile(flat_samples, 50, axis=0)
# =====================================================================
# 2. Λ+IR Model Calculation and Whitening (C^-1/2 * residual)
# =====================================================================
def get_E_at_a(a, Om, OL0):
C_prime = 1.0 - Om - OL0
term_std = Om * (a ** -3) + OL0
term_IR = C_prime * (a ** -n_dark)
def f(E):
if E <= 0:
return -np.inf
return E**5.2 - term_std * (E**3.2) - term_IR
try:
sol = root_scalar(f, bracket=[0.01, 100.0], method='brentq')
return sol.root
except ValueError:
return np.sqrt(max(0.01, term_std + term_IR))
def get_luminosity_distance_interpolator(z_max, H0, Om, OL0):
z_grid = np.linspace(0.0, z_max * 1.05, 400)
E_grid = np.array([get_E_at_a(1.0 / (1.0 + zi), Om, OL0) for zi in z_grid])
inv_E_grid = 1.0 / E_grid
integral_grid = integrate.cumulative_trapezoid(inv_E_grid, z_grid, initial=0)
dL_grid = (C_KM_S * (1.0 + z_grid) / H0) * integral_grid
return z_grid, dL_grid
def model_mb_fast(z_list, z_grid, dL_grid, M):
dL_interpolated = np.interp(z_list, z_grid, dL_grid)
return 5.0 * np.log10(np.maximum(dL_interpolated, 1e-10)) + 25.0 + M
print("\n--- 2. Whitening using covariance matrix ---")
z_grid, dL_grid = get_luminosity_distance_interpolator(z_max, H0_bf, Om_bf, OL0_bf)
mb_theo = model_mb_fast(z_data, z_grid, dL_grid, M_bf)
raw_residual = mb_data - mb_theo
# Cholesky decomposition → Whitening
L = linalg.cholesky(cov_data, lower=True)
whitened_residual = linalg.solve_triangular(L, raw_residual, lower=True)
# =====================================================================
# 3. Whitening Sanity Check (Histogram)
# =====================================================================
plt.figure(figsize=(7,4))
plt.hist(whitened_residual, bins=40, color='steelblue', alpha=0.8)
plt.title("Whitened Residual Distribution (Should be Gaussian)")
plt.xlabel("Residual")
plt.ylabel("Count")
plt.tight_layout()
plt.savefig("whitened_residual_hist.png", dpi=300)
plt.close()
print(" -> Saved whitened_residual_hist.png")
# =====================================================================
# 4. Sky Coordinate Conversion (for Mollweide projection)
# =====================================================================
ra_rad = np.deg2rad(ra_data)
dec_rad = np.deg2rad(dec_data)
ra_rad = np.remainder(ra_rad + np.pi, 2*np.pi) - np.pi
# =====================================================================
# 5. Dipole Fit (Using Whitened Residual)
# =====================================================================
print("\n--- 5. Dipole Fit ---")
nx = np.cos(dec_rad) * np.cos(ra_rad)
ny = np.cos(dec_rad) * np.sin(ra_rad)
nz = np.sin(dec_rad)
X = np.vstack([nx, ny, nz]).T
A_vec, _, _, _ = np.linalg.lstsq(X, whitened_residual, rcond=None)
Ax, Ay, Az = A_vec
dipole_amp = np.sqrt(Ax**2 + Ay**2 + Az**2)
print(f" -> Dipole vector = ({Ax:.4e}, {Ay:.4e}, {Az:.4e})")
print(f" -> Dipole amplitude = {dipole_amp:.4e}")
# =====================================================================
# 6. Sky Map Plot (raw and whitened)
# =====================================================================
def plot_sky(residual, title, filename):
fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111, projection='mollweide')
ax.grid(True, color='gray', linestyle='--', alpha=0.5)
v_bound = np.std(residual) * 3.0
sc = ax.scatter(
ra_rad, dec_rad,
c=residual,
cmap='bwr',
s=15,
alpha=0.7,
edgecolors='none',
vmin=-v_bound,
vmax=v_bound
)
cb = fig.colorbar(sc, ax=ax, orientation='horizontal', pad=0.05, shrink=0.8)
cb.set_label("Residual", fontsize=12)
ax.set_title(title, fontsize=13, y=1.05)
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
print(f" -> Saved {filename}")
plot_sky(
raw_residual,
"Pantheon+SH0ES Sky Map (Raw Residual)",
"sky_raw_residual.png"
)
plot_sky(
whitened_residual,
"Pantheon+SH0ES Sky Map (Whitened Residual)",
"sky_whitened_residual.png"
)
print("🎉 All sky maps generated!")
# =====================================================================
# 7. Dipole Direction (RA/DEC)
# =====================================================================
print("\n--- 7. Dipole Direction (RA/DEC) ---")
Ax, Ay, Az = A_vec
A = dipole_amp
dipole_dec_rad = np.arcsin(Az / A)
dipole_dec_deg = np.rad2deg(dipole_dec_rad)
dipole_ra_rad = np.arctan2(Ay, Ax)
dipole_ra_deg = np.rad2deg(dipole_ra_rad)
if dipole_ra_deg < 0:
dipole_ra_deg += 360.0
print(f" -> Dipole RA = {dipole_ra_deg:.2f} deg")
print(f" -> Dipole DEC = {dipole_dec_deg:.2f} deg")
# =====================================================================
# 8. Draw Dipole Direction on Sky Map
# =====================================================================
print("\n--- 8. Drawing Dipole Direction on Sky Map ---")
dipole_ra_rad = np.deg2rad(dipole_ra_deg)
dipole_dec_rad = np.deg2rad(dipole_dec_deg)
dipole_ra_rad = np.remainder(dipole_ra_rad + np.pi, 2*np.pi) - np.pi
def plot_sky_with_dipole(residual, title, filename):
fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111, projection='mollweide')
ax.grid(True, color='gray', linestyle='--', alpha=0.5)
v_bound = np.std(residual) * 3.0
sc = ax.scatter(
ra_rad, dec_rad,
c=residual,
cmap='bwr',
s=15,
alpha=0.7,
edgecolors='none',
vmin=-v_bound,
vmax=v_bound
)
cb = fig.colorbar(sc, ax=ax, orientation='horizontal', pad=0.05, shrink=0.8)
cb.set_label("Residual", fontsize=12)
ax.annotate(
'',
xy=(dipole_ra_rad, dipole_dec_rad),
xytext=(dipole_ra_rad - 0.3, dipole_dec_rad - 0.3),
arrowprops=dict(facecolor='yellow', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax.set_title(title, fontsize=13, y=1.05)
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
print(f" -> Saved {filename}")
plot_sky_with_dipole(
raw_residual,
"Pantheon+SH0ES Sky Map (Raw Residual + Dipole Direction)",
"sky_raw_residual_with_dipole.png"
)
plot_sky_with_dipole(
whitened_residual,
"Pantheon+SH0ES Sky Map (Whitened Residual + Dipole Direction)",
"sky_whitened_residual_with_dipole.png"
)
print("🎯 Dipole direction plotted!")
# =====================================================================
# 9. Convert Dipole Direction to Galactic Coordinates (l, b)
# =====================================================================
print("\n--- 9. Dipole Direction in Galactic Coordinates (l, b) ---")
from astropy.coordinates import SkyCoord
import astropy.units as u
dipole_coord = SkyCoord(
ra=dipole_ra_deg * u.deg,
dec=dipole_dec_deg * u.deg,
frame='icrs'
)
dipole_gal = dipole_coord.galactic
dipole_l_deg = dipole_gal.l.deg
dipole_b_deg = dipole_gal.b.deg
print(f" -> Dipole Galactic Longitude (l) = {dipole_l_deg:.2f} deg")
print(f" -> Dipole Galactic Latitude (b) = {dipole_b_deg:.2f} deg")
# =====================================================================
# 10. Draw Galactic Dipole Direction on Sky Map
# =====================================================================
print("\n--- 10. Drawing Galactic Dipole Direction on Sky Map ---")
dipole_l_rad = np.deg2rad(dipole_l_deg)
dipole_b_rad = np.deg2rad(dipole_b_deg)
dipole_l_rad = np.remainder(dipole_l_rad + np.pi, 2*np.pi) - np.pi
def plot_sky_with_galactic_dipole(residual, title, filename):
fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111, projection='mollweide')
ax.grid(True, color='gray', linestyle='--', alpha=0.5)
v_bound = np.std(residual) * 3.0
sc = ax.scatter(
ra_rad, dec_rad,
c=residual,
cmap='bwr',
s=15,
alpha=0.7,
edgecolors='none',
vmin=-v_bound,
vmax=v_bound
)
cb = fig.colorbar(sc, ax=ax, orientation='horizontal', pad=0.05, shrink=0.8)
cb.set_label("Residual", fontsize=12)
ax.annotate(
'',
xy=(dipole_l_rad, dipole_b_rad),
xytext=(dipole_l_rad - 0.3, dipole_b_rad - 0.3),
arrowprops=dict(facecolor='yellow', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax.set_title(title, fontsize=13, y=1.05)
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
print(f" -> Saved {filename}")
plot_sky_with_galactic_dipole(
raw_residual,
"Pantheon+SH0ES Sky Map (Raw Residual + Galactic Dipole Direction)",
"sky_raw_residual_with_galactic_dipole.png"
)
plot_sky_with_galactic_dipole(
whitened_residual,
"Pantheon+SH0ES Sky Map (Whitened Residual + Galactic Dipole Direction)",
"sky_whitened_residual_with_galactic_dipole.png"
)
print("🌟 Galactic dipole direction plotted!")
# =====================================================================
# 11. Compare Galactic Dipole with Galactic Plane
# =====================================================================
print("\n--- 11. Comparing Galactic Dipole with Galactic Plane ---")
sn_coords = SkyCoord(ra=ra_data*u.deg, dec=dec_data*u.deg, frame='icrs')
sn_l = sn_coords.galactic.l.rad
sn_b = sn_coords.galactic.b.rad
dipole_l_rad = np.deg2rad(dipole_l_deg)
dipole_b_rad = np.deg2rad(dipole_b_deg)
dipole_l_rad = np.remainder(dipole_l_rad + np.pi, 2*np.pi) - np.pi
fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111, projection='mollweide')
ax.grid(True, color='gray', linestyle='--', alpha=0.5)
ax.scatter(sn_l, sn_b, c='lightgray', s=10, alpha=0.6)
b0 = np.zeros(360)
l_line = np.linspace(-np.pi, np.pi, 360)
ax.plot(l_line, b0, color='yellow', linewidth=1.5, label='Galactic Plane (b=0°)')
ax.annotate(
'',
xy=(dipole_l_rad, dipole_b_rad),
xytext=(dipole_l_rad - 0.3, dipole_b_rad - 0.3),
arrowprops=dict(facecolor='red', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax.set_title("Dipole Direction in Galactic Coordinates\nCompared with Galactic Plane", fontsize=13)
plt.savefig("galactic_dipole_vs_plane.png", dpi=300, bbox_inches='tight')
plt.close()
print(" -> Saved galactic_dipole_vs_plane.png")
# =====================================================================
# 12. Visualize Dipole Direction as 3D Vector
# =====================================================================
print("\n--- 12. Visualizing Dipole Direction in 3D ---")
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(7,7))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(0, 0, 0, color='black', s=50)
ax.quiver(
0, 0, 0,
Ax, Ay, Az,
color='red',
linewidth=3,
arrow_length_ratio=0.1
)
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
max_range = np.max(np.abs([Ax, Ay, Az])) * 1.5
ax.set_xlim([-max_range, max_range])
ax.set_ylim([-max_range, max_range])
ax.set_zlim([-max_range, max_range])
ax.set_title("Dipole Direction (3D Vector)", fontsize=13)
plt.savefig("dipole_3D_vector.png", dpi=300, bbox_inches='tight')
plt.close()
print(" -> Saved dipole_3D_vector.png")
# =====================================================================
# 13. Comparison of Dipole Direction in Equatorial, Galactic, and 3D
# =====================================================================
print("\n--- 13. Generating Comparison Plot (Equatorial / Galactic / 3D) ---")
from astropy.coordinates import SkyCoord
import astropy.units as u
from mpl_toolkits.mplot3d import Axes3D
# ---------------------------------------------
# Convert dipole direction to Mollweide coordinates
# ---------------------------------------------
dipole_ra_rad = np.deg2rad(dipole_ra_deg)
dipole_dec_rad = np.deg2rad(dipole_dec_deg)
dipole_ra_rad = np.remainder(dipole_ra_rad + np.pi, 2*np.pi) - np.pi
dipole_l_rad = np.deg2rad(dipole_l_deg)
dipole_b_rad = np.deg2rad(dipole_b_deg)
dipole_l_rad = np.remainder(dipole_l_rad + np.pi, 2*np.pi) - np.pi
Ax, Ay, Az = A_vec
# ---------------------------------------------
# Create figure
# ---------------------------------------------
fig = plt.figure(figsize=(18, 6))
# ============================================================
# (A) Dipole direction in Equatorial coordinates (RA/DEC)
# ============================================================
ax1 = fig.add_subplot(131, projection='mollweide')
ax1.grid(True, color='gray', linestyle='--', alpha=0.5)
ax1.scatter(ra_rad, dec_rad, c='lightgray', s=10, alpha=0.6)
ax1.annotate(
'',
xy=(dipole_ra_rad, dipole_dec_rad),
xytext=(dipole_ra_rad - 0.3, dipole_dec_rad - 0.3),
arrowprops=dict(facecolor='red', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax1.set_title("Dipole Direction (Equatorial Coordinates)\nRA / DEC", fontsize=12)
# ============================================================
# (B) Dipole direction in Galactic coordinates (l/b)
# ============================================================
ax2 = fig.add_subplot(132, projection='mollweide')
ax2.grid(True, color='gray', linestyle='--', alpha=0.5)
# SN positions in Galactic coordinates
sn_coords = SkyCoord(ra=ra_data*u.deg, dec=dec_data*u.deg, frame='icrs')
sn_l = sn_coords.galactic.l.rad
sn_b = sn_coords.galactic.b.rad
ax2.scatter(sn_l, sn_b, c='lightgray', s=10, alpha=0.6)
# Galactic plane (b = 0°)
l_line = np.linspace(-np.pi, np.pi, 360)
ax2.plot(l_line, np.zeros_like(l_line), color='yellow', linewidth=1.5)
ax2.annotate(
'',
xy=(dipole_l_rad, dipole_b_rad),
xytext=(dipole_l_rad - 0.3, dipole_b_rad - 0.3),
arrowprops=dict(facecolor='red', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax2.set_title("Dipole Direction (Galactic Coordinates)\nl / b", fontsize=12)
# ============================================================
# (C) Dipole direction as a 3D vector
# ============================================================
ax3 = fig.add_subplot(133, projection='3d')
# Origin
ax3.scatter(0, 0, 0, color='black', s=50)
# Dipole vector
ax3.quiver(
0, 0, 0,
Ax, Ay, Az,
color='red',
linewidth=3,
arrow_length_ratio=0.1
)
ax3.set_xlabel("X")
ax3.set_ylabel("Y")
ax3.set_zlabel("Z")
max_range = np.max(np.abs([Ax, Ay, Az])) * 1.5
ax3.set_xlim([-max_range, max_range])
ax3.set_ylim([-max_range, max_range])
ax3.set_zlim([-max_range, max_range])
ax3.set_title("Dipole Direction (3D Vector)", fontsize=12)
plt.tight_layout()
plt.savefig("dipole_direction_comparison.png", dpi=300, bbox_inches='tight')
plt.close()
print(" -> Saved dipole_direction_comparison.png")
print("🎉 Comparison plot generated!")
# =====================================================================
# 14. Dipole Direction Animation (with progress bar)
# =====================================================================
print("\n--- 14. Generating Dipole Direction Animation (with progress bar) ---")
import matplotlib.pyplot as plt
import numpy as np
from astropy.coordinates import SkyCoord
import astropy.units as u
from mpl_toolkits.mplot3d import Axes3D
from tqdm import tqdm
import os
# ---------------------------------------------
# 1. Coordinate conversion (using previously computed values)
# ---------------------------------------------
dipole_ra_rad = np.deg2rad(dipole_ra_deg)
dipole_dec_rad = np.deg2rad(dipole_dec_deg)
dipole_ra_rad = np.remainder(dipole_ra_rad + np.pi, 2*np.pi) - np.pi
dipole_l_rad = np.deg2rad(dipole_l_deg)
dipole_b_rad = np.deg2rad(dipole_b_deg)
dipole_l_rad = np.remainder(dipole_l_rad + np.pi, 2*np.pi) - np.pi
Ax, Ay, Az = A_vec
# SN Galactic coordinates
sn_coords = SkyCoord(ra=ra_data*u.deg, dec=dec_data*u.deg, frame='icrs')
sn_l = sn_coords.galactic.l.rad
sn_b = sn_coords.galactic.b.rad
# ---------------------------------------------
# 2. Output folder
# ---------------------------------------------
os.makedirs("dipole_frames", exist_ok=True)
# ---------------------------------------------
# 3. Generate frames (with progress bar)
# ---------------------------------------------
n_frames = 180
for frame in tqdm(range(n_frames), desc="Generating frames"):
angle = frame * np.pi / 180.0
fig = plt.figure(figsize=(18, 6))
ax1 = fig.add_subplot(131, projection='mollweide')
ax2 = fig.add_subplot(132, projection='mollweide')
ax3 = fig.add_subplot(133, projection='3d')
# ============================================================
# (A) Equatorial coordinates
# ============================================================
ax1.grid(True, color='gray', linestyle='--', alpha=0.5)
ax1.scatter(ra_rad, dec_rad, c='lightgray', s=10, alpha=0.6)
ax1.annotate(
'',
xy=(dipole_ra_rad, dipole_dec_rad),
xytext=(dipole_ra_rad - 0.3*np.cos(angle),
dipole_dec_rad - 0.3*np.sin(angle)),
arrowprops=dict(facecolor='red', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax1.set_title("Equatorial Coordinates (RA/DEC)", fontsize=12)
# ============================================================
# (B) Galactic coordinates
# ============================================================
ax2.grid(True, color='gray', linestyle='--', alpha=0.5)
ax2.scatter(sn_l, sn_b, c='lightgray', s=10, alpha=0.6)
l_line = np.linspace(-np.pi, np.pi, 360)
ax2.plot(l_line, np.zeros_like(l_line), color='yellow', linewidth=1.5)
ax2.annotate(
'',
xy=(dipole_l_rad, dipole_b_rad),
xytext=(dipole_l_rad - 0.3*np.cos(angle),
dipole_b_rad - 0.3*np.sin(angle)),
arrowprops=dict(facecolor='red', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax2.set_title("Galactic Coordinates (l/b)", fontsize=12)
# ============================================================
# (C) 3D dipole vector
# ============================================================
ax3.scatter(0, 0, 0, color='black', s=50)
ax3.set_xlabel("X")
ax3.set_ylabel("Y")
ax3.set_zlabel("Z")
max_range = np.max(np.abs([Ax, Ay, Az])) * 1.5
ax3.set_xlim([-max_range, max_range])
ax3.set_ylim([-max_range, max_range])
ax3.set_zlim([-max_range, max_range])
R = np.array([
[np.cos(angle), -np.sin(angle), 0],
[np.sin(angle), np.cos(angle), 0],
[0, 0, 1]
])
vec_rot = R @ np.array([Ax, Ay, Az])
ax3.quiver(
0, 0, 0,
vec_rot[0], vec_rot[1], vec_rot[2],
color='red',
linewidth=3,
arrow_length_ratio=0.1
)
ax3.set_title("3D Dipole Vector", fontsize=12)
plt.savefig(f"dipole_frames/frame_{frame:03d}.png", dpi=150)
plt.close()
# ---------------------------------------------
# 4. ffmpeg to create mp4
# ---------------------------------------------
print("Generating mp4 with ffmpeg...")
os.system("ffmpeg -y -framerate 20 -i dipole_frames/frame_%03d.png -c:v libx264 -pix_fmt yuv420p dipole_direction_animation.mp4")
print("🎉 Saved dipole_direction_animation.mp4!")
# =====================================================================
# 15. z-dependent Dipole Animation (with progress bar)
# =====================================================================
print("\n--- 15. Generating z-dependent Dipole Animation ---")
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from astropy.coordinates import SkyCoord
import astropy.units as u
from mpl_toolkits.mplot3d import Axes3D
from tqdm import tqdm
# ---------------------------------------------
# 1. Redshift binning
# ---------------------------------------------
n_bins = 12
z_bins = np.linspace(np.min(z_data), np.max(z_data), n_bins+1)
dipole_vectors = []
for i in tqdm(range(n_bins), desc="Dipole fitting by z-bin"):
z_min = z_bins[i]
z_max = z_bins[i+1]
mask = (z_data >= z_min) & (z_data < z_max)
if np.sum(mask) < 20:
dipole_vectors.append((0,0,0))
continue
r_bin = whitened_residual[mask]
ra_bin = ra_rad[mask]
dec_bin = dec_rad[mask]
nx = np.cos(dec_bin) * np.cos(ra_bin)
ny = np.cos(dec_bin) * np.sin(ra_bin)
nz = np.sin(dec_bin)
X = np.vstack([nx, ny, nz]).T
A_vec_bin, _, _, _ = np.linalg.lstsq(X, r_bin, rcond=None)
dipole_vectors.append(A_vec_bin)
dipole_vectors = np.array(dipole_vectors)
# ---------------------------------------------
# 2. Animation setup
# ---------------------------------------------
fig = plt.figure(figsize=(18, 6))
ax1 = fig.add_subplot(131, projection='mollweide')
ax2 = fig.add_subplot(132, projection='mollweide')
ax3 = fig.add_subplot(133, projection='3d')
sn_coords = SkyCoord(ra=ra_data*u.deg, dec=dec_data*u.deg, frame='icrs')
sn_l = sn_coords.galactic.l.rad
sn_b = sn_coords.galactic.b.rad
# ---------------------------------------------
# 3. Initialization
# ---------------------------------------------
def init():
ax1.grid(True, color='gray', linestyle='--', alpha=0.5)
ax1.scatter(ra_rad, dec_rad, c='lightgray', s=10, alpha=0.6)
ax1.set_title("Equatorial Dipole (z-dependent)", fontsize=12)
ax2.grid(True, color='gray', linestyle='--', alpha=0.5)
ax2.scatter(sn_l, sn_b, c='lightgray', s=10, alpha=0.6)
l_line = np.linspace(-np.pi, np.pi, 360)
ax2.plot(l_line, np.zeros_like(l_line), color='yellow', linewidth=1.5)
ax2.set_title("Galactic Dipole (z-dependent)", fontsize=12)
ax3.scatter(0, 0, 0, color='black', s=50)
ax3.set_xlabel("X")
ax3.set_ylabel("Y")
ax3.set_zlabel("Z")
ax3.set_title("3D Dipole Vector (z-dependent)", fontsize=12)
return []
# ---------------------------------------------
# 4. Frame update
# ---------------------------------------------
def update(frame):
Ax_bin, Ay_bin, Az_bin = dipole_vectors[frame]
ax1.cla()
ax1.grid(True, color='gray', linestyle='--', alpha=0.5)
ax1.scatter(ra_rad, dec_rad, c='lightgray', s=10, alpha=0.6)
A = np.sqrt(Ax_bin**2 + Ay_bin**2 + Az_bin**2)
if A > 0:
dec = np.arcsin(Az_bin / A)
ra = np.arctan2(Ay_bin, Ax_bin)
ra = np.remainder(ra + np.pi, 2*np.pi) - np.pi
ax1.annotate(
'',
xy=(ra, dec),
xytext=(ra - 0.3, dec - 0.3),
arrowprops=dict(facecolor='red', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax1.set_title(f"Equatorial Dipole (z-bin {frame+1}/{n_bins})", fontsize=12)
ax2.cla()
ax2.grid(True, color='gray', linestyle='--', alpha=0.5)
ax2.scatter(sn_l, sn_b, c='lightgray', s=10, alpha=0.6)
l_line = np.linspace(-np.pi, np.pi, 360)
ax2.plot(l_line, np.zeros_like(l_line), color='yellow', linewidth=1.5)
if A > 0:
coord_bin = SkyCoord(
x=Ax_bin, y=Ay_bin, z=Az_bin,
representation_type='cartesian'
).represent_as('spherical')
dipole_coord = SkyCoord(
ra=coord_bin.lon, dec=coord_bin.lat, frame='icrs'
).galactic
l = dipole_coord.l.rad
b = dipole_coord.b.rad
l = np.remainder(l + np.pi, 2*np.pi) - np.pi
ax2.annotate(
'',
xy=(l, b),
xytext=(l - 0.3, b - 0.3),
arrowprops=dict(facecolor='red', edgecolor='black',
shrink=0.05, width=2, headwidth=10)
)
ax2.set_title(f"Galactic Dipole (z-bin {frame+1}/{n_bins})", fontsize=12)
ax3.cla()
ax3.scatter(0, 0, 0, color='black', s=50)
ax3.set_xlabel("X")
ax3.set_ylabel("Y")
ax3.set_zlabel("Z")
ax3.set_title(f"3D Dipole Vector (z-bin {frame+1}/{n_bins})", fontsize=12)
ax3.quiver(
0, 0, 0,
Ax_bin, Ay_bin, Az_bin,
color='red',
linewidth=3,
arrow_length_ratio=0.1
)
max_range = np.max(np.abs(dipole_vectors)) * 1.5
ax3.set_xlim([-max_range, max_range])
ax3.set_ylim([-max_range, max_range])
ax3.set_zlim([-max_range, max_range])
return []
# ---------------------------------------------
# 5. Generate animation
# ---------------------------------------------
print("Generating animation...")
anim = FuncAnimation(fig, update, init_func=init, frames=n_bins, interval=200)
anim.save("dipole_z_dependence_animation.mp4", fps=10, dpi=300)
print("🎉 Saved dipole_z_dependence_animation.mp4!")
# =====================================================================
# 16. 3D Dipole Track Animation (z-dependent, with progress bar)
# =====================================================================
print("\n--- 16. Generating 3D Dipole Track Animation (z-dependent) ---")
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from tqdm import tqdm
import os
# ---------------------------------------------
# 1. Use dipole_vectors computed in Section 15
# ---------------------------------------------
# dipole_vectors: shape = (n_bins, 3)
# ---------------------------------------------
# 2. Output folder
# ---------------------------------------------
os.makedirs("dipole_track_frames", exist_ok=True)
# ---------------------------------------------
# 3. Generate animation frames (with progress bar)
# ---------------------------------------------
n_frames = len(dipole_vectors)
# 3D scale
max_range = np.max(np.abs(dipole_vectors)) * 1.5
for frame in tqdm(range(n_frames), desc="Generating 3D track frames"):
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection='3d')
# Origin
ax.scatter(0, 0, 0, color='black', s=50)
# Current dipole vector
Ax_bin, Ay_bin, Az_bin = dipole_vectors[frame]
# Track up to current frame
track = dipole_vectors[:frame+1]
ax.plot(track[:,0], track[:,1], track[:,2],
color='blue', linewidth=2, label='Dipole Track')
# Current dipole vector arrow
ax.quiver(
0, 0, 0,
Ax_bin, Ay_bin, Az_bin,
color='red',
linewidth=3,
arrow_length_ratio=0.1
)
# Axis labels
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
# Scale
ax.set_xlim([-max_range, max_range])
ax.set_ylim([-max_range, max_range])
ax.set_zlim([-max_range, max_range])
ax.set_title(f"Dipole 3D Track (z-bin {frame+1}/{n_frames})", fontsize=13)
plt.savefig(f"dipole_track_frames/frame_{frame:03d}.png", dpi=150)
plt.close()
# ---------------------------------------------
# 4. Combine frames into mp4 using ffmpeg
# ---------------------------------------------
print("Generating mp4 with ffmpeg...")
os.system("ffmpeg -y -framerate 10 -i dipole_track_frames/frame_%03d.png -c:v libx264 -pix_fmt yuv420p dipole_3D_track_animation.mp4")
print("🎉 Saved dipole_3D_track_animation.mp4!")
# =====================================================================
# 17. Dipole Amplitude as a Function of Redshift
# =====================================================================
print("\n--- 17. Generating Dipole Amplitude vs Redshift Plot ---")
import numpy as np
import matplotlib.pyplot as plt
dipole_amplitudes = np.linalg.norm(dipole_vectors, axis=1)
# z-bin centers
z_bin_centers = 0.5 * (z_bins[:-1] + z_bins[1:])
plt.figure(figsize=(8, 5))
plt.plot(z_bin_centers, dipole_amplitudes, marker='o', color='red', linewidth=2)
plt.xlabel("Redshift z")
plt.ylabel("Dipole Amplitude |A|")
plt.title("Dipole Amplitude as a Function of Redshift", fontsize=13)
plt.grid(True, linestyle='--', alpha=0.5)
plt.savefig("dipole_amplitude_vs_z.png", dpi=300, bbox_inches='tight')
plt.close()
print(" -> Saved dipole_amplitude_vs_z.png")
# =====================================================================
# 18. Dipole Direction (RA/DEC, l/b) as a Function of Redshift
# =====================================================================
print("\n--- 18. Generating Dipole Direction vs Redshift Plot ---")
import numpy as np
import matplotlib.pyplot as plt
from astropy.coordinates import SkyCoord
import astropy.units as u
z_bin_centers = 0.5 * (z_bins[:-1] + z_bins[1:])
RA_list = []
DEC_list = []
l_list = []
b_list = []
for Ax_bin, Ay_bin, Az_bin in dipole_vectors:
A = np.sqrt(Ax_bin**2 + Ay_bin**2 + Az_bin**2)
if A == 0:
RA_list.append(np.nan)
DEC_list.append(np.nan)
l_list.append(np.nan)
b_list.append(np.nan)
continue
# RA/DEC
dec = np.arcsin(Az_bin / A)
ra = np.arctan2(Ay_bin, Ax_bin)
ra = np.remainder(ra + np.pi, 2*np.pi) - np.pi
RA_list.append(np.rad2deg(ra))
DEC_list.append(np.rad2deg(dec))
# Galactic coordinates
coord_bin = SkyCoord(
x=Ax_bin, y=Ay_bin, z=Az_bin,
representation_type='cartesian'
).represent_as('spherical')
dipole_coord = SkyCoord(
ra=coord_bin.lon, dec=coord_bin.lat, frame='icrs'
).galactic
l_list.append(dipole_coord.l.deg)
b_list.append(dipole_coord.b.deg)
plt.figure(figsize=(10, 8))
plt.subplot(2, 2, 1)
plt.plot(z_bin_centers, RA_list, marker='o', color='blue')
plt.xlabel("Redshift z")
plt.ylabel("Dipole RA (deg)")
plt.title("Dipole RA vs Redshift")
plt.grid(True, linestyle='--', alpha=0.5)
plt.subplot(2, 2, 2)
plt.plot(z_bin_centers, DEC_list, marker='o', color='green')
plt.xlabel("Redshift z")
plt.ylabel("Dipole DEC (deg)")
plt.title("Dipole DEC vs Redshift")
plt.grid(True, linestyle='--', alpha=0.5)
plt.subplot(2, 2, 3)
plt.plot(z_bin_centers, l_list, marker='o', color='red')
plt.xlabel("Redshift z")
plt.ylabel("Galactic Longitude l (deg)")
plt.title("Dipole l vs Redshift")
plt.grid(True, linestyle='--', alpha=0.5)
plt.subplot(2, 2, 4)
plt.plot(z_bin_centers, b_list, marker='o', color='purple')
plt.xlabel("Redshift z")
plt.ylabel("Galactic Latitude b (deg)")
plt.title("Dipole b vs Redshift")
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
plt.savefig("dipole_direction_vs_z.png", dpi=300, bbox_inches='tight')
plt.close()
print(" -> Saved dipole_direction_vs_z.png")
# =====================================================================
# 19. Dipole Significance via χ² and p-value
# =====================================================================
print("\n--- 19. Evaluating Dipole Significance (χ² & p-value) ---")
import numpy as np
from scipy.stats import chi2
r = whitened_residual
nx = np.cos(dec_rad) * np.cos(ra_rad)
ny = np.cos(dec_rad) * np.sin(ra_rad)
nz = np.sin(dec_rad)
X = np.vstack([nx, ny, nz]).T
Ax, Ay, Az = A_vec
model_dipole = Ax * nx + Ay * ny + Az * nz
chi2_no_dipole = np.sum(r**2)
chi2_with_dipole = np.sum((r - model_dipole)**2)
delta_chi2 = chi2_no_dipole - chi2_with_dipole
p_value = 1 - chi2.cdf(delta_chi2, df=3)
print(f" -> χ²(no dipole) = {chi2_no_dipole:.4f}")
print(f" -> χ²(with dipole) = {chi2_with_dipole:.4f}")
print(f" -> Δχ² = {delta_chi2:.4f}")
print(f" -> p-value = {p_value:.4e}")
# =====================================================================
# 20. Bayesian Evidence (log-evidence)
# =====================================================================
print("\n--- 20. Computing Bayesian Evidence (log-evidence) ---")
import numpy as np
from numpy.linalg import det
r = whitened_residual
nx = np.cos(dec_rad) * np.cos(ra_rad)
ny = np.cos(dec_rad) * np.sin(ra_rad)
nz = np.sin(dec_rad)
X = np.vstack([nx, ny, nz]).T
Ax, Ay, Az = A_vec
model_dipole = Ax * nx + Ay * ny + Az * nz
chi2_no_dipole = np.sum(r**2)
chi2_with_dipole = np.sum((r - model_dipole)**2)
F = X.T @ X
cov_A = np.linalg.inv(F)
logZ_dipole = -0.5 * chi2_with_dipole + 0.5 * np.log((2*np.pi)**3 * det(cov_A))
logZ_iso = -0.5 * chi2_no_dipole
log_Bayes_factor = logZ_dipole - logZ_iso
Bayes_factor = np.exp(log_Bayes_factor)
print(f" -> log Evidence(dipole) = {logZ_dipole:.4f}")
print(f" -> log Evidence(iso) = {logZ_iso:.4f}")
print(f" -> log Bayes factor = {log_Bayes_factor:.4f}")
print(f" -> Bayes factor = {Bayes_factor:.4f}")
# =====================================================================
# 21. z-dependent Bayesian Evidence (log Bayes factor)
# =====================================================================
print("\n--- 21. Computing z-dependent Bayesian Evidence (log Bayes factor) ---")
logZ_dipole_list = []
logZ_iso_list = []
logBF_list = []
BF_list = []
z_bin_centers = 0.5 * (z_bins[:-1] + z_bins[1:])
for i in range(len(dipole_vectors)):
Ax_bin, Ay_bin, Az_bin = dipole_vectors[i]
z_min = z_bins[i]
z_max = z_bins[i+1]
mask = (z_data >= z_min) & (z_data < z_max)
if np.sum(mask) < 20:
logZ_dipole_list.append(np.nan)
logZ_iso_list.append(np.nan)
logBF_list.append(np.nan)
BF_list.append(np.nan)
continue
r_bin = whitened_residual[mask]
ra_bin = ra_rad[mask]
dec_bin = dec_rad[mask]
nx = np.cos(dec_bin) * np.cos(ra_bin)
ny = np.cos(dec_bin) * np.sin(ra_bin)
nz = np.sin(dec_bin)
X = np.vstack([nx, ny, nz]).T
model_bin = Ax_bin * nx + Ay_bin * ny + Az_bin * nz
chi2_iso = np.sum(r_bin**2)
chi2_dipole = np.sum((r_bin - model_bin)**2)
F = X.T @ X
cov_A = np.linalg.inv(F)
logZ_dipole = -0.5 * chi2_dipole + 0.5 * np.log((2*np.pi)**3 * det(cov_A))
logZ_iso = -0.5 * chi2_iso
logBF = logZ_dipole - logZ_iso
BF = np.exp(logBF)
logZ_dipole_list.append(logZ_dipole)
logZ_iso_list.append(logZ_iso)
logBF_list.append(logBF)
BF_list.append(BF)
plt.figure(figsize=(10, 6))
plt.plot(z_bin_centers, logBF_list, marker='o', color='red', linewidth=2)
plt.xlabel("Redshift z")
plt.ylabel("log Bayes factor")
plt.title("z-dependent Bayesian Evidence (log Bayes factor)", fontsize=13)
plt.grid(True, linestyle='--', alpha=0.5)
plt.savefig("bayes_factor_vs_z.png", dpi=300, bbox_inches='tight')
plt.close()
print(" -> Saved bayes_factor_vs_z.png")
```
---
**Next:** [Anisotropic Analysis of Luminosity Distance Residuals from Pantheon+ Type Ia Supernovae: Re-evaluating Dipole Significance and Redshift Dependence Using Whitening and Equal-Count Binning](https://talkwithgai.blogspot.com/2026/07/blog-post_809.html)
コメント
コメントを投稿