| |
| """ |
| 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, |
| ) |
|
|
| |
| |
| |
|
|
| 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): |
| |
| rate = rng.uniform(0.0008, 0.0040) |
| knee = rng.uniform(0.0, 1.2e-5) |
| c0 = rng.uniform(0.99, 1.01) |
|
|
| cap = c0 - rate * np.sqrt(cycles) - knee * cycles ** 2 |
| cap = cap + rng.normal(0.0, 0.0015, size=cap.shape) |
| cap = np.clip(cap, 0.0, 1.05) |
|
|
| observed.append(cap[:obs_len]) |
| target.append(float(cap[-1] / c0)) |
|
|
| 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()) |
| 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]}") |
|
|
| |
| 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) |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| def run_materials_demo(): |
| print("\n" + "=" * 70) |
| print("2. MATERIALS DESCRIPTOR DEMO β MHM Chi/HIN features on real cathodes") |
| print("=" * 70) |
|
|
| |
| 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] |
| |
| 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) |
|
|