A researcher's notebook: how I built a two-part toolchain for my transition-metal chalcogenide work — an AI-powered PDF library organiser that ingests the literature, and a physics-constrained ML pipeline (v4.0) that fetches Materials Project data, engineers GKA/Mott/ZSA descriptors, trains a stacking ensemble, and explains predictions with SHAP.

📚
Tool 1

PDF Library Organiser + Gemini AI

⚗️
Tool 2

MₓCᵧ Pipeline v4.0

🔬
Dataset

Materials Project + empirical DFT

🧠
ML Core

Stacking RF + SHAP explainability

My DFT work on transition-metal chalcogenides (MₓCᵧ: M = Fe, Ni, Co, Mn, Cr, Ti; C = S, Se, Te) has produced a growing library of first-principles results alongside an even larger pile of literature PDFs. This post documents the two Python tools I built to handle both: a PDF library organiser that classifies hundreds of papers automatically with Gemini AI, and the MₓCᵧ Pipeline v4.0 that feeds those papers — together with Materials Project data — into a physics-constrained ML model for band gap, magnetisation, and magnetic ordering prediction.

🔬
Research context

This pipeline grew directly out of my doctoral work (Reggad, Université de Sidi-Bel-Abbès) and subsequent publications (Reggad et al., Physica B 526, 2017, 89–95). The goal is not to replace DFT — it is to extract cross-compound trends that single calculations cannot reveal, and to build a predictor that flags which MₓCᵧ candidates are worth computing next.

Part 1 — PDF Library Organiser with Gemini AI

Before any ML pipeline can ingest the literature, the literature needs to be organised. My G:\MyResearch\AllPapers folder had accumulated hundreds of PDFs with meaningless filenames like 1-s2.0-S0921452617302386.pdf. The organiser solves this in three steps: extract real titles from inside each PDF, rename them, classify them into four categories using Gemini AI, move them to matching subfolders, and export a colour-coded Excel report.

Architecture

📄
Scan PDFs
Path.glob
🏷️
Extract Title
pypdf
🤖
Classify
Gemini 1.5 Flash
📁
Move Files
shutil
📊
Excel Report
openpyxl

Four output categories

FolderCategoryDescriptionExcel colour
4_Subject_Paperssubject_paperResearch on MₓCᵧ: band gap, magnetic properties, DFT calculationsGreen
2_Physics_Booksphysics_bookTextbooks: solid-state physics, quantum mechanics, thermodynamicsYellow
3_ManuscriptsmanuscriptPhD theses, dissertations, memoires de magistèreOrange
1_Out_of_Subjectout_of_subjectUnrelated: biology, ML, economics, etc.Red

Title extraction strategy

The script uses a three-level fallback: (1) PDF metadata title field if it is non-trivial (length > 5, contains spaces), (2) first meaningful line from page 1 text — skipping lines that match journal headers, DOIs, dates, or single words, (3) the original filename stem as last resort.

def extract_title(pdf_path):
    import pypdf
    reader = pypdf.PdfReader(str(pdf_path))
    # 1. Metadata field
    meta = reader.metadata
    if meta and getattr(meta, "title", None):
        t = meta.title.strip()
        if len(t) > 5 and not re.fullmatch(r"[\w\-]+", t):
            return t
    # 2. First meaningful line of page 1
    lines = [l.strip() for l in reader.pages[0].extract_text().splitlines() if l.strip()]
    for line in lines[:15]:
        skip = re.search(
            r"^(abstract|doi|http|journal|vol\.\s*\d|received|copyright)",
            line, re.IGNORECASE)
        if not skip and 8 <= len(line) <= 150:
            return line
    return pdf_path.stem # 3. Filename fallback

Gemini AI classification prompt

Classification uses Gemini 1.5 Flash — free at 1500 requests/day, no GPU required. The prompt sends title + abstract extract (max 600 chars) and asks for exactly one of four category keys. A keyword-based fallback activates automatically when the API quota is hit or returns an unexpected response.

# Gemini prompt (simplified)
PROMPT = """Classify the document into EXACTLY ONE of:
  subject_paper — MxCy compounds, DFT, band gap, magnetic properties
  physics_book — textbook on solid-state physics, QM, thermodynamics
  manuscript — PhD thesis, dissertation, memoire de magistère
  out_of_subject — anything else
Respond with ONLY the category key."""

# Rate limit: 4.5 s between calls → stays under 15 RPM free tier
RATE_LIMIT_SEC = 4.5

