用 AI 编程工具做射电天文 Radio Astronomy with AI Coding Tools
一个端到端案例:从 HI 21cm 谱线数据立方体,到矩图与科学图。所有代码都是完整可运行的,并已生成好示例数据,放在 example/ 文件夹。
One end-to-end case: from an HI 21cm spectral cube to moment maps and a publication-style figure. Every script is complete and runnable, and the example data is already generated in the example/ folder.
案例概览 Case Overview
科学问题 Science Question
我们有一份中性氢(HI)21cm 谱线数据立方体(右升 RA、赤纬 Dec、视向速度轴),想回答三个问题:
We have an HI 21cm spectral-line data cube (RA, Dec, line-of-sight velocity) and want to answer three questions:
- Moment 0(积分强度):这个天体总共辐射了多少 HI?How much HI does the source emit in total?
- Moment 1(速度):HI 气体的流量加权速度是多少?(旋转/运动信息)What is the flux-weighted velocity? (kinematics)
- Moment 2(弥散):速度弥散多大?(温度/湍流的指示)How large is the velocity dispersion? (temperature/turbulence)
make_synthetic_cube.py 生成),数据 hi_cube.fits 与全部脚本已放在 example/ 下,开箱即用。真实科研可替换为公开巡天数据(THOR、HI4PI)或你自己的观测。
We use a synthetic test cube; data and scripts live in example/, ready to run. For real science, swap in public survey cutouts (THOR, HI4PI) or your own observations.
工具链与环境 Toolchain & Environment
Python 核心 Core
numpy / scipy / matplotlib:数组、统计、绘图。
Arrays, statistics, plotting.
天文库 Astronomy libs
astropy(FITS/WCS/单位)、spectral-cube(立方体与矩图)、radio-beam(合成束)。
astropy (FITS/WCS/units), spectral-cube (cubes & moments), radio-beam (beams).
CASA CASA
射电干涉阵数据处理标准软件(calibration/imaging/immoments),用于交叉验证。
Standard radio-interferometry software (calibration/imaging/immoments) — our independent cross-check.
python -m pip install -r example/requirements.txt;CASA 需单独安装。本网站的 example 工程已在 Python 3.9 下验证。
Install with the requirements file; CASA installs separately. The example project is verified on Python 3.9.
Step -1:合成测试立方体(可选) Synthetic Test Cube (Optional)
没有真实数据时,先让 Agent 生成一个可复现的模拟立方体——这样你能知道"正确结果应该长什么样",方便验证后面每一步。示例数据已生成:example/hi_cube.fits(约 33 MB)。
With no real data yet, have the agent generate a reproducible mock cube — then you already know what the "right answer" looks like. The example data is already generated: example/hi_cube.fits (~33 MB).
写一个 Python 脚本,用 astropy 合成一个 HI 21cm 谱线数据立方体: - 尺寸 256×256×128,WCS 为 RA---SIN / DEC--SIN / VELO-LSR,速度单位 m/s,参考系 LSRK; - 中心放一个高斯"云"(空间 σ=30 像素,速度 σ=4 通道),峰值 20 Jy/beam; - 叠加 0.1 Jy/beam 的高斯噪声,BUNIT 设为 Jy/beam; - 保存为 hi_cube.fits,并打印 header 关键键。Write a Python script using astropy to synthesize an HI 21cm spectral cube: 256×256×128, WCS RA---SIN / DEC--SIN / VELO-LSR, velocity in m/s, LSRK frame; one Gaussian "cloud" (spatial σ=30 px, velocity σ=4 ch), peak 20 Jy/beam; add 0.1 Jy/beam Gaussian noise; BUNIT = Jy/beam; save hi_cube.fits and print key header keys.
完整脚本 · Full script:example/make_synthetic_cube.py(点击展开)
"""生成用于测试的 HI 21cm 谱线数据立方体(合成测试数据)。"""
import sys
import numpy as np
from astropy.io import fits
from astropy.wcs import WCS
def make_cube(outpath="hi_cube.fits", nx=256, ny=256, nv=128, seed=42):
w = WCS(naxis=3)
w.wcs.ctype = ["RA---SIN", "DEC--SIN", "VELO-LSR"]
w.wcs.cunit = ["deg", "deg", "m/s"]
w.wcs.crval = [83.633, 22.014, -5000.0] # 云心,中心速度 -5 km/s
w.wcs.crpix = [nx / 2, ny / 2, nv / 2]
w.wcs.cdelt = [-0.01, 0.01, 1000.0] # 0.01 deg/px,1 km/s/channel
w.wcs.restfrq = 1420.40575177e6 # HI 21cm 静止频率 (Hz)
yy, xx = np.mgrid[0:ny, 0:nx]
r = np.hypot(xx - nx / 2, yy - ny / 2)
vv = np.arange(nv)[:, None, None]
gauss_sky = np.exp(-(r ** 2) / (2 * 30.0 ** 2))
gauss_v = np.exp(-((vv - nv / 2) ** 2) / (2 * 4.0 ** 2))
cube = 20.0 * gauss_v * gauss_sky # 峰值 20 Jy/beam
rng = np.random.default_rng(seed)
cube = cube + rng.normal(0, 0.1, cube.shape) # 0.1 Jy/beam 噪声
hdr = w.to_header()
hdr["BUNIT"] = "Jy/beam"
fits.writeto(outpath, cube, hdr, overwrite=True)
print(f"已生成 {outpath},形状 {cube.shape}")
if __name__ == "__main__":
make_cube(sys.argv[1] if len(sys.argv) > 1 else "hi_cube.fits")运行:python make_synthetic_cube.py → 产物 hi_cube.fits
Step 0:先探索,别急着处理 Explore Before You Process
让 Agent 先打印 FITS header 的关键键。这一步能避免 90% 的坐标系/单位错误。
Have the agent print the key FITS header keys first. This avoids 90% of coordinate/unit mistakes.
用 astropy 打开 hi_cube.fits,打印: 1) 数据形状与 dtype; 2) CTYPE/CUNIT/CDELT/CRVAL/CRPIX(三轴); 3) BUNIT 与 RESTFRQ; 4) 简要说明每个轴是什么、速度轴用的是什么参考系与单位。Open hi_cube.fits with astropy and print: 1) data shape & dtype; 2) CTYPE/CUNIT/CDELT/CRVAL/CRPIX for all three axes; 3) BUNIT and RESTFRQ; 4) a short explanation of what each axis is and which reference frame/units the velocity axis uses.
完整脚本 · Full script:example/explore_cube.py(点击展开)
"""Step 0:探索数据 — 打印 FITS header 关键键。"""
import sys
from astropy.io import fits
KEYS = ["NAXIS1", "NAXIS2", "NAXIS3",
"CTYPE1", "CTYPE2", "CTYPE3",
"CUNIT1", "CUNIT2", "CUNIT3",
"CDELT1", "CDELT2", "CDELT3",
"CRVAL1", "CRVAL2", "CRVAL3",
"CRPIX1", "CRPIX2", "CRPIX3",
"BUNIT", "RESTFRQ", "VELREF"]
def explore(path="hi_cube.fits"):
hdu = fits.open(path)[0]
hdr = hdu.header
print("文件 / file :", path)
# 注意:numpy 数组轴与 FITS NAXIS 相反 → [速度, DEC, RA]
print("shape (numpy, [速度, DEC, RA]):", hdu.data.shape)
print("dtype:", hdu.data.dtype)
for k in KEYS:
print(f" {k:8s} = {hdr.get(k)}")
if __name__ == "__main__":
explore(sys.argv[1] if len(sys.argv) > 1 else "hi_cube.fits")运行:python explore_cube.py
RESTFRQ 与 CTYPE3(VELO-LSR 是常见约定)。
FITS NAXIS1/2/3 are reversed vs numpy axes ([velocity, DEC, RA]). Velocity may be m/s, km/s, or Hz — normalize before any math. Check RESTFRQ and CTYPE3 (VELO-LSR is the common convention).
Step 1:噪声掩膜 + 矩图 Noise Masking & Moment Maps
核心步骤:估计噪声 → 构建 3σ 掩膜 → 用 spectral-cube 计算矩图 → 保留 WCS 输出。示例产物已生成:example/mom0.fits / mom1.fits / mom2.fits。
The core step: estimate noise → build a 3σ mask → compute moments with spectral-cube → write outputs that preserve WCS. Example outputs are generated: example/mom0.fits / mom1.fits / mom2.fits.
用 spectral-cube 和 astropy 处理 hi_cube.fits: 1) 把速度轴统一为 km/s(radio 约定); 2) 用 MAD(median absolute deviation)估计噪声; 3) 构建 3σ 掩膜,排除 NaN 与噪声像素; 4) 计算 moment 0/1/2 并分别保存为 FITS,必须保留 WCS; 5) 打印三张矩图的峰值与单位。Process hi_cube.fits with spectral-cube and astropy: 1) convert velocity to km/s (radio convention); 2) estimate noise via MAD; 3) build a 3σ mask excluding NaN and noise; 4) compute moment 0/1/2 and save each as FITS preserving WCS; 5) print peaks and units of all three moments.
完整脚本 · Full script:example/make_moments.py(点击展开)
"""Step 1:噪声掩膜 + 矩图(spectral-cube + astropy)。"""
import sys
import numpy as np
from astropy import units as u
import spectral_cube as sc
def make_moments(path="hi_cube.fits"):
cube = sc.SpectralCube.read(path)
# 统一速度轴为 km/s(radio 约定,参考系随 FITS 头,通常 LSRK)
cube = cube.with_spectral_unit(u.km / u.s, velocity_convention="radio")
# 用 MAD 估计噪声(对离群值稳健)
data = cube.filled_data[:].value
mad = np.nanmedian(np.abs(data - np.nanmedian(data))) / 0.6745
print(f"MAD 噪声估计 / noise estimate: {mad:.4f} Jy/beam")
# 3σ 掩膜(自动排除 NaN;比较需带单位)
mask = cube > (3 * mad) * cube.unit
frac = mask.include().mean()
print(f"掩膜覆盖比例 / mask coverage: {frac:.1%}")
masked = cube.with_mask(mask)
mom0 = masked.moment0() # Jy/beam·km/s 积分强度
mom1 = masked.moment1() # km/s 流量加权速度
mom2 = masked.moment2() # km/s 速度弥散
mom0.write("mom0.fits", overwrite=True)
mom1.write("mom1.fits", overwrite=True)
mom2.write("mom2.fits", overwrite=True)
print("已写出 / wrote: mom0.fits, mom1.fits, mom2.fits")
print("M0 峰值 / peak:", np.nanmax(mom0.value), mom0.unit)
print("M1 范围 / range:", np.nanmin(mom1.value), "-", np.nanmax(mom1.value), mom1.unit)
print("M2 范围 / range:", np.nanmin(mom2.value), "-", np.nanmax(mom2.value), mom2.unit)
if __name__ == "__main__":
make_moments(sys.argv[1] if len(sys.argv) > 1 else "hi_cube.fits")运行:python make_moments.py → 产物 mom0.fits / mom1.fits / mom2.fits
Step 2:用 CASA 交叉验证 Cross-Check with CASA
用完全独立的软件(CASA immoments)重算矩图,对比结果。这是科研严谨性的关键一步——AI 生成的代码也可能有 bug。
Recompute the moments with fully independent software (CASA immoments) and compare. This is the key rigor step — agent-generated code can have bugs too.
写一段 CASA 脚本:把 hi_cube.fits 导入为 CASA image,用阈值掩膜(噪声的 3 倍)计算 moments 0/1/2,导出为 FITS。并说明 CASA 与 spectral-cube 的掩膜约定差异。Write a CASA script: import hi_cube.fits as a CASA image, compute moments 0/1/2 with a threshold mask (3× noise), export to FITS. Also explain the masking-convention differences between CASA and spectral-cube.
完整脚本 · Full script:example/casa_check.py(在 CASA 中运行)
"""Step 2:CASA 交叉验证 — 在 CASA 中运行: casa --nogui -c casa_check.py"""
# --- 1. 导入 ---
importfits(fitsimage="hi_cube.fits", imagename="hi_cube.image")
# --- 2. 先看统计,确定掩膜阈值(3σ ≈ 0.3 Jy/beam,请按实际噪声设定)---
imstat(imagename="hi_cube.image")
# --- 3. 阈值掩膜 + 矩 ---
immoments(imagename="hi_cube.image", moments=[0, 1, 2],
mask='"hi_cube.image" > 0.3', # 示意阈值,请按实际噪声设定
outfile="hi_moments.image")
# --- 4. 导出为 FITS 以便与 mom0/1/2.fits 对比 ---
exportfits(imagename="hi_moments.image", fitsimage="hi_moments_casa.fits")运行:casa --nogui -c casa_check.py → 产物 hi_moments_casa.fits
mom0.fits 与 hi_moments_casa.fits,比较:①峰值位置一致?②M0 峰值差异 < 几个 %?③M1 在云心附近是否接近 −5 km/s?把对比结果贴回给 Agent,让它解释差异来源(单位约定、掩膜边界、速度参考系)。
Read mom0.fits and hi_moments_casa.fits and compare: ① same peak position? ② M0 peak within a few %? ③ M1 near −5 km/s at the cloud center? Paste the comparison back to the agent and have it explain differences (unit conventions, mask edges, velocity frame).
Step 3:出版级科学图 Publication-Style Figures
三面板矩图 + 谱线抽取,坐标轴、单位、colorbar 齐全。示例图已生成:example/moments.png 与 example/spectrum.png。
Three-panel moment maps plus a spectrum extraction, with proper axes, units and colorbars. Example figures are generated: example/moments.png and example/spectrum.png.
用 matplotlib 和 astropy 的 WCS 绘制三面板图(moment 0/1/2):每面板带 RA/Dec 坐标轴、单位正确的 colorbar;再单独画一张"云心处的 HI 谱线"(速度-流量),标注 V_LSR。保存为 moments.png,dpi=200。Plot a three-panel figure (moment 0/1/2) with matplotlib + astropy WCS: RA/Dec axes, unit-correct colorbars per panel; plus a separate panel of the HI spectrum at the cloud center (velocity vs flux) labeled V_LSR. Save as moments.png, dpi=200.
完整脚本 · Full script:example/plot_figure.py(点击展开)
"""Step 3:出版级科学图 — 三面板矩图 + 云心谱线。"""
import os
import numpy as np
# 把 matplotlib 配置/字体缓存放到脚本目录(避免写入用户目录失败)
os.environ.setdefault(
"MPLCONFIGDIR",
os.path.join(os.path.dirname(os.path.abspath(__file__)), ".mplconfig"),
)
import matplotlib
matplotlib.use("Agg") # 无界面环境
import matplotlib.pyplot as plt
from astropy.io import fits
from astropy.wcs import WCS
def plot_moments():
fig = plt.figure(figsize=(14, 4.6))
files = ["mom0.fits", "mom1.fits", "mom2.fits"]
titles = ["Moment 0 · Integrated Intensity",
"Moment 1 · Velocity",
"Moment 2 · Dispersion"]
for i, (f, t) in enumerate(zip(files, titles)):
hdu = fits.open(f)[0]
ax = fig.add_subplot(1, 3, i + 1, projection=WCS(hdu.header))
im = ax.imshow(hdu.data, origin="lower", cmap="inferno")
ax.set_title(t, fontsize=10)
ax.coords[0].set_axislabel("RA (J2000)")
ax.coords[1].set_axislabel("Dec (J2000)")
cb = fig.colorbar(im, ax=ax, fraction=0.046)
cb.set_label(hdu.header.get("BUNIT", ""))
plt.tight_layout()
plt.savefig("moments.png", dpi=200)
print("已保存 / saved: moments.png")
def plot_spectrum(ra=83.633, dec=22.014):
"""抽取云心位置的 HI 谱线(世界坐标 → 像素坐标)。"""
hdu = fits.open("hi_cube.fits")[0]
w = WCS(hdu.header)
px, py, _ = w.all_world2pix(ra, dec, 0.0, 0)
spectrum = hdu.data[:, int(round(float(py))), int(round(float(px)))]
v = w.all_pix2world(0, 0, np.arange(hdu.data.shape[0]), 0)[2] / 1000.0
plt.figure(figsize=(6, 3.5))
plt.plot(v, spectrum)
plt.xlabel("V_LSR (km/s)")
plt.ylabel("Flux (Jy/beam)")
plt.title("HI spectrum at cloud center")
plt.tight_layout()
plt.savefig("spectrum.png", dpi=200)
print("已保存 / saved: spectrum.png")
if __name__ == "__main__":
plot_moments()
plot_spectrum()运行:python plot_figure.py → 产物 moments.png / spectrum.png
WCS 投影自动处理。矩图单位若显示为 Jy/beam·km/s,需要合成束面积(radio-beam)才能换算成柱密度或质量——让 Agent 写这一步换算,并给出公式依据。
Never hand-roll RA/Dec ticks — use WCS projections. If moments come out in Jy/beam·km/s, you need the synthesized beam (radio-beam) to convert to column density or mass — have the agent do that conversion and show the formula.
运行整个案例 Run the Whole Case
在 example/ 目录下,一条命令跑通前 5 步:
Inside example/, one command runs steps 1–5:
python make_synthetic_cube.py && python explore_cube.py && python make_moments.py && python plot_figure.py| 脚本 Script | 作用 Purpose | 产物 Output |
|---|---|---|
make_synthetic_cube.py | 合成 HI 立方体 | hi_cube.fits |
explore_cube.py | 打印 header 关键键 | 终端输出 |
make_moments.py | 3σ 掩膜 + 矩图 | mom0.fits mom1.fits mom2.fits |
plot_figure.py | 科学图 | moments.png spectrum.png |
casa_check.py | CASA 交叉验证(需 CASA) | hi_moments_casa.fits |
linewidth_sigma())。对不上就说明某一步有 bug——把输出贴回给 Agent 排查。
Cloud center (83.633°, 22.014°), velocity ≈ −5 km/s (LSRK), peak 20 Jy/beam, noise 0.1 Jy/beam (3σ≈0.3). Expect M0 peak ≈ 200 Jy/beam·km/s, M1 ≈ −4 to −5 km/s at center, M2 is a variance map (km²/s², ≈16 at center, i.e. σ≈4 km/s; use linewidth_sigma() for a linewidth map). If outputs disagree, paste them back to the agent.
射电数据处理常见坑清单 Radio-Data Pitfall Checklist
| 坑 Pitfall | 正确做法 Right approach |
|---|---|
| 轴顺序弄反(FITS vs numpy)Axis order reversed | 始终先打印 shape 与 NAXIS 对照 always print shape and NAXIS first |
| 速度单位/参考系混乱 Velocity units/frame confusion | 统一为 km/s + LSRK;核对 RESTFRQ normalize to km/s + LSRK; check RESTFRQ |
| BUNIT 缺失或错误 Missing/wrong BUNIT | 写入结果时显式设置 BUNIT set BUNIT explicitly on outputs |
| 对 NaN 直接算矩 Moments over NaN | 先掩膜(with_mask),打印掩膜覆盖比例 mask first; print coverage fraction |
| 大立方体内存爆炸 Memory blowup on big cubes | spectral-cube 的 dask 后端 / memmap,分块处理 use spectral-cube's dask backend / memmap |
| CASA 与 astropy 约定不同 CASA vs astropy conventions | 交叉验证时统一坐标系与掩膜语法;对比前先对齐 align frames & mask syntax before comparing |
| Jy/beam 与 Jy/pixel、K 混淆 Jy/beam vs Jy/pixel vs K | 用 radio-beam 换算;让 Agent 给出换算公式 convert with radio-beam; demand the formula |
| 忘记录中间结果 No intermediate outputs | 每步保存 FITS 与打印统计,方便回滚定位 save every step; print stats for traceability |
与 Agent 协作的实战技巧 Practical Tips for Working with Agents
📝 提示词里写清楚"数据语境" Give the data context
告诉 Agent:文件格式(FITS)、轴结构、单位、参考系、期望输出。它知道得越多,幻觉越少。
Tell the agent the format (FITS), axis layout, units, frame, expected output. The more context, the fewer hallucinations.
🔎 让它"先读再写" Read before writing
要求 Agent 先打印 header、先写 5 行探索代码,确认理解无误后再写完整管线。
Require it to print headers and write a 5-line exploration first, then the full pipeline.
🐛 报错整段回贴 Paste errors verbatim
把 traceback 原样贴回,并附上"我期望 XX,实际得到 YY"。
Paste the full traceback plus "I expected X, got Y".
🧪 让 Agent 写测试 Ask for tests
把单位换算、掩膜统计等拆成小函数,并让 Agent 写单元测试(pytest)。
Factor unit conversions and mask stats into small functions and ask for pytest unit tests.
⚖️ 独立交叉验证 Independent cross-checks
与 CASA 或文献值对比;对关键数字要求 Agent 给出推导依据。
Compare with CASA or literature values; demand derivation for key numbers.
🛡️ 防幻觉红线 Anti-hallucination red lines
坐标、单位、公式、数值结论必须可追溯;不确定就让它说"不确定",而不是编一个答案。
Coordinates, units, formulas, and numeric conclusions must be traceable; when unsure it must say so instead of inventing.
SKILL.md 或 SOP 放进课题组共享——下次任何成员用任何编程 Agent 都能一键复用。参考 技能生态篇 的写法。
Once the pipeline stabilizes, write it up as a SKILL.md or SOP shared with your group — any member can reuse it with any coding agent. See the Skills & Ecosystem page.
其他射电天文任务的起点 Starting Points for Other Radio Tasks
可见度数据(干涉阵) Visibility data
MS/UVFITS 格式,CASA 校准与成像(gaincal/clean),Python 侧可用 pyuvdata 读取。
MS/UVFITS formats, CASA calibration & imaging (gaincal/clean); pyuvdata reads them in Python.
脉冲星计时 Pulsar timing
PINT(Python 计时库)、PRESTO(折叠与搜索)——让 Agent 帮你写折叠脚本与 TOA 拟合。
PINT (Python timing), PRESTO (folding/search) — let agents write folding scripts and TOA fits.
FRB / 瞬变搜索 FRB / transients
Filterbank 数据(blimpy、your)去色散、去射频干扰、候选筛选——机器学习辅助分类是热门方向。
Filterbank data (blimpy, your): dedispersion, RFI mitigation, candidate screening — ML-assisted classification is hot.