Update cif.py - #122
Conversation
Resubmit of materialsproject/pymatgen#3071
|
Thanks. Pls add a unittest for this bug fix. |
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude (on behalf of @shyuep)
Fix is correct in principle — species properties (e.g. spin) should not leak into _atom_site_type_symbol (fixes materialsproject/pymatgen#3065). Two suggestions:
- Avoid
copy.deepcopy(sp)+ mutation of the private_propertiesattr. Constructing a clean symbol is cheaper and doesn't touch internals, e.g.str(Species(sp.symbol, sp.oxi_state))forSpecies(falling back tostr(sp)forElement/DummySpecies). Deepcopy per (site × species) is measurable overhead when writing large structures. - Please add a regression test round-tripping a structure with spin-decorated species (see original issue) so this doesn't regress again.
CI is green (5/5).
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude on behalf of @shyuep.
Re-review after new commits. The fix (rebuilding a clean Species/Element so CIF _atom_site_type_symbol doesn't carry spin/property decorations; resubmit of materialsproject#3071, fixes materialsproject#3065) is reasonable. Remaining items:
import copyis added but never used — remove it.- Single-quoted strings will fail
ruff format— use double quotes. - The clean-species logic is duplicated in both branches; factor it into a small local helper.
- Please add a regression test round-tripping a structure with spin-decorated species (the #3065 case).
- CI has not run on this branch — needs a workflow approval after the above.
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude on behalf of @shyuep. Re-review after the 2026-08-14 commits; static analysis only.
Thanks — items 1–3 from the last round are addressed (unused copy import gone, double quotes, logic factored into _clean_species). Three things still outstanding:
-
Regression for
DummySpecies(new concern).DummySpecies.__init__defaultsoxidation_state=0, sosp.oxi_state is not NoneisTrueand the helper callsSpecies("X", 0), which raises sinceXis not an element. Any structure containing a dummy/vacancy species will now fail to write. Guard withisinstance(sp, DummySpecies)(already imported atcif.py:22) or rebuild viatype(sp)(sp.symbol, sp.oxi_state). -
Lint will fail. The blank line after
return get_el_sp(sp.symbol)contains trailing whitespace (ruff W293), and there is no blank line between the__init__docstring and the nesteddef. Please runruff check src/pymatgen/io/cif.py && ruff format src/pymatgen/io/cif.py. -
Still no test (item 4 from last time). Please add a round-trip covering materialsproject/pymatgen#3065 — a spin-decorated
Speciesshould write a clean_atom_site_type_symbol— plus one case withDummySpeciesonce (1) is fixed.
Also consider hoisting _clean_species to a module-level private function rather than redefining it per CifWriter.__init__ call. CI has still not run on this branch.
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude on behalf of @shyuep. Re-review after the 2026-08-15 commits; static analysis only.
Both blockers from the last round are fixed: DummySpecies is guarded before the Species(...) reconstruction, and tests now exist. Remaining items, roughly in order of importance:
- Formatting will fail lint. There is only one blank line between
str2floatanddef _clean_species(cif.py:1565);ruff formatrequires two before a module-level def. Please runruff check src/pymatgen/io/cif.py && ruff format src/pymatgen/io/cif.py. - Unrelated change — please restore the
# type: ignore[reportPossiblyUnboundVariable]on thespg_analyzer.get_symmetrized_structure()line.spg_analyzergenuinely is conditionally bound there, so dropping the suppression will likely fail the type check for reasons unrelated to this fix. _clean_specieshas no type hints, e.g.def _clean_species(sp: Element | Species | DummySpecies) -> Element | Species | DummySpecies:. Since both call sites only usestr(clean_sp), having the helper returnstrdirectly would be simpler still.test_write_dummy_speciesis close to a tautology.assert "X" in cif_strpasses off thedata_X1block name alone, so it would still pass if the type symbol were wrong. Assert the actual_atom_site_type_symbolrow, astest_write_spin_specieseffectively does.- Suggest dropping the
__author__edit — contributor attribution in this repo goes through git history and CHANGES.md, and the module__author__list is reserved for original authorship. - Add a CHANGES.md entry (fixes materialsproject/pymatgen#3065; resubmit of materialsproject/pymatgen#3071).
CI has still not run on this branch — no checks reported on patch-1 across all 11 commits. A workflow approval is needed before merge; nothing above has been verified by an actual test run. The 11 Update cif.py commits should also be squashed on merge.
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude on behalf of @shyuep.
Re-review of 13b1abd/2f8e2e4. The core fix is right: writing str(Species(symbol, oxi_state)) instead of str(sp) keeps Fe3+ in _atom_site_type_symbol rather than leaking ,spin=5.0 into the CIF. Three blockers:
test_write_dummy_specieslooks incorrect._clean_speciesreturnsDummySpeciesuntouched, andDummySpecies("X")defaults tooxidation_state=0, sostr(...)is"X0+", not"X". The assertionblock.data["_atom_site_type_symbol"] == ["X"]should therefore fail. Please confirm locally — CI has never run on this branch (checks are pending awaiting first-time-contributor approval), so nothing has verified it.- Stray unrelated formatting change in
cif.py:equivalent_sites # type: ignore[...]lost one of its two leading spaces. This repo runsruffwithselect = ["ALL"], soE261andruff format --checkwill both fail. Please revert that hunk. - No
CHANGES.mdentry. Output ofCifWriterchanges for spin-carryingSpecies, which downstream string comparisons may depend on — worth a changelog line.
Minor: for a plain Element, get_el_sp(sp.symbol) is a needless round-trip; return sp is equivalent and cheaper. A short docstring note that spin is intentionally dropped (it is emitted separately in the magmom loop) would also help.
Sanftperlig
left a comment
There was a problem hiding this comment.
I looked at your _clean_species function and I think you can keep your general structure, but improve it a little bit to be cleaner
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude (posted on behalf of @shyuep — not a human review; please treat as advisory).
Re-reviewing after the new commit. CI is red on all four test jobs, and the failure is this PR's own new test:
tests/io/test_cif.py::TestCifWriter::test_write_dummy_species
E AssertionError: assert ['X0+'] == ['X']
(1 failed, 2780 passed — same single failure on ubuntu-3.11, ubuntu-3.14, macos-3.13 and windows-3.13, so it is deterministic, not flaky. lint passes.)
Root cause: DummySpecies("X") defaults to oxi_state = 0, not None, so str(sp) is "X0+". _clean_species returns DummySpecies unchanged via an early return, so the plain-dummy case never gets normalized. This contradicts the CHANGES.md entry added in this PR, which claims "plain DummySpecies output as X". Either the code or the claim needs to change:
if isinstance(sp, DummySpecies):
return DummySpecies(sp.symbol) if not sp.oxi_state else spwould make the code match the stated behaviour and the test. Note not sp.oxi_state (rather than is None) is what catches the 0 default — worth a comment so it does not get "cleaned up" later.
Other comments:
hasattr(sp, "oxi_state")is redundant. By that linespis neitherElementnorDummySpecies, so it is aSpecies, which always hasoxi_state.if sp.oxi_state is not Noneis clearer and lets the type checker help.- The
get_el_sp(sp.symbol)fallback (Species withoxi_state=None) round-trips through string parsing to get back anElement.Element(sp.symbol)is more direct and avoidsget_el_sp's ambiguity for symbols that also parse as dummies. - Stripping spin from
_atom_site_type_symbolis the correct call —Fe3+,spin=5.0is not a valid CIF type symbol — and keeping the magmom loop (covered bytest_write_spin_species) means no information is lost. Good. - CHANGES.md wording: "and resolves two failing tests" reads as if the PR fixes pre-existing failures; if the two tests are the ones added here, drop that clause.
- Consider adding a round-trip assertion (
CifParseron the written string recovers the same species) for the spin case — currentlytest_write_spin_speciesonly does substring checks, and"Fe3+" in cif_strwould also pass if the spin suffix were still present.
Please push a fix for the failing test; happy to re-review after CI is green.
There was a problem hiding this comment.
I think this is more or less the best you can do if you want to return a non-str object
If you return a str instead, you can do a one-liner (assuming I am not missing anything):
return str(sp).partition(",spin=")[0].replace("X0+", "X")This always returns the pre-comma part (if there is a stated spin) and the dummy X0+ is forced to be X.
I believe this would also be faster (if I remember correctly isinstance is rather slow).
(Note: If you allow non-X dummies, you have to separate out the dummy case)
Sanftperlig
left a comment
There was a problem hiding this comment.
That is the correct implementation of my conversion line (with sp as str).
You could add a longer docstring, but I think it is fine (I also am not a maintainer though).
shyuep
left a comment
There was a problem hiding this comment.
🤖 Automated PR review generated by Claude (posted via @shyuep's account; static analysis of the diff + CI only, no code executed)
Good catch on the bug — str(sp) leaking ,spin= and X0+ into _atom_site_type_symbol produces CIFs that don't round-trip. A few things before merge:
1. _atom_type_symbol block is still unfixed. In the same __init__:
symbol_to_oxi_num = {str(el): float(el.oxi_state or 0) for el in sorted(comp.elements)}
blocks["_atom_type_symbol"] = list(symbol_to_oxi_num)This still emits Fe3+,spin=5 / X0+ while _atom_site_type_symbol now emits Fe3+ / X. The two loops must agree or parsers that cross-reference _atom_type_symbol will mismatch. Apply _clean_species there too.
2. .replace("X0+", "X") is fragile. It only special-cases the symbol X, so DummySpecies("Xa") still yields Xa0+, and it silently discards a legitimate zero oxidation state for X only (Species("Fe", 0) keeps Fe0+). Prefer explicit logic, e.g. return sp.symbol when isinstance(sp, DummySpecies) and not sp.oxi_state.
3. Silent spin loss. With write_magmoms=False, spin is now dropped with no record anywhere in the file. That's better than an invalid symbol, but worth a docstring note on CifWriter (or a warnings.warn) so users know spin needs write_magmoms=True to survive.
4. test_write_spin_species assertion is too weak. assert "Fe3+" in cif_str also passes on the old buggy output Fe3+,spin=5, so the test doesn't guard the regression. Assert on the parsed value instead:
assert CifBlock.from_str(cif_str).data["_atom_site_type_symbol"] == ["Fe3+"]A CifParser(...).parse_structures() round-trip check (species and magmom recovered) would be the strongest guard here.
5. Nit: the trailing comment in test_write_dummy_species ("No exception raised during write is already handled by the test") is a no-op — drop it.
CI: gh pr checks reports no checks reported on patch-1, so nothing has actually been validated yet. A maintainer needs to approve workflow runs for this first-time contributor branch before this can be assessed for merge.
shyuep
left a comment
There was a problem hiding this comment.
🤖 Automated PR review generated by Claude (posted on behalf of @shyuep)
Thanks for resubmitting this. The intent (strip spin from species strings in CIF output) is right, but a few issues need fixing before this can merge:
- Dead/overriding assignment: in
CifWriter.__init__, the newblocks["_atom_type_oxidation_number"] = list(symbol_to_oxi_num.values())is immediately overwritten by the leftover old lineblocks["_atom_type_oxidation_number"] = symbol_to_oxi_num.values(). Delete the old line. _clean_speciesbreaks on float oxidation states:Species.oxi_stateis a float (and can be non-integral, e.g.Fe2.5+), sof"{sym}{oxi:+d}"raisesValueError. It also emitsFe+3rather than the CIF-conventionalFe3+that your own test asserts —test_write_spin_specieswill fail as written. Suggest reusing the oxi-state formatting logic fromSpecies.__str__(symbol + magnitude + sign) instead of reimplementing.- Style: only one blank line between
_clean_speciesandclass CifWriter— ruff (E302) requires two. - CI has not run on this branch; please push a fix so checks trigger.
|
Automated PR review generated by Claude (scheduled routine, posted on behalf of @shyuep) Thanks for resubmitting this — the goal (strip spin, keep oxi state in CIF species strings) is right, but CI is red (lint + 4 test jobs) and the diff has a few concrete bugs:
Happy to re-review once CI is green. |
|
Automated PR review generated by Claude (scheduled routine, posted on behalf of @shyuep) Re-review after latest commits — all five issues from the previous automated review are now addressed: magnitude-then-sign formatting ( Two minor remaining points:
CI has not run on this branch (no checks reported — likely awaiting maintainer approval). Looks close to mergeable once the above are fixed and CI is green. |
Resubmit of materialsproject/pymatgen#3071
Fixed materialsproject/pymatgen#3065