Files
AstroResearch/skills/plotting/SKILL.md
T
fmq d6b064a490 feat: 科研分析层全栈落地——光谱/时域/运动学分析工具链 + JWST/X 射线数据源 + 定时文献同步
数据分析层(新增 services/{spectrum,timeseries,analysis}):
- 光谱参数提取 parameters.rs:LAMOST/SDSS/APOGEE/DESI FITS header 跨源归一化读取
  Teff/logg/[Fe/H]/RV 及 ASPCAP 20+ 元素丰度,rayon 并发批量提取
- 谱线测量 lines.rs:内置真空/空气波长谱线表,窗口内极值搜索 + 梯形法积分 EW + FWHM,支持自定义谱线
- 交叉相关测速 cross_correlate.rs:对数波长重采样对齐,内置 Pickles 模板按光谱型插值,
  CCF 峰值位置提取 RV 及不确定度
- 周期搜索 periodicity.rs:Lomb-Scargle 周期图(含 FAP 误报概率)+ BLS 凌星检测 + 相位折叠
- 变星分类 classification.rs:振幅/偏度/峰度/过零率/eta 等统计特征 + 规则分类(RR Lyrae/Cepheid/食双星/AGN 等)
- SED 拟合 sed.rs:多波段测光黑体模型拟合,输出 T_eff/半径/消光 A_V/光度及不确定度
- 运动学 kinematics.rs:视差+自行+RV → 银河系 UVW 空间速度,含移动星群成员概率(Banyan Σ 简化版)
- 化学丰度 chemistry.rs:[α/Fe] vs [Fe/H] 计算,厚盘/薄盘/晕星族判别
- 观测规划 observability.rs:目标升落时间/airmass/月相影响/曝光时间估算
- 赫罗图 hr_diagram.rs:Gaia TAP CMD 查询,新增 GET /api/analysis/hr-diagram 端点

数据获取层:
- JWST:clients/mast/jwst.rs 封装 MAST Portal 锥形检索 + JwstSpectrumFetcher(NIRSpec/MIRI 光谱)
- X 射线:clients/heasarc 封装 HEASARC TAP(ADQL)+ XMM-Newton/Chandra 光谱 fetcher
- 图像 cutout:SDSS SkyServer/STScI DSS/Pan-STARRS 三源 cutout + 发现图(Finding Chart)生成
- Source 枚举新增 Jwst/Xmm/Chandra 并注册 ObservationRegistry,前端 SOURCE_THEME 与筛选器同步三源

Agent 工具集(24→35):
- 新增 9 个分析工具:get_spectrum_parameters / measure_spectral_lines / measure_radial_velocity /
  find_period / classify_variable_star / fit_sed / analyze_kinematics / analyze_abundance_pattern / plan_observation
- batch_process:批量样本"查询→下载→分析→报告"流水线,并发控制防数据源速率限制
- literature_monitor:按 ADS 查询式/时间窗/最低引用数检查最新文献

定时文献同步:
- sync_queries 表新增 is_scheduled 列(migration 20260713)
- 新增 POST /sync/queries/:id/schedule 端点
- 服务启动时拉起每小时调度器,对 is_scheduled=1 的检索配置静默执行 ADS(entdate 增量)/arXiv 增量同步
- search_history 工具收敛至 services/search::search_agent_history,消除 FTS 查询逻辑重复

其他:
- plotting skill 由占位填充为完整科研绘图规范:光谱/光变/折叠曲线/CMD/SED/[α/Fe]/周期图/Mollweide/发现图 9 类 matplotlib 模板
- 删除死代码 streaming_executor.rs(929 行,仅剩 mod 声明引用,无调用方)
- 新增 docs/roadmap-research-features.md 科研功能路线图及实现状态
2026-09-07 21:50:32 +08:00

8.8 KiB
Raw Blame History

name, description, context, allowed-tools
name description context allowed-tools
plotting 科研绘图规范 —— 天文学常用图表的 Python matplotlib 绘制模板 fork
bash
save_note
write
read

科研绘图规范

环境要求

  • Python 3.8+
  • matplotlib >= 3.7
  • numpy
  • 可选:seaborn(统计图)、astropy(天文学单位和坐标)

安装:

pip install matplotlib numpy seaborn astropy --quiet

输出规范

目标 分辨率 格式 说明
论文投稿 300+ dpi PDF/SVG(矢量优先) APS/A&A/MNRAS 标准
演示文稿 150 dpi PNG 宽度 ≥ 1200px
快速预览 100 dpi PNG 屏幕查看

通用样式模板

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

