---
title: "SQUDE vs Chandra 三分区光谱对比"
subtitle: "在 SQUDE 模拟谱上 overlay Chandra CCD 实测谱 + best-fit model"
author: "黄瑞"
date: "2026-07-16"
format:
html:
theme: darkly
toc: true
toc-depth: 3
code-fold: true
code-tools: true
embed-resources: false
html-math-method: mathjax
page-layout: full
css: |
body { max-width: 960px; margin: 0 auto; }
figure img { max-width: 100%; }
include-in-header:
text: |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
lang: zh
execute:
echo: true
warning: false
message: true
jupyter: sherpa-m82
---
## 概述
本教程在 **SQUDE 50 ks 模拟谱** 上 overlay **Chandra ACIS 实测谱 + best-fit model**,
对比两种仪器在 M82 三分区上的分辨率与灵敏度差异。
**关键对比**:
| 仪器 | 能段 | 分辨率 (FWHM) | M82 有效面积 | 本教程曝光 |
|------|------|--------------|-------------|-----------|
| **Chandra ACIS-S/CCD** | 0.3–10 keV | ~150 eV @ 1 keV | ~50 cm² @ 1 keV | 137 ks (8 ObsID) |
| **SQUDE TES 微量热器** | 0.3–4 keV | ~4 eV | ~125 cm² @ 0.5 keV | 50 ks (单次指向) |
**分辨率差异**: SQUDE 比 Chandra 高 ~40 倍。
::: {.callout-important}
## 关键看点
- **Chandra 数据**: 宽 bin, 计数率高, 但相邻发射线被合并成宽鼓包
- **Chandra model**: best-fit 光谱模型 (powerlaw + 3 emission lines)
- **SQUDE 模拟**: 窄 bin, 4 eV 分辨率, 可以清晰分辨 O VII/VIII 三重态
:::
---
## Step 1: 环境设置 {#step1}
```{python}
#| label: setup
#| output: false
import os
from pathlib import Path
os.environ.setdefault("MPLCONFIGDIR", "./mpl-cache")
os.environ.setdefault("XDG_CACHE_HOME", "./xdg-cache")
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import scienceplots # noqa: F401
import numpy as np
from matplotlib.ticker import FixedLocator, FuncFormatter
from matplotlib.lines import Line2D
plt.style.use(["science", "no-latex"])
from sherpa.astro.ui import *
print("Environment ready")
```
---
## Step 2: 加载 Chandra 数据并拟合 {#step2}
对每个 region 加载 Chandra 早期 100 ks PHA,用 sherpa-native 模型 (powerlaw + 3 Gaussians) 拟合。
```{python}
#| label: chandra-load-fit
#| output: true
PHA_DIR = Path(os.environ.get("M82_PHA_DIR", "pha"))
BINS = ["v80_north", "v40_center", "v80_south"]
BIN_LABELS = {
"v80_north": 'North wind (80"×80")',
"v40_center": 'Center starburst (40"×80")',
"v80_south": 'South wind (80"×80")',
}
CHANDRA_COLORS = ["dimgray", "dimgray", "dimgray"]
SQUDE_COLORS = ["tab:blue", "tab:red", "tab:purple"]
# Load Chandra data + fit a simple model for each region
chandra_results = {}
for bin_name in BINS:
clean()
pha_path = PHA_DIR / f"{bin_name}_early100ks_combined_src.pi"
arf_path = PHA_DIR / f"{bin_name}_early100ks_combined_src.arf"
rmf_path = PHA_DIR / f"{bin_name}_early100ks_combined_src.rmf"
load_pha(str(pha_path))
load_arf(str(arf_path))
load_rmf(str(rmf_path))
set_analysis("energy")
notice(0.3, 7.0)
group_counts(20)
# sherpa-native model: 3rd-order polynomial continuum + 4 emission lines
# More flexible than powerlaw for thermal plasma; better matches Chandra data
set_source(polynom1d.poly3 + gauss1d.g1 + gauss1d.g2 + gauss1d.g3 + gauss1d.g4)
g1.pos = 0.65; g1.fwhm = 0.10 # O VIII Lyα
g2.pos = 1.02; g2.fwhm = 0.10 # Ne X Lyα
g3.pos = 1.33; g3.fwhm = 0.10 # Mg XI
g4.pos = 1.74; g4.fwhm = 0.10 # Si XIII
fit()
data = get_data_plot()
mdl = get_model_plot()
chandra_results[bin_name] = {
"x": data.x.copy(),
"y": data.y.copy(),
"yerr": data.yerr.copy(),
"model_x": mdl.x.copy(),
"model_y": mdl.y.copy(),
"exposure": get_data().exposure,
"raw_counts": get_data().counts.sum(),
}
print(f"{bin_name}: exposure={get_data().exposure:.0f}s, "
f"raw counts={get_data().counts.sum():.0f}, "
f"grouped bins={len(data.x)}")
print("\nAll 3 regions loaded and fit with Chandra data.")
```
---
## Step 3: 加载 SQUDE 模拟数据 {#step3}
```{python}
#| label: squde-load
#| output: true
# Load SQUDE simulated PHA (from previous tutorial)
squde_results = {}
for bin_name in BINS:
clean()
pha_path = PHA_DIR / f"{bin_name}_squde_50ks.pha"
load_pha(str(pha_path))
load_arf(str(PHA_DIR / "SQUDE_rsp_v1.1.1.arf"))
load_rmf(str(PHA_DIR / "SQUDE_rsp_v1.1.2.rmf"))
set_analysis("energy")
notice(0.3, 7.0)
group_counts(20)
data = get_data_plot()
squde_results[bin_name] = {
"x": data.x.copy(),
"y": data.y.copy(),
"yerr": data.yerr.copy(),
"exposure": get_data().exposure,
"raw_counts": get_data().counts.sum(),
}
print(f"{bin_name}: exposure={get_data().exposure:.0f}s, "
f"raw counts={get_data().counts.sum():.0f}, "
f"grouped bins={len(data.x)}")
print("\nSQUDE 50 ks simulated data loaded.")
```
---
## Step 4: 三分区对比图 — Chandra + SQUDE Overlay {#step4}
::: {.callout-note}
## 图例说明
- **灰色粗点带误差棒**: Chandra ACIS 实测数据 (137 ks, group_counts=20)
- **灰色细线**: Chandra best-fit model (powerlaw + 3 Gaussians)
- **彩色细线**: SQUDE 模拟数据 (50 ks, group_counts=20)
- **彩色虚线**: SQUDE 数据点的连线(强调高分辨率结构)
:::
```{python}
#| label: plot-overlay-3panels
#| output: true
#| fig-cap: "M82 三分区:Chandra 实测 + SQUDE 模拟 overlay"
#| fig-width: 14
#| fig-height: 10
fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)
for i, bin_name in enumerate(BINS):
ax = axes[i]
# --- Chandra data ---
ch = chandra_results[bin_name]
ax.errorbar(
ch["x"], ch["y"], yerr=ch["yerr"],
fmt="o", ms=3.5, color="dimgray", alpha=0.7,
label=f'Chandra ACIS ({ch["exposure"]/1000:.0f} ks)',
)
# --- Chandra model ---
ax.plot(
ch["model_x"], ch["model_y"],
color="black", lw=1.2, alpha=0.7,
label="Chandra best-fit model",
)
# --- SQUDE data ---
sq = squde_results[bin_name]
color = SQUDE_COLORS[i]
ax.errorbar(
sq["x"], sq["y"], yerr=sq["yerr"],
fmt=".", ms=2.0, color=color, alpha=0.7,
label=f'SQUDE sim (50 ks)',
)
ax.plot(
sq["x"], sq["y"],
color=color, lw=0.5, alpha=0.5,
)
# --- Format ---
ax.set_yscale("log")
ax.set_xscale("log")
ax.set_xlim(0.4, 2.5)
ax.set_ylabel(r"Counts s$^{-1}$ keV$^{-1}$")
ax.set_title(BIN_LABELS[bin_name], loc="left", fontsize=11)
ax.legend(loc="upper right", fontsize=8, framealpha=0.92)
ax.grid(True, which="both", alpha=0.22)
ax.xaxis.set_major_locator(FixedLocator([0.5, 0.7, 1.0, 1.5, 2.0]))
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{x:g}"))
axes[-1].set_xlabel("Energy (keV)")
fig.suptitle(
"M82 Three Vertical Regions: Chandra ACIS + SQUDE Overlay\n"
"(Chandra ~150 eV FWHM vs SQUDE ~4 eV FWHM)",
fontsize=13, y=0.995,
)
fig.subplots_adjust(left=0.07, right=0.98, top=0.94, bottom=0.08, hspace=0.18)
# Save to figures/ for web deployment
outpath = Path("figures/m82_chandra_squde_overlay_3panels.png")
outpath.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(outpath, dpi=180)
# plt.show() # Quarto handles fig-cap # Let Quarto embed the figure
print(f"Saved: {outpath}")
```

