batterymhm / demo.py
williamTLmiller's picture
Make it a full tech repo: packaging, CI, docs, examples, community files
4671c68 verified
Raw
History Blame Contribute Delete
6.44 kB
#!/usr/bin/env python3
"""
BatteryMHM β€” self-contained demonstration that the Miller Harmonic Method
(MHM) genuinely carries predictive signal, with NO external data downloads
and NO pre-trained weights.
It does two things:
1. CELL-HEALTH DEMO (synthetic).
Generates synthetic lithium-ion capacity-fade curves, each driven by a
hidden degradation rate. From only an EARLY observation window of each
curve, it quantises the capacity trace into harmonic identity numbers
(HINs), extracts the full MHM descriptor, trains the MHM ensemble, and
predicts each cell's EVENTUAL capacity retention. The MHM model is then
compared against a mean-predictor baseline β€” if MHM carries signal, it
beats the baseline by a wide margin.
2. MATERIALS DESCRIPTOR DEMO (real element symbols).
Builds the MHM neighbour/Chi descriptor for real cathode compositions,
showing the materials feature path runs end-to-end.
This demo does NOT reproduce the published benchmark numbers (those are on
the real public Severson and Matbench datasets β€” see the README). It exists
so anyone can verify, in seconds and offline, that the open method works.
Run: python demo.py
"""
import numpy as np
from batterymhm import (
MHMEnsemble,
compute_metrics,
element_hin,
mhm_full_features,
mhm_matter8_neighbor_histograms,
seq_to_harmonics,
)
# ─────────────────────────────────────────────────────────────────────────────
# 1. Synthetic cell-health demonstration
# ─────────────────────────────────────────────────────────────────────────────
def make_synthetic_cells(n_cells=200, total_cycles=200, obs_frac=0.30, seed=42):
"""
Build synthetic capacity-fade curves with a hidden per-cell degradation
rate. Returns (observed_curves, eventual_retention).
observed_curves[i] : capacity vs cycle over the first obs_frac of life
eventual_retention[i] : capacity / initial at total_cycles (the target)
"""
rng = np.random.default_rng(seed)
obs_len = max(8, int(total_cycles * obs_frac))
cycles = np.arange(1, total_cycles + 1)
observed, target = [], []
for _ in range(n_cells):
# Hidden degradation drivers (the "manufacturing + protocol" signature)
rate = rng.uniform(0.0008, 0.0040) # sqrt-fade rate
knee = rng.uniform(0.0, 1.2e-5) # late-life acceleration
c0 = rng.uniform(0.99, 1.01) # initial capacity spread
cap = c0 - rate * np.sqrt(cycles) - knee * cycles ** 2
cap = cap + rng.normal(0.0, 0.0015, size=cap.shape) # measurement noise
cap = np.clip(cap, 0.0, 1.05)
observed.append(cap[:obs_len])
target.append(float(cap[-1] / c0)) # eventual retention (SOH)
return observed, np.asarray(target)
def featurize_curve(curve):
"""Quantise a capacity curve to HINs and compute the MHM descriptor."""
hins = seq_to_harmonics(list(curve), bins=9)
feats = mhm_full_features(hins)
return feats
def run_cell_demo():
print("=" * 70)
print("1. CELL-HEALTH DEMO β€” predict eventual retention from early cycles")
print("=" * 70)
curves, y = make_synthetic_cells()
feat_dicts = [featurize_curve(c) for c in curves]
keys = sorted(feat_dicts[0].keys()) # stable column order
X = np.array([[d[k] for k in keys] for d in feat_dicts], dtype=float)
print(f" cells : {X.shape[0]}")
print(f" MHM features/cell : {X.shape[1]}")
# 75 / 25 split
rng = np.random.default_rng(0)
idx = rng.permutation(len(y))
cut = int(0.75 * len(y))
tr, te = idx[:cut], idx[cut:]
model = MHMEnsemble(use_stacking=False) # ET (+XGB if installed)
model.fit(X[tr], y[tr], feature_names=keys)
pred = model.predict(X[te])
m = compute_metrics(y[te], pred)
baseline_mae = float(np.mean(np.abs(y[te] - y[tr].mean())))
print(f"\n MHM ensemble : {m}")
print(f" mean-predictor MAE : {baseline_mae:.4f}")
factor = baseline_mae / max(m.mae, 1e-9)
print(f" β†’ MHM is {factor:.1f}Γ— better than predicting the mean.")
print(" Top MHM features :")
for name, imp in model.top_features(6):
print(f" {imp:6.4f} {name}")
return m.mae < baseline_mae * 0.6 # demo passes if clearly better
# ─────────────────────────────────────────────────────────────────────────────
# 2. Materials descriptor demonstration
# ─────────────────────────────────────────────────────────────────────────────
def run_materials_demo():
print("\n" + "=" * 70)
print("2. MATERIALS DESCRIPTOR DEMO β€” MHM Chi/HIN features on real cathodes")
print("=" * 70)
# (formula, element list) for a few well-known cathode chemistries.
cathodes = {
"LiFePO4": ["Li", "Fe", "P", "O", "O", "O", "O"],
"LiCoO2": ["Li", "Co", "O", "O"],
"LiNiMnCoO2 (NMC)": ["Li", "Ni", "Mn", "Co", "O", "O"],
"NaFePO4": ["Na", "Fe", "P", "O", "O", "O", "O"],
}
for name, elements in cathodes.items():
hins = [element_hin(e) for e in elements]
# Treat the site list as a neighbour list paired with itself (demo).
feats = mhm_matter8_neighbor_histograms(hins, hins)
nonzero = sum(1 for v in feats.values() if abs(v) > 1e-12)
print(f" {name:20s} HINs={hins} "
f"descriptor={len(feats)} features ({nonzero} active)")
print(" β†’ Materials feature path runs end-to-end on real compositions.")
return True
if __name__ == "__main__":
ok1 = run_cell_demo()
ok2 = run_materials_demo()
print("\n" + "=" * 70)
print("RESULT:", "PASS β€” the open method runs and carries signal."
if (ok1 and ok2) else "CHECK β€” see output above.")
print("=" * 70)