Running the script

# Windows — run once, API key saved automatically after first entry
python pdf_organizer.py "G:\MyResearch\AllPapers"

# Or just double-click and enter path when prompted
python pdf_organizer.py

# Output structure created automatically:
# G:\MyResearch\AllPapers\
# ├── 4_Subject_Papers\
# ├── 3_Manuscripts\
# ├── 2_Physics_Books\
# ├── 1_Out_of_Subject\
# └── PDF_Library_Report.xlsx
⚠️
Windows path trap

If you hard-code a path in a Python script, always use a raw string: r"G:\MyResearch\AllPapers" not "G:\MyResearch\AllPapers". The backslash before M, A etc. creates escape sequences (\M is not valid, causing a SyntaxError). Raw strings treat backslashes literally.

Part 2 — MₓCᵧ Pipeline v4.0: Physics-Constrained ML

The pipeline is a 13-cell Jupyter notebook that runs identically on Windows (local), Google Colab, and Kaggle. Version 4.0 introduced three major changes over v3.9: the Materials Project hull filter was raised to 0.15 eV/atom to include more metastable polymorphs (>300 training entries), magnetic ordering (NM/AFM/FM) was added as a third classification target, and an explicit 80/20 train/test split replaced pure cross-validation for more honest reporting.

Pipeline overview (13 cells)

CellPurposeKey outputs
1 — SetupInstall packages, detect platform (local/Colab/Kaggle), restart kernel oncePLATFORM, output dir
2 — ImportsAll imports — run after kernel restartAll libraries ready
3 — Platform & API KeyRe-detect platform, set MP API key, configure output pathsOUTPUT_DIR, API_KEY
4 — Atomic ConstantsPhysics lookup table: n_d, r_cov, χ, IE, U_Hubbard, r_ionic for each elementATOMIC_DATA
5 — Physics FormulasBond length, bandwidth W, crystal field Δ, Mott criterion, ZSA classifier, GKA flagFeature engineering functions
6 — Data FetchMaterials Project API query — band gap, magnetisation, ordering, E-hullreal_dict (~300 entries)
7 — TrainStacking RF ensemble — 80/20 split, 3 targets (gap, mag, ordering)Model bundle, CV + test metrics
7b — ComparePredicted vs real scatter plots, Bland-Altman, confusion matrixComparison figures
8 — Fine-TuneBlend empirical physics corrections into modelTuned model bundle
9 — PredictSingle-compound prediction APIpredict_compound(M, C, structure)
10 — SHAPTreeExplainer on RF gap sub-model, beeswarm summary plotFeature importance ranking
11 — HeatmapPeriodic-table style heatmap of all MₓCᵧ predictionsPublication-ready figure
12 — Excel Export5-worksheet workbook: dataset, features, CV results, predictions, SHAPMxCy_v4_Full_Results.xlsx

Physics-informed feature table

FeaturePhysical meaningEncodes
n_dd-electron countCrystal field occupancy, Hund's rule coupling
Δ (crystal field)t₂g–eₘ splittingLigand field strength → gap vs metallic d-band
U/WHubbard U / bandwidth WMott criterion — correlated insulator boundary
ΔENElectronegativity differenceIonic/covalent character → ZSA charge-transfer
GKA flagGoodenough–Kanamori–Anderson180° superexchange → AFM if half-filled d-shell
bond lengthM–C ionic radii sum × structure factorOverlap integral → bandwidth
chalcogen periodS=3, Se=4, Te=5Covalency trend: S < Se < Te → decreasing gap

Cell 4 — Atomic constants table (excerpt)

# [n_d, r_cov, chi_Pauling, IE_eV, U_Hubbard_eV, r_ionic_Å]
ATOMIC_DATA = {
  'Ti': [2, 1.40, 1.54, 6.82, 3.0, 0.86],
  'V': [3, 1.35, 1.63, 6.74, 3.5, 0.79],
  'Cr': [4, 1.28, 1.66, 6.77, 3.5, 0.87],
  'Mn': [5, 1.27, 1.55, 7.43, 4.0, 0.97],
  'Fe': [6, 1.26, 1.83, 7.90, 4.5, 0.92],
  'Co': [7, 1.25, 1.88, 7.88, 5.0, 0.88],
  'Ni': [8, 1.24, 1.91, 7.64, 5.5, 0.83],
  'Cu': [9, 1.28, 1.90, 7.73, 7.0, 0.87],
}
# Sources: r_cov (Alvarez 2008), chi (Pauling scale), IE (NIST),
# U_Hubbard (Dudarev scheme, typical DFT+U values),
# r_ionic (Shannon 1976, 6-coordinate high-spin)