---
## Step 5: 单区域放大 — 中心星暴区 {#step5}
```{python}
#| label: plot-center-zoom
#| output: true
#| fig-cap: "中心 40×80 arcsec: Chandra + SQUDE 详细对比"
#| fig-width: 12
#| fig-height: 5
fig, ax = plt.subplots(figsize=(12, 5))
bin_name = "v40_center"
ch = chandra_results[bin_name]
sq = squde_results[bin_name]
# Chandra
ax.errorbar(ch["x"], ch["y"], yerr=ch["yerr"],
fmt="o", ms=4, color="dimgray", alpha=0.8,
label=f'Chandra ACIS-S ({ch["exposure"]/1000:.0f} ks, group=20)')
ax.plot(ch["model_x"], ch["model_y"], color="black", lw=1.4, alpha=0.7,
label="Chandra best-fit model")
# SQUDE
ax.errorbar(sq["x"], sq["y"], yerr=sq["yerr"],
fmt=".", ms=2.0, color="tab:red", alpha=0.8,
label='SQUDE sim (50 ks, group=20)')
ax.plot(sq["x"], sq["y"], color="tab:red", lw=0.5, alpha=0.5)
ax.set_yscale("log")
ax.set_xscale("log")
ax.set_xlim(0.5, 2.0)
ax.set_xlabel("Energy (keV)")
ax.set_ylabel(r"Counts s$^{-1}$ keV$^{-1}$")
ax.set_title("M82 Center (40\"×80\"): Chandra vs SQUDE — Resolution Comparison")
ax.legend(loc="upper right", fontsize=9, framealpha=0.92)
ax.grid(True, which="both", alpha=0.22)
ax.xaxis.set_major_locator(FixedLocator([0.5, 0.7, 1.0, 1.5, 2.0]))
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{x:g}"))
# Mark key emission lines
for E, label in [(0.57, "O VII triplet"), (0.65, "O VIII Lyα"),
(1.02, "Ne X Lyα"), (1.33, "Mg XI"),
(1.47, "Mg XII Lyα")]:
ax.axvline(E, color="orange", ls=":", lw=0.6, alpha=0.5)
ax.text(E, ax.get_ylim()[1]*0.95, label, rotation=90, fontsize=7,
va="top", ha="right", color="orange", alpha=0.8)
fig.subplots_adjust(left=0.07, right=0.98, top=0.94, bottom=0.10)
outpath = Path("figures/m82_center_chandra_squde_zoom.png")
fig.savefig(outpath, dpi=180)
print(f"Saved: {outpath}")
```

