Make MaterialsProjectDFTMixingScheme reproducible; match structures across cell sizes - #4704
Conversation
…e) for reproducible grouping in MaterialsProjectDFTMixingScheme
… release timeline
… differing cell sizes)
|
Tests/linting currently fail as this requires a new |
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude (posted on behalf of @shyuep).
Solid fix for both #3113 and #4261. The approach is right: canonical (energy, str(entry_id)) sort with NaN handling before greedy grouping, group_structures(symmetric=True) for order-independence, and grouping on reduced_composition so primitive/conventional cells land in one row. The struct.copy() before stamping entry_id also fixes a real caller-mutation bug, and the tests are well-designed (the RecordingStructureMatcher + non-vacuous-assertion guard is a nice touch).
Comments:
- CI is red purely at the install step — all failures (lint + 3 test matrices) die at "Install dependencies via uv" because
pymatgen-core>=2026.8.24isn't resolvable on PyPI yet. This PR is blocked on pymatgen-core#133 (symmetrickwarg) being merged and released. Re-run CI after that release; no code issues are visible in the failed runs. - The conversion of
pymatgen/entries/mixing_scheme.pyto aimport *deprecation stub fires a module-levelDeprecationWarningon import — fine, but note downstream packages with-W errorfilters (atomate2, emmet) will break until they migrate. Consider coordinating. symmetric=Truegrouping is quadratic-ish vs. the greedy path — likely acceptable, but worth a quick benchmark on a large chemsys (e.g. Li-Fe-P-O with full GGA+R2SCAN thermo types) before merge, sinceprocess_entriessits in doped/emmet hot paths.
Recommendation: approve pending pymatgen-core release + green CI.
rkingsbury
left a comment
There was a problem hiding this comment.
Wow, thanks for such detailed analysis @kavanase ! I'm no longer actively using the mixing scheme, but I reviewed your changes and they all make sense to me.
I never would have imagined that the sort order of EntrySet would be non-deterministic!
|
Thanks @rkingsbury! Yes it was a tricky issue to nail down! |
Signed-off-by: Seán Kavanagh <51478689+kavanase@users.noreply.github.com>
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude (posted on behalf of @shyuep). Updated review following new commits since the last automated review (2026-08-25).
Strong PR that fixes two real bugs. The reproducibility fix (#3113) is cleanly done: _entry_sort_key establishes a total order — the NaN guard and str(entry_id) tiebreak are exactly right, since a NaN energy or unorderable entry_id would silently degrade the sort back to arrival (hash) order. Sorting at both get_mixing_state_data and _filter_and_sort_entries, plus group_structures(symmetric=True), closes the remaining order-dependence. Grouping by reduced_composition fixes the primitive/conventional cell split (#4261), and entry.structure.copy() stops mutating caller structures (a nice drive-by fix — the old code attached entry_id to the caller's Structure). CI is green.
Comments:
symmetric=Truegrouping does full pairwise matching rather than greedy first-match, so cost grows for large chemsys pulls. Probably acceptable given groups are pre-partitioned by composition + spacegroup, but worth a quick timing sanity check on a large chemsys (e.g. Li-Fe-P-O with both thermo types).- The module move to
pymatgen.analysis.compatibility.mixing_schemewith a star-import stub +DeprecationWarning(removal v2027.1) is handled correctly, and the pymatgen-core pin bump to >=2026.8.30 for thesymmetrickwarg is properly annotated. Confirm that pymatgen-core 2026.8.30 is actually released before merging, or CI on downstream consumers will break. - Behavior note for the changelog readers: deterministic energy-sorted ordering changes which duplicate entry is retained relative to previous (hash-ordered) runs, so processed results may shift vs. previously computed hulls. That is the point of the PR, but downstream users (e.g. doped) should be aware.
Overall: approve. Only item (1) is worth verifying pre-merge.
… across cell sizes (materialsproject#4704) * Remove duplicate module (from 1c7086f) with deprecation handling * Sort entries, use Structure.copy() and group_structures(symmetric=True) for reproducible grouping in MaterialsProjectDFTMixingScheme * Bump `pymatgen-core` requirement, may need to be updated depending on release timeline * Group entries by reduced composition (to avoid false negatives due to differing cell sizes) * Pre-commit formatting * Update pymatgen-core dependency (to exact version) Signed-off-by: Seán Kavanagh <51478689+kavanase@users.noreply.github.com> --------- Signed-off-by: Seán Kavanagh <51478689+kavanase@users.noreply.github.com> Co-authored-by: Shyue Ping Ong <shyuep@users.noreply.github.com>
Fixes #3113. Fixes #4261.
MaterialsProjectDFTMixingScheme.process_entriescan return a different phase diagram for the same input entries from one interpreter run to the next. The variable isPYTHONHASHSEEDand nothing else (same entries, order, code etc.). This was originally reported in #3113, and noted by ourdopedtests now that we are following the newmp-apidefault of using the mixed GGA/R2SCAN thermo-types (as on the MP website).Separately, the same grouping code splits a primitive and a conventional cell of one material before
StructureMatcherever sees them, silently discarding the r2SCAN entry in several cases (#4261) — also fixed here.MWE (#3113)
On
main— 3 of 8 seeds give a different hull:On this updated branch all eight print
fp=206456a5.Repeats at a fixed seed are identical, so this is hash-iteration order, not (other) randomness; matching the original #3113 report of the count alternating 637/638 "roughly every third run". It is also not degenerate tie-breaking. Over the 58 entries common to both outcomes, formation energies differ by up to 0.254 eV/atom and
e_above_hullby 0.007, and the stable set flips (mp-1250007-r2SCANvsmp-568264-GGA), one entry being silently dropped in each direction.Origin
_filter_and_sort_entriesreturnslist(EntrySet(...)), i.e.list(set(...)).ComputedEntry.__hash__hashes an f-string, and CPython randomisesstrhashing per process for security (PEP 456), so the set's iteration order and the returned list order changes every run. So the method sorts, but non-deterministically.groupby(sorted(...))calls inget_mixing_state_dataare stable, so that order survivesStructureMatcher.group_structures, which is then greedy (unmatched.pop(0)picks a reference).StructureMatcher.fitis directional (symmetric=Falseby default) andgroup_structuresalways passes its asstruct1. Among the five SiSe2 spacegroup-72 structures, 3 of the 10 cross pairs match one way only — e.g.fit(mp-1179443-GGA, mp-568264-r2SCAN)isTrue, the reverseFalse. Over all 120 permutations of that pre-group: 3 distinct groupings (60/30/30), vs 1 withsymmetric=True(added in this PR). Changing tolerances (e.g.stol,ltol, etc) can't fix this.all(mixing_state_data[mixing_state_data["is_stable_1"]]["entry_id_2"].notna())inget_adjustments, re-anchoring the whole hull from the r2SCAN scale onto GGA.Changes/Fixes
Sorting: New
_entry_sort_key, applied at therun_typesplit inget_mixing_state_dataand on_filter_and_sort_entriesreturn. Both are needed: the former is public and callable without the latter, and the latter is whatprocess_entriesbuilds its own return order from. The key must be a robust total order; now using the energy per atom (NaN guarded to avoid silent degradations) andstr()onEntry.entry_id.symmetric=Truein thegroup_structurescall, so the grouping no longer depends on which structure is popped as reference. This requires the concurrent PR I just raised topymatgen-corehere: Addsymmetricoption togroup_structures, pass through tosm.fit()and test pymatgen-core#133 (and hence thepymatgen-corerequirement bump).entry.structure.copy()before stampingentry_idonto it..structurereturns the live structure, so two entries built from oneStructureboth end up labelled with the second'sentry_idand the first vanishes from the mixing state; in a 3-entry test case that discards every r2SCAN entry. MP's own path is safe (from_dictbuilds a freshStructureeach time); user code comparing two functionals on one geometry is not.entries/mixing_scheme.py-> re-export stub. It was missed by 1c7086f, which stubbed its five siblings, so two identical copies have existed as separate classes. fix: correctly clear energy_adjustments in clean mode (avoid skipped removals) #4615 had to "apply fix in both mixing_scheme modules", and because the two stamp different@moduleinto adjustmentcls, re-processing across the two paths withclean=Falseappends a duplicate adjustment.Pre-group on
reduced_composition(fixes Mixing scheme fails to mix entries from the same material id even when their structures match #4261). The firstgroupbyinget_mixing_state_datakeyed on the full composition, so a 1-atom and a 2-atom cell of one material ('Fe1' != 'Fe2') landed in different pre-groups and never reachedgroup_structures— even thoughStructureMatcher().fit()would match them correctly. mp-13, the entry reported in Mixing scheme fails to mix entries from the same material id even when their structures match #4261, is exactly this case. Safe to pass on downstream:get_hull_energy_per_atomis scale-invariant (verified), theformulacolumn already usedreduced_formula, and the fuzzy-diatomic branch still splits by site count. MWE:Unlike the reproducibility changes, this one is a deliberate behaviour change on real data. On 5 test MP chemical systems, 3 are identical and 2 move; in each changed case a GGA entry is newly paired with its r2SCAN partner and replaced by it, as the scheme intends:
mp-13(the Mixing scheme fails to mix entries from the same material id even when their structures match #4261 entry),mp-18905,mp-757614mp-122,mp-12205,mp-2657,mp-2998,mp-882— the last three's r2SCAN twins were already in the output unpaired, i.e. the same phase counted twice at inconsistent energiesVerification
5 chemical systems (Si-Se, Li-Fe-O, Ba-Ti-O, Zn-S, Na-Cl-O), 972 processed entries, each under
PYTHONHASHSEED0/3/7, unseeded, and with a permuted input list -> exactly one fingerprint per system. New tests, each mutation-tested: removing any one of the source changes here fails exactly one of them. No behaviour change to the existing suite.Not fixed here
get_adjustments: removing a single r2SCAN entry unrelated to the composition of interest re-anchors the whole diagram (from r2SCAN to GGA).Note for downstream (namely
emmet)emmet-coreimportsMaterialsProjectDFTMixingSchemefromentries.mixing_scheme(emmet/core/io/pymatgen.py:135), which is now deprecated (new import path isanalysis.compatibility.mixing_scheme-> should be updated to avoidDeprecationWarnings. cc @esoteric-ephemera @tsmathisAlso relevant to MP directly:
emmet-builders' thermo builder calls the samecompatibility.process_entries(...)over mixed GGA/R2SCAN entries, so published mixed hulls are one arbitrary sample from this distribution, and a rebuild could shift reported formation energies (in the desired direction, favouring r2SCAN hulls when possible, but worth noting).cc @rkingsbury as author of the mixing schemce
Checklist
ruff.mypy.duecredit@due.dcitedecorators to reference relevant papers by DOI (example)