MatIntel Audit — July 2026

Scope: src/matintel/gaussian_scoring.py (2,664 lines, 71 scorers), viability.py, gaussian_pipeline.py, and the contents of data/processed/.

Findings are ordered by impact on output quality, not by effort.


Part 1 — Defects that are currently corrupting results

1.1 CRITICAL: the top-10 rankings are partly meaningless (score saturation)

Every scorer accumulates additive weighted terms and then calls _clamp(), which truncates to [0, 1] and rounds to 3 decimal places. But in most scorers the term weights sum to well over 1.0. Example — score_solid_electrolyte (line 360):

0.40 (LLZO) + 0.30 (bandgap) + 0.22 (framework) + 0.22 (sulfide)
+ 0.20 (halide) + ...  =  1.34+ before clamping

Measured against the current top10_per_category_gaussian_raw_score.csv:

Symptom Count
Categories where all 10 top materials score exactly 1.000 8 / 71
Categories where the top-10 score range is < 0.01 29 / 71
Median top-10 score range across all categories 0.018

The eight fully-saturated categories are Battery Anode, Dehydrogenation/LOHC Catalyst, Hard Coating, High-k Dielectric, Optical Fibre/Waveguide, Semiconductor (General), Superconductor, and Supercapacitor Electrode. For these, the reported "top 10" is an arbitrary slice of what may be thousands of tied materials — the ordering is whatever pandas' sort happened to produce.

For the 29 low-range categories, 3-decimal rounding means the ranking is determined by numerical noise well below the accuracy of any underlying DFT input.

Fix: replace additive-then-clamp with a normalised aggregation. Two options:

I'd recommend geometric for the ~20 scorers where criteria are genuinely conjunctive (electrolytes, catalysts, coatings), normalised-additive elsewhere. Also raise _clamp rounding from 3 dp to 6 dp.

1.2 CRITICAL: the ALIGNN property predictions are 100% empty

data/processed/alignn_predictions.csv has 554,054 rows and nine property columns — MBJ_Bandgap, Dielectric_Const, Bulk_Modulus, Shear_Modulus, Seebeck_n, Seebeck_p, Exfoliation_Energy, Magnetic_Moment, Ehull.

All nine are entirely NaN. Non-null fraction: 0.000.

alignn_predictions_retry.csv is 0 bytes, so the retry never ran either. Neither scoring engine references these columns anywhere (grep -c ALIGNN returns 0 in both scoring.py and gaussian_scoring.py), so nothing is silently reading garbage — but it means the system is scoring thermoelectrics with no Seebeck coefficient, hard coatings with no elastic moduli, high-k dielectrics with no dielectric constant, and 2D materials with no exfoliation energy. Those are the defining properties of those applications; they're all currently inferred from composition heuristics alone.

This is the single largest available quality improvement in the project.

1.3 HIGH: CHGNet_Formation_Energy is not a formation energy

CHGNet_Formation_Energy:  mean -6.25, min -13.44, max -0.42  eV/atom
Formation Energy Per Atom (GNoME):  typically -1 to -2 eV/atom

CHGNet's predict_structure() returns e as total energy per atom referenced to its own training baseline, not a formation energy. The column is mislabeled by roughly a 4–5 eV offset. It isn't currently consumed by any scorer, which is fortunate: feeding it to _formation_sigmoid(center=-0.5, k=3.0) would saturate every material in the dataset at weight 1.0 and silently null out that term's discriminative power.

Either convert to a true formation energy by subtracting CHGNet elemental reference energies, or rename the column to CHGNet_Energy_Per_Atom so it can't be misused later.

CHGNet_Magnetic_Moment has the same class of problem: max value 219.75 μB, which is a cell-summed moment. It needs normalising by nsites before it means anything.

1.4 MEDIUM: three common elements are missing from ABUNDANCE_PPM

viability.py:186 defaults any unlisted element to 0.001 ppm — the abundance of rhodium. Elements present in the dataset but absent from the table:

Element Materials affected (in a 300k sample) True crustal abundance
I 6,608 ~0.45 ppm
Cs 5,221 ~3 ppm
Be 4,163 ~2.8 ppm

Iodine, caesium and beryllium are all being treated as rarer than platinum. This directly suppresses the viability of every halide-perovskite, caesium-containing optoelectronic, and beryllium ceramic in the database — several of which are technologically important classes.