Cell 6 — Fetching data from Materials Project (v4.0)

from mp_api.client import MPRester

# v4.0: hull raised to 0.15 eV/atom → more polymorphs → ~300+ entries
CHEMSYS = [f"{m}-{c}" for m in ["Fe","Ni","Co","Mn","Cr","Ti"]
                    for c in ["S","Se","Te"]]

with MPRester() as mpr:
    docs = mpr.materials.summary.search(
        chemsys=CHEMSYS,
        energy_above_hull=(0, 0.15), # raised from 0.10 in v3.9
        fields=["material_id", "formula_pretty",
                "band_gap", "ordering", # NEW in v4.0
                "total_magnetization", "energy_above_hull",
                "crystal_system", "spacegroup_number"]
    )

Cell 7 — Training: three targets simultaneously

# v4.0: three prediction targets
TARGETS = {
  'band_gap_eV': 'regression', # eV — continuous
  'magnetization_muB': 'regression', # μB/f.u. — continuous
  'ordering_label': 'classification', # NM=0 / AFM=1 / FM=2
}

# Stacking ensemble: two RF regressors + one RF classifier
# rf_gap trained on log1p(band_gap) to handle the heavy zero-gap spike
# rf_mag trained on log1p(|magnetization|) then sign restored
# rf_ord trained on 3-class ordering labels

# 80/20 stratified split — ensures ≥250 train rows, ≥50 test rows
X_train, X_test, y_train, y_test = train_test_split(
    X_real, Y_real, test_size=0.20, random_state=42)

Cell 9 — Single-compound prediction API

# Predict band gap, magnetisation, and ordering for any MₓCᵧ compound
result = predict_compound(M='Ni', C='S', structure='zincblende')

# Example output:
# ┌─────────────────────────────────────────────┐
# │ NiS (zincblende) │
# │ Band gap : 0.43 eV (semiconductor) │
# │ Magnetisation : 1.82 μB/f.u. │
# │ Magnetic order : AFM │
# │ Half-metal : No │
# └─────────────────────────────────────────────┘

Cell 10 — SHAP analysis

import shap

# Extract RF gap sub-model from v4.0 bundle dict
bundle = joblib.load(model_path)
gap_model = bundle['rf_gap'] # (legacy: bundle.estimators_[0])

explainer = shap.TreeExplainer(gap_model)
shap_vals = explainer.shap_values(X_real[FEATURES])
shap.summary_plot(shap_vals, X_real[FEATURES],
    feature_names=FEATURES, plot_type='dot')

Lessons Learned — Common Issues and Fixes

IssueRoot causeFix applied
Windows path SyntaxError PAPER_DIR = "G:\MyResearch\AllPapers" — backslash creates escape sequences Use raw string: r"G:\MyResearch\AllPapers"
G:\ not found on Colab Colab is a Linux server — has no access to local Windows drives Use Google Drive path: /content/drive/MyDrive/AllPapers after mounting
Drive not mounted on Colab from google.colab import drive; drive.mount() was missing Auto-mount only on Colab: if PLATFORM == 'colab': drive.mount('/content/drive')
Gemini rate limit (429) Free tier: 15 RPM. Hitting limit on large libraries 4.5 s sleep between calls + automatic 30 s retry on 429
predict_compound KeyError v3.8 model bundle saved as MultiOutputRegressor; v4.0 expects dict with rf_gap key Added compatibility branch: isinstance(bundle, dict) check
SHAP on wrong sub-model Calling TreeExplainer on full MultiOutputRegressor instead of inner RF Extract with bundle['rf_gap'] before passing to TreeExplainer
Magnetic ordering missing v3.9 did not fetch the ordering field from MP API Added to v4.0 field list; labels mapped: NM=0, AFM/FiM=1, FM=2
📥
Downloads

Both scripts are available on GitHub:

🐍 pdf_organizer.py — standalone Windows Python script, no Jupyter required.
📓 MxCy_pipeline_v4_0.ipynb — runs on local Python, Colab, and Kaggle.

MₓCᵧ Pipeline PDF Organiser Gemini AI Materials Project Physics-Informed ML GKA Rules SHAP Wien2k Random Forest