---
## Step 6: 关键发射线区域放大对比 {#step6}
### 6.1 O VII/VIII (0.54–0.69 keV)
```{python}
#| label: plot-o-zoom
#| output: true
#| fig-cap: "O VII 三重态区域 (0.54–0.69 keV) — Chandra vs SQUDE"
#| fig-width: 10
#| fig-height: 4
fig, ax = plt.subplots(figsize=(10, 4))
for bin_name in BINS:
ch = chandra_results[bin_name]
sq = squde_results[bin_name]
color = SQUDE_COLORS[BINS.index(bin_name)]
# Chandra
mask_ch = (ch["x"] >= 0.54) & (ch["x"] <= 0.69)
if mask_ch.sum() > 0:
ax.errorbar(ch["x"][mask_ch], ch["y"][mask_ch], yerr=ch["yerr"][mask_ch],
fmt="o", ms=5, color="dimgray", alpha=0.7)
ax.plot(ch["model_x"][(ch["model_x"]>=0.54)&(ch["model_x"]<=0.69)],
ch["model_y"][(ch["model_x"]>=0.54)&(ch["model_x"]<=0.69)],
color="black", lw=1.2, alpha=0.6)
# SQUDE
mask_sq = (sq["x"] >= 0.54) & (sq["x"] <= 0.69)
if mask_sq.sum() > 0:
ax.errorbar(sq["x"][mask_sq], sq["y"][mask_sq], yerr=sq["yerr"][mask_sq],
fmt=".", ms=2.5, color=color, alpha=0.75,
label=f'{BIN_LABELS[bin_name]} (SQUDE)')
ax.plot(sq["x"][mask_sq], sq["y"][mask_sq], color=color, lw=0.5, alpha=0.5)
ax.set_xlim(0.54, 0.69)
ax.set_yscale("log")
ax.set_xlabel("Energy (keV)")
ax.set_ylabel(r"Counts s$^{-1}$ keV$^{-1}$")
ax.set_title("O VII Triplet + O VIII Lyα Region (0.54–0.69 keV)")
# Add O VII triplet line positions
for E, lbl in [(0.561, "O VII r"), (0.569, "O VII i"), (0.574, "O VII f"),
(0.654, "O VIII Lyα")]:
ax.axvline(E, color="orange", ls=":", lw=0.5, alpha=0.4)
ax.legend(loc="upper right", fontsize=8, framealpha=0.92)
ax.grid(True, alpha=0.25)
outpath = Path("figures/m82_o_region_chandra_squde.png")
fig.savefig(outpath, dpi=180)
print(f"Saved: {outpath}")
```