(The other missing symbols — Tc, Pm, Pa, Ac, Np, Pu — are correctly handled by ELEMENT_PENALTIES instead, so their abundance default doesn't matter.)

1.5 MEDIUM: element_penalty ignores stoichiometry

def element_penalty(composition):
    product = 1.0
    for symbol in composition.elements:
        product *= ELEMENT_PENALTIES.get(str(symbol), 1.0)
    return float(max(product, 0.02))

This is binary presence logic — exactly what the Gaussian rewrite set out to eliminate. A material with 0.5% lanthanum dopant is penalised identically to one that is 40% lanthanum. And penalties compound multiplicatively across elements, so a La–Ce–Nd compound takes 0.92 × 0.92 × 0.85 = 0.72 regardless of composition.

Fix: weight by molar fraction, e.g. penalty_i ** frac_i, consistent with the _elem_frac approach used throughout the Gaussian scorers.

There's also a conceptual double-count: rare-earth penalties encode scarcity, but abundance_score and supply_risk_score already do that independently. The penalty table should be restricted to hazard (toxicity, radioactivity) and let the abundance/supply terms carry criticality.

1.6 LOW: housekeeping

data/processed/failed_clscore.csv is 3.0 GB — larger than the rest of processed/ combined, and appears to be a runaway error log rather than data. Worth inspecting and deleting.


Part 2 — Systematic physics issues

2.1 The band gap is PBE/GGA, but the Gaussian centres are experimental targets

This affects roughly 30 of the 71 scorers and is the most pervasive methodological problem in the system.

GGA/PBE band gaps underestimate experimental gaps by ~40–50% on average, and the error is not a constant offset — it scales roughly with gap magnitude. The Bandgap column in the GNoME-derived dataset is a GGA value. But the Gaussian peaks are placed at experimental/physical optima:

A material whose true gap is 1.34 eV typically shows a GGA gap near 0.7–0.9 eV, so it lands on the shoulder of the Gaussian rather than the peak — while a material with a true gap of ~2.2 eV lands on the peak and is wrongly promoted. The rankings for every gap-sensitive category are systematically biased toward wide-gap materials.

Three fixes, in increasing order of quality:

  1. Shift the Gaussian centres to GGA-equivalent values. Cheap, no new data, but the correction is material-class-dependent and this bakes in an average.
  2. Apply a scissor/linear correction to the gap column before scoring (E_corrected ≈ a·E_GGA + b, fit against a set with known experimental gaps — you have experimental_compounds.csv to fit against).
  3. Use MBJ gaps from ALIGNN (see 1.2). This is the correct fix. The JARVIS-DFT MBJ model exists precisely for this; TBmBJ gaps are substantially closer to experiment than GGA.

Whichever route, the choice should be documented in the README, because right now the scorers look like they encode experimental physics and don't.

2.2 Hard return 0.0 gates contradict the continuous design

The module docstring says "hard disqualifiers are preserved where physical reasoning demands them", which is defensible — but the gates are much more numerous than that framing implies. score_battery_anode alone has three, and score_solid_electrolyte has three including bg < 2.0 → 0.0.

A material with a GGA gap of 1.98 eV — which may well have a true gap above 4 eV — is hard-zeroed as a solid electrolyte. Combined with 2.1, these cliffs are being applied to a systematically biased variable.

Recommend converting all soft-physics gates to steep sigmoids (k=10–20), and retaining true return 0.0 only for genuine categorical impossibilities: no Li in a Li-ion cathode, centrosymmetric structure for a piezoelectric, radioactive element in a biomedical implant.

2.3 14 of 21 computed structural descriptors are never used

cif_structural_descriptors.csv has full coverage (604,125 rows, no missing values) for 21 descriptors. The scorers read exactly seven: is_centrosymmetric, crystal_system, c_over_a, density, volume_per_atom, space_group_number, NSites.

Unused, despite being computed and available: packing_fraction, mean_cn, mean_nn_distance, min_nn_distance, mean_voronoi_volume, dimensionality, and the individual lattice parameters a, b, c, alpha, beta, gamma.

Several map directly onto scorers that currently rely on weak composition proxies:

Descriptor Should feed
packing_fraction hydrogen storage (free volume), radiation shielding, CO2 sorbent, desalination membrane
mean_voronoi_volume ionic conductors (mobile-ion site volume), hydrogen storage
min_nn_distance solid electrolyte (migration-bottleneck proxy), superionic screening
mean_cn catalysis (undercoordinated surface-site proxy), OER/HER/CO2RR
dimensionality material_2d (currently reconstructed from c_over_a + space group, when a direct label exists)
alpha/beta/gamma ferroelectric/piezoelectric polar-axis reasoning

This is free signal already sitting on disk.


Part 3 — Proposed new application categories

The current 71 have notable gaps. These are all scoreable with data you already have (composition, gap, formation energy, symmetry, density, structural descriptors), and represent active research areas:

Energy storage - Multivalent-ion cathode (Mg²⁺ / Ca²⁺ / Al³⁺) — a major field with zero coverage - Zn-ion (aqueous) battery cathode - K-ion cathode - Li-metal-compatible interlayer / anode-free current collector - Anti-perovskite superionic conductor (distinct structural family)

Energy conversion & catalysis - Proton-conducting ceramic (protonic ceramic fuel cells — BaZrO₃/BaCeO₃ family) - Chemical-looping combustion oxygen carrier - Ammonia oxidation / ammonia cracking catalyst - Hydrogen separation membrane (Pd-alternative) - Thermochemical energy storage redox pair

Electronics & magnetics - Transparent p-type semiconductor — a long-standing open problem, and distinct from the existing (implicitly n-type) transparent_conductor - Magnetocaloric material — significant field, currently absent - Elastocaloric / barocaloric (solid-state cooling) - Electrocaloric - Solar-blind UV photodetector (wide-gap, distinct from photodetector) - Thermionic emitter / low-work-function material

Nuclear & extreme - Neutron absorber / control rod material (distinct from radiation_shielding — cross-section driven, not density driven) - Scintillator (distinct from radiation_detector)

Other - Solid lubricant (layered, low shear strength) - High-entropy alloy candidate

My picks for highest value-per-effort: magnetocaloric, multivalent-ion cathode, transparent p-type semiconductor, proton conductor, neutron absorber, and scintillator. Each is well-defined by available features and each fills a real gap.


Part 4 — Retiring the discrete engine

scoring.py (1,967 lines) and gaussian_scoring.py (2,664 lines) are maintained in parallel, with gaussian_pipeline.py producing a comparison artifact and app.py:606 exposing an engine toggle. This doubles the maintenance surface and means every fix above would need applying twice.

Proposed sequence:

  1. Freeze legacy. No further changes to scoring.py.
  2. Establish that Gaussian is at least as good before deleting anything — scored_comparison_legacy_vs_gaussian.csv already exists but only records deltas, not correctness. Use experimental_compounds.csv as ground truth: for each category, does the engine rank known-good materials higher? This is the retrospective-validation study, and it doubles as the justification for the removal.
  3. Fix the defects above in the Gaussian engine only.
  4. Remove the toggle from app.py, default to Gaussian.
  5. Delete scoring.py and the *_gaussian filename suffixes (once Gaussian is the only engine, the suffix is noise — scored_dataset.csv should just be the Gaussian output).

Step 2 is important and shouldn't be skipped. Right now there is no evidence in the repo that the Gaussian rewrite improved anything — only that it changed things. Retiring the baseline without measuring against it discards the only comparison you have.


# Task Impact Effort
1 Fix score saturation (normalised aggregation) Very high Medium
2 Re-run ALIGNN to populate the 9 property columns Very high Medium
3 Band gap correction (scissor fit, then MBJ when available) High Low → Medium
4 Wire ALIGNN properties into the ~20 scorers that need them High Medium
5 Fix ABUNDANCE_PPM gaps + stoichiometry-weight element_penalty Medium Low
6 Use the 14 unused structural descriptors Medium Medium
7 Soften hard gates to steep sigmoids Medium Medium
8 Add new application categories Medium Medium
9 Validation study vs. experimental_compounds.csv High Medium
10 Retire the discrete engine Maintenance Low

Items 1, 3 and 5 are self-contained and can be done immediately. Item 2 is a GPU job on the RTX 4070 and gates items 4 and part of 3.