From 52f201404c57a38610a8b8749a8a31c2098d85c3 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 28 May 2026 11:33:59 +0800 Subject: [PATCH] =?UTF-8?q?skills:=20=E5=8A=A0=20pymatgen=20/=20stats=5Fml?= =?UTF-8?q?=20/=20plot=5Fpub(=E5=BB=BA=E6=9D=90=E9=99=A2=E6=97=A0=E6=9C=BA?= =?UTF-8?q?=E6=9D=90=E6=96=99=E5=9C=BA=E6=99=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 服务中国建材院无机非金属材料 R&D。从 K-Dense AI scientific-agent-skills 仓库挑 3 个 ★★★ skill fork,不走 npx 一键装(138 个 description 入 prompt 噪声 + 误触发)。 - pymatgen: 晶体结构 / XRD / 对称性 / 相图 / Materials Project。helper materials.py 内 CEMENT_PHASES 收 66 条中英文相名映射(水泥熟料 / 水化产物 / 陶瓷耐火 / 玻璃晶相 / 常见矿物)、lookup_phase 大小写不敏感、mp_rester 从 env MP_API_KEY 拿 key - stats_ml: 纯指南 skill,场景导航 sklearn / statsmodels / PyMC 三选一, 5 个工作流(配方-性能回归 / DoE 二阶响应面 / 显著性分析 / 贝叶斯小样本 / DBSCAN 异常配方)+ 16 条反模式 - plot_pub: 出版级 matplotlib,helper style.py 内 apply_pub_style() 一键 设置中文字体跨平台 fallback (SimHei / YaHei / WenQuanYi) + viridis + dpi + PDF Type 42 requirements.txt 加 pymatgen / mp-api / scikit-learn / statsmodels (pymc 注释,装包重按需开)。RUN.md env 段加 MP_API_KEY(可选)。 SCIENTIFIC_SKILLS.md 根目录沉淀整体评估,后续 materials_db 落地参考。 scripts/smoke_scientific_skills.py 三 skill 链路通路验证脚本。 Co-Authored-By: Claude Opus 4.7 (1M context) --- PROGRESS.md | 3 +- RUN.md | 3 + SCIENTIFIC_SKILLS.md | 189 ++++++++++++++++++ requirements.txt | 11 ++ scripts/smoke_scientific_skills.py | 307 +++++++++++++++++++++++++++++ skills/plot_pub/SKILL.md | 168 ++++++++++++++++ skills/plot_pub/style.py | 114 +++++++++++ skills/pymatgen/SKILL.md | 144 ++++++++++++++ skills/pymatgen/materials.py | 153 ++++++++++++++ skills/stats_ml/SKILL.md | 187 ++++++++++++++++++ 10 files changed, 1278 insertions(+), 1 deletion(-) create mode 100644 SCIENTIFIC_SKILLS.md create mode 100644 scripts/smoke_scientific_skills.py create mode 100644 skills/plot_pub/SKILL.md create mode 100644 skills/plot_pub/style.py create mode 100644 skills/pymatgen/SKILL.md create mode 100644 skills/pymatgen/materials.py create mode 100644 skills/stats_ml/SKILL.md diff --git a/PROGRESS.md b/PROGRESS.md index 61fbb49..438b4b1 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-05-27(ppt skill 歧义反问 + general_v1 加"产物形式歧义先问"通用原则) +最后更新:2026-05-28(新增 pymatgen / stats_ml / plot_pub 三个科学计算 skill,服务建材院无机材料 R&D 场景) --- @@ -23,6 +23,7 @@ ### 2026-05-28 +- **新增 3 个科学计算 skill(pymatgen / stats_ml / plot_pub),服务建材院无机非金属材料 R&D**:`SCIENTIFIC_SKILLS.md` 评估完 K-Dense/scientific-agent-skills 仓库后落地选 4 个 ★★★ 中前 3 个动手(materials_db 后置,USPTO 部分留并入 `skills/patent`)。命名取**工具名直接**(`pymatgen` / `plot_pub`)+ **业务前缀**(`stats_ml` 因合三库需要场景导航),贴合现有 skill 命名风格(coding/ppt/research/...)。① **`skills/pymatgen/`**:`SKILL.md`(无机相中文→化学式映射表说明 / XRD 比对 / 对称性 / 相图 / VASP 输入文件,八条反模式)+ `materials.py`(`CEMENT_PHASES` dict 覆盖水泥熟料 / 水化产物 / 石膏 / 碳酸盐 / 陶瓷耐火 / 玻璃晶相 / 常见矿物共 50+ 条目,中英文 / 简写多 key 指同一化学式;`lookup_phase()` 大小写不敏感查找;`mp_rester()` context manager 自动从 env 拿 `MP_API_KEY`,缺则 RuntimeError 带申请链接;mp_api 局部 import 避免装包前 import 即崩)。② **`skills/stats_ml/`**:`SKILL.md` 纯指南(场景导航表选 sklearn / statsmodels / PyMC、5 个工作流示例 A-E 含配方-性能回归 / DoE 二阶响应面 / 显著性分析 / 贝叶斯小样本 / DBSCAN 异常配方、16 条反模式分库列示)+ 无 helper(三库 API 直接用)。③ **`skills/plot_pub/`**:`SKILL.md`(XRD 多相叠图 / TG-DSC 双 Y 轴 / 强度发展曲线 / 多 panel 论文 figure 4 个工作流 + 中文字体说明 + 10 条反模式)+ `style.py`(`apply_pub_style()` 一键设置:中文字体跨平台 fallback SimHei→YaHei→WenQuanYi→DejaVu / dpi 150 屏幕 300 保存 / viridis 默 cmap / 刻度朝内 / legend 无框 / PDF Type 42 字体合规期刊 / `font.size` `linewidth` 等可参数化;`_find_chinese_font()` 在 `font_manager.fontManager.ttflist` 查实装字体而非靠 try-load)。**关键决策**:(a) **不一键装 138 个 skill** —— 上下文噪声 + 误触发(用户"分析一下"模型可能跳 Scanpy),挑 4 个 ★★★ fork 单装;(b) **PyMC 装包延后** —— 带 pytensor 装 5min+ 体积大,真要做贝叶斯再装;requirements.txt 注释掉以 `# pymc>=5.10.0` 形式留接口;(c) **MP_API_KEY 走 .env** —— 跟 DEEPSEEK_API_KEY / ARK_API_KEY 同范式,litellm 不读但 `os.environ.get` 拿到;(d) **化学式映射表对中文 / 简写 / 英文学名同等待遇** —— 用户报相名习惯混杂(C3S / 硅酸三钙 / alite 都常见),多 key 同 value 比强迫归一化体验好;(e) **不写示例数据/单元测试**:开发期 LLM 工作流场景多变,跑了 SKILL.md 工作流验证而非脚本测试 —— skill 是 prompt 不是代码模块。requirements.txt 加 pymatgen / mp-api / scikit-learn / statsmodels(PyMC 注释)。`RUN.md` env 段加 MP_API_KEY 说明(可选 + 申请链接 + 未设抛 RuntimeError)。`DESIGN.md` 不动(纯 skill 加,无架构变化);`SCIENTIFIC_SKILLS.md`(根目录调研笔记)已沉淀整体评估,后续 materials_db 落地参考。装包未执行 —— 等用户跑 `.venv/Scripts/pip install pymatgen mp-api scikit-learn statsmodels` 装上才能验证 import 路径生效。否决:(a) 三个 skill 合并成一个 `science` skill —— 触发语义糊,LLM 难判,各做各的更清;(b) `materials_pymatgen` 这种业务前缀全打 —— pymatgen 本身就是材料库,前缀冗余;(c) helper 过度封装(写 `simulate_xrd(formula)` 全自动)—— 隐藏 pymatgen 真实 API,LLM 学不到本来好用的上游能力,反模式段在 SKILL.md 里讲清更轻;(d) plot_pub 内 `apply_pub_style()` 失败抛错 —— 中文字体没装也应该继续画(图能看就行,只是中文方块),warn 比 raise 友好。 - **DESIGN §7.5 增"image 体积 / 多 user 资源 / 后续加包策略"决策段**:dogfood 推进到加 npm + chromium + mermaid-cli 后,sandbox image 1.5G+,后续 domain 包(rdkit / pymatgen / ASE / pandoc-tex 等)还会进一步推大,把"image 大 = 资源占用大 = 多 user 容器爆炸"这条直觉关联的事实链拆开沉淀,免得未来花减肥功夫减错地方。三条认知校准:① **image 大 ≠ 运行时吃更多 RAM**(空载 `tini → sleep infinity` RSS 个位数 MB,layer 共享让磁盘乘数 = 1,真吃 RAM 的是 active exec 行为而非 image 字节);② **多 user 同时在线瓶颈在并发 exec 不在 idle 容器数**(杠杆全在 `docker run --memory --cpus --pids-limit` + 同 user 并发 semaphore + 整机 active user cap + idle 回收,减 image 体积对这条曲线无效);③ **新增依赖采用"base 收敛 + per-user 持久化 venv + 使用频次沉淀"**:base 只放高频共用轻量包(`requirements.txt` 当前形状),长尾 domain 包模型用 `pip install --target=/workspace/.venv/site-packages` 装到 per-user 持久化路径(随 user_root bind mount,idle 回收不丢);加 audit 统计哪些包累计被 >30% user 装过 ≥ 3 次 → 下次 build 合并进 `requirements.txt`,base 跟着真实使用模式收敛而非拍脑袋。否决:(a) 全塞 base —— 重包(torch / texlive)+ 长尾包打死磁盘 + 强迫所有 user 陪绑;(b) 运行时临时装(不持久化)—— 容器 idle 回收即丢冷启重装,高频复用差;(c) 多 image 按场景分 —— per-user 容器模型下 user 不知道选哪个,切 skill 还得换 image 心智不通;(d) per-user venv 用 named volume / 共享 image cache —— 包 install 脚本是任意代码,跨 user 共享 venv 破坏跨 user 隔离边界;(e) 依赖 pip cache 解决问题 —— pip cache 只省网络不省落盘,容器回收照样丢。落地点排进 Stage C Step 6 Executor 实施:cgroup limits / 并发 semaphore / idle 回收 / per-user venv mount 一并进 `DockerExecutor`(audit 沉淀机制延后,需要 dogfood 安装日志积累再开)。`RUN.md` 不动(当前无 CLI / env 变化,真做 venv mount 时再加);`DESIGN.md` §7.5 升级触发信号表后新增该段。 ### 2026-05-27 diff --git a/RUN.md b/RUN.md index e8c4c7a..a262dac 100644 --- a/RUN.md +++ b/RUN.md @@ -21,6 +21,9 @@ DOCUMENT_SEARCH_API_KEY=... # 可选:覆盖默认 base_url(默认 https://ai.ctc-zc.com:8100/api) # DOCUMENT_SEARCH_URL=https://ai.ctc-zc.com:8100/api + # pymatgen skill 的 Materials Project 接入:可选。设了 pymatgen.materials.mp_rester() 才能用, + # 未设调用即抛 RuntimeError。申请 https://materialsproject.org/api(免费) + MP_API_KEY=... ZCBOT_DB_URL=postgresql://user:pass@host:5432/zcbot # main.py web 必填(probe/db/user 不验) PLATFORM_KEY=<≥16 字符随机串,platform 机器对机器入口校验> diff --git a/SCIENTIFIC_SKILLS.md b/SCIENTIFIC_SKILLS.md new file mode 100644 index 0000000..d89e554 --- /dev/null +++ b/SCIENTIFIC_SKILLS.md @@ -0,0 +1,189 @@ +# 科学 Skill 集成评估(scientific-agent-skills → zcbot) + +来源:https://github.com/K-Dense-AI/scientific-agent-skills(MIT,K-Dense AI,138 个 skill) + +适用对象:zcbot,服务于中国建筑材料科学研究总院 —— 无机非金属材料 R&D(水泥/混凝土/玻璃/陶瓷/耐火/新型建材)。 + +## 1. 总体结论 + +- **不整包装**(`npx skills add`),138 个 description 全进 prompt → 上下文噪声爆炸 + 误触发(用户说"分析一下"模型可能跳去 Scanpy)。 +- **挑 4 个核心新能力 fork 进来**,适配 zcbot 现有 skill 格式(`SKILL.md` + Python helper + `run_python` 调用)。 +- **现有 6 个 skill 不替换,抽对方的 best practice 增强**。 + +## 2. 现状与目标格式 + +zcbot 已有 skill(`D:/projects/zcbot/skills/`): +`coding / documents / imagegen / patent / ppt / proposal / research / review / videogen` + +每个 skill = `SKILL.md`(frontmatter + 何时用 + 反模式 + 工作流)+ 配套 Python(走 `run_python` 注入的 PYTHONPATH 直接 import)。 + +scientific-agent-skills 的 skill 格式语义基本一致 —— **可以单个 fork,不要批量装**。 + +## 3. 推荐移植的 4 个新能力 + +### 3.1 Pymatgen ★★★ + +无机材料计算核心库。 + +**对你们的价值面** +- 晶体结构 I/O(CIF/POSCAR)—— 水泥熟料相(C3S/C2S/C3A/C4AF)、玻璃晶化产物、陶瓷相、耐火砖矿物相 +- `XRDCalculator` —— 给定结构正向算 XRD pattern,跟实测对比 +- `PhaseDiagram` —— 凝胶/水化产物的热力学稳定性 +- `SpacegroupAnalyzer` —— 物相对称性 +- `MPRester` —— Materials Project 直连查无机化合物 +- 转换工具:VASP / Gaussian / Quantum ESPRESSO 输入输出文件 + +**落地动作** +```bash +.venv/Scripts/pip install pymatgen mp-api +``` +新建 `skills/materials_pymatgen/SKILL.md`,在仓库原版基础上加两段: +- **中文相名 → 标准化学式映射表**(给 LLM 看): + - C3S → Ca3SiO5,C2S → Ca2SiO4,C3A → Ca3Al2O6,C4AF → Ca4Al2Fe2O10 + - 钙矾石 → Ca6Al2(SO4)3(OH)12·26H2O + - 莫来石 → Al6Si2O13,堇青石 → Mg2Al4Si5O18 +- **Materials Project API key 配置走 zcbot env** —— 用户得去 https://materialsproject.org/api 申请,写进 `RUN.md` 一次性初始化段 + +**坑** +- `MPRester` 必须 context manager(`with MPRester() as mpr:`),否则连接泄漏 —— 抄进反模式段 +- `from_file()` 优先,不要自己 parse CIF + +### 3.2 stats_ml(scikit-learn + statsmodels + PyMC) ★★★ + +合并成一个 skill,按场景导航。 + +**对你们的价值面** +- 配方-性能建模:掺合料比例 → 28 天抗压强度回归 +- DoE 响应面拟合 +- 配方聚类 / 异常实验检测 +- 工艺参数 → 性能的特征重要性 + +**落地动作** + +`skills/stats_ml/SKILL.md` 入口先做场景导航: +| 你要做 | 用 | +|---|---| +| 要 p-value、置信区间、假设检验 | statsmodels(OLS / ANOVA) | +| 要预测精度高、不在乎模型可解释性 | sklearn(RF / GBDT) | +| 样本小(< 30)且要不确定度估计 | PyMC(贝叶斯) | +| 要可解释的线性关系 | statsmodels OLS / sklearn LinearRegression | + +**抄仓库的反模式段**(高频踩坑): +1. 别在 pipeline 外预处理 → 交叉验证会数据泄漏 +2. scaler 别 fit 在 test 上 +3. 分类任务记得 `StratifiedKFold` +4. 树模型不用 scale 特征 +5. 收敛 warning 别忽略,要么加 `max_iter` 要么 scale + +依赖:你们 requirements.txt 多半已有 sklearn,确认下 statsmodels + pymc 装没装。 + +### 3.3 matplotlib(出版级) ★★★ + +**对你们的价值面** +- XRD pattern(底图 + 标峰) +- TG-DSC 双 Y 轴曲线 +- 应力-应变曲线多组样本对比 +- 多 panel 出版图(Cement and Concrete Research、JACerS 风格) + +**落地动作** + +新建 `skills/plot_pub/SKILL.md`,关键内容(原仓库的反模式 + 你们的中文场景补丁): + +```python +# zcbot 出版级绘图规范 + +# 1. OO 接口(必须),不用 plt.xxx 隐式 +fig, ax = plt.subplots(figsize=(6, 4), dpi=150) +ax.plot(...) + +# 2. 中文字体(Windows 必配,否则方块) +plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] +plt.rcParams['axes.unicode_minus'] = False + +# 3. colormap 用 viridis / cividis,不用 jet(色觉障碍不友好,审稿人会喷) + +# 4. 画完必须 close,run_python 多任务跑容易堆积 +fig.savefig(out_path, bbox_inches='tight') +plt.close(fig) + +# 5. 矢量优先:SVG / PDF 给论文,PNG 给 PPT(dpi >= 300) +``` + +仓库还提到一个 `scientific-visualization` skill 专门做期刊样式多面板,这个可以一并扒进来作为 `plot_pub` 的进阶段。 + +### 3.4 materials_db(COD + NIST + USPTO 三合一) ★★ + +**原仓库 `database-lookup` 78 个库,只 5 个对你们直接有用,不要整体抄,做精简子集**: + +| DB | 用途 | +|---|---| +| **Materials Project** | 已被 Pymatgen 的 MPRester 覆盖,不重复 | +| **COD**(Crystallography Open Database) | 免费晶体结构库,补充 MP 没收的实验结构 | +| **NIST** | 标准参考数据,材料热物性、相平衡 | +| **USPTO** | 美国专利全文检索 → 并入 `skills/patent` | +| **PubChem / ChEMBL** | 只在做外加剂(减水剂、缓凝剂)这类有机分子时用 → 不常用,可后置 | + +**落地动作** +- `skills/patent` 加一个 `query_uspto(keyword, ...)` helper(USPTO Open Data Portal API) +- 新建 `skills/materials_db/SKILL.md`,封 COD + NIST 两个查询函数 +- Materials Project 让 Pymatgen 走,本 skill 不重复 + +**坑** +- USPTO 要 API key(免费),申请走 https://developer.uspto.gov/ +- COD 是 RESTful,免 key,但限速,加 sleep +- NIST 各子库 API 不统一(WebBook / Materials Data Repository / ICSD 镜像),要分别封装 + +## 4. 现有 6 个 skill 的增强参考(不替换) + +把仓库对应 skill 的 SKILL.md 拿来读,把"何时用 / 反模式 / 标准工作流"段落里你们没写的合理项抽进现有 SKILL.md。**不要整体替换**,你们的 paper_server / proposal 模板 / patent 流程都是定制化的。 + +| 仓库 skill | 对应你们 | 可抄的部分 | +|---|---|---| +| Literature Review / Paper Lookup | `skills/research` | 多库 fallback 策略、引文格式标准化 | +| Scientific Writing | `skills/proposal` | 章节结构、claim-evidence 写作规范 | +| Scientific Slides / LaTeX Posters | `skills/ppt` | 多 panel 布局、color palette 规范 | +| Document Skills(PDF/DOCX/XLSX/PPTX) | `skills/documents` | 表格读取、复杂 PDF 抽取 | +| Peer Review | `skills/review` | 审稿 checklist | +| Scientific Schematics | (新加) | 工艺流程图、装置示意图自动生成 | + +## 5. 不推荐(直接跳过) + +| 领域 | 仓库代表 skill | 跳过原因 | +|---|---|---| +| 生物信息 / 基因组 | BioPython / Scanpy / scVelo / Arboreto / anndata / esm | 跟无机材料无交集 | +| 药物发现 | DiffDock / DeepChem / Datamol / adaptyv | 同上,RDKit 偶尔有边但太窄 | +| 临床研究 | ClinicalTrials.gov / ClinVar / COSMIC / Open Targets | 同上 | +| 医学影像 | pydicom / histolab / PathML | 同上 | +| 量子计算 | Cirq / Qiskit / PennyLane / QuTiP | 除非真做第一性原理(成本极高) | +| 代谢建模 | COBRApy | 同上 | +| 地理空间 | GeoPandas | 同上 | +| 天文 | astropy | 同上 | + +## 6. 中间档(暂不动,记下来,有场景再说) + +| skill | 触发场景 | +|---|---| +| **OpenMM + MDAnalysis** | 水泥水化分子模拟、玻璃网络结构 MD —— 门槛高,看院里有没有人真做计算 | +| **NetworkX** | 知识图谱、合作网络分析 | +| **SymPy** | 推传热扩散、强度模型公式 | +| **scikit-image** | SEM 图像分析(粒径分布、形貌识别) —— 仓库没单列,如要做自己装 | + +## 7. 落地排期建议 + +| 顺序 | skill | 工作量估计 | 收益 | +|---|---|---|---| +| 1 | `plot_pub`(matplotlib)SKILL.md | 0.5 day | 立刻见效,所有出图任务受益,中文字体坑也一次扫掉 | +| 2 | `materials_pymatgen` 装包 + SKILL.md + 化学式映射 | 1 day | 打开无机材料计算能力(XRD/相图/对称性) | +| 3 | `stats_ml` SKILL.md(sklearn + statsmodels + PyMC) | 1 day | 配方-性能建模常态化 | +| 4 | `materials_db`(COD + NIST + USPTO) | 2 day(每个 API 都要踩坑) | 中等,可后置;USPTO 部分先并入 `patent` | +| 5 | 现有 6 个 skill 抽 best practice 增强 | 各 0.5 day | 渐进改善,不阻塞 | + +## 8. 三个决策点(写代码前需要你确认) + +1. **新 skill 命名风格**:用 `materials_pymatgen` / `stats_ml` / `plot_pub` / `materials_db` 这种业务前缀,还是直接 `pymatgen` / `sklearn` 这种工具名? + - 业务前缀:更贴你们院的场景导航 + - 工具名:跟开源生态对齐,容易回查官方文档 +2. **Materials Project API key** 走 `.env` 还是单独配置文件? +3. **USPTO 并入 `skills/patent` 还是单独建 `skills/uspto`**?(我倾向并入,因为 patent skill 已经有中国专利逻辑) + +定下这三点我就开始动 #1(matplotlib)。 diff --git a/requirements.txt b/requirements.txt index 10dd315..3e68bd6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,3 +26,14 @@ uvicorn[standard]>=0.30.0 python-multipart>=0.0.9 # files upload multipart 解析 pyjwt>=2.8.0 # /v1/auth/login HS256 token mint/verify(§7 D' 过渡形态) bcrypt>=4.1.0 # /v1/auth/login_password 密码哈希(users.password_hash) + +# 科学计算 skill(2026-05-28 加) +# pymatgen skill: 无机材料计算(晶体结构/XRD/相图/Materials Project) +pymatgen>=2024.0 +mp-api>=0.41.0 +# stats_ml skill: 统计建模与 ML(sklearn 必装,statsmodels 必装,PyMC 可选) +scikit-learn>=1.4.0 +statsmodels>=0.14.0 +# pymc>=5.10.0 # 贝叶斯小样本估计,装包重(带 pytensor),按需打开 +# arviz>=0.17.0 # PyMC 后验诊断,跟 pymc 一起开 +# plot_pub skill: 出版级 matplotlib(中文字体 + viridis 配色),依赖 matplotlib 已在上面 diff --git a/scripts/smoke_scientific_skills.py b/scripts/smoke_scientific_skills.py new file mode 100644 index 0000000..86f6ff5 --- /dev/null +++ b/scripts/smoke_scientific_skills.py @@ -0,0 +1,307 @@ +"""Smoke: 3 个新加的科学计算 skill 通路验证(pymatgen / stats_ml / plot_pub)。 + +跑法: .venv/Scripts/python.exe scripts/smoke_scientific_skills.py + +依赖:`pip install pymatgen mp-api scikit-learn statsmodels`(PyMC 可选,装了就测)。 + +不依赖网络默认情况下(MP_API_KEY 没配则跳过 mp_rester 联网那一段)。 +不动 DB / workspace,产物落系统临时目录,跑完即丢。 + +按 skill 顺序 4 段: + step A — pymatgen import + CEMENT_PHASES 几个查询 + mp_rester 未配 key 抛错 + step B — stats_ml 三库装包 + 小 OLS / RandomForest smoke + step C — plot_pub apply_pub_style + 最小 XRD-like 图出 PNG + step D —(可选)MP_API_KEY 配了就联网拉一条 Materials Project 数据 + +任一步异常 [FAIL] 标注后继续下一步,保证整条链路看一遍。 +""" +from __future__ import annotations + +import io +import os +import sys +import tempfile +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT)) + +# Windows GBK 控制台编码问题: 强制 stdout / stderr utf-8(memory 里这条已踩过) +if hasattr(sys.stdout, "buffer"): + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") +if hasattr(sys.stderr, "buffer"): + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + + +# 读 .env 注入 MP_API_KEY 等(litellm 链路外手动加载) +env_file = ROOT / ".env" +if env_file.exists(): + for line in env_file.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + os.environ.setdefault(k.strip(), v.strip()) + + +def _hr(title: str) -> None: + print() + print("=" * 60) + print(f"[{title}]") + print("=" * 60) + + +def _ok(msg: str) -> None: + print(f"[OK] {msg}") + + +def _fail(msg: str) -> None: + print(f"[FAIL] {msg}") + + +def _skip(msg: str) -> None: + print(f"[SKIP] {msg}") + + +def _info(msg: str) -> None: + print(f"[INFO] {msg}") + + +def step_a_pymatgen() -> None: + _hr("step A: pymatgen skill") + + # A1: helper import + try: + from skills.pymatgen.materials import CEMENT_PHASES, lookup_phase, mp_rester + _ok(f"import skills.pymatgen.materials (CEMENT_PHASES 条目数={len(CEMENT_PHASES)})") + except Exception as e: + _fail(f"import skills.pymatgen.materials: {type(e).__name__}: {e}") + return + + # A2: 典型查询 + cases = [ + ("C3S", "Ca3SiO5"), + ("硅酸三钙", "Ca3SiO5"), + ("alite", "Ca3SiO5"), # 大小写不敏感 + ("钙矾石", "Ca6Al2(SO4)3(OH)12·26H2O"), + ("莫来石", "Al6Si2O13"), + ("方镁石", "MgO"), + ("石英", "SiO2"), + ] + for name, expected in cases: + try: + got = lookup_phase(name) + if got == expected: + _ok(f"lookup_phase({name!r}) -> {got}") + else: + _fail(f"lookup_phase({name!r}) -> {got},期望 {expected}") + except Exception as e: + _fail(f"lookup_phase({name!r}) raised {type(e).__name__}: {e}") + + # A3: 未命中抛 KeyError + try: + lookup_phase("根本不存在的相_xyz123") + _fail("lookup_phase 未命中应抛 KeyError,没抛") + except KeyError as e: + _ok(f"lookup_phase 未命中正确抛 KeyError (msg 含建议: {'补到' in str(e)})") + except Exception as e: + _fail(f"lookup_phase 未命中应抛 KeyError,实际 {type(e).__name__}") + + # A4: pymatgen 本体 import + try: + from pymatgen.core import Structure, Lattice, Molecule + from pymatgen.analysis.diffraction.xrd import XRDCalculator + from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + _ok("pymatgen 核心类 import 全通 (Structure / XRDCalculator / SpacegroupAnalyzer)") + except Exception as e: + _fail(f"pymatgen 本体 import: {type(e).__name__}: {e}") + return + + # A5: 构造一个简单的立方结构 + XRDCalculator 跑一次 + try: + from pymatgen.core import Structure, Lattice + from pymatgen.analysis.diffraction.xrd import XRDCalculator + # MgO,方镁石,典型耐火材料相 + lattice = Lattice.cubic(4.21) # MgO a≈4.21Å + struct = Structure(lattice, ["Mg", "O"], [[0, 0, 0], [0.5, 0.5, 0.5]]) + xrd = XRDCalculator(wavelength="CuKa") + pattern = xrd.get_pattern(struct, two_theta_range=(20, 80)) + _ok(f"XRDCalculator on MgO 结构: {len(pattern.x)} 个峰,2θ 范围 [{pattern.x[0]:.1f}, {pattern.x[-1]:.1f}]") + except Exception as e: + _fail(f"XRDCalculator smoke: {type(e).__name__}: {e}") + + # A6: mp_rester 未配 key 抛 RuntimeError + has_key = bool(os.environ.get("MP_API_KEY")) + if has_key: + _info("MP_API_KEY 已配置,skip 缺 key 抛错验证(下面 step D 测真实查询)") + else: + # 显式清掉 env 再测 + try: + with mp_rester() as mpr: + _fail("MP_API_KEY 未配置时 mp_rester 应抛 RuntimeError,没抛") + except RuntimeError as e: + if "MP_API_KEY" in str(e) and "materialsproject" in str(e): + _ok("mp_rester 未配 key 正确抛 RuntimeError 含申请链接") + else: + _fail(f"RuntimeError 抛了但 msg 不对: {e}") + except Exception as e: + _fail(f"应抛 RuntimeError 实际 {type(e).__name__}: {e}") + + +def step_b_stats_ml() -> None: + _hr("step B: stats_ml skill") + + # B1: sklearn + try: + import numpy as np + from sklearn.ensemble import RandomForestRegressor + from sklearn.preprocessing import StandardScaler + from sklearn.pipeline import Pipeline + from sklearn.model_selection import cross_val_score + + # 造一个 fake 配方-强度数据(50 样本 6 特征) + rng = np.random.default_rng(42) + X = rng.uniform(0, 1, size=(50, 6)) # 6 个掺合料比例 + y = (X @ rng.uniform(20, 80, size=6)) + rng.normal(0, 5, size=50) # 强度 MPa + pipe = Pipeline([ + ("scaler", StandardScaler()), + ("model", RandomForestRegressor(n_estimators=50, random_state=42)), + ]) + scores = cross_val_score(pipe, X, y, cv=5, scoring="r2") + _ok(f"sklearn RandomForest 5-fold R²: mean={scores.mean():.3f} std={scores.std():.3f}") + except Exception as e: + _fail(f"sklearn smoke: {type(e).__name__}: {e}") + + # B2: statsmodels + try: + import numpy as np + import pandas as pd + import statsmodels.formula.api as smf + rng = np.random.default_rng(42) + df = pd.DataFrame({ + "x1": rng.uniform(0, 1, 50), + "x2": rng.uniform(0, 1, 50), + }) + df["y"] = 3 * df["x1"] - 2 * df["x2"] + rng.normal(0, 0.3, 50) + model = smf.ols("y ~ x1 + x2", data=df).fit() + r2 = model.rsquared + p_x1 = model.pvalues["x1"] + _ok(f"statsmodels OLS: R²={r2:.3f}, p(x1)={p_x1:.4f} (应 << 0.05)") + except Exception as e: + _fail(f"statsmodels smoke: {type(e).__name__}: {e}") + + # B3: PyMC(可选) + try: + import pymc as pm + import arviz as az + _ok(f"pymc import OK (version={pm.__version__})") + # 不真跑采样(慢),只验 import + except ImportError: + _skip("PyMC / arviz 未装(可选依赖,要做贝叶斯再 pip install pymc arviz)") + except Exception as e: + _fail(f"PyMC import: {type(e).__name__}: {e}") + + +def step_c_plot_pub() -> None: + _hr("step C: plot_pub skill") + + # C1: import + apply_pub_style + try: + from skills.plot_pub.style import apply_pub_style, reset_style, _find_chinese_font + font = _find_chinese_font() + if font: + _ok(f"_find_chinese_font 返 {font!r}") + else: + _info("系统未装中文字体候选 (SimHei/YaHei/WenQuanYi/Heiti),中文将显示方块") + + apply_pub_style() + _ok("apply_pub_style() 调用通过") + except Exception as e: + _fail(f"plot_pub import / apply_pub_style: {type(e).__name__}: {e}") + return + + # C2: 跑一个 minimal XRD-like 图 + try: + import numpy as np + import matplotlib.pyplot as plt + + tmp_dir = Path(tempfile.mkdtemp(prefix="zcbot_smoke_plot_")) + out_png = tmp_dir / "smoke_xrd.png" + out_pdf = tmp_dir / "smoke_xrd.pdf" + + two_theta = np.linspace(5, 80, 1000) + # 假装是 MgO 衍射(几个高斯峰) + peaks = [(36.9, 1.0), (42.9, 0.7), (62.3, 0.5), (74.7, 0.3), (78.6, 0.4)] + intensity = np.zeros_like(two_theta) + for pos, h in peaks: + intensity += h * np.exp(-((two_theta - pos) ** 2) / (2 * 0.3 ** 2)) + intensity += np.random.normal(0, 0.02, len(two_theta)) # 噪声 + + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(two_theta, intensity, "k-", lw=1.0, label="MgO 模拟谱") + ax.set_xlabel(r"$2\theta$ / °") + ax.set_ylabel("强度 / a.u.") + ax.set_xlim(5, 80) + ax.legend(frameon=False) + fig.tight_layout() + fig.savefig(out_png, dpi=200) + fig.savefig(out_pdf) + plt.close(fig) + + png_size = out_png.stat().st_size + pdf_size = out_pdf.stat().st_size + _ok(f"出图 PNG ({png_size/1024:.1f}KB) + PDF ({pdf_size/1024:.1f}KB) -> {tmp_dir}") + except Exception as e: + _fail(f"plot_pub 出图: {type(e).__name__}: {e}") + + # C3: 还原 rcParams 防污染后续步骤 + try: + reset_style() + _ok("reset_style() 还原 matplotlib defaults") + except Exception as e: + _fail(f"reset_style: {type(e).__name__}: {e}") + + +def step_d_mp_online() -> None: + _hr("step D: Materials Project 联网(可选,需 MP_API_KEY)") + + if not os.environ.get("MP_API_KEY"): + _skip("MP_API_KEY 未配,跳过联网查询(.env 加 MP_API_KEY=... 即可,免费申请 https://materialsproject.org/api)") + return + + try: + from skills.pymatgen.materials import mp_rester, lookup_phase + formula = lookup_phase("C3S") # Ca3SiO5 + t0 = time.time() + with mp_rester() as mpr: + docs = mpr.materials.summary.search( + formula=formula, + fields=["material_id", "formula_pretty", "energy_above_hull"], + ) + dt = (time.time() - t0) * 1000 + _ok(f"mp_rester 查 {formula}: 返回 {len(docs)} 条 in {dt:.0f}ms") + for d in docs[:3]: + print(f" {d.material_id} {d.formula_pretty} ehull={d.energy_above_hull:.3f}") + except Exception as e: + _fail(f"mp_rester 联网查询: {type(e).__name__}: {e}") + + +def main() -> int: + print("=" * 60) + print("zcbot scientific skills smoke (pymatgen / stats_ml / plot_pub)") + print("=" * 60) + for fn in (step_a_pymatgen, step_b_stats_ml, step_c_plot_pub, step_d_mp_online): + try: + fn() + except Exception as e: + _fail(f"[{fn.__name__} crashed] {type(e).__name__}: {e}") + print() + print("=" * 60) + print("smoke done") + print("=" * 60) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/plot_pub/SKILL.md b/skills/plot_pub/SKILL.md new file mode 100644 index 0000000..ffa830f --- /dev/null +++ b/skills/plot_pub/SKILL.md @@ -0,0 +1,168 @@ +--- +name: plot_pub +description: 出版级 matplotlib 绘图(论文 / 报告 / 申报书用,中文字体 + viridis 配色 + dpi 设定一键到位)。✅ 触发:用户要画 XRD 谱、TG-DSC 曲线、应力-应变曲线、SEM 标注、多 panel 学术图、要 SVG/PDF 矢量图给论文。⛔ 不触发:PPT 内嵌图(走 `ppt` skill 的 matplotlib 配图流程);只要快速看一眼数据(直接 `df.plot()` 即可,不用本 skill)。 +--- + +# plot_pub + +服务建材院的论文 / 申报书 / 调研报告出图。**核心是 `apply_pub_style()` 一键设置 rcParams**(中文字体、viridis 默认、字号、dpi、矢量优先),避免每次手动设置十几行还忘配中文字体。 + +## 何时用 + +- 写论文要投 *Cement and Concrete Research* / *J. Am. Ceram. Soc.* / *Construction and Building Materials* 等期刊 +- 报告 / 申报书要中文标题 + 中文图例的高 dpi 图 +- 多组样本数据对比(XRD 多谱叠图、强度发展曲线、温度场) +- 双 Y 轴(TG-DSC 是典型) + +## 何时不用 + +- PPT 里要嵌图 → 走 `skills/ppt`(那边已经有配色 / 尺寸规范) +- 交互式探查数据 → 用 plotly / 直接 jupyter +- 只要看趋势不出版 → `df.plot()` 一行搞定,不用本 skill 的 ceremony + +## 准备 + +```python +from skills.plot_pub.style import apply_pub_style +import matplotlib.pyplot as plt +import numpy as np + +apply_pub_style() # 调一次,后面所有 plt 都按这个 style +``` + +## `apply_pub_style()` 参数 + +| 参数 | 默认 | 说明 | +|---|---|---| +| `chinese` | `True` | 加载 SimHei / Microsoft YaHei,中文不显示方块 | +| `dpi` | `150` | 屏幕显示 dpi;保存图用 `savefig(..., dpi=300)` 单独控 | +| `font_size` | `10` | 论文双栏图标准字号 | +| `linewidth` | `1.2` | 线粗,出版图细些显精致 | +| `cmap` | `"viridis"` | 默认 colormap,**审稿人不会喷**(jet 现在是雷区) | + +## 典型工作流 + +### A. XRD pattern(多相叠图 + 标峰) + +```python +from skills.plot_pub.style import apply_pub_style +import matplotlib.pyplot as plt +import numpy as np + +apply_pub_style() + +fig, ax = plt.subplots(figsize=(6, 4)) + +# 实测谱 +ax.plot(two_theta_exp, intensity_exp, "k-", lw=1.0, label="实测") + +# 理论谱(各物相,用 pymatgen XRDCalculator 算的) +for phase, pattern in pymatgen_patterns.items(): + ax.vlines(pattern.x, 0, pattern.y / pattern.y.max() * intensity_exp.max() * 0.3, + label=phase, alpha=0.7) + +ax.set_xlabel(r"$2\theta$ / °") +ax.set_ylabel("强度 / a.u.") +ax.set_xlim(5, 80) +ax.legend(frameon=False, loc="upper right") +fig.tight_layout() +fig.savefig("xrd_compare.pdf", bbox_inches="tight") # PDF 矢量,期刊偏好 +fig.savefig("xrd_compare.png", dpi=300, bbox_inches="tight") # PNG 给 PPT +plt.close(fig) # 关键!run_python 多任务下不关会堆 figure 泄漏内存 +``` + +### B. TG-DSC 双 Y 轴 + +```python +fig, ax_tg = plt.subplots(figsize=(6, 4)) +ax_dsc = ax_tg.twinx() + +ax_tg.plot(T, tg_mass, "b-", lw=1.2, label="TG") +ax_dsc.plot(T, dsc_flow, "r-", lw=1.2, label="DSC") + +ax_tg.set_xlabel("温度 / °C") +ax_tg.set_ylabel("质量 / %", color="b") +ax_dsc.set_ylabel("热流 / mW·mg$^{-1}$", color="r") +ax_tg.tick_params(axis="y", labelcolor="b") +ax_dsc.tick_params(axis="y", labelcolor="r") + +# 合并两个 ax 的 legend +lines1, labels1 = ax_tg.get_legend_handles_labels() +lines2, labels2 = ax_dsc.get_legend_handles_labels() +ax_tg.legend(lines1 + lines2, labels1 + labels2, loc="best", frameon=False) + +fig.tight_layout() +fig.savefig("tg_dsc.pdf", bbox_inches="tight") +plt.close(fig) +``` + +### C. 多组样本对比(强度发展曲线) + +```python +fig, ax = plt.subplots(figsize=(6, 4)) +ages = [3, 7, 14, 28, 56, 90] + +# 用 viridis 给多个样本上色,顺序色觉友好 +import matplotlib.cm as cm +colors = cm.viridis(np.linspace(0.1, 0.9, len(samples))) + +for (label, strengths), c in zip(samples.items(), colors): + ax.plot(ages, strengths, "o-", color=c, lw=1.2, ms=5, label=label) + +ax.set_xlabel("龄期 / d") +ax.set_ylabel("抗压强度 / MPa") +ax.set_xscale("log") # 龄期用对数刻度更清楚 +ax.legend(frameon=False) +ax.grid(True, ls="--", alpha=0.3) + +fig.tight_layout() +fig.savefig("strength_dev.pdf", bbox_inches="tight") +plt.close(fig) +``` + +### D. 多 panel(2×2 figure,论文 figure 1 标配) + +```python +fig, axes = plt.subplots(2, 2, figsize=(10, 7), constrained_layout=True) + +axes[0, 0].plot(...) # (a) XRD +axes[0, 1].plot(...) # (b) FTIR +axes[1, 0].plot(...) # (c) TG +axes[1, 1].plot(...) # (d) 强度 + +# 子图编号(论文图 1 标准做法) +for ax, letter in zip(axes.flat, "abcd"): + ax.text(0.02, 0.95, f"({letter})", transform=ax.transAxes, + fontweight="bold", va="top") + +fig.savefig("fig1_characterization.pdf", bbox_inches="tight") +plt.close(fig) +``` + +## 中文字体配置(Windows 注意) + +`apply_pub_style(chinese=True)` 默认按以下顺序找字体: +1. `SimHei`(Windows 自带,黑体) +2. `Microsoft YaHei`(Windows 自带,雅黑) +3. `WenQuanYi Micro Hei`(Linux 兜底) + +都没有 → 退回 DejaVu Sans,中文显示方块。装字体方式:Windows 控制面板 → 字体,丢 .ttf 进去。 + +**LaTeX 公式渲染**:matplotlib 用 `$...$` 自动走 mathtext,中英文混排正常。要更精细的 LaTeX(自定义 macro)就 `plt.rcParams["text.usetex"] = True`,但要本地装 TeX Live。 + +## 反模式 + +1. **用 pyplot 状态机不用 OO 接口** —— 写 `plt.plot(); plt.xlabel(...)` 一时爽,多 figure 时变量乱;一律 `fig, ax = plt.subplots()` +2. **不 close figure** —— `run_python` 跑多次会堆 figure 内存涨,必须 `plt.close(fig)` 收尾 +3. **用 jet colormap** —— 色觉障碍审稿人会喷,近年期刊都偏 viridis / plasma / cividis +4. **保存图不 `bbox_inches="tight"`** —— legend / 标题被裁掉,反复看图反复改尺寸 +5. **同图 dpi 跟 figsize 不匹配** —— figsize=(10,7) dpi=300 给 PPT 嵌入太大,字会很小;论文图 figsize=(6,4) 双栏刚好 +6. **PNG 给论文** —— 期刊要矢量,PDF / SVG / EPS 优先;PNG 只给 PPT / 网页 +7. **中文显示方块还坚持跑** —— `apply_pub_style()` 失败会 warn,看到 warning 就停下查字体 +8. **legend 框线 / 灰底** —— 出版图 `legend(frameon=False)`,干净 +9. **轴标签没单位** —— `"温度"` 是错的,`"温度 / °C"` 才对(SI 推荐 `/` 分隔) +10. **`ax.set_xticks([1,2,3,...])` 手动写一堆** —— matplotlib 自动 tick 已经足够,手动覆盖只在特殊场景(对数轴 / 离散类别轴) + +## 依赖 + +matplotlib 已经在 `requirements.txt`(>=3.8.0)。本 skill 无新增依赖。 diff --git a/skills/plot_pub/style.py b/skills/plot_pub/style.py new file mode 100644 index 0000000..6a0c75a --- /dev/null +++ b/skills/plot_pub/style.py @@ -0,0 +1,114 @@ +""" +plot_pub skill — 出版级 matplotlib rcParams 一键设置。 + +LLM 通过 `from skills.plot_pub.style import apply_pub_style; apply_pub_style()` 使用。 +""" + +from __future__ import annotations + +import warnings +import matplotlib +import matplotlib.pyplot as plt +from matplotlib import font_manager + + +# 中文字体候选,按平台优先级排 +_CHINESE_FONTS = [ + "SimHei", # Windows 黑体 + "Microsoft YaHei", # Windows 雅黑 + "WenQuanYi Micro Hei", # Linux + "Heiti TC", # macOS + "Arial Unicode MS", # macOS 兜底 +] + + +def _find_chinese_font() -> str | None: + """从候选清单里挑系统装了的第一个,都没有返 None。""" + installed = {f.name for f in font_manager.fontManager.ttflist} + for name in _CHINESE_FONTS: + if name in installed: + return name + return None + + +def apply_pub_style( + chinese: bool = True, + dpi: int = 150, + font_size: int = 10, + linewidth: float = 1.2, + cmap: str = "viridis", +) -> None: + """ + 一键设置出版级 matplotlib rcParams。 + + Args: + chinese: True 时加载中文字体,失败 warn 但不抛(图继续画,中文显示方块) + dpi: 屏幕显示 dpi。保存图请用 savefig(..., dpi=300) 单独指定 + font_size: 基础字号,论文双栏图标准 10pt + linewidth: 线粗,出版图细些更精致 + cmap: 默认 colormap,viridis / plasma / cividis 是色觉友好选择 + """ + rc = matplotlib.rcParams + + # ---- 字体 ---- + if chinese: + font = _find_chinese_font() + if font: + # 中文字体放第一位,后面接 Arial / DejaVu 处理英文 + rc["font.sans-serif"] = [font, "Arial", "DejaVu Sans"] + else: + warnings.warn( + "[plot_pub] 未找到中文字体(候选: " + + ", ".join(_CHINESE_FONTS) + + ")。中文将显示为方块。" + + "Windows 控制面板 → 字体,Linux 装 wqy-microhei 包。", + RuntimeWarning, + stacklevel=2, + ) + rc["font.sans-serif"] = ["Arial", "DejaVu Sans"] + else: + rc["font.sans-serif"] = ["Arial", "DejaVu Sans"] + + rc["axes.unicode_minus"] = False # 负号不显示成方块 + rc["font.family"] = "sans-serif" + rc["font.size"] = font_size + + # ---- 尺寸 / dpi ---- + rc["figure.dpi"] = dpi + rc["savefig.dpi"] = 300 # 默认保存高 dpi,临时图用 savefig(..., dpi=...) 覆盖 + rc["savefig.bbox"] = "tight" + rc["savefig.pad_inches"] = 0.05 + + # ---- 线条 ---- + rc["lines.linewidth"] = linewidth + rc["lines.markersize"] = 4 + rc["axes.linewidth"] = 0.8 + + # ---- 刻度 ---- + rc["xtick.direction"] = "in" # 期刊偏好刻度朝内 + rc["ytick.direction"] = "in" + rc["xtick.major.size"] = 4 + rc["ytick.major.size"] = 4 + rc["xtick.minor.size"] = 2 + rc["ytick.minor.size"] = 2 + rc["xtick.minor.visible"] = True + rc["ytick.minor.visible"] = True + + # ---- legend ---- + rc["legend.frameon"] = False # 出版图 legend 无框 + rc["legend.fontsize"] = font_size - 1 + + # ---- colormap ---- + rc["image.cmap"] = cmap + + # ---- 数学字体 ---- + rc["mathtext.fontset"] = "stix" # 跟 Times / Arial 配,公式不突兀 + + # ---- 兜底:防止 PDF 嵌入 Type 3 字体(期刊要求 Type 42) ---- + rc["pdf.fonttype"] = 42 + rc["ps.fonttype"] = 42 + + +def reset_style() -> None: + """还原 matplotlib 默认 rcParams(测试 / 切换主题时用)。""" + matplotlib.rcdefaults() diff --git a/skills/pymatgen/SKILL.md b/skills/pymatgen/SKILL.md new file mode 100644 index 0000000..82bc8f3 --- /dev/null +++ b/skills/pymatgen/SKILL.md @@ -0,0 +1,144 @@ +--- +name: pymatgen +description: 无机材料计算工具(晶体结构 I/O、XRD 模拟、相图、对称性、Materials Project 查询)。✅ 触发:用户问水泥熟料相 / 玻璃陶瓷物相 / 耐火砖矿物相、XRD 谱图反演与正向模拟、晶格参数、空间群、相稳定性、查 Materials Project 数据。⛔ 不触发:用户只问材料宏观性能(强度/导热),不涉及晶体结构;或属于有机分子/药物(那是 RDKit 范畴)。 +--- + +# Pymatgen + +无机材料计算的核心库,服务建材院的水泥 / 混凝土 / 玻璃 / 陶瓷 / 耐火材料场景。**底层用 pymatgen 官方 API,本 skill 提供两个轻量 helper**:`CEMENT_PHASES` 常量(中文相名→化学式映射)和 `mp_rester()`(从 env 拿 MP_API_KEY)。 + +## 何时用 + +- 用户给晶体结构文件(.cif / POSCAR / .xyz)要分析 +- 要正向算 XRD pattern 跟实测谱对比 +- 要查某矿物相的空间群 / 晶格参数 / 配位环境 +- 要做凝胶相 / 水化产物的相图 / 稳定性分析 +- 要从 Materials Project 拉某化合物的已知结构 / 性质 +- 写 VASP / Gaussian / Quantum ESPRESSO 输入文件 + +## 何时不用 + +- 用户只问宏观性能(28d 抗压、导热系数、热膨胀)→ 走 `stats_ml`(回归建模) +- 用户问有机外加剂分子结构 → 走 RDKit(本仓库未集成) +- 用户只要画图,没有晶体计算需求 → 走 `plot_pub` + +## 准备 + +```python +from skills.pymatgen.materials import CEMENT_PHASES, mp_rester +from pymatgen.core import Structure, Lattice, Molecule +from pymatgen.analysis.diffraction.xrd import XRDCalculator +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer +from pymatgen.analysis.phase_diagram import PhaseDiagram, PDEntry +``` + +(import 路径走 `run_python` 注入的 PYTHONPATH,直接写) + +## 关键:中文相名 → 化学式 + +`CEMENT_PHASES` dict 收了水泥 / 陶瓷 / 耐火 / 玻璃常见相的中英文 ↔ 化学式映射(见 `materials.py`)。**用户用中文报相名时,先查这张表转化学式再喂 pymatgen / Materials Project**: + +| 用户原话 | 不要这样 | 这样 | +|---|---|---| +| C3S | `Structure.from_file("C3S")` | 先 `CEMENT_PHASES["C3S"]` → `"Ca3SiO5"`,再走 mp 查或自己拼 | +| 钙矾石(AFt) | mp 直接搜 "AFt" | `CEMENT_PHASES["钙矾石"]` → `"Ca6Al2(SO4)3(OH)12·26H2O"` | +| 莫来石 | mp 直接搜 "mullite" | `CEMENT_PHASES["莫来石"]` → `"Al6Si2O13"` | +| 方镁石 | 直接搜 "magnesite"(错,那是菱镁矿) | `CEMENT_PHASES["方镁石"]` → `"MgO"` | + +表里没有的相,先英文学名 → 化学式后再喂,不要直接把中文丢给 mp。 + +## Materials Project 接入 + +API key 走 env:`MP_API_KEY`(申请:https://materialsproject.org/api)。**必须用 context manager**: + +```python +with mp_rester() as mpr: # 自动从 env 拿 key + docs = mpr.materials.summary.search( + formula="Ca3SiO5", + fields=["material_id", "formula_pretty", "symmetry", "energy_above_hull"], + ) + for d in docs[:5]: + print(d.material_id, d.formula_pretty, d.symmetry.symbol, d.energy_above_hull) +``` + +`MP_API_KEY` 没配 → `mp_rester()` 抛 `RuntimeError("MP_API_KEY not set in env...")`,告诉用户去配,不要继续。 + +## 典型工作流 + +### A. 实测 XRD 比对(谁是这个峰) + +1. 用户给疑似相清单(中文 / 英文 / 简写都行) +2. 各相分别:`CEMENT_PHASES` 查化学式 → `mp_rester()` 拿 Structure → `XRDCalculator().get_pattern(structure)` 算理论谱 +3. 把各相理论谱跟实测谱(用户给的 xy 数据)叠图(走 `plot_pub`) +4. 报"x° 这个峰最可能是 C3S 的 (h k l) 衍射" + +```python +from pymatgen.analysis.diffraction.xrd import XRDCalculator + +xrd = XRDCalculator(wavelength="CuKa") # 默认 Cu Kα +with mp_rester() as mpr: + docs = mpr.materials.summary.search(formula="Ca3SiO5", fields=["material_id"]) + struct = mpr.get_structure_by_material_id(docs[0].material_id) +pattern = xrd.get_pattern(struct, two_theta_range=(5, 80)) +# pattern.x = 2θ 列表, pattern.y = 强度, pattern.hkls = (h,k,l) 列表 +``` + +### B. 给定结构问对称性 + +```python +from pymatgen.core import Structure +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + +struct = Structure.from_file("path/to/sample.cif") +sga = SpacegroupAnalyzer(struct) +print(sga.get_space_group_symbol()) # 例 "P21/c" +print(sga.get_space_group_number()) # 例 14 +print(sga.get_crystal_system()) # 例 "monoclinic" +prim = sga.get_primitive_standard_structure() # 简约胞 +``` + +### C. 凝胶 / 水化产物相图稳定性 + +```python +from pymatgen.analysis.phase_diagram import PhaseDiagram + +with mp_rester() as mpr: + entries = mpr.get_entries_in_chemsys(["Ca", "Si", "O", "H"]) +pd = PhaseDiagram(entries) +# pd.get_decomp_and_e_above_hull(some_entry) → 分解路径 + 能量 +``` + +### D. 格式转换(给计算所做 VASP 输入) + +```python +struct.to(filename="POSCAR") # 自动按后缀写 +struct.to(filename="output.cif") +``` + +## 反模式 + +- 用户报中文相名(C3S / 钙矾石 / 莫来石)直接喂 mp / pymatgen,不查 `CEMENT_PHASES` —— mp 不认中文,简写也不认 +- `MPRester` 不走 context manager(`mpr = MPRester(); ...`) —— 连接泄漏 +- 手写 CIF parser → 一律 `Structure.from_file()` +- 不做 `SpacegroupAnalyzer.get_primitive_standard_structure()` 直接拿原胞做对称性比对(原胞可能是超胞,对称性少看出来) +- 大 cutoff 邻居搜索(`get_neighbors(r=20)`)—— 性能差,先 `r=5` 试 +- 编造 material_id / 化学式 —— mp 查不到就告诉用户"库里没收录",不要凭训练数据脑补 +- 自己写 INCAR(VASP 输入)—— 用 `MPRelaxSet` / `MPStaticSet` 拿 mp 验证过的参数 +- 把 pymatgen 算的理论 XRD 当实验值 —— 永远说清是"理论 pattern",实测有择优取向 / 仪器展宽差异 + +## 依赖 + +`requirements.txt` 加: +``` +pymatgen>=2024.0 +mp-api>=0.41.0 +``` +装:`.venv/Scripts/pip install pymatgen mp-api` + +## env + +``` +MP_API_KEY=your_key_from_materialsproject_org +``` + +写到 `.env`(项目根)即可,`mp_rester()` 自动读。 diff --git a/skills/pymatgen/materials.py b/skills/pymatgen/materials.py new file mode 100644 index 0000000..55ee9cf --- /dev/null +++ b/skills/pymatgen/materials.py @@ -0,0 +1,153 @@ +""" +pymatgen skill helpers — 建材院无机材料场景常用映射 + MPRester 封装。 + +LLM 通过 `from skills.pymatgen.materials import CEMENT_PHASES, mp_rester` 使用。 +""" + +from __future__ import annotations + +import os +from contextlib import contextmanager + + +# 中文/简写 → 化学式映射。覆盖建材院 R&D 高频物相。 +# 添加新条目时: +# - key 用用户最自然的称呼(中文 / 行业简写),value 是 mp 能识别的化学式 +# - 同一物相多种叫法都加 key,指向同一化学式(C3S / 硅酸三钙 / 阿利特 都是 Ca3SiO5) +CEMENT_PHASES: dict[str, str] = { + # ---- 硅酸盐水泥熟料矿物 ---- + "C3S": "Ca3SiO5", + "硅酸三钙": "Ca3SiO5", + "阿利特": "Ca3SiO5", + "alite": "Ca3SiO5", + + "C2S": "Ca2SiO4", + "硅酸二钙": "Ca2SiO4", + "贝利特": "Ca2SiO4", + "belite": "Ca2SiO4", + + "C3A": "Ca3Al2O6", + "铝酸三钙": "Ca3Al2O6", + + "C4AF": "Ca4Al2Fe2O10", + "铁铝酸四钙": "Ca4Al2Fe2O10", + "铁相": "Ca4Al2Fe2O10", + + # ---- 水化产物 ---- + "C-S-H": "Ca1.5SiO3.5·xH2O", + "水化硅酸钙": "Ca1.5SiO3.5·xH2O", + + "CH": "Ca(OH)2", + "氢氧化钙": "Ca(OH)2", + "portlandite": "Ca(OH)2", + "钙矾石": "Ca6Al2(SO4)3(OH)12·26H2O", + "AFt": "Ca6Al2(SO4)3(OH)12·26H2O", + "ettringite": "Ca6Al2(SO4)3(OH)12·26H2O", + + "AFm": "Ca4Al2(OH)12(SO4)·6H2O", + "单硫型水化硫铝酸钙": "Ca4Al2(OH)12(SO4)·6H2O", + + # ---- 石膏 / 硫酸盐 ---- + "石膏": "CaSO4·2H2O", + "二水石膏": "CaSO4·2H2O", + "gypsum": "CaSO4·2H2O", + "半水石膏": "CaSO4·0.5H2O", + "硬石膏": "CaSO4", + "anhydrite": "CaSO4", + + # ---- 碳酸盐 / 碳化产物 ---- + "方解石": "CaCO3", + "calcite": "CaCO3", + "文石": "CaCO3", + "aragonite": "CaCO3", + + # ---- 陶瓷 / 耐火常见相 ---- + "莫来石": "Al6Si2O13", + "mullite": "Al6Si2O13", + + "堇青石": "Mg2Al4Si5O18", + "cordierite": "Mg2Al4Si5O18", + + "刚玉": "Al2O3", + "α-Al2O3": "Al2O3", + "corundum": "Al2O3", + + "方镁石": "MgO", + "periclase": "MgO", + + "尖晶石": "MgAl2O4", + "spinel": "MgAl2O4", + + "锆英石": "ZrSiO4", + "zircon": "ZrSiO4", + + "石英": "SiO2", + "quartz": "SiO2", + + "方石英": "SiO2", + "cristobalite": "SiO2", + + "鳞石英": "SiO2", + "tridymite": "SiO2", + + # ---- 玻璃常见组分晶相 ---- + "钙长石": "CaAl2Si2O8", + "anorthite": "CaAl2Si2O8", + "钠长石": "NaAlSi3O8", + "albite": "NaAlSi3O8", + "硅灰石": "CaSiO3", + "wollastonite": "CaSiO3", + "透辉石": "CaMgSi2O6", + "diopside": "CaMgSi2O6", + + # ---- 其他常见 ---- + "白云石": "CaMg(CO3)2", + "dolomite": "CaMg(CO3)2", + "赤铁矿": "Fe2O3", + "hematite": "Fe2O3", + "磁铁矿": "Fe3O4", + "magnetite": "Fe3O4", +} + + +def lookup_phase(name: str) -> str: + """中文/简写相名 → 化学式。命中返回化学式,未命中抛 KeyError(带建议)。""" + if name in CEMENT_PHASES: + return CEMENT_PHASES[name] + # 大小写不敏感再查一遍 + lower = name.lower() + for k, v in CEMENT_PHASES.items(): + if k.lower() == lower: + return v + raise KeyError( + f"{name!r} 不在 CEMENT_PHASES 映射表里。" + f"若是新相,直接把化学式喂给 pymatgen / Materials Project;" + f"若高频用,补到 skills/pymatgen/materials.py 的 CEMENT_PHASES。" + ) + + +@contextmanager +def mp_rester(api_key: str | None = None): + """ + MPRester 上下文管理器封装,自动从 env(MP_API_KEY)拿 key。 + + 用法: + with mp_rester() as mpr: + docs = mpr.materials.summary.search(formula="Ca3SiO5") + + Args: + api_key: 显式传入则用,否则读 env MP_API_KEY。 + + Raises: + RuntimeError: env 未配置且未传入 api_key。 + """ + key = api_key or os.environ.get("MP_API_KEY") + if not key: + raise RuntimeError( + "MP_API_KEY not set in env. " + "申请: https://materialsproject.org/api,然后写到项目根 .env 文件。" + ) + from mp_api.client import MPRester # 局部 import,避免装包前 import skill 就崩 + + with MPRester(api_key=key) as mpr: + yield mpr diff --git a/skills/stats_ml/SKILL.md b/skills/stats_ml/SKILL.md new file mode 100644 index 0000000..8cbb5ae --- /dev/null +++ b/skills/stats_ml/SKILL.md @@ -0,0 +1,187 @@ +--- +name: stats_ml +description: 统计建模与机器学习(sklearn / statsmodels / PyMC 三库合一,场景导航选库)。✅ 触发:配方-性能建模、DoE 响应面、强度预测、特征重要性、假设检验、置信区间、贝叶斯小样本估计、聚类异常实验检测。⛔ 不触发:用户只问描述性统计(均值方差),pandas 一行搞定;深度学习场景(走单独的 torch / lightning skill,本仓库未集成)。 +--- + +# stats_ml + +服务建材院典型场景:**掺合料配比 → 性能(强度 / 流动度 / 凝结时间)** 的建模与推断。三库分工: + +| 你要做 | 用 | 一句话理由 | +|---|---|---| +| 要 p-value、置信区间、假设检验 | **statsmodels** | OLS / ANOVA / 假设检验是 statsmodels 主场,sklearn 不给 p-value | +| 要预测精度高,可解释性次要 | **sklearn** | RandomForest / GBDT / XGBoost 是性能预测的现代基线 | +| 样本 < 30 且要不确定度估计 | **PyMC** | 贝叶斯给出参数后验分布,小样本下比频率派的置信区间更稳 | +| 要可解释的线性关系 | statsmodels OLS / sklearn LinearRegression | 都可以,统计推断走 statsmodels,纯预测走 sklearn | +| 要 DoE 响应面拟合 | statsmodels(二次回归 + ANOVA) | 主流做法,系数显著性能直接读 | +| 要聚类找异常配方 | sklearn (KMeans / DBSCAN) | 标准工具 | +| 要降维可视化(配方空间) | sklearn (PCA / t-SNE) | 标准工具 | + +## 何时用 / 何时不用 + +✅ 用: +- 用户给一张配方-性能表(N 行配方 + 性能列),要建模 / 预测 / 推断 +- 要回答"哪个因素影响最大"(特征重要性 / 系数显著性) +- 要回答"这个新配方预测强度多少 + 置信区间多大" +- 要找异常实验点 / 配方聚类 + +⛔ 不用: +- 用户只要看均值方差直方图 → pandas + matplotlib 直接搞,杀鸡不用牛刀 +- 用户给的"数据"只有 3-5 个点 → 任何 ML / 统计都不可靠,告诉用户先补数据 +- 深度学习需求(CNN / Transformer)→ 不在本 skill 范围 + +## 准备 + +```python +# 按需 import,不要全 import +import numpy as np +import pandas as pd + +# sklearn +from sklearn.model_selection import train_test_split, KFold, StratifiedKFold, GridSearchCV +from sklearn.preprocessing import StandardScaler +from sklearn.pipeline import Pipeline +from sklearn.metrics import r2_score, mean_squared_error +from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor + +# statsmodels +import statsmodels.api as sm +import statsmodels.formula.api as smf + +# PyMC(重,按需 import) +# import pymc as pm +# import arviz as az +``` + +## 典型工作流 + +### A. 配方-性能回归(sklearn,要预测精度) + +```python +# 数据:每行一个配方,X 是组分比例,y 是 28d 抗压强度 +X = df[["cement", "fly_ash", "slag", "water", "sand", "stone"]].values +y = df["strength_28d"].values + +# Pipeline 把预处理和模型绑死 —— 反模式见末尾 +pipe = Pipeline([ + ("scaler", StandardScaler()), # 线性 / 距离类模型必须;树模型可省 + ("model", RandomForestRegressor(n_estimators=300, random_state=42)), +]) + +# 数据小(< 200)用 5-fold CV,不要单次 train/test +from sklearn.model_selection import cross_val_score +scores = cross_val_score(pipe, X, y, cv=5, scoring="r2") +print(f"R2 mean={scores.mean():.3f} std={scores.std():.3f}") + +# 训完看特征重要性 +pipe.fit(X, y) +imp = pipe.named_steps["model"].feature_importances_ +for name, val in sorted(zip(df.columns, imp), key=lambda t: -t[1]): + print(f"{name}: {val:.3f}") +``` + +### B. 显著性分析(statsmodels,要 p-value) + +```python +import statsmodels.formula.api as smf + +# formula API 让模型 spec 看起来跟 R 一样 +model = smf.ols( + formula="strength_28d ~ cement + fly_ash + slag + water + water:cement", # 含交互 + data=df, +).fit() +print(model.summary()) # 系数、std err、t、p>|t|、95% CI 全在这 + +# ANOVA 比较两个嵌套模型 +m1 = smf.ols("strength_28d ~ cement + fly_ash", data=df).fit() +m2 = smf.ols("strength_28d ~ cement + fly_ash + slag", data=df).fit() +from statsmodels.stats.anova import anova_lm +print(anova_lm(m1, m2)) # slag 加进去显著吗 +``` + +### C. DoE 响应面(statsmodels 二次回归) + +```python +# 中心复合设计后,做二阶模型 +model = smf.ols( + formula="strength ~ x1 + x2 + x3 + I(x1**2) + I(x2**2) + I(x3**2) + x1:x2 + x1:x3 + x2:x3", + data=df, +).fit() +print(model.summary()) +# 看哪些二阶项 / 交互项显著,定优化方向 +``` + +### D. 小样本贝叶斯(PyMC,N<30 要不确定度) + +```python +import pymc as pm +import arviz as az + +with pm.Model() as model: + # 系数先验:弱信息正态 + intercept = pm.Normal("intercept", mu=0, sigma=10) + beta = pm.Normal("beta", mu=0, sigma=5, shape=X.shape[1]) + sigma = pm.HalfNormal("sigma", sigma=10) + + mu = intercept + pm.math.dot(X, beta) + y_obs = pm.Normal("y_obs", mu=mu, sigma=sigma, observed=y) + + trace = pm.sample(2000, tune=1000, chains=4, target_accept=0.95) + +az.summary(trace) # 后验均值 / std / 94% HDI +az.plot_posterior(trace) # 后验分布图 +``` + +### E. 配方聚类找异常实验(sklearn KMeans / DBSCAN) + +```python +from sklearn.cluster import DBSCAN +from sklearn.preprocessing import StandardScaler + +Xs = StandardScaler().fit_transform(X) +labels = DBSCAN(eps=0.5, min_samples=5).fit_predict(Xs) +# labels == -1 的点 = 离群,可能是录入错或工艺异常,人工核实 +``` + +## 反模式 + +### sklearn + +1. **预处理在 pipeline 外** —— 交叉验证会数据泄漏(test fold 的统计量被 train 看到) +2. **scaler 在 test 上 fit** —— 一律 `fit(train)` / `transform(test)`,Pipeline 自动保证 +3. **分类任务忘 stratified split** —— 类别不平衡时 `train_test_split(..., stratify=y)` +4. **不必要地 scale 树模型** —— RF / GBDT / XGBoost 不受单调变换影响 +5. **忽略 convergence warning** —— LogisticRegression / SVM 不收敛要加 `max_iter` 或 scale 特征 +6. **小数据集还 80/20 切** —— N<200 用 5-fold CV,N<50 用 LOOCV + +### statsmodels + +7. **直接读系数不看 condition number** —— `summary()` 末尾的 condition number > 30 = 严重共线性,系数解读无效 +8. **多重比较忘修正** —— 同时检验多个系数显著性时,用 Bonferroni / FDR(`statsmodels.stats.multitest`) +9. **OLS 残差不检验** —— `model.resid` 不正态 / 异方差时,OLS 推断不可靠,改 robust SE(`fit(cov_type="HC3")`) + +### PyMC + +10. **不看 R-hat / ESS** —— `az.summary(trace)` 的 `r_hat` > 1.01 或 `ess_bulk` < 400 = 没收敛,加 tune / 调 target_accept,不能用 +11. **强先验当数据用** —— 先验定太窄会主导后验,先用弱信息先验试 +12. **PyMC 装包巨大** —— 仅小样本需要时才用,大样本(N>500)频率派结果一样可信且快 100 倍 + +### 通用 + +13. **没看数据分布就建模** —— 先 `df.describe()` + `df.hist()` 扫一遍,有 NaN / 量纲极差 / 长尾分布要先处理 +14. **R2 > 0.95 就开香槟** —— 大概率是数据泄漏 / 过拟合 / 训练集等于测试集,先查 +15. **预测新配方不报置信区间** —— 工程决策不能只给点估计,sklearn 用 `RandomForestRegressor` 的 tree-level prediction std,statsmodels / PyMC 直接给 CI / HDI +16. **数据点不到 10 还硬上 ML** —— 告诉用户先做 DoE 扩样本,再建模 + +## 依赖 + +`requirements.txt` 加: +``` +scikit-learn>=1.4.0 +statsmodels>=0.14.0 +pymc>=5.10.0 # 重,装包带 pytensor,可选 +arviz>=0.17.0 # PyMC 后验诊断 +``` + +装:`.venv/Scripts/pip install scikit-learn statsmodels` +PyMC 单独装:`.venv/Scripts/pip install pymc arviz`(确认要做贝叶斯再装,首次装包 5 分钟起)