
做DFT计算的人早晚都会遇到同一个问题论文里配了一个结构想要拿来当初始模型但CIF文件找半天下不到或者想批量拉一批同体系材料做对比手动一个个去网站点半天时间就没了。这时候Materials Project数据库和它的官方Python接口mp_api就是绕不开的趁手工具。这篇学习记录是我自己在用mp_api调取数据、再用pymatgen做结构保存时踩坑总结出来的完整流程适合刚接触DFT计算、想批量拿结构文件当输入数据的人也适合已经会用命令行提交计算、但还没正经用过Materials Project API的同行。先说清楚一件事mp_api不是爬虫它是Materials Project官方维护的Python客户端底层封装了RESTful API所有数据都是数据库里DFT计算好的结果包括晶体结构、能带、态密度、热力学稳定性这些。pymatgen则是材料计算圈的瑞士军刀负责把拿到的数据解析成可操作的对象、再落盘成各种计算软件需要的格式。两者搭配起来就是一条完整的数据获取→格式转换→科研使用流水线。1. 内容整体设计与思路拆解1.1 Materials Project是什么数据从哪来Materials Project是美国劳伦斯伯克利国家实验室主导的材料数据库项目核心思路很简单用高通量DFT计算把已知的晶体结构全部算一遍然后把计算结果总能、带隙、形成能、弹性常数、能带结构等统一入库开放给科研界查询和下载。我在实战里最常用的就是它的结构数据和热力学数据比如查某个材料的energy_above_hull凸包以上能量判断相稳定性、band_gap带隙、formation_energy_per_atom每原子形成能。这套体系对计算化学/材料学工作的价值在于你不用重复造轮子。以前拿到一个新体系要么手工在文献里找结构要么自己搭一个可能不靠谱的初始结构。现在直接在库里按元素、按空间群、按稳定性条件筛几分钟能拿到几百个候选结构而且每个都附带了DFT计算好的结果可以用来做初步筛选。我自己最常用的场景就是搜一系列含特定元素的稳定相拿它们的结构作为后续掺杂、界面模型的初始输入。1.2 mp_api与pymatgen的分工mp_api全称Materials Project API客户端负责取数pymatgen负责处理。两者不是竞争关系而是上下游配合。mp_api返回的数据是经过封装的pydantic模型对象比如SummaryDoc、StructureDoc这些对象里天然嵌着pymatgen的Structure对象。换句话说mp_api本身依赖pymatgen的数据结构定义你取回来的doc对象直接.structure拿到的就是一个完整的pymatgen Structure可以马上调用它的各种方法去做对称性分析、构建超胞、替换元素或者直接写入VASP的POSCAR、CIF、JSON。我刚接触时犯过一个理解错误以为要先请求数据再手动解析字符串最后自己写代码构造Structure。实际上完全不需要mp_api在请求层就把JSON映射成了pymatgen对象你只拿现成的就行。所以pymatgen在这里的真正作用是保存和再加工把Structure转成各种文件格式落盘或者对它做进一步的结构操作。1.3 什么时候必须用API什么时候可以偷懒不是说所有数据都必须走API。单个结构、偶尔用一次直接上网站app.materialsproject.org搜索页面里点Download CIF比写代码快得多。API的价值体现在批量、筛选、可复现三件事上。批量一次拉几十上百个材料的结构和性质网页操作不现实。可复现写进脚本里参数一改就能重跑论文里的数据来源也经得起查。筛选API支持按元素组合、元素种类数、稳定性、带隙范围等条件组合查询这是网页交互式操作很难精确实现的。所以我的经验是查一两个结构用网页做数据集、做筛选、做流程化处理果断上mp_api。2. 环境准备装包、申请密钥、配置2.1 安装mp-api和pymatgen这一步没什么特别的用pip直接装就行。要注意的是mp-api和pymatgen的版本有配套关系别一个装最新版、一个停在老版本。我的建议是开一个干净的conda环境避免和科研组里其他项目的依赖打架。conda create -n mp_env python3.10 -y conda activate mp_env pip install mp-api pymatgen装完后建议顺手确认一下版本和导入是否正常import pymatgen print(pymatgen version:, pymatgen.__version__) from mp_api.client import MPRester print(mp_api import OK)这里有个隐含要求pymatgen对Python版本有要求3.10是现在比较稳的选择。如果用的是老旧Python 3.7、3.8环境直接升级环境比重装依赖省心。2.2 申请API Key并配置环境变量使用mp_api必须要有API Key。申请流程不复杂在Materials Project官网注册账号需要用教育或科研机构邮箱个人邮箱有时过不了审核登录后在个人设置里的API页面生成一个Key格式是一长串字母数字类似你的邮箱前缀加一段随机字符串。拿到Key之后千万别直接硬编码写在脚本里。一方面上传代码仓库时会泄露另一方面你换机器、换环境还得改代码。正确做法是用环境变量export MP_API_KEY你的API Key把这个写进~/.bashrc或者~/.zshrcsource一下。Python里读取就用import os API_KEY os.getenv(MP_API_KEY) if not API_KEY: raise ValueError(请先设置 MP_API_KEY 环境变量)环境变量的名字要注意新版mp-api读的是MP_API_KEY旧版的pymatgen扩展读的是PMG_MAPI_KEY。如果你看到网上老教程让你设PMG_MAPI_KEY那是旧时代的东西了新客户端不一定认。2.3 验证连接第一行代码环境配好后的第一个测试我建议从最简单、最稳定的查询开始先确认网络和密钥没问题再上复杂查询。from mp_api.client import MPRester import os API_KEY os.getenv(MP_API_KEY) with MPRester(API_KEY) as mpr: doc mpr.get_structure_by_material_id(mp-149) print(doc.formula) print(doc.lattice.abc)mp-149是MgO的标准ID几乎不会变动适合当连通性测试。如果能打印出Mg4 O4之类的化学式和晶格常数说明整个链路已经通了。这里要注意with MPRester(...) as mpr这种写法它能在退出时自动关闭连接比裸写mpr MPRester(...)再手动mpr.close()靠谱省得忘关连接导致程序挂起。3. mp_api调取数据的核心套路3.1 新老API的区别别再用过时代码我在网上搜教程时发现大量旧帖子用的还是这种写法from pymatgen.ext.matproj import MPRester # 老写法已弃用或者更老的from mp_api.rest import MPRester # 更老已经删了这两种都要避开。现在的导入方式是from mp_api.client import MPRester新旧API最大的区别在于查询方式。老API是散装方法名什么get_materials_ids、query满天飞新API则把所有查询按数据类别组织成子模块比如mpr.summary.search(...)、mpr.structures.search(...)。这样做的好处是参数提示更清晰返回到doc对象的字段也更统一。如果看文档时发现字段名对不上先确认你的mp-api是不是新版、导入路径对不对。强烈建议在项目里统一用from mp_api.client import MPRester这是当前官方主推的入口。3.2 常用查询按元素、按化学式、按稳定性/带隙筛选查询的主力接口是mpr.summary.search()。summary这个词的意思是这个接口返回的是汇总数据结构、带隙、能量、稳定性等常用性质都包含在内比单独调结构、调能带更省事。我平时80%的需求用这一个接口就解决了。最常见的筛选条件按元素组合查。比如我想找所有包含Fe和O的二元化合物from mp_api.client import MPRester import os API_KEY os.getenv(MP_API_KEY) with MPRester(API_KEY) as mpr: docs mpr.summary.search( elements[Fe, O], num_elements2, ) for doc in docs: print(doc.material_id, doc.formula_pretty, doc.energy_above_hull)这里的elements表示必须包含这些元素num_elements2限制必须是二元化合物这一组合拳能把Fe-O体系里的FeO、Fe2O3、Fe3O4等相都筛出来。energy_above_hull单位是eV/atom小于0说明是凸包上的稳定相大于0说明可能分解是很重要的热力学稳定判据。再加一个稳定性筛选只拿DFT计算预测稳定的相docs mpr.summary.search( elements[Fe, O], num_elements2, is_stableTrue, )is_stableTrue等价于energy_above_hull 0是官方封装好的过滤条件。需要按带隙筛选也可以加参数docs mpr.summary.search( elements[Si], num_elements1, band_gap(0.5, 2.0), # 带隙范围单位eV )这里band_gap接受一个元组表示闭区间范围。实际操作中我很喜欢这种参数化筛法因为可以从大到小逐步加条件看每个条件能过滤掉多少结构比一股脑把所有条件堆上去更容易理解数据分布。3.3 分页、线程与网络请求的工程细节当查询结果量很大的时候比如全库搜含Li的化合物动辄上千条mp-api默认不会一次性把所有结果都灌进内存而是做了分页处理。新版API里分页参数是num_chunks和chunk_sizedocs mpr.summary.search( elements[Li, O], is_stableTrue, num_chunks5, # 最多拉取5个分块 chunk_size500, # 每块500条 )chunk_size是每个请求返回的最大条数num_chunks是最多请求多少个块。简单说返回总数约等于两者的乘积。不设置的话客户端会尝试把所有符合条件的结果全部拉完数量多时很慢。如果我只想要部分样本就明确限制块数如果确实需要全量数据就把num_chunks设大一点。另外mp-api内部用了并发请求来加速批量拉取默认线程数是1也就是串行请求。网络状况好、对速度有要求时可以调高并发with MPRester(API_KEY, num_threads4) as mpr: ...我的建议是别贪心4个线程足够。官方服务端有每分钟请求频率限制线程开太多容易触发限流反而被服务器拒绝速度不升反降。4. 数据落盘pymatgen保存结构文件的正确姿势4.1 从API返回的doc到Structure对象搞清楚查询之后下一步就是把数据变成我们能用的文件。先用一个具体的例子串联整个过程查MgO拿到它的SummaryDoc然后取出Structure。from mp_api.client import MPRester import os API_KEY os.getenv(MP_API_KEY) with MPRester(API_KEY) as mpr: docs mpr.summary.search( elements[Mg, O], num_elements2, is_stableTrue, ) doc docs[0] struct doc.structure print(type(struct)) print(struct.composition) print(struct.lattice)doc.structure返回的是pymatgen的Structure对象。这个东西是整个pymatgen生态的核心它同时保存了晶格、原子坐标、元素种类和磁性、电荷等信息远比一个单纯的CIF内容更丰富。取到Structure之后你可以直接做很多操作比如struct.make_supercell([2, 2, 2])做超胞struct.replace(0, Fe)替换元素造掺杂模型或者用struct.get_symmetry()分析空间群。这些操作全部建立在正确的Structure对象上所以怎么拿到Structure是整条链路的命门。这里有个小细节summary里的structure是DFT结构弛豫之后的最终结构也就是理论上能量极小点对应的构型。如果你做输入结构用这个就行。如果你需要弛豫前的初始结构要在structures子模块里找initial_structure字段不过绝大多数场景用不上。4.2 保存为CIF、POSCAR、JSONpymatgen最让我省心的一点是Structure对象自带.to()方法按文件后缀自动识别格式struct.to(MgO.cif) # CIF格式 struct.to(POSCAR) # VASP输入文件 struct.to(MgO.json) # pymatgen JSON格式POSCAR没带路径前缀所以会直接写到当前工作目录。VASP计算组里经常需要批量生成POSCAR这个struct.to(POSCAR)就是最简单的实现方式。CIF是通用的晶体信息文件用来分享给同事或者导入其他软件VESTA、Materials Studio等都很方便。如果要把结构转成JSON长期存储我推荐用一个更稳妥的姿势as_dict()和from_dict()配合使用import json d struct.as_dict() # 转成可序列化的字典 with open(MgO_struct.json, w, encodingutf-8) as f: json.dump(d, f, indent2) # 读回 from pymatgen.core import Structure with open(MgO_struct.json, r, encodingutf-8) as f: d json.load(f) struct_loaded Structure.from_dict(d)为啥不直接用.to(MgO.json)因为.to()生成的JSON内部已经包含了pymatgen的类标记读回去时需要用pymatgen.core.Structure.from_str(json_str, fmtJSON)这种写法而as_dict()/from_dict()的语义更直白出错率低。两种都能用我自己更习惯后者。4.3 批量保存与文件命名规范真实任务里很少只处理一个材料更常见的是拉一批然后批量保存。下面是我实际在用的批量脚本骨架import os from mp_api.client import MPRester API_KEY os.getenv(MP_API_KEY) OUT_DIR mp_structures os.makedirs(OUT_DIR, exist_okTrue) with MPRester(API_KEY, num_threads4) as mpr: docs mpr.summary.search( elements[Li, Fe, P, O], num_elements4, is_stableTrue, ) print(f共找到 {len(docs)} 个结构) for doc in docs: mid doc.material_id formula doc.formula_pretty.replace( , ) struct doc.structure filename f{OUT_DIR}/{mid}_{formula}.cif struct.to(filename) print(已保存:, filename)这个例子搜的是Li-Fe-P-O四元稳定相会筛出一批橄榄石类似物对研究磷酸铁锂正极材料的人很有用。命名这里我强烈建议把material_id放在文件名最前面因为它是Materials Project里的唯一标识一个ID对应一个确定结构比用化学式命名更不容易撞车。同一个化学式可能有不同空间群的变体只用化学式命名会互相覆盖。保存完一定要抽查几个文件用文本工具打开CIF看看原子坐标是否合理或者用VESTA可视化验证一下别等到提交VASP任务才发现结构是错的那才是真浪费时间。4.4 一个完整的查询-筛选-保存代码模板把前面所有要点收拢成一个可以直接改参数使用的模板import os import json from datetime import datetime from mp_api.client import MPRester API_KEY os.getenv(MP_API_KEY) OUT_DIR datetime.now().strftime(mp_data_%Y%m%d) os.makedirs(OUT_DIR, exist_okTrue) # 在这里改你的筛选条件 QUERY_KWARGS dict( elements[Si], num_elements1, is_stableTrue, ) summary_path os.path.join(OUT_DIR, summary.json) os.makedirs(os.path.join(OUT_DIR, cifs), exist_okTrue) with MPRester(API_KEY, num_threads4) as mpr: docs mpr.summary.search(**QUERY_KWARGS) # 保存汇总数据不含structure避免JSON过大 summary_list [] for doc in docs: summary_list.append({ material_id: doc.material_id, formula: doc.formula_pretty, band_gap: doc.band_gap, energy_above_hull: doc.energy_above_hull, is_stable: doc.is_stable, }) with open(summary_path, w, encodingutf-8) as f: json.dump(summary_list, f, indent2) # 批量保存CIF和POSCAR for doc in docs: mid doc.material_id struct doc.structure struct.to(os.path.join(OUT_DIR, cifs, f{mid}.cif)) struct.to(os.path.join(OUT_DIR, cifs, f{mid}_POSCAR)) print(f完成共 {len(docs)} 条结果保存在 {OUT_DIR}/)这个模板把查询和保存拆开先存一份轻量的summary JSON方便后续用脚本快速筛选和统计再把结构单独存成CIF和POSCAR两个目录。等到要用数据时我可以只读summary来决定用哪些ID再按需加载结构文件不用每次跑全量。5. 实战案例批量拉取某个体系的稳定材料这一节用一个完整案例把前面的知识串起来我想找一批适合做硅基负极界面研究的材料条件是含Si、二元以上、稳定、且带隙不太大模拟导电性不能太差。实际操作时我会把条件和排除条件分开处理。5.1 确定筛选条件先明确目标假设我要找含Si且含O的二元和三元稳定化合物带隙在1.5 eV以下的材料。这个条件在网页上得点半天在API里就是几行参数的事docs mpr.summary.search( elements[Si, O], num_elements(2, 3), # 二元或三元都可以 is_stableTrue, band_gap(0, 1.5), )注意这里num_elements也可以传元组表示范围。band_gap(0, 1.5)表示从0到1.5的闭区间对带隙为0的金属体系也适用。5.2 运行结果与字段解读实际跑一遍拿到doc之后我最关心的字段是这几个字段含义单位/类型material_idMaterials Project唯一ID字符串如mp-1234formula_pretty美化后的化学式字符串spacegroup.symbol空间群符号字符串如Fm-3mband_gapDFT带隙eVenergy_above_hull凸包以上能量eV/atomformation_energy_per_atom每原子形成能eV/atomdensity密度g/cm3打印出来的效果类似这样mp-1234 SiO2 14/mmm band_gap0.87 e_above_hull0.00 density2.65 mp-4321 Si3O P-43m band_gap1.20 e_above_hull0.03 ...这里有个非常关键的认知energy_above_hull等于0.00的说明正好落在凸包上是热力学稳定相大于0的虽然被API标记为is_stableFalse但很多亚稳相在实际合成中也能存在。如果你做的是可能的候选材料筛选亚稳相反而是一块富矿不要一棍子打死。我在实际项目中经常先看stable再放宽到energy_above_hull 0.05去捞亚稳相。5.3 数据质量检查拿到批量数据后不能盲信直接开算。我每次都要做的检查有三项第一元素比例是否合理。用Structure.composition核对一下化学式防止筛出离谱的东西。第二原子间距是否合理。调用struct.distance_matrix.min()看看最近原子间距有没有小于0.8埃的情况DFT库里的结构一般不会出这种问题但下载传输过程中偶发损坏时这是一个快速报警信号。第三文件能否被重新正常解析。from pymatgen.core import Structure s Structure.from_file(cifs/mp-1234.cif) print(重新解析OK:, s.composition)这步检查虽然简单但能从源头拦截结构文件损坏导致计算一直报错的悲剧。我最早跑批量计算时跳过这一步结果有几个结构在VASP里怎么算怎么崩排查半天发现是CIF从API落盘时编码问题导致坐标飘了。6. 常见问题与排查记录6.1 认证报错与密钥问题最经典的报错是AuthenticationError或者Unauthorized。这种情况99%是API Key有问题排查顺序我从上到下确认环境变量真的设置了在Python里打印os.getenv(MP_API_KEY)看看是不是None。确认Key没有多余空格复制粘贴时常犯这个错。确认Key是官网API页面生成的那串不是登录密码。确认账号没有被封禁或Key没被手动重置。官网可以重新生成Key但生成后旧Key立刻失效。一个容易踩的坑是在某些IDE比如Jupyter Notebook里环境变量是启动时就固定下来的你改了~/.bashrc之后必须重启IDE否则os.getenv读到的还是旧值。别问我怎么知道的问就是我改了半天Key都不生效最后发现是没重启notebook。6.2 字段不存在与pydantic版本坑新版mp-api返回的是pydantic模型数据访问用doc.xxx很直接。但如果你想把doc转成字典在旧版用doc.dict()新版pydantic v2里这个方法被标记为不推荐要用doc.model_dump()。我见过不少人在网上提问为什么doc.dict()报错基本都是pydantic版本切换造成的。解决办法很简单# 老写法pydantic v1 data doc.dict() # 新写法pydantic v2 data doc.model_dump()如果项目里同时装了其他依赖导致pydantic版本被锁定最省事的办法是在pip安装时让mp-api自己解析依赖别手动乱降pydantic版本。还有一类字段不存在的问题是用了错误的doc类型。比如你想访问formation_energy_per_atom但当前返回的是StructureDoc而不是SummaryDoc就会找不到字段。遇到属性不存在时报错先print(type(doc))确认类型再回头查对应字段。6.3 请求被限流与超时处理请求量大了会收到429或者RATE_LIMIT之类的报错。这是服务端的频率限制不是你代码写错了。处理思路有三个。第一降低并发。把MPRester的num_threads从4调回1。第二增加请求间隔用time.sleep(1)在两次批量查询之间歇一歇。第三把大查询拆成小查询。比如一次全库搜几千条不如按元素分段搜每段几百条既保住数据完整性又不容易触发限流。如果遇到网络超时mp-api底层用的requests库会抛出连接错误。建议在批量脚本的外层包一层重试逻辑import time def fetch_with_retry(func, retries3, wait5, **kwargs): for attempt in range(retries): try: return func(**kwargs) except Exception as e: print(f第{attempt1}次请求失败: {e}) if attempt retries - 1: time.sleep(wait) raise RuntimeError(重试多次仍然失败)配合上下文管理器使用能明显提高批量拉取的稳定性。我自己的经验是995的请求成功率靠的是重试不是运气。6.4 保存格式的一线坑用.to()方法保存文件时有两个格式细节需要额外留意。一个是CIF保存路径如果目录不存在pymatgen不会自动创建目录会直接抛FileNotFoundError。所以批量保存前一定要os.makedirs(out_dir, exist_okTrue)。另一个是POSCAR的保存默认写入的是VASP 5格式的POSCAR部分老版VASP4.x不认这种格式。如果你还在用老版本VASP可能需要在写入时指定vasp4True这个选项或者自己写个转换小函数。现在主流VASP版本基本都兼容5格式这个问题越来越少见了但碰到了要知道原因。7. 一点个人实操体会写了这么多最后分享两个我自己的使用习惯。第一个习惯是凡是涉及Materials Project数据的工作我都会在论文的方法部分写清楚数据来源引用它的DOI并且把当时拉数据的脚本、筛选条件和版本号打包存档。科研数据讲究可追溯这既是对数据库建设者劳动的尊重也是对自己工作负责。第二个习惯是拿到结构后先用pymatgen做一次对称性确认看看空间群和文献对不对得上。我吃过一次亏从库里拿到一个标注为四方相的结构结果载入后发现对称性分析给的是一阶空间群后面所有基于对称性的分析全得重来。所以现在哪怕数据来源再可靠我也会在本地跑一遍SpacegroupAnalyzer确认花十几秒省的可能是一整天。mp_api和pymatgen这套组合往后还能扩展很多比如直接拉能带和态密度做电子结构初筛或者配合robocrys做结构描述文本生成。不过那都是后话了把调取-保存这条最基础的路走顺后面再往上加功能都会轻松很多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。