CoolFace
Datasetpublic

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.

sourceHugging Facecc-by-4.0updated 2mo agoView on Hugging Face
2likes2.4kdownloads
audit_phase2_structure.py212 linesDownload Raw Back to scripts
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