Scandium-Labs/Scandium-Dataset
Dataset Card — Scandium-Dataset v1.0.0 Summary Scandium-Dataset provides a harmonized, quality-scored foundation of DFT-computed structural and thermodynamic properties across 267,230 materials from Materials Project, OQMD, and JARVIS-DFT. It supports the early screening stage of battery materials discovery — filtering by phase stability, electronic structure, and structural family — before downstream property prediction (ionic conductivity, mechanical stability… See the full description on the dataset page: https://huggingface.co/datasets/Scandium-Labs/Scandium-Dataset.
22.4k
1"""Phase 2: Structure Audit — geometry, crystal systems, outliers.2 3Every structure is validated for physical reasonableness.4"""5import json, os, sys, time6from pathlib import Path7from collections import Counter8 9sys.path.insert(0, str(Path.cwd()))10import numpy as np11 12OUT = Path("scripts/audit_reports")13OUT.mkdir(parents=True, exist_ok=True)14DATASET = "dataset/entries_final_v3.json"15AUDIT_DIR = Path.cwd() if Path.cwd().name == "Scandium-Dataset" else Path("/home/shamique/Scandium Labs SSB/Scandium-Dataset")16 17severity_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "PASS": 0}18findings = []19 20def finding(severity, phase, check, status, detail):21 severity_counts[severity] += 122 findings.append({"severity": severity, "phase": phase, "check": check, "status": status, "detail": str(detail)[:200]})23 s = "🔴" if severity == "CRITICAL" else "🟠" if severity == "HIGH" else "🟡" if severity == "MEDIUM" else "🔵" if severity == "LOW" else "✅"24 print(f" {s} [{severity:8s}] {check}: {str(detail)[:120]}")25 26def main():27 print("=" * 60)28 print(" PHASE 2: STRUCTURE AUDIT")29 print("=" * 60)30 31 with open(AUDIT_DIR / DATASET) as f:32 entries = json.load(f)33 N = len(entries)34 print(f" Loaded {N:,} entries\n")35 36 # Collect geometry stats37 volumes = []38 densities = []39 nsites_list = []40 min_dists = []41 sgs = Counter()42 crystal_systems = Counter()43 sg_failures = []44 45 for i, e in enumerate(entries):46 if i % 50000 == 0:47 print(f" [{i}/{N}]", flush=True)48 49 vol = e.get("volume", 0)50 if vol and vol > 0:51 volumes.append(vol)52 dens = e.get("density", 0)53 if dens and dens > 0:54 densities.append(dens)55 ns = e.get("nsites", 0)56 if ns > 0:57 nsites_list.append(ns)58 sg = e.get("space_group")59 if sg is not None:60 try:61 sgs[int(sg)] += 162 # Crystal system from space group number63 sg_num = int(sg)64 if 1 <= sg_num <= 2: crystal_systems["Triclinic"] += 165 elif sg_num <= 15: crystal_systems["Monoclinic"] += 166 elif sg_num <= 74: crystal_systems["Orthorhombic"] += 167 elif sg_num <= 142: crystal_systems["Tetragonal"] += 168 elif sg_num <= 167: crystal_systems["Trigonal"] += 169 elif sg_num <= 194: crystal_systems["Hexagonal"] += 170 elif sg_num <= 230: crystal_systems["Cubic"] += 171 else: crystal_systems["Unknown"] += 172 except (ValueError, TypeError):73 sg_failures.append(i)74 else:75 sg_failures.append(i)76 77 volumes = np.array(volumes)78 densities = np.array(densities)79 nsites_arr = np.array(nsites_list)80 81 # --- Geometry Validation ---82 print("\n--- Geometry Validation ---")83 84 # Zero volume85 zero_vol = sum(1 for e in entries if e.get("volume") is None or e.get("volume", 0) <= 0)86 if zero_vol:87 finding("CRITICAL", "geometry", "zero_volume", f"{zero_vol:,} entries", "")88 else:89 finding("PASS", "geometry", "zero_volume", "0 entries", "all have volume > 0")90 91 # Negative volume92 neg_vol = sum(1 for e in entries if e.get("volume", 0) < 0)93 if neg_vol:94 finding("CRITICAL", "geometry", "negative_volume", f"{neg_vol:,} entries", "")95 else:96 finding("PASS", "geometry", "negative_volume", "0 entries", "")97 98 # NaN coordinates — check structure_json for NaN99 nan_coords = 0100 for i, e in enumerate(entries):101 sj = e.get("structure_json", "")102 if isinstance(sj, str) and ("NaN" in sj or "nan" in sj or "Infinity" in sj):103 nan_coords += 1104 if nan_coords:105 finding("CRITICAL", "geometry", "nan_coordinates", f"{nan_coords:,} entries with NaN in structure", "")106 else:107 finding("PASS", "geometry", "nan_coordinates", "0 entries", "")108 109 # Nsites ≥ 2 (single atoms)110 single_atom = sum(1 for n in nsites_arr if n < 2)111 if single_atom:112 finding("HIGH", "geometry", "single_atom_entries", f"{single_atom:,} entries", "")113 else:114 finding("PASS", "geometry", "single_atom_entries", "0 entries", "")115 116 # --- Crystal Validation ---117 print("\n--- Crystal Validation ---")118 119 if sgs:120 most_common_sg = sgs.most_common(5)121 finding("PASS", "crystal", "space_groups", f"{len(sgs)} unique SGs", 122 f"top: {dict(most_common_sg)}")123 124 if crystal_systems:125 finding("PASS", "crystal", "crystal_systems", 126 f"{dict(crystal_systems.most_common())}", "")127 128 # SG failures129 if sg_failures:130 pct = 100 * len(sg_failures) / N131 finding("HIGH" if pct > 1 else "MEDIUM", "crystal", "space_group_failures",132 f"{len(sg_failures):,} / {N:,} ({pct:.2f}%)", "")133 else:134 finding("PASS", "crystal", "space_group_failures", "0", "")135 136 # Volume stats137 print(f"\n Volume stats (n={len(volumes):,}):")138 print(f" mean={np.mean(volumes):.1f} median={np.median(volumes):.1f} "139 f"min={np.min(volumes):.1f} max={np.max(volumes):.1f}")140 141 # Density stats142 print(f"\n Density stats (n={len(densities):,}):")143 print(f" mean={np.mean(densities):.2f} median={np.median(densities):.2f} "144 f"min={np.min(densities):.2f} max={np.max(densities):.2f}")145 146 # --- Structural Outliers ---147 print("\n--- Structural Outliers ---")148 149 # Largest volumes150 vol_sorted = sorted(enumerate(entries), key=lambda x: x[1].get("volume", 0) or 0, reverse=True)151 largest_vol = [(i, e.get("volume", 0), e.get("formula", "")) for i, e in vol_sorted[:5]]152 finding("LOW", "outliers", "largest_volume", f"top: {largest_vol[0][2]} ({largest_vol[0][1]:.0f} ų)", "")153 154 # Smallest volumes155 smallest_vol = [(i, e.get("volume", 0) or float("inf"), e.get("formula", "")) for i, e in enumerate(entries) if e.get("volume", 0) > 0]156 smallest_vol.sort(key=lambda x: x[1])157 finding("LOW", "outliers", "smallest_volume", 158 f"top: {smallest_vol[0][2]} ({smallest_vol[0][1]:.1f} ų)", "")159 160 # Highest density161 dens_sorted = sorted(enumerate(entries), key=lambda x: x[1].get("density", 0) or 0, reverse=True)162 finding("LOW", "outliers", "highest_density", 163 f"top: {dens_sorted[0][1].get('formula','?')} ({dens_sorted[0][1].get('density',0):.1f} g/cm³)", "")164 165 # Most atoms166 ns_sorted = sorted(enumerate(entries), key=lambda x: x[1].get("nsites", 0) or 0, reverse=True)167 max_ns = ns_sorted[0][1].get("nsites", 0) if ns_sorted else 0168 finding("LOW", "outliers", "most_atoms", f"max nsites = {max_ns}", "")169 170 # --- Summary ---171 print(f"\n{'=' * 60}")172 print(f" PHASE 2 SUMMARY")173 print(f" CRITICAL: {severity_counts['CRITICAL']}")174 print(f" HIGH: {severity_counts['HIGH']}")175 print(f" MEDIUM: {severity_counts['MEDIUM']}")176 print(f" LOW: {severity_counts['LOW']}")177 print(f" PASS: {severity_counts['PASS']}")178 print(f"{'=' * 60}")179 180 # Crystal system report181 total_cs = sum(crystal_systems.values())182 print(f"\n Crystal System Distribution:")183 for cs, cnt in crystal_systems.most_common():184 print(f" {cs:15s}: {cnt:>7,} ({100*cnt/total_cs:.1f}%)")185 186 report = {187 "phase": "Phase 2: Structure Audit",188 "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),189 "total_entries": N,190 "geometry": {191 "volume_mean": float(np.mean(volumes)),192 "volume_median": float(np.median(volumes)),193 "volume_min": float(np.min(volumes)),194 "volume_max": float(np.max(volumes)),195 "density_mean": float(np.mean(densities)),196 "density_median": float(np.median(densities)),197 "nsites_mean": float(np.mean(nsites_arr)),198 "nsites_median": float(np.median(nsites_arr)),199 },200 "crystal_systems": dict(crystal_systems.most_common()),201 "unique_space_groups": len(sgs),202 "top_space_groups": dict(sgs.most_common(10)),203 "findings": findings,204 "summary": dict(severity_counts),205 }206 with open(OUT / "phase2_structure_audit.json", "w") as f:207 json.dump(report, f, indent=2)208 print(f"\n Report: {OUT / 'phase2_structure_audit.json'}")209 210if __name__ == "__main__":211 main()212 