MP2020 anion correction dropped for mixed-anion compounds (oxynitrides)

Dear Materials Project,

I have been looking in to oxynitrides lately (especially Zr-N-O systems), and I have come across a bug in the anion correction which are applied via pymatgen to your PBE data.

An example is the system Zr-O-N. Here, for Zr-O systems, O is correctly assigned the anion correction determined by Wang et al. and likewise N-corrections are assigned to systems of Zr-N. The problem arises when both N and O are anions in a system. Here only O is detected as an anion by pymatgen (i have been using pymatgen version 2026.5.4), and therefor, only the O atom receives an anion correction in an Zr-O-N system.

This is for example evident for Zr2ON2, which on the Materials Project website, is found to be metastable - which it infarct will not be if N is additionally assigned the anion correction (as it should). You can for instance compute the mixing energy of Zr2ON2 form Zr3N4 and ZrO2 with and without the MP2020 corrections. These energies should be the same, given that both O and N are anions in all mixed compounds (the anion corrections should cancel and be equivalent to uncorrected PBE).

Note that the error is largest when adding a small amount of O to an Zr-N system, as this strips the N-correction, while only applying it to O.

I have been able to resolve the issue locally, by use of pymatgens “Composition.oxi_state_guesses”, and then just running the ComputedStructureEntry though the standard MaterialsProject2020Compatibility class.

Thanks for your attention, and for your great database.

Best regards,
Mathias Nissen

Hi @Matminator

Thank you for looking into this issue, and apologies for the very late reply. We’ve been a bit short-handed lately.

Regarding the issue you raised, could you please share your corrected code snippets here? That would help our team take a closer look and investigate further.

Hi @Min-Hsueh_Chiu

Sorry for my late reply (I hand in my PhD in 12 days).

I got Claude code to track down the issue, and provide the code below to illustrate the issue.

It has to do with how MP2020 looks up the oxidation state of N, using a string key against a dict the API serves keyed by Element objects. I suspect that it is not confined to N, but that the issue occurs for the lookup of Br, I, Se, Si, Sb, Te, H, N, F and Cl, so in any compound with two or more of these, all but the most electronegative silently lose their correction.

This error drops the N oxidation state, and for instance undercorrects Zr2ON2 as it only receives a correction for O and not N.

under GGA (GGA_GGA+U) only your web page gives Zr2ON2 as 126.2 meV/atom above the hull - where as it should be on the hull (0 meV/atom) if it also gets the N correction as it should (also a problem for GGA/r2SCAN mixing of course, but slightly different values).

This is just one example - but probably extends across your database.

Reproduction, pymatgen 2026.5.4 with mp-api 0.46.4:

"""
MP2020 silently drops the N anion correction for oxynitrides.

MaterialsProject2020Compatibility looks up the anion oxidation state with a string key,
`entry.data["oxidation_states"].get("N", 0)`, but the MP API serves that dict keyed by
Element objects. The lookup misses, .get returns 0, so N is not recognised as an anion
and its correction is never applied. No warning is raised.

Paste your MP API key below and run.  pymatgen 2026.5.4, mp-api 0.46.4.
"""

import os

from mp_api.client import MPRester
from pymatgen.core import Element
from pymatgen.entries.compatibility import MaterialsProject2020Compatibility
from pymatgen.entries.computed_entries import ComputedStructureEntry

MP_API_KEY = ""                      # <-- paste your Materials Project API key here

with MPRester(MP_API_KEY or os.environ["MP_API_KEY"]) as mpr:
    entry = next(e for e in mpr.get_entries("Zr2N2O") if e.parameters["run_type"] == "GGA")

states = entry.data["oxidation_states"]

print("oxidation_states served by MP:", states)
print('  states.get("N", 0)        ->', states.get("N", 0), "   <-- the lookup MP2020 does")
print('  states.get(Element("N"))  ->', states.get(Element("N")))


def corrections_applied(oxidation_states):
    """Names of the MP2020 adjustments for this entry, given an oxidation-state dict."""
    fresh = ComputedStructureEntry(entry.structure, entry.uncorrected_energy,
                                   parameters={"run_type": "GGA"},
                                   data={"oxidation_states": oxidation_states})
    corrected = MaterialsProject2020Compatibility(check_potcar=False).process_entries(
        [fresh], clean=True, verbose=False)[0]
    return [adj.name for adj in corrected.energy_adjustments]


print("\nMP2020 corrections applied to", entry.entry_id)
print("  keys as served (Element):", corrections_applied(states))
print("  keys cast to str        :", corrections_applied({str(k): v for k, v in states.items()}))

Output:

oxidation_states served by MP: {Element Zr: 4.0, Element N: -3.0, Element O: -2.0}
  states.get("N", 0)        -> 0    <-- the lookup MP2020 does
  states.get(Element("N"))  -> -3.0

MP2020 corrections applied to mp-775849-GGA
  keys as served (Element): ['MP2020 anion correction (oxide)']
  keys cast to str        : ['MP2020 anion correction (oxide)', 'MP2020 anion correction (N)']