- Point all scripts at repo data/figures/output/magpie via Path(__file__) - Skip 01_process when raw JSONL absent but cleaned CSVs exist - Vendor Magpie elemental .table files; harden 03 loader (54 feats) - Tighten oxide: sole anion must be O (exclude mixed chalcogenide/pnictide; Si treated as B-site / oxide-perovskite-like, not anion) - Regenerate figures, ML/BO metrics, report.html end-to-end
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
||
"""01_process.py — NOMAD 钙钛矿 JSONL → 清洗后的条目级与聚合级 CSV
|
||
数据源: NOMAD (nomad-lab.eu) 公开 DFT 数据库, structure_name=perovskite
|
||
许可: CC BY 4.0 (NOMAD 公开数据)
|
||
"""
|
||
import json
|
||
import re
|
||
from collections import Counter
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
from pathlib import Path
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
RAW = ROOT / "data_raw" / "nomad_perovskite.jsonl"
|
||
OUT_ENTRIES = ROOT / "data" / "nomad_perovskite_entries.csv"
|
||
OUT_AGG = ROOT / "data" / "nomad_perovskite_aggregated.csv"
|
||
OUT_DIR = ROOT / "output"
|
||
|
||
|
||
def parse_band_gap(bg_list):
|
||
"""band_gap 为数组(每个 spin channel / 来源一项), 取最大值作为该条目带隙
|
||
注意: NOMAD 存储单位为 J(焦耳), 此处换算为 eV"""
|
||
if not bg_list:
|
||
return np.nan, None
|
||
vals, types = [], []
|
||
for g in bg_list:
|
||
v = g.get("value")
|
||
if v is None:
|
||
continue
|
||
vals.append(float(v))
|
||
types.append(g.get("type"))
|
||
if not vals:
|
||
return np.nan, None
|
||
imax = int(np.argmax(vals))
|
||
return vals[imax] / 1.602176634e-19, types[imax] # J -> eV
|
||
|
||
|
||
def classify_functional(names):
|
||
"""将 xc_functional_names 数组归入大类"""
|
||
if not names:
|
||
return "unknown"
|
||
s = " ".join(str(x) for x in names).lower()
|
||
if "hf" in s or "hse" in s or "hybrid" in s:
|
||
return "hybrid"
|
||
if "gga" in s:
|
||
if "u" in s and "-u" in s.replace("gga-u", "gga_u"):
|
||
return "gga_u"
|
||
return "gga"
|
||
if "lda" in s:
|
||
return "lda"
|
||
if "meta" in s or "scan" in s or "tpss" in s:
|
||
return "meta_gga"
|
||
return "other"
|
||
|
||
|
||
FORMULA_RE = re.compile(r"([A-Z][a-z]?)(\d*\.?\d*)")
|
||
|
||
|
||
def parse_formula(formula):
|
||
"""解析化学式为 {元素: 化学计量} 字典"""
|
||
if not formula or not isinstance(formula, str):
|
||
return {}
|
||
out = {}
|
||
for el, num in FORMULA_RE.findall(formula):
|
||
if not el:
|
||
continue
|
||
n = float(num) if num else 1.0
|
||
out[el] = out.get(el, 0.0) + n
|
||
return out
|
||
|
||
|
||
def main():
|
||
if not Path(RAW).exists():
|
||
if Path(OUT_ENTRIES).exists() and Path(OUT_AGG).exists():
|
||
print(f"SKIP 01_process: raw JSONL missing ({RAW}), but cleaned CSVs already present in data/")
|
||
return
|
||
raise FileNotFoundError(f"Missing raw JSONL and cleaned CSVs: {RAW}")
|
||
rows = []
|
||
with open(RAW) as f:
|
||
for line in f:
|
||
try:
|
||
e = json.loads(line)
|
||
except json.JSONDecodeError:
|
||
continue
|
||
r = e.get("results", {}) or {}
|
||
mat = r.get("material", {}) or {}
|
||
props = (r.get("properties", {}) or {}).get("electronic", {}) or {}
|
||
sym = mat.get("symmetry", {}) or {}
|
||
meth = (r.get("method", {}) or {}).get("simulation", {}) or {}
|
||
dft = meth.get("dft", {}) or {}
|
||
|
||
bg_val, bg_type = parse_band_gap(props.get("band_gap"))
|
||
rows.append(
|
||
{
|
||
"entry_id": e.get("entry_id"),
|
||
"formula": mat.get("chemical_formula_reduced"),
|
||
"formula_hill": mat.get("chemical_formula_hill"),
|
||
"space_group": sym.get("space_group_number"),
|
||
"crystal_system": sym.get("crystal_system"),
|
||
"band_gap_eV": bg_val,
|
||
"band_gap_type": bg_type,
|
||
"functional_names": dft.get("xc_functional_names"),
|
||
"functional_class": classify_functional(dft.get("xc_functional_names")),
|
||
}
|
||
)
|
||
df = pd.DataFrame(rows)
|
||
print(f"原始条目: {len(df)}")
|
||
print(f"含带隙数值条目: {df.band_gap_eV.notna().sum()}")
|
||
print(f"唯一化学式(reduced): {df.formula.nunique()}")
|
||
|
||
# ---- 条目级清洗 ----
|
||
df = df.dropna(subset=["formula", "band_gap_eV"])
|
||
df = df[(df.band_gap_eV >= 0) & (df.band_gap_eV <= 15)] # 物理范围
|
||
df["n_elements"] = df.formula.map(lambda s: len(parse_formula(s)))
|
||
print(f"清洗后条目: {len(df)}")
|
||
|
||
print("\n泛函分布:")
|
||
print(df.functional_class.value_counts().to_string())
|
||
print("\n带隙类型分布:")
|
||
print(df.band_gap_type.value_counts(dropna=False).to_string())
|
||
print("\n晶体系统分布:")
|
||
print(df.crystal_system.value_counts().to_string())
|
||
|
||
df.to_csv(OUT_ENTRIES, index=False)
|
||
|
||
# ---- 聚合: (formula, functional_class) 中位数 ----
|
||
agg = (
|
||
df.groupby(["formula", "functional_class"])
|
||
.agg(
|
||
band_gap_eV=("band_gap_eV", "median"),
|
||
band_gap_std=("band_gap_eV", "std"),
|
||
n_entries=("band_gap_eV", "size"),
|
||
space_group=("space_group", lambda s: s.mode().iloc[0] if len(s.mode()) else np.nan),
|
||
crystal_system=("crystal_system", lambda s: s.mode().iloc[0] if len(s.mode()) else None),
|
||
band_gap_type=("band_gap_type", lambda s: s.mode().iloc[0] if len(s.mode()) else None),
|
||
)
|
||
.reset_index()
|
||
)
|
||
agg["band_gap_std"] = agg["band_gap_std"].fillna(0)
|
||
print(f"\n聚合后 (formula × functional_class) 组合: {len(agg)}")
|
||
print(f"唯一化学式: {agg.formula.nunique()}")
|
||
agg.to_csv(OUT_AGG, index=False)
|
||
|
||
# 顶层泛函子集规模(用于选择训练集)
|
||
top = agg.groupby("functional_class").size().sort_values(ascending=False)
|
||
print("\n各泛函类聚合组数:")
|
||
print(top.to_string())
|
||
|
||
# 元素统计
|
||
elem_counter = Counter()
|
||
for s in agg.formula:
|
||
for el in parse_formula(s):
|
||
elem_counter[el] += 1
|
||
print(f"\n涉及元素数: {len(elem_counter)}")
|
||
print("Top-15 元素:", elem_counter.most_common(15))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|