Repository navigation
[breaking] Support mixed-supercell orderings in HeisenbergMapper via a shared parent-cell sublattice definition - #4700
Conversation
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude (on behalf of @shyuep)
Substantial, well-motivated refactor (parent-cell sublattice definition + StructureMatcher mapping + least-squares fit) with unusually thorough docstrings and new test coverage. Main comments:
- Attribution: don't comment out the original
__author__block (ncfrey). Either keep both authors or drop the dunder metadata entirely — git history carries attribution. raise SystemExit(...)was carried over from the old code — please useValueErrorinstead;SystemExitkills interactive sessions and notebooks.- This is a significant API break for
HeisenbergMapper(attributes likesgraphs,unique_site_ids,wyckoff_idsare gone;estimate_exchange/get_mft_temperaturedeprecated). A short migration note in the PR description / docs would help downstream users. - CI has not run on this branch (first-time contributor — needs a maintainer to approve workflows). Please rebase on master and a maintainer should trigger CI before merge.
- Default-parent inference from the lowest-energy ordering is a documented footgun; consider emitting a
UserWarningwhenparent=Noneso users can't miss it.
Nice work overall — the shell-midpoint interaction labeling and pooled magnetic species are clear improvements.
|
@shyuep @mkhorton As per the discussion in materialsproject/atomate2#1528, should I restore the vampire caller into pymatgen, because it is written to be specific to this workflow and relevant to local execution? |
|
Automated PR review generated by Claude (scheduled routine, posted on behalf of @shyuep) Substantial and well-motivated refactor — defining sublattices once on a parent cell and mapping orderings via
|
HeisenbergMapper via a shared parent-cell sublattice definitionHeisenbergMapper via a shared parent-cell sublattice definition
HeisenbergMapper via a shared parent-cell sublattice definitionHeisenbergMapper via a shared parent-cell sublattice definition
|
Agreed on the title, I'll mark it [breaking] so it shows up in the compatibility log. On the deprecation shims, I'd rather not add them here, and it's not only about cost. Taking them one at a time:
get_exchange is breaking regardless of the return type. The There's also one break no shim can cover: Given that, I think the honest version is a hard break with a good migration path rather than a half-compatible surface. The module docstring already carries a migration guide covering every renamed and removed attribute, the constructor order, the |
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude (posted on behalf of @shyuep; scheduled review routine).
This is a well-executed refactor. The test suite is a highlight: round-tripping against a synthetic Hamiltonian with known J's, ground-truth energies computed independently of the mapper's own graph, and explicit mixed-supercell (1x1/1x2/2x2) recovery. The migration notes in the module docstring are thorough.
Comments:
- Legacy
HeisenbergModeldeserialization is broken.from_dictnow requiresmagnetic_structures,nn_graphs,sublattice_ids,sublattice_wyckoff_symbols, andresidual. Any previously stored MSON dict (withsgraphs,unique_site_ids,wyckoff_ids,javg) willKeyError. You handle oldex_matstring forms, so partial back-compat is clearly intended — either extendfrom_dictto accept the old key set (even if lossy) or raise a clear "serialized with HeisenbergModel <0.2" error. toldefault silently changed from 0.02 to 0.05 (DEFAULT_TOL). Not mentioned in the migration section — please add it, since it changes shell-merging behavior for existing callers.- Positional-arg shift:
HeisenbergMapper(structs, energies, 5.0)previously meantcutoff=5.0; now5.0lands inparent. It fails loudly in_nonmagnetic, but a friendlierisinstance(parent, Structure)check with a pointed error message would help migrating users. self.residualis only set inget_exchange— initialize toNonein__init__alongsideex_paramsso attribute access before fitting doesn't raiseAttributeError(the class docstring implies it's an attribute).- Explicit
parentis silently primitivized viaget_primitive_structure(). Worth a docstring note, since a user supplying a deliberate supercell parent may not expect orbit analysis on the primitive cell. - The
pymatgen-coresubmodule bump is included — confirm it's intentional and points at merged main (it matches merged #130, so likely fine, just flagging). - CI has not run on the latest commits ("no checks reported" on the branch). A maintainer needs to approve/trigger workflows before this can be evaluated for merge.
The deprecations of estimate_exchange/get_mft_temperature with concrete alternatives (shell-resolved fit; MC solvers) are the right call, and pinning the flawed multi-sublattice MFT formula in a test rather than silently "fixing" it is good hygiene.
|
@mkhorton I suggest you review this since I am not familiar with this code. |
|
Automated PR review (generated by Claude on behalf of @shyuep). This is a well-motivated redesign — defining sublattices once on an explicit parent cell and mapping each relaxed ordering via Requested changes:
|
…improve ex_mat serialization according to PR materialsproject#4664
…parent once and with the nonmagnetic ions present. Orderings are mapped onto parent to determine what magnetic sublattice each site belongs to.
…itioned fit warning, correct units in docstrings
…arentOrdering classes
…ultiplying with the absolute magmoms at both ends of the edge
…bug and fix deprecation warnings. Remove TestHeisenbergMapper and move relevant tests from it to TestHeisenbergMapperHamiltonian
…t being labeled at all
An earlier commit on this branch (d0db2e5, before the rebase onto master) dropped the literal_eval fallback that accepts a string-serialized ex_mat (from as_dict() calling jsanitize() directly on the DataFrame, as done before materialsproject#4664, or by materialsproject#4664 itself). The fallback was restored when this branch merged master, which added the same protection independently -- but a plain rebase onto master (which discards merge commits) lost it again since no later commit on this branch reintroduced it. Restoring it to match the pre-rebase branch tip.
The physics motivation is right, and the core insight — define sublattices once on a paramagnetic parent cell so orderings in different supercells can be fitted together — is a real improvement over deriving them from the orderings themselves. That said, at +1427/−699 in one module with a breaking public surface and no CI results reported (0 checks), this is not mergeable as-is. Blocking
Should fix
Positive notes
|
|
Automated PR review generated by Claude (sanity-check before acting on it) The design direction is good: defining sublattices once on a shared parent cell and mapping each relaxed ordering onto it via However, this cannot merge as-is: CI is failing because the file doesn't parse. Ruff reports this as Given the size of this diff (1427/-699 across 2 files) and that it's marked |
|
Thanks for the detailed reviews. I pushed a round of fixes; here's where each point stands. CI failure. The syntax error at
Positional
Deprecation shims / removed attributes. I'd still rather not add them, for the reasons in my earlier comment:
Maintainer field. @ncfrey hasn't been active on this module for a while, and I'm happy to take over maintaining it, which is why I'm listed. If you'd rather decide that separately, I'll revert the field. Splitting the PR. I'd prefer to keep it as one. |
- Remove a stray fragment appended to the ex_params dict in get_exchange that broke parsing (and with it lint and every test job). The species alignment check it was meant to add already lives in RelaxedOrdering.set_sublattice_ids. - Make `energies` hold total energies on HeisenbergMapper and HeisenbergModel, and add `energies_per_magnetic_ion` for the per-ion energies the fit uses. Pre-0.2 `energies` were already per magnetic ion; the migration guide now states the switch. - Report bonds between sublattice pairs missing from the parent graph as a UserWarning instead of a log message, like the other fit warnings. - Document why DEFAULT_TOL is 0.05 Angstrom and how tol relates to DISTANCE_ROUND_DECIMALS; reject tol below the 0.01 Angstrom rounding resolution, where it would have no effect. - Add tests for the tol check, the new warning and both energy fields.
5d05567 to
031d18e
Compare
The exact-fit asserts used abs=1e-12 meV/ion, which is below float64 noise; the macOS lowest-direct job got 1.17e-12. Use abs=1e-9.
|
Automated PR review generated by Claude (posted under @shyuep's account) — please sanity-check before acting. Good: Defining sublattices once on a parent cell and mapping each ordering onto it via Substantive
Minor
|
With cutoff=0 the graph holds the nearest shell of every sublattice pair, and with a cutoff every bond up to it, so "nearest-neighbor" no longer describes it. No behavior change. - nn_graph/nn_graphs/parent_nn_graph -> coupling_graph/coupling_graphs/ parent_coupling_graph, on MagneticOrdering, HeisenbergMapper and HeisenbergModel (including the serialized keys). - nn_interactions -> interactions; the migration guide notes the rename.
n orderings constrain only E0 and n - 1 J_ij. The surplus interactions were cut silently, and before the all-zero columns were removed, so an interaction that never appears could take the slot of one that does. - Build every J column, drop the all-zero ones, then truncate to n - 1 in _build_exchange_mat; _exchange_columns is gone. - Warn with the dropped labels, which also get no edge in the interaction graph. - Point the "needs at least 2" error at supplying more orderings when interactions were dropped.
Coupling graphs were built on each relaxed ordering, so relaxation decided which pairs couple and which shell they fall in. Bond lengths spread around the parent's, which DEFAULT_TOL = 0.05, rounding to 0.01 Angstrom and closest-shell labelling were there to absorb; a bond relaxed past SublatticeMinimumDistanceNN's window still dropped out. - RelaxedOrdering keeps ideal_magnetic_structure: its magnetic sites at the matched parent positions, with its own moments. Coupling graphs and the interaction graph are built on it. - Bond lengths are now exact parent distances: drop the rounding, DISTANCE_ROUND_DECIMALS and the minimum-tol check, and restore DEFAULT_TOL = 0.02. A bond joins a shell within tol of the shell's shortest bond, and _interaction_label applies the same rule. - A coupling the parent does not have now raises a ValueError instead of being warned about and left out of the fit. - Add tests for relaxed orderings, shell grouping by tol with and without a cutoff, and the new error; drop the tests of the removed tol check and warning.
tol grouped a pair's bond lengths by their distance to the shortest bond of the current shell, so which side of a boundary a bond fell on depended on where the shell started, not on its neighbors. - Start a new shell wherever two consecutive sorted lengths differ by more than tol. The grouping is symmetric; a shell can span more than tol if its bonds are closer together. Shells stay consecutive ranges, so _interaction_label keeps its rule. - Add symprec (default 0.01, as in SpacegroupAnalyzer) for finding the parent's sublattices. A parent relaxed in an antiferromagnetic state, as some Materials Project ground states are, can be distorted beyond it and split sublattices that are equivalent in the paramagnetic phase. HeisenbergModel stores it next to tol. - Replace the labelling test with one where gap and anchored grouping disagree, and test that symprec merges the sublattices of a distorted parent.
RelaxedOrdering now requires the parent and labels its sites on construction, which removes set_parent and the unlabelled state that coupling_graph guarded against. An inferred parent is picked as the lowest energy per magnetic ion from the raw inputs.
- Cut docstrings and comments down to the contract (arguments, units, return values, errors); repeated rationale is stated once. - Inline one-line helpers (_magnetic_only, _magnetic_species, _order_sublattice_ids, _get_j_exc) and drop the unused parent_coupling_graph and parent_sublattice_ids properties. - Use ConnectedSite field names instead of tuple indices, and plain loops for the magnetic species and the shell lookup. - Document interactions and dists where they are declared, so the editor shows their structure on hover. - Order the mapper's methods as they run: properties, setup steps, fit, export, then the deprecated methods. - Deprecate get_low_energy_orderings, which only estimate_exchange uses.
|
@shyuep Where the last review's points stand
What changed in this round
A guide to reviewing
|
|
@shyuep This PR is part of a larger workflow that will be integrated in the atomate2 infrastructure. Thus, it would be of high interest that it is integrated into pymatgen. |
|
Automated PR review generated by Claude (posted under @shyuep's account) — sanity-check before acting. Based on code and CI only; nothing was run. This is a 117k-char diff; I read the first ~900 lines of Good: Defining sublattices once on a parent cell and mapping each ordering by structure matching fixes a real limitation (index/cell assumptions across orderings). The migration notes in the module docstring are excellent, Substantive
Nits
|
Without a cutoff, bonds in a pair's nearest-shell window were still split into nn/nnn at gaps larger than tol. Each pair now gets one interaction, and tol only applies with a cutoff. Also note in the get_exchange docstring and migration guide that the residual is only meaningful with substantially more orderings than fitted parameters.
A cutoff used to replace the per-sublattice nearest-neighbour search, so a sublattice pair with no bond within the cutoff got no coupling at all. SublatticeMinimumDistanceNN now keeps every bond within the cutoff and adds the nearest shell of each pair that has none, so cutoff=0 is the special case "nearest shell only" and both paths use one strategy. The nearest shell now ends at the first gap larger than tol (Angstrom), the same rule that splits shells in _set_interactions, instead of a 10% window beyond the nearest bond. Shell splitting is unconditional again. MAX_SEARCH_DIST and DIST_EPSILON replace the literal 10 and 1e-6.
|
@shyuep Thanks for the review. Two commits address it; point by point: Residual when the system is square. Agreed, the docs oversold it. The
So
Disordered sites. Not reachable: every structure goes through Breaking API / Splitting the PR / author block. The pieces depend on each other (the screener and deprecations change because the mapper's data model changes), so I'd prefer to keep it together; the figures below and the review guide in my previous comment walk through the module in execution order. The author block appends to @ncfrey's entry instead of replacing it, as requested in an earlier round. Updated figures
Step 5 now states which bonds are taken, and step 7 the caveat on the residual:
The PR description is updated to match. All 31 tests pass locally. |
It only runs from each ordering's constructor; calling it again from outside would just redo the labelling.
|
Automated PR review generated by Claude (posted via @shyuep's account); sanity-check before acting. Good: Defining sublattices once on a parent cell and mapping each ordering onto it is a sound design, and lets orderings in different supercells share a single J set. The Substantive:
Nits: the |
The parent is usually the ferromagnetically relaxed cell, and each ordering relaxes away from it under its own moments, so the default matcher tolerances can reject a valid ordering. A `matcher` argument lets the caller loosen them. A matcher that cannot match supercells (primitive_cell=True or attempt_supercell=False) now raises a ValueError, and the mapping errors point to the matcher as a fix.
A sublattice pair with no neighbor within the 10 Angstrom search distance was silently left out of the model. HeisenbergMapper now names such pairs in a UserWarning.
|
@shyuep Thanks for the review. Two commits address it; point by point: Cost of Silent uncoupling. Agreed. A sublattice pair with no neighbor within the search distance now raises a
Splitting the PR. Unchanged from my previous reply: the pieces depend on each other. Units of |
|
Automated PR review generated by Claude (posted from @shyuep's account). Based on code and CI status only, nothing was run. Please sanity-check before acting. Good: Making a paramagnetic parent cell the single source of truth for sublattices (orbits of Substantive:
Nits:
|
E0 and one J are fixed by two orderings that give J different coefficients, e.g. FM and AFM. get_exchange now requires one J instead of two.
|
@shyuep Thanks for the review. One commit addresses it. However, the last few automated rounds have mostly repeated points that were already answered, with no change in between. I take that as a sign the PR is ready for a human review.
Mixed units in Cost of the neighbor search. Same answer as last round. Building the coupling graph takes 0.04 s for 32 magnetic sites and 0.32 s for 256. The Truncation. Nits. |
|
Automated PR review generated by Claude (posted from @shyuep's account). Based on code and CI status only, nothing was run. Please sanity-check before acting. Follow-up on 5e6a6c4 ("Let get_exchange fit a single J"). This resolves item 1 of my previous review: with one shell, Caveat: with one J and two orderings the fit is exactly determined, so Still open from my previous review (not touched by this commit): item 2 (deprecated |




Summary
Refactors
HeisenbergMapperso magnetic sublattices are defined once on a common parent cell, and every DFT-relaxed ordering is mapped onto it by structure matching rather than by direct index comparison. Orderings relaxed in different-sized supercells of the same parent can now be combined in one fit.Closes #4667.
Motivation
The old implementation identified "unique sites" from the geometry of
ordered_structures[0]and assumed every other ordering shared that exact indexing and cell. Orderings from aMagneticOrderingsWF-style workflow often don't: they relax into different, commensurate supercells of the same parent, and the mapper had no way to reconcile them. This PR makes an explicit parent cell the single source of truth for sublattice identity and matches each ordering onto it withStructureMatcher(attempt_supercell=True).How it works
HeisenbergMapperowns oneParentOrdering, whose symmetry orbits are the sublattices, and two or moreRelaxedOrderings, each labelled by the parent. Both inherit their magnetic-only structure and coupling graph fromMagneticOrdering, which builds the graph with the newSublatticeMinimumDistanceNN.Steps 1–6 run in the constructor, step 7 in
get_exchange()and step 8 in the export methods. Step 6 writes out the linear system with the column names ofex_matand the keys ofex_params.Changes
New behaviour
symprecargument (default 0.01) sets the tolerance.ValueErrorinstead of mislabelling sites.matcherargument takes aStructureMatcherwithprimitive_cell=Falseandattempt_supercell=True; any other setting raises aValueError. Loosen itsltol,stolorangle_tolwhen relaxation distorts an ordering beyond the defaults.ValueError.tol(default 0.02 Å).SublatticeMinimumDistanceNNkeeps every bond withincutoff, plus the nearest shell (up to the first gap larger thantol) of each sublattice pair with no bond within it. Every sublattice pair with a neighbor within 10 Å therefore couples, also with a cutoff, andcutoff=0gives one shell per pair; long intra-sublattice bonds no longer drop out behind shorter inter-sublattice ones. A pair with no neighbor that close emits aUserWarningnaming it.get_exchange()fits by least squares over all orderings instead of solving a square system. It warns when the fit is ill-conditioned or rank deficient and reports an RMS residual in meV per magnetic ion. The residual is only meaningful with substantially more orderings than fitted parameters; otherwise the extra parameters overfit and lower it without improving the fit.UserWarningthat names them.Breaking API changes (also in the migration guide in the module docstring)
HeisenbergMapper(ordered_structures, energies, parent=None, cutoff=0, tol=0.02, symprec=0.01, matcher=None):parentis now third, and a non-Structurepassed there raises aTypeErrorthat points to the change.get_exchange()returns(ex_params, residual).sgraphs,unique_site_ids,wyckoff_idsandnn_interactionsare replaced byorderings,coupling_graphs,sublattice_ids,sublattice_wyckoff_symbolsandinteractions. J labels are now'<i>-<j>-nn','<i>-<j>-nnn', ... per sublattice pair.energiesnow holds total energies as passed in; per-ion energies are inenergies_per_magnetic_ion. The same applies toHeisenbergModel.HeisenbergScreenertakesRelaxedOrderingobjects and exposesscreened_orderings.HeisenbergModel.from_dictrejects 0.1 dicts with a pointer to recompute, since the old site labels don't map onto parent sublattices.Deprecations (deadline 2027-08-01)
estimate_exchange,get_low_energy_orderingsandget_mft_temperature: use the shell-resolved couplings fromget_exchange(), and a Monte Carlo code such as VAMPIRE for critical temperatures.Units
E0is in eV per magnetic ion.get_interaction_graph()converts the couplings to meV for unit spins, the convention of VAMPIRE, UppASD and TB2J.Testing
All 35 tests in
tests/analysis/magnetism/test_heisenberg.pypass:TestHeisenbergMapperKnownHamiltonian: fits energies generated from a known Hamiltonian and recovers the couplings, including mixed 1×1, 1×2 and 2×2 supercells and relaxed orderings; also covers shell splitting bytol, the nearest-shell floor with and without a cutoff,SublatticeMinimumDistanceNNdirectly,symprec, thematcher, the residual, the warnings and the errors.TestHeisenbergMeanFieldTemperature: the deprecated mean-field path, single- and multi-sublattice.TestHeisenbergMapperFullCellSymmetry: nonmagnetic ions splitting sublattices, and equivalent sites sharing one.TestHeisenbergMapperZeroMomentIon: an ion whose moment relaxed to zero stays on the magnetic lattice.Reviewing
heisenberg.pyreads top to bottom in the order of the figures:SublatticeMinimumDistanceNN, the three ordering classes (steps 1–3), thenHeisenbergMapper(steps 1–8), and finallyHeisenbergScreenerandHeisenbergModel. The part that most needs a careful look is the alignment check inRelaxedOrdering._set_sublattice_ids.