Repository navigation
Reuse first termination slabs in CoherentInterfaceBuilder._find_matches - #4705
Sanftperlig wants to merge 7 commits into
Conversation
…aceBuilder to keep the same shift, fixes materialsproject#4651
shyuep
left a comment
There was a problem hiding this comment.
Automated PR review generated by Claude (posted on behalf of @shyuep by a scheduled routine; static analysis of the diff + CI only — advisory, not a human review).
The diagnosis is right: _find_matches built its own SlabGenerator and took get_slab(shift=0), while _find_terminations uses get_slabs(...) at physically determined shifts. With primitive=True the Niggli/primitive reduction is shift-dependent, so ZSL matching could be done on a lattice that no constructed interface ever uses. Reusing a real termination slab is the correct direction, and dropping the duplicate generators is a free win.
Three things before this can go in:
-
film_slabs[0]is still only one termination. If primitivization is genuinely shift-dependent, slabs at other shifts can have different reduced lattices, so the matches remain inconsistent for terminations 2..N — the bug is narrowed, not removed. Preferred fixes, in order: (a) assert all slabs share the same lattice (np.allcloseon the matrices) and raise/warn if not, so the silent inconsistency becomes loud; or (b) match on a non-primitivized generator so the lattice is shift-independent by construction. At minimum, add a comment stating the assumption that all terminations share a reduced lattice. -
film_slabs[0]/sub_slabs[0]canIndexError.get_slabs(filter_out_sym_slabs=...)is not guaranteed non-empty; the oldget_slab(shift=0)always returned a slab. Guard and raise a clear error naming the miller index. -
No test. Understood that the #4651 reproducer already passes, but a regression guard is cheap and does not need a shift-dependent case: assert that the lattice
_find_matchesconsumes is exactlynext(iter(...))of the termination slabs (e.g. viafilm_slabs[0].lattice.matrix), plus a characterization test on a standard pair (Si/GaAs) showingzsl_matchesis unchanged. That pins the invariant this PR establishes.
Smaller points:
__init__now runs_find_terminations()before_find_matches(). Order inversion means the "Substrate lattice vectors changed during ZSL match"ValueErrorsurfaces only after the expensiveget_slabs()calls, andself.zsl_matchesis now set last. Both private, both fine — just worth a line in the release notes since_find_matches's signature changed and downstream subclasses overriding it will break.- Docstring formatting:
_find_terminationsneeds a blank line between the summary andReturns:for the Google convention, and the Returns entry should describe the tuple rather than name itfirst_pair(tuple[Slab, Slab]: film and substrate slabs of the first termination). Same for theArgs:block, which reads fine otherwise. - Nit:
Slabis annotation-only — it belongs in theTYPE_CHECKINGblock rather than the runtime import.
CI: no checks have reported on shift-change and mergeable_state is blocked — workflows need maintainer approval for this fork. Please approve the run; the review above is static-analysis only, with no test evidence behind it.
|
Automated PR review (generated by Claude on behalf of @shyuep). The diagnosis is right: matching against
No CI has run on this branch (workflows awaiting approval), so correctness against the existing interface tests is unverified. |
|
Thanks for fixing this issue. Can you add a unittest pls? Using a new example is fine. There is no need to rely on the existing examples. |
|
I sadly don't have time to work on this more significantly right now, but I will see if I can create a small test - I'm not too familiar with the To address the Claude comments: To my knowledge, Sorry for the delay and thank you for running the CI. |
|
Automated PR review generated by Claude (posted on behalf of @shyuep; not a human review — please sanity-check before acting.) Good catch on the root cause: hard-coding 1. The new test will error, not pass. assert np.isclose(inter.lattice[0, 0], 5, atol=1e-4) # TypeError: 'Lattice' object is not subscriptableUse 2.
3. Behavior change is user-visible and undocumented. Minor: the hard-coded termination label Nothing else concerning in the refactor — the |
|
Automated PR review generated by Claude (sanity-check before acting on it) Nice, well-scoped fix. Two things worth a look before merge:
Diff itself (interfaces + reordered |
… explicitly stated that the names may change (but that the slabs shouldn't)
|
Automated PR review generated by Claude (posted on behalf of @shyuep; not a human review — please sanity-check before acting.) Solid fix, well-targeted at the root cause. Two things worth a look before merge:
|
|
Automated PR review generated by Claude (on behalf of @shyuep). Sanity-check before acting. Good: Sound fix for the shift-0 vs shifted-primitivization mismatch (#4651), and reusing the slabs from Issues:
Nits: |
|
On the different comments:
I have never encountered a case where one selected termination (by
I added the name check intentionally such that it fails if someone modifies the function more significantly. It's supposed to make them aware of a potential difference in the chosen termination - so that a fail due to differing lengths can be assigned to the correct source (different asserted termination instead of incorrect primitivization).
It's the same SlabGenerator, just with different terminations. Also, as far as I am aware (based on the paper), ZSL matching is 2D based on the surface and has no third-vector component.
I don't understand this issue. |
|
Automated PR review generated by Claude (posted under @shyuep's account) — sanity-check before acting. Based on code and CI only; nothing was run. Good: Sound idea: Substantive
Nits
|
Currently, CoherentInterfaceBuilder._find_matches always uses shift 0. As discussed in #4651 and materialsproject/pymatgen-core#105 this will lead to some primitivizations differing from the nonzero shifts in other functions.
I believe that to be the source of issues such as #4651 (and thus it closes #4651), but I do not have a test case, given the one in the issue works fine already (I do believe that this will cause errors in other cases where primitivization is shift-dependent though).
Additionally, passing the slabs to find_matches saves creating the SlabGenerator and generating the two slabs.
Note that this can change the returned interfaces and ZSL matches! This is the case if primitivization failed before (returning a non-primitivized slab) - now it should return the primitivized slab and thus the matches based on that.
Checklist
ruff.mypy.