# 天文学论文标准样式
plt.rcParams.update({
    'font.family': 'serif',
    'font.size': 12,
    'axes.labelsize': 14,
    'axes.titlesize': 14,
    'xtick.labelsize': 11,
    'ytick.labelsize': 11,
    'legend.fontsize': 11,
    'figure.figsize': (8, 6),
    'figure.dpi': 300,
    'savefig.dpi': 300,
    'savefig.bbox': 'tight',
    'savefig.pad_inches': 0.05,
    'lines.linewidth': 1.5,
    'axes.linewidth': 1.0,
    'xtick.major.width': 0.8,
    'ytick.major.width': 0.8,
    'xtick.minor.width': 0.5,
    'ytick.minor.width': 0.5,
    'xtick.direction': 'in',
    'ytick.direction': 'in',
    'xtick.top': True,
    'ytick.right': True,
})

图表模板

1. 光谱图

def plot_spectrum(wavelength, flux, title="Spectrum", xlabel=r"Wavelength ($\AA$)", ylabel=r"Flux (erg/s/cm$^2$/$\AA$)", save_path=None):
    fig, ax = plt.subplots()
    ax.plot(wavelength, flux, 'k-', linewidth=0.8)
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_title(title)
    ax.minorticks_on()
    if save_path:
        fig.savefig(save_path)
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

2. 光变曲线

def plot_light_curve(time, flux, time_err=None, flux_err=None, title="Light Curve", xlabel="Time (MJD)", ylabel="Flux", save_path=None):
    fig, ax = plt.subplots()
    if flux_err is not None:
        ax.errorbar(time, flux, yerr=flux_err, xerr=time_err, fmt='o', markersize=3, capsize=2, color='black', ecolor='gray')
    else:
        ax.plot(time, flux, 'ko', markersize=3)
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_title(title)
    ax.minorticks_on()
    if save_path:
        fig.savefig(save_path)
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

3. 折叠光变曲线

def plot_folded_lc(phase, flux, flux_err=None, period=None, title="Folded Light Curve", save_path=None):
    fig, ax = plt.subplots()
    if flux_err is not None:
        ax.errorbar(phase, flux, yerr=flux_err, fmt='o', markersize=2, capsize=1, color='black', ecolor='gray', alpha=0.7)
    else:
        ax.plot(phase, flux, 'ko', markersize=2, alpha=0.7)
    ax.set_xlabel("Phase")
    ax.set_ylabel("Flux")
    if period:
        ax.set_title(f"{title} (P = {period:.4f} d)")
    else:
        ax.set_title(title)
    ax.set_xlim(0, 1)
    ax.minorticks_on()
    if save_path:
        fig.savefig(save_path)
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

