zerothi / sisl

Electronic structure Python package for post analysis and large scale tight-binding DFT/NEGF calculations
https://zerothi.github.io/sisl
Mozilla Public License 2.0
173 stars 57 forks source link

Modifying the returns of the neighbor finder #765

Closed pfebrer closed 3 weeks ago

pfebrer commented 2 months ago

As discussed in #744, this PR modifies the return values of the neighbor finder so that the user has to ask explicitly for a certain quantity, making sure that they get what they want.

There is a general Interactions class, but the different ways in which the finder can work require slightly different manipulations, and I have created multiple classes to facilitate that the user notices what they have at hand.

For finding neighbors (atom-atom interactions), we have the following classes:

For finding atoms that are close to certain points in space we have:

This is not finished, but before finishing the details I would like to get your opinion to see if you think it is on the right track or not.

One thing I noticed is that the FullNeighborsList and UniqueNeighborsList are very similar to a SparseAtoms object. I don't know if we should return that instead, or subclass SparseAtoms. Although with a neighbors list it is probably more convenient to have the matrix in COO format.

Here is an example of how one might get and use a FullNeighborsList:

import sisl
from sisl.geom import NeighborFinder

# Create a graphene sheet with a defect
graphene = sisl.geom.graphene().tile(6, 0).tile(6, 1)
graphene = graphene.remove(15).translate2uc()

# Initialize a finder
finder = NeighborFinder(graphene, R=1.5)

# Get the neighbors
# Here "neighs" is a FullNeighborsList
neighs = finder.find_neighbors()

# neighs has attributes like `ì`, `j`, `isc`, `j_sc`, `n_neighbors`
# Here we plot the graphene sheet, coloring the atoms by the number of neighbors they have
graphene.plot(
    axes="xy", nsc=[2, 2, 1], atoms_style={"color": neighs.n_neighbors}
).update_layout(
    title="Number of neighbors in the graphene sheet",
    height=700
).show()

# We can loop over it to get the neighbors of each atom
for neigh in neighs:
    # neigh here is an AtomNeighborsList containing
    # the neighbors of a given atom.
    # We can get the atom to which the neighbors belong
    neigh.atom
    # We can get the neighbors, or their isc
    neigh.j
    neigh.isc
    # Or the neighbors in supercell indices
    neigh.j_sc
    break

# Instead of looping we can also get the neighbors of a given atom directly
neighs[10]

# And we can plot the neighbors of a given atom. We color the atom in black and
# its neighbors in pink

# Atom for which we want to plot neighbors
at = 0

graphene.plot(
    axes="xy", 
    nsc=[2, 2, 1], 
    atoms_style=[
        {"atoms": at, "color": "black"},
        {"atoms": neighs[at].j, "color": "pink"}
    ]
).update_layout(
    title=f"Neighbors of atom {at}",
    height=700
).show()

newplot (63) newplot (64)

pfebrer commented 2 months ago

I really like this, however, I am a bit worried about the complexity of the different classes? Are there too many, should we coerce them into 1 with flags to denote their content? What would a user expect, for generic functionality they would have to do lots of isinstance where this could possibly be leveraged by have_symmetric_neighbors or similar, I am not saying that is the way to go, but we should at least consider it?

The first implementation was actually a single class with flags, but the logic inside each method/property was kind of a mess. The code was not easy to read. Also depending on the thing stored, the functionality changes significantly and attributes with the same name mean different things. So I think it is clearer for the user if each type of return is a different class. Also there wasn't much interplay between the different flags really, so in the end you would have flags that are something like is_class_x which would be equivalent to the current isinstance(class_x).

zerothi commented 2 months ago

True... It might be fine like this!

pfebrer commented 2 months ago

I changed it so that the iterator works with __getitem__, to allow directly getting the neighbors of a given atom instead of requiring the user to loop. (code snippet in the description is updated)

zerothi commented 1 month ago

@pfebrer what should I help with here?

pfebrer commented 1 month ago

I can do it myself, but probably next week.

codecov[bot] commented 1 month ago

Codecov Report

Attention: Patch coverage is 51.25000% with 117 lines in your changes missing coverage. Please review.

Project coverage is 87.28%. Comparing base (81dd25e) to head (48d4c02). Report is 2 commits behind head on main.

Files Patch % Lines
src/sisl/geom/_neighbors/tests/test_finder.py 0.00% 96 Missing :warning:
src/sisl/geom/_neighbors/_neighborlists.py 83.60% 20 Missing :warning:
src/sisl/geom/_neighbors/_operations.py 0.00% 1 Missing :warning:
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #765 +/- ## ========================================== - Coverage 87.35% 87.28% -0.07% ========================================== Files 396 397 +1 Lines 50752 50915 +163 ========================================== + Hits 44332 44439 +107 - Misses 6420 6476 +56 ```

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

pfebrer commented 1 month ago

Done! Everything is there, including docs. You can reorganize the docs as you wish.

zerothi commented 3 weeks ago

Thanks!