%load_ext autoreload
%autoreload 2
from pylattica.core import Lattice, PeriodicStructure
from pylattica.core.neighborhood_builders import DistanceNeighborhoodBuilder, AnnularNeighborhoodBuilder, MotifNeighborhoodBuilder
Defining Neighborhoods¶
pylattica provides several methods for defining the neighborhood of each site in the simulation. Neighborhoods are built using a PeriodicStructure
object, so it is assumed that you have defined your structure beforehand when creating a neighborhood.
We will illustrate the use of some common neighborhood types here, using the 5x5 2D square grid as an example.
lattice = Lattice([
[1, 0],
[0, 1]
])
motif = [[0.5, 0.5]]
structure = PeriodicStructure.build_from(lattice, (5,5), motif)
Distance Neighborhoods¶
An extremely common way of defining neighbors is using a distance cutoff. The DistanceNeighborhoodBuilder
allows you to build a neighborhood that consists of each site within some cutoff radius of the site under consideration.
In our case of a square grid, we can illustrate this by building a neighborhood with a distance cutoff of 1.01. This will contain the 4 closest sites, but no others.
builder = DistanceNeighborhoodBuilder(1.01)
nbhood = builder.get(structure)
100%|███████████████████████████████████████| 25/25 [00:00<00:00, 2868.25it/s]
The nbhood
variable is an instance of the Neighborhood
class. It allows you to retrieve the neighbors for a given site in the structure.
Let's look at the central site in our structure. This site should have coordinates (2.5, 2.5). It should have 4 neighbors, one in each of the cardinal directions. We retrieve it's neighbors by passing its ID to the .neighbors_of
method on the Neighborhood
object. A generator is returned that allows you to iterate through the IDs of the neighboring sites.
central_site = structure.site_at((2.5, 2.5))
central_id = central_site["_site_id"]
nbs = nbhood.neighbors_of(central_id)
for nb in nbs:
print(nb)
16 8 18 22 6 14 10 2
We can use the structure object to get the location of these neighbors, and confirm our expectation that they are displaced from the central site by 1 unit in each of the cardinal directions.
for nb in nbs:
print(f"Location of {nb}: ", structure.site_location(nb))
Location of 17: (3.5, 2.5) Location of 11: (2.5, 1.5) Location of 13: (2.5, 3.5) Location of 7: (1.5, 2.5)
Incidentally, this is the Von Neumann neighborhood:
If we expand the distance cutoff to just over the square root of 2, we can get the Moore Neighborhood:
builder = DistanceNeighborhoodBuilder(1.50)
nbhood = builder.get(structure)
nbs = nbhood.neighbors_of(central_id)
for nb in nbs:
print(f"Location of {nb}: ", structure.site_location(nb))
100%|███████████████████████████████████████| 25/25 [00:00<00:00, 2249.97it/s]
Location of 6: (1.5, 1.5) Location of 11: (2.5, 1.5) Location of 7: (1.5, 2.5) Location of 16: (3.5, 1.5) Location of 13: (2.5, 3.5) Location of 8: (1.5, 3.5) Location of 18: (3.5, 3.5) Location of 17: (3.5, 2.5)
Shown here:
Iterating through sites and distances¶
Neighborhood
s also allow you to read the distance between sites as you iterate through the neighbors by including the include_weights=True
parameter to the neighbors_of
method:
nbs = nbhood.neighbors_of(central_site["_site_id"], include_weights=True)
for nb, distance in nbs:
print(f"Location of {nb}: ", structure.site_location(nb), " and distance: ", distance)
Location of 18: (3.5, 3.5) and distance: 1.414 Location of 16: (3.5, 1.5) and distance: 1.414 Location of 11: (2.5, 1.5) and distance: 1.0 Location of 8: (1.5, 3.5) and distance: 1.414 Location of 13: (2.5, 3.5) and distance: 1.0 Location of 6: (1.5, 1.5) and distance: 1.414 Location of 7: (1.5, 2.5) and distance: 1.0 Location of 17: (3.5, 2.5) and distance: 1.0
Building complex neighborhoods¶
Other simulations might call for other types of neighborhoods that are not given by a simple distance cutoff. Consider the following shape:
This neighborhood requires more information. This shape could be achieved by two of the built-in neighborhood types in pylattica.
Using the Annular Neighborhood¶
The first option is to use the AnnularNeighborhoodBuilder
. This NB builder is similar to the DistanceNeighborhoodBuilder
but instead of taking a single distance cutoff, it takes both a minimum and maximum distance.
builder = AnnularNeighborhoodBuilder(1.3, 2.01)
nbhood = builder.get(structure)
nbs = nbhood.neighbors_of(central_id, include_weights=True)
print(f"This gives {len(nbs)} neighbors, as expected.\n")
for nb, distance in nbs:
print(f"Location of {nb}: ", structure.site_location(nb), " and distance: ", distance)
100%|███████████████████████████████████████| 25/25 [00:00<00:00, 2423.67it/s]
This gives 8 neighbors, as expected. Location of 6: (1.5, 1.5) and distance: 1.414 Location of 2: (0.5, 2.5) and distance: 2.0 Location of 14: (2.5, 4.5) and distance: 2.0 Location of 22: (4.5, 2.5) and distance: 2.0 Location of 16: (3.5, 1.5) and distance: 1.414 Location of 10: (2.5, 0.5) and distance: 2.0 Location of 18: (3.5, 3.5) and distance: 1.414 Location of 8: (1.5, 3.5) and distance: 1.414
Using the Motif Neighborhood¶
The most flexible neighborhood builder provided by pylattica is the MotifNeighborhoodBuilder
. This NB builder uses a list of displacement vectors that specify the relative positions of the neighbors of each site.
To specify the desired neighborhood above, we can use the following motif.
motif = [
(0, 2),
(1, 1),
(2, 0),
(1, -1),
(0, -2),
(-1, -1),
(-2, 0),
(-1, 1)
]
builder = MotifNeighborhoodBuilder(motif)
nbhood = builder.get(structure)
nbs = nbhood.neighbors_of(central_id, include_weights=True)
assert len(nbs) == 8
print(f"This gives {len(nbs)} neighbors, as expected.\n")
for nb, distance in nbs:
print(f"Location of {nb}: ", structure.site_location(nb), " and distance: ", distance)
100%|███████████████████████████████████████| 25/25 [00:00<00:00, 9459.41it/s]
This gives 8 neighbors, as expected. Location of 22: (4.5, 2.5) and distance: 2.0 Location of 8: (1.5, 3.5) and distance: 1.41 Location of 18: (3.5, 3.5) and distance: 1.41 Location of 2: (0.5, 2.5) and distance: 2.0 Location of 16: (3.5, 1.5) and distance: 1.41 Location of 6: (1.5, 1.5) and distance: 1.41 Location of 14: (2.5, 4.5) and distance: 2.0 Location of 10: (2.5, 0.5) and distance: 2.0
See how the neighbors are the same as the ones given by the annular neighborhood?
Multi Neighborhoods¶
Though what we have covered so far will enable most use cases, there is another type of helper neighborhood that pylattica provides. In some cases, the neighborhood for a given site should be different depending on some condition. It could also be random. The following sections describe these two cases.
Stochastic Neighborhoods¶
from pylattica.core.neighborhood_builders import StochasticNeighborhoodBuilder
Say we wanted the neighbors of a given site to just be a random selection of the nearest neighbors. The way to do this is to use the StochasticNeighborhood
. The StochasticNeighborhood
choose randomly from a list of neighborhoods each time the neighbors are requested.
We can define 4 motif neighborhood builders - one for each cardinal directions, and then use the StochasticNeighborhoodBuilder
to make a neighborhood that selects randomly from among them.
motif1 = MotifNeighborhoodBuilder([(0,1)])
motif2 = MotifNeighborhoodBuilder([(0,-1)])
motif3 = MotifNeighborhoodBuilder([(1,0)])
motif4 = MotifNeighborhoodBuilder([(-1, 0)])
stoch_nbbuilder = StochasticNeighborhoodBuilder([motif1, motif2, motif3, motif4])
stoch_nb = stoch_nbbuilder.get(structure)
100%|██████████████████████████████████████| 25/25 [00:00<00:00, 22598.62it/s] 100%|██████████████████████████████████████| 25/25 [00:00<00:00, 46561.99it/s] 100%|██████████████████████████████████████| 25/25 [00:00<00:00, 42956.82it/s] 100%|██████████████████████████████████████| 25/25 [00:00<00:00, 28934.22it/s]
We can see the effect by asking for the neighbors of the central cell several times in a row:
for _ in range(10):
print("Neighbors of central cell: ", stoch_nb.neighbors_of(central_id))
Neighbors of central cell: [11] Neighbors of central cell: [13] Neighbors of central cell: [7] Neighbors of central cell: [13] Neighbors of central cell: [11] Neighbors of central cell: [17] Neighbors of central cell: [13] Neighbors of central cell: [11] Neighbors of central cell: [7] Neighbors of central cell: [13]
Class Based Neighborhoods¶
We can also define neighborhoods where each site has a neighborhood specific to its class. We do this by mapping class names to neighborhood builders and using the SiteClassNeighborhoodBuilder
from pylattica.core.neighborhood_builders import SiteClassNeighborhoodBuilder
structure_motif = {
"A": [(0.25, 0.25)],
"B": [(0.75, 0.75)],
}
class_struct = PeriodicStructure.build_from(lattice, (3,3), structure_motif)
A_nbhood = MotifNeighborhoodBuilder([(0, 1)])
B_nbhood = MotifNeighborhoodBuilder([(1, 0)])
class_nbbuilder = SiteClassNeighborhoodBuilder({"A": A_nbhood, "B": B_nbhood})
class_nb = class_nbbuilder.get(class_struct)
100%|████████████████████████████████████████| 9/9 [00:00<00:00, 35444.82it/s] 100%|████████████████████████████████████████| 9/9 [00:00<00:00, 41665.27it/s]
Having built this neighborhood, we can inspect the neighbors of an A site and the neighbors of a B site.
a_site_id = class_struct.id_at((0.25, 0.25))
b_site_id = class_struct.id_at((1.75, 1.75))
a_neighbs = class_nb.neighbors_of(a_site_id)
print("A's location:", class_struct.site_location(a_site_id), "and A neighbors:", a_neighbs, "and it's location", class_struct.site_location(a_neighbs[0]))
b_neighbs = class_nb.neighbors_of(b_site_id)
print("B's location:", class_struct.site_location(b_site_id), "and B neighbors:", b_neighbs, "and it's location", class_struct.site_location(b_neighbs[0]))
A's location: (0.25, 0.25) and A neighbors: [2] and it's location (0.25, 1.25) B's location: (1.75, 1.75) and B neighbors: [15] and it's location (2.75, 1.75)
Periodic Boundary Conditions and Neighborhoods¶
Neighborhoods adhere to periodic boundary conditions as well. Below, we will illustrate this using a Von Neumann neighborhood.
lattice_vecs = [
[1, 0],
[0, 1]
]
motif = [[0.5, 0.5]]
von_neumann_nb_builder = DistanceNeighborhoodBuilder(1.01)
full_periodic_lattice = Lattice(lattice_vecs, True)
full_periodic_struct = PeriodicStructure.build_from(full_periodic_lattice, (3,3), motif)
full_periodic_nbhood = von_neumann_nb_builder.get(full_periodic_struct)
partial_periodic_lattice = Lattice(lattice_vecs, (False, True))
partial_periodic_struct = PeriodicStructure.build_from(partial_periodic_lattice, (3,3), motif)
partial_periodic_nbhood = von_neumann_nb_builder.get(partial_periodic_struct)
non_periodic_lattice = Lattice(lattice_vecs, False)
non_periodic_struct = PeriodicStructure.build_from(non_periodic_lattice, (3,3), motif)
non_periodic_nbhood = von_neumann_nb_builder.get(non_periodic_struct)
100%|█████████████████████████████████████████| 9/9 [00:00<00:00, 4706.82it/s] 100%|█████████████████████████████████████████| 9/9 [00:00<00:00, 4641.43it/s] 100%|█████████████████████████████████████████| 9/9 [00:00<00:00, 5596.55it/s]
We will illustrate the behavior of this system by looking at corner and edge sites.
Full Periodicity¶
We expect the neighborhoods of edge and corner sites in the fully periodic system to look like this:
To see that, lets get references to the edge site (coordinates [0.5, 1.5]) and the corner site (coordinates [0.5, 2.5]) and look at the number of their neighbors and the coordinates of their neighbors.
edge_coords = (0.5, 1.5)
corner_coords = (0.5, 2.5)
edge_id = full_periodic_struct.id_at(edge_coords)
edge_nbs = full_periodic_nbhood.neighbors_of(edge_id)
print(f"The edge site {edge_coords} has", len(edge_nbs), "neighbors in the fully periodic system")
print("Their coordinates are:", [full_periodic_struct.site_location(nb_id) for nb_id in edge_nbs])
corner_id = full_periodic_struct.id_at(corner_coords)
corner_nbs = full_periodic_nbhood.neighbors_of(corner_id)
print()
print(f"The corner {corner_coords} site has", len(corner_nbs), "neighbors in the fully periodic system")
print("Their coordinates are:", [full_periodic_struct.site_location(nb_id) for nb_id in corner_nbs])
The edge site (0.5, 1.5) has 4 neighbors in the fully periodic system Their coordinates are: [(2.5, 1.5), (1.5, 1.5), (0.5, 2.5), (0.5, 0.5)] The corner (0.5, 2.5) site has 4 neighbors in the fully periodic system Their coordinates are: [(0.5, 0.5), (1.5, 2.5), (0.5, 1.5), (2.5, 2.5)]
Partial Periodicity¶
In the partial periodic example, we expect these neighborhoods to look like this:
Note that the neighborhood has not "rolled over" into the right side of the simulation box. Let's see the code for this.
edge_id = partial_periodic_struct.id_at(edge_coords)
edge_nbs = partial_periodic_nbhood.neighbors_of(edge_id)
print(f"The edge site {edge_coords} has", len(edge_nbs), "neighbors in the partially periodic system")
print("Their coordinates are:", [partial_periodic_struct.site_location(nb_id) for nb_id in edge_nbs])
corner_id = partial_periodic_struct.id_at(corner_coords)
corner_nbs = partial_periodic_nbhood.neighbors_of(corner_id)
print()
print(f"The corner {corner_coords} site has", len(corner_nbs), "neighbors in the partially periodic system")
print("Their coordinates are:", [partial_periodic_struct.site_location(nb_id) for nb_id in corner_nbs])
The edge site (0.5, 1.5) has 3 neighbors in the partially periodic system Their coordinates are: [(0.5, 2.5), (0.5, 0.5), (1.5, 1.5)] The corner (0.5, 2.5) site has 3 neighbors in the partially periodic system Their coordinates are: [(1.5, 2.5), (0.5, 0.5), (0.5, 1.5)]
Non-periodic System¶
In the system without any periodicity, we expect neighborhoods for these sites to look like so:
Note that now the neighborhood does not roll over in either direction.
edge_id = non_periodic_struct.id_at(edge_coords)
edge_nbs = non_periodic_nbhood.neighbors_of(edge_id)
print(f"The edge site {edge_coords} has", len(edge_nbs), "neighbors in the non-periodic system")
print("Their coordinates are:", [non_periodic_struct.site_location(nb_id) for nb_id in edge_nbs])
corner_id = non_periodic_struct.id_at(corner_coords)
corner_nbs = non_periodic_nbhood.neighbors_of(corner_id)
print()
print(f"The corner {corner_coords} site has", len(corner_nbs), "neighbors in the non-periodic system")
print("Their coordinates are:", [non_periodic_struct.site_location(nb_id) for nb_id in corner_nbs])
The edge site (0.5, 1.5) has 3 neighbors in the non-periodic system Their coordinates are: [(0.5, 2.5), (1.5, 1.5), (0.5, 0.5)] The corner (0.5, 2.5) site has 2 neighbors in the non-periodic system Their coordinates are: [(0.5, 1.5), (1.5, 2.5)]