4. 赫罗图(CMD

def plot_hr_diagram(bp_rp, abs_g, title="HR Diagram", save_path=None, color_by_density=False):
    fig, ax = plt.subplots()
    if color_by_density and len(bp_rp) > 100:
        from scipy.stats import gaussian_kde
        xy = np.vstack([bp_rp, abs_g])
        z = gaussian_kde(xy)(xy)
        idx = z.argsort()
        ax.scatter(bp_rp[idx], abs_g[idx], c=z[idx], s=5, cmap='viridis_r', edgecolors='none')
        cb = fig.colorbar(ax.collections[0], ax=ax, pad=0.02)
        cb.set_label("Stellar density")
    else:
        ax.plot(bp_rp, abs_g, 'k.', markersize=1, alpha=0.5)
    ax.set_xlabel(r"$G_{BP} - G_{RP}$ (mag)")
    ax.set_ylabel(r"$M_G$ (mag)")
    ax.set_title(title)
    ax.invert_yaxis()
    ax.minorticks_on()
    if save_path:
        fig.savefig(save_path)
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

5. SED 图

def plot_sed(wavelength_angstrom, flux_obs, flux_err=None, flux_model=None, title="SED", save_path=None):
    fig, ax = plt.subplots()
    ax.scatter(wavelength_angstrom, flux_obs, c='black', s=30, zorder=5, label='Observed')
    if flux_err is not None:
        ax.errorbar(wavelength_angstrom, flux_obs, yerr=flux_err, fmt='none', ecolor='gray', capsize=3)
    if flux_model is not None:
        model_wave, model_flux = zip(*flux_model) if isinstance(flux_model, list) else (flux_model[0], flux_model[1])
        ax.plot(model_wave, model_flux, 'r-', linewidth=1.5, label='Model', alpha=0.8)
    ax.set_xscale('log')
    ax.set_yscale('log')
    ax.set_xlabel(r"Wavelength ($\AA$)")
    ax.set_ylabel(r"Flux")
    ax.set_title(title)
    ax.legend()
    ax.minorticks_on()
    if save_path:
        fig.savefig(save_path)
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

6. [α/Fe] vs [Fe/H] 图

def plot_abundance(feh, alpha_feh, labels=None, title="[α/Fe] vs [Fe/H]", save_path=None):
    fig, ax = plt.subplots()
    if labels is not None:
        from matplotlib.colors import ListedColormap
        colors = ['#3498db', '#e74c3c', '#2ecc71']
        cmap = ListedColormap(colors[:len(set(labels))])
        unique = sorted(set(labels))
        for i, lab in enumerate(unique):
            mask = np.array(labels) == lab
            ax.scatter(np.array(feh)[mask], np.array(alpha_feh)[mask], s=10, alpha=0.7, label=lab, color=colors[i % len(colors)])
        ax.legend()
    else:
        ax.scatter(feh, alpha_feh, s=10, alpha=0.7, c='black')
    ax.set_xlabel("[Fe/H] (dex)")
    ax.set_ylabel(r"[$\alpha$/Fe] (dex)")
    ax.set_title(title)
    ax.axhline(y=0.25, color='gray', linestyle='--', linewidth=0.8, alpha=0.5)
    ax.minorticks_on()
    if save_path:
        fig.savefig(save_path)
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

7. 周期图

def plot_periodogram(periods, powers, best_period=None, title="Periodogram", save_path=None):
    fig, ax = plt.subplots()
    ax.plot(periods, powers, 'k-', linewidth=0.8)
    if best_period:
        ax.axvline(x=best_period, color='red', linestyle='--', linewidth=1, label=f'Best P = {best_period:.4f} d')
        ax.legend()
    ax.set_xlabel("Period (days)")
    ax.set_ylabel("Power")
    ax.set_title(title)
    ax.minorticks_on()
    if save_path:
        fig.savefig(save_path)
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

8. 天球投影(Mollweide

def plot_skymap(ra_deg, dec_deg, values=None, title="Sky Map", save_path=None):
    fig, ax = plt.subplots(figsize=(10, 5), subplot_kw={'projection': 'mollweide'})
    # 转换为弧度,RA 中心在 180°
    ra_rad = np.deg2rad(np.array(ra_deg))
    dec_rad = np.deg2rad(np.array(dec_deg))
    ra_rad = ra_rad - np.pi  # 中心化
    if values is not None:
        sc = ax.scatter(ra_rad, dec_rad, c=values, s=5, cmap='viridis', alpha=0.7, edgecolors='none')
        fig.colorbar(sc, ax=ax, pad=0.05, shrink=0.6)
    else:
        ax.scatter(ra_rad, dec_rad, c='black', s=5, alpha=0.5, edgecolors='none')
    ax.set_title(title)
    ax.grid(True, alpha=0.3)
    if save_path:
        fig.savefig(save_path, bbox_inches='tight')
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

9. 发现图(Finding Chart

def plot_finding_chart(image_data, ra, dec, label=None, save_path=None):
    """image_data: 2D numpy array (from FITS or JPEG)"""
    fig, ax = plt.subplots()
    ax.imshow(image_data, cmap='gray_r', origin='lower')
    # 标注中心
    h, w = image_data.shape
    ax.plot(w/2, h/2, 'r+', markersize=15, markeredgewidth=2)
    if label:
        ax.annotate(label, (w/2, h/2), textcoords="offset points", xytext=(10, 10),
                    color='red', fontsize=10, fontweight='bold')
    ax.set_title(f"Finding Chart ({ra:.4f}, {dec:.4f})")
    ax.set_xlabel("X (pix)")
    ax.set_ylabel("Y (pix)")
    if save_path:
        fig.savefig(save_path)
        plt.close(fig)
    else:
        plt.show()
    return fig, ax

工作流程

  1. 确定图表类型:根据数据特征选择合适的图表模板
  2. 准备数据:从文献、数据库或分析结果中提取数据
  3. 生成脚本:使用上述模板,填入实际数据
  4. 执行python3 script.py
  5. 验证:检查输出图片是否正确
  6. 保存:保存到 figures/ 目录,命名格式:{description}_{source}.{ext}

保存路径约定

  • figures/ — 项目根目录下的图表输出目录
  • 文件名格式:{描述}_{来源}_{日期}.{格式}
    • 示例:spectrum_lamost_438809089_20260710.pdf
    • 示例:hr_diagram_m31_field_20260710.png