### 6.2 Fe L-shell (0.8–1.2 keV)
```{python}
#| label: plot-fe-zoom
#| output: true
#| fig-cap: "Fe L-shell 区域 (0.8–1.2 keV) — Chandra vs SQUDE"
#| fig-width: 10
#| fig-height: 4
fig, ax = plt.subplots(figsize=(10, 4))
for bin_name in BINS:
ch = chandra_results[bin_name]
sq = squde_results[bin_name]
color = SQUDE_COLORS[BINS.index(bin_name)]
mask_ch = (ch["x"] >= 0.8) & (ch["x"] <= 1.2)
if mask_ch.sum() > 0:
ax.errorbar(ch["x"][mask_ch], ch["y"][mask_ch], yerr=ch["yerr"][mask_ch],
fmt="o", ms=5, color="dimgray", alpha=0.7)
ax.plot(ch["model_x"][(ch["model_x"]>=0.8)&(ch["model_x"]<=1.2)],
ch["model_y"][(ch["model_x"]>=0.8)&(ch["model_x"]<=1.2)],
color="black", lw=1.2, alpha=0.6)
mask_sq = (sq["x"] >= 0.8) & (sq["x"] <= 1.2)
if mask_sq.sum() > 0:
ax.errorbar(sq["x"][mask_sq], sq["y"][mask_sq], yerr=sq["yerr"][mask_sq],
fmt=".", ms=2.5, color=color, alpha=0.75,
label=f'{BIN_LABELS[bin_name]} (SQUDE)')
ax.plot(sq["x"][mask_sq], sq["y"][mask_sq], color=color, lw=0.5, alpha=0.5)
ax.set_xlim(0.8, 1.2)
ax.set_yscale("log")
ax.set_xlabel("Energy (keV)")
ax.set_ylabel(r"Counts s$^{-1}$ keV$^{-1}$")
ax.set_title("Fe L-shell Complex + Ne IX/X Region (0.8–1.2 keV)")
ax.legend(loc="upper right", fontsize=8, framealpha=0.92)
ax.grid(True, alpha=0.25)
outpath = Path("figures/m82_fe_l_chandra_squde.png")
fig.savefig(outpath, dpi=180)
print(f"Saved: {outpath}")
```

---
## Step 7: 计数与曝光对比统计 {#step7}
```{python}
#| label: stats-comparison
#| output: true
print("=" * 80)
print("Chandra ACIS vs SQUDE — 三分区计数对比")
print("=" * 80)
print()
print(f"{'区域':<22} {'Chandra 137 ks':>16} {'SQUDE 50 ks':>14} {'比值':>10}")
print(f"{'':22} {'counts':>16} {'counts':>14} {'SQUDE/Chandra':>10}")
print("-" * 80)
for bin_name in BINS:
ch_count = chandra_results[bin_name]["raw_counts"]
sq_count = squde_results[bin_name]["raw_counts"]
ratio = sq_count / ch_count if ch_count > 0 else 0
# Normalize to per-second rate for fair comparison
ch_rate = ch_count / chandra_results[bin_name]["exposure"]
sq_rate = sq_count / squde_results[bin_name]["exposure"]
rate_ratio = sq_rate / ch_rate if ch_rate > 0 else 0
print(f"{BIN_LABELS[bin_name]:<22} {ch_count:>16.0f} {sq_count:>14.0f} {ratio:>10.2f}×")
print()
print("Per-second count rate:")
print(f"{'区域':<22} {'Chandra (cts/s)':>16} {'SQUDE (cts/s)':>14} {'SQUDE/Chandra':>16}")
print("-" * 80)
for bin_name in BINS:
ch_count = chandra_results[bin_name]["raw_counts"]
sq_count = squde_results[bin_name]["raw_counts"]
ch_rate = ch_count / chandra_results[bin_name]["exposure"]
sq_rate = sq_count / squde_results[bin_name]["exposure"]
rate_ratio = sq_rate / ch_rate if ch_rate > 0 else 0
print(f"{BIN_LABELS[bin_name]:<22} {ch_rate:>16.3f} {sq_rate:>14.3f} {rate_ratio:>16.2f}×")
print()
print("Note: SQUDE 50 ks 在更短曝光下获得更高计数率,")
print(" 主要是由于 SQUDE 在 0.5-2 keV 的有效面积 (~125 cm²) 比 ACIS-S (~50 cm²) 大 ~2.5×")
```
---
## 输出文件清单 {#output}
```{python}
#| label: output-list
#| output: true
import os
outdir = Path("figures")
for f in sorted(outdir.glob("*.png")):
size_kb = os.path.getsize(f) / 1024
print(f" {f.name} ({size_kb:.0f} KB)")
```
---
## 关键发现 {#findings}
::: {.callout-important}
## 分辨率差异的视觉效果
1. **Chandra 数据** (灰色点): 宽 bin,每个能量点跨度 ~50–150 eV。相邻发射线 (如 O VII 三重态 + O VIII Lyα) 被合并成宽鼓包。
2. **SQUDE 模拟** (彩色点): 窄 bin (4 eV),可以清晰分辨:
- O VII 三重态 (r, i, f 三个分量相隔 < 15 eV)
- O VIII Lyα vs Lyβ
- Ne IX vs Ne X
- Mg XI vs Mg XII
3. **中心区域** (v40_center): SQUDE 计数率比 Chandra 高 ~2.5×,因为 SQUDE 有效面积更大
4. **风区** (v80_north/south): 计数率提升更显著 (~3-5×),因为 SQUDE 的高分辨率减少了宽 bin 内的稀释
:::
---
*运行环境: Sherpa standalone + matplotlib 3.11 + scienceplots 2.2 · Python 3.11*
*数据来源: Chandra ACIS-S 三分区 137 ks 实测 + SQUDE 50 ks fake_pha 模拟*