Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial periodic boundary conditions #2429

Merged
merged 7 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 61 additions & 24 deletions pymatgen/core/lattice.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Lattice(MSONable):

# Properties lazily generated for efficiency.

def __init__(self, matrix: ArrayLike):
def __init__(self, matrix: ArrayLike, pbc: tuple[bool, bool, bool] | None = None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a need to use None here? We can just default to (True, True, True)? This is a tuple, which is not mutable. It is allowed as an arg.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I think I did it because it was easier to avoid issues with some tests failing during the first draft. Then I didn't think of changing it. It should be fixed now

"""
Create a lattice from any sequence of 9 numbers. Note that the sequence
is assumed to be read one row at a time. Each row represents one
Expand All @@ -56,6 +56,8 @@ def __init__(self, matrix: ArrayLike):
Each row should correspond to a lattice vector.
E.g., [[10, 0, 0], [20, 10, 0], [0, 0, 30]] specifies a lattice
with lattice vectors [10, 0, 0], [20, 10, 0] and [0, 0, 30].
pbc: a tuple defining the periodic boundary conditions along the three
axis of the lattice. If None periodic in all directions.
"""
m = np.array(matrix, dtype=np.float64).reshape((3, 3))
m.setflags(write=False)
Expand All @@ -64,6 +66,11 @@ def __init__(self, matrix: ArrayLike):
self._diags = None
self._lll_matrix_mappings = {} # type: Dict[float, Tuple[np.ndarray, np.ndarray]]
self._lll_inverse = None
if pbc is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed.

pbc = (True, True, True)
else:
pbc = tuple(pbc) # type: ignore
self._pbc = pbc

@property
def lengths(self) -> tuple[float, float, float]:
Expand Down Expand Up @@ -122,13 +129,23 @@ def __format__(self, fmt_spec=""):

def copy(self):
"""Deep copy of self."""
return self.__class__(self.matrix.copy())
return self.__class__(self.matrix.copy(), pbc=self.pbc)

@property
def matrix(self) -> np.ndarray:
"""Copy of matrix representing the Lattice"""
return self._matrix

@property
def pbc(self) -> tuple[bool, bool, bool]:
"""Tuple defining the periodicity of the Lattice"""
return self._pbc

@property
def is_3d_periodic(self) -> bool:
"""True if the Lattice is periodic in all directions"""
return all(self._pbc)

@property
def inv_matrix(self) -> np.ndarray:
"""
Expand Down Expand Up @@ -206,49 +223,55 @@ def d_hkl(self, miller_index: ArrayLike) -> float:
return 1 / ((dot(dot(hkl, gstar), hkl.T)) ** (1 / 2))

@staticmethod
def cubic(a: float) -> Lattice:
def cubic(a: float, pbc: tuple[bool, bool, bool] | None = None) -> Lattice:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as the __init__.

"""
Convenience constructor for a cubic lattice.

Args:
a (float): The *a* lattice parameter of the cubic cell.
pbc (tuple): a tuple defining the periodic boundary conditions along the three
axis of the lattice. If None periodic in all directions.

Returns:
Cubic lattice of dimensions a x a x a.
"""
return Lattice([[a, 0.0, 0.0], [0.0, a, 0.0], [0.0, 0.0, a]])
return Lattice([[a, 0.0, 0.0], [0.0, a, 0.0], [0.0, 0.0, a]], pbc)

@staticmethod
def tetragonal(a: float, c: float) -> Lattice:
def tetragonal(a: float, c: float, pbc: tuple[bool, bool, bool] | None = None) -> Lattice:
"""
Convenience constructor for a tetragonal lattice.

Args:
a (float): *a* lattice parameter of the tetragonal cell.
c (float): *c* lattice parameter of the tetragonal cell.
pbc (tuple): a tuple defining the periodic boundary conditions along the three
axis of the lattice. If None periodic in all directions.

Returns:
Tetragonal lattice of dimensions a x a x c.
"""
return Lattice.from_parameters(a, a, c, 90, 90, 90)
return Lattice.from_parameters(a, a, c, 90, 90, 90, pbc=pbc)

@staticmethod
def orthorhombic(a: float, b: float, c: float) -> Lattice:
def orthorhombic(a: float, b: float, c: float, pbc: tuple[bool, bool, bool] | None = None) -> Lattice:
"""
Convenience constructor for an orthorhombic lattice.

Args:
a (float): *a* lattice parameter of the orthorhombic cell.
b (float): *b* lattice parameter of the orthorhombic cell.
c (float): *c* lattice parameter of the orthorhombic cell.
pbc (tuple): a tuple defining the periodic boundary conditions along the three
axis of the lattice. If None periodic in all directions.

Returns:
Orthorhombic lattice of dimensions a x b x c.
"""
return Lattice.from_parameters(a, b, c, 90, 90, 90)
return Lattice.from_parameters(a, b, c, 90, 90, 90, pbc=pbc)

@staticmethod
def monoclinic(a: float, b: float, c: float, beta: float) -> Lattice:
def monoclinic(a: float, b: float, c: float, beta: float, pbc: tuple[bool, bool, bool] | None = None) -> Lattice:
"""
Convenience constructor for a monoclinic lattice.

Expand All @@ -258,40 +281,46 @@ def monoclinic(a: float, b: float, c: float, beta: float) -> Lattice:
c (float): *c* lattice parameter of the monoclinc cell.
beta (float): *beta* angle between lattice vectors b and c in
degrees.
pbc (tuple): a tuple defining the periodic boundary conditions along the three
axis of the lattice. If None periodic in all directions.

Returns:
Monoclinic lattice of dimensions a x b x c with non right-angle
beta between lattice vectors a and c.
"""
return Lattice.from_parameters(a, b, c, 90, beta, 90)
return Lattice.from_parameters(a, b, c, 90, beta, 90, pbc=pbc)

@staticmethod
def hexagonal(a: float, c: float) -> Lattice:
def hexagonal(a: float, c: float, pbc: tuple[bool, bool, bool] | None = None) -> Lattice:
"""
Convenience constructor for a hexagonal lattice.

Args:
a (float): *a* lattice parameter of the hexagonal cell.
c (float): *c* lattice parameter of the hexagonal cell.
pbc (tuple): a tuple defining the periodic boundary conditions along the three
axis of the lattice. If None periodic in all directions.

Returns:
Hexagonal lattice of dimensions a x a x c.
"""
return Lattice.from_parameters(a, a, c, 90, 90, 120)
return Lattice.from_parameters(a, a, c, 90, 90, 120, pbc=pbc)

@staticmethod
def rhombohedral(a: float, alpha: float) -> Lattice:
def rhombohedral(a: float, alpha: float, pbc: tuple[bool, bool, bool] | None = None) -> Lattice:
"""
Convenience constructor for a rhombohedral lattice.

Args:
a (float): *a* lattice parameter of the rhombohedral cell.
alpha (float): Angle for the rhombohedral lattice in degrees.
pbc (tuple): a tuple defining the periodic boundary conditions along the three
axis of the lattice. If None periodic in all directions.

Returns:
Rhombohedral lattice of dimensions a x a x a.
"""
return Lattice.from_parameters(a, a, a, alpha, alpha, alpha)
return Lattice.from_parameters(a, a, a, alpha, alpha, alpha, pbc=pbc)

@classmethod
def from_parameters(
Expand All @@ -303,6 +332,7 @@ def from_parameters(
beta: float,
gamma: float,
vesta: bool = False,
pbc: tuple[bool, bool, bool] | None = None,
):
"""
Create a Lattice using unit cell lengths and angles (in degrees).
Expand All @@ -315,6 +345,8 @@ def from_parameters(
beta (float): *beta* angle in degrees.
gamma (float): *gamma* angle in degrees.
vesta: True if you import Cartesian coordinates from VESTA.
pbc (tuple): a tuple defining the periodic boundary conditions along the three
axis of the lattice. If None periodic in all directions.

Returns:
Lattice with the specified lattice parameters.
Expand Down Expand Up @@ -346,7 +378,7 @@ def from_parameters(
]
vector_c = [0.0, 0.0, float(c)]

return Lattice([vector_a, vector_b, vector_c])
return Lattice([vector_a, vector_b, vector_c], pbc)

@classmethod
def from_dict(cls, d: dict, fmt: str = None, **kwargs):
Expand All @@ -370,8 +402,8 @@ def from_dict(cls, d: dict, fmt: str = None, **kwargs):
return lattice_from_abivars(cls=cls, **kwargs)

if "matrix" in d:
return cls(d["matrix"])
return cls.from_parameters(d["a"], d["b"], d["c"], d["alpha"], d["beta"], d["gamma"])
return cls(d["matrix"], d.get("pbc"))
return cls.from_parameters(d["a"], d["b"], d["c"], d["alpha"], d["beta"], d["gamma"], pbc=d.get("pbc"))

@property
def a(self) -> float:
Expand Down Expand Up @@ -908,19 +940,21 @@ def __repr__(self):
" A : " + " ".join(map(repr, self._matrix[0])),
" B : " + " ".join(map(repr, self._matrix[1])),
" C : " + " ".join(map(repr, self._matrix[2])),
" pbc : " + " ".join(map(repr, self._pbc)),
]
return "\n".join(outs)

def __eq__(self, other):
"""
A lattice is considered to be equal to another if the internal matrix
representation satisfies np.allclose(matrix1, matrix2) to be True.
representation satisfies np.allclose(matrix1, matrix2) to be True and
share the same periodicity.
"""
if other is None:
return False
# shortcut the np.allclose if the memory addresses are the same
# (very common in Structure.from_sites)
return self is other or np.allclose(self.matrix, other.matrix)
return self is other or (np.allclose(self.matrix, other.matrix) and self.pbc == other.pbc)

def __ne__(self, other):
return not self.__eq__(other)
Expand All @@ -944,6 +978,7 @@ def as_dict(self, verbosity: int = 0) -> dict:
"@module": type(self).__module__,
"@class": type(self).__name__,
"matrix": self._matrix.tolist(),
"pbc": self._pbc,
}
a, b, c, alpha, beta, gamma = self.parameters
if verbosity > 0:
Expand Down Expand Up @@ -1326,7 +1361,7 @@ def scale(self, new_volume: float) -> Lattice:

new_c = (new_volume / (geo_factor * np.prod(ratios))) ** (1 / 3.0)

return Lattice(versors * (new_c * ratios))
return Lattice(versors * (new_c * ratios), pbc=self.pbc)

def get_wigner_seitz_cell(self) -> list[list[np.ndarray]]:
"""
Expand Down Expand Up @@ -1469,7 +1504,7 @@ def get_points_in_sphere(
all_coords=cart_coords,
center_coords=np.ascontiguousarray([center], dtype=float),
r=r,
pbc=np.array([1, 1, 1]),
pbc=np.array(self.pbc, dtype=int),
lattice=lattice_matrix,
tol=1e-8,
)
Expand Down Expand Up @@ -1535,7 +1570,7 @@ def get_points_in_sphere_py(
all_coords=cart_coords,
center_coords=np.array([center]),
r=r,
pbc=True,
pbc=self.pbc,
numerical_tol=1e-8,
lattice=self,
return_fcoords=True,
Expand All @@ -1560,7 +1595,7 @@ def get_points_in_sphere_old(
"""
Find all points within a sphere from the point taking into account
periodic boundary conditions. This includes sites in other periodic
images.
images. Does not support partial periodic boundary conditions.

Algorithm:

Expand Down Expand Up @@ -1588,6 +1623,8 @@ def get_points_in_sphere_old(
else:
fcoords, dists, inds, image
"""
if self.pbc != (True, True, True):
raise RuntimeError("get_points_in_sphere_old does not support partial periodic boundary conditions")
# TODO: refactor to use lll matrix (nmax will be smaller)
# Determine the maximum number of supercells in each direction
# required to contain a sphere of radius n
Expand Down Expand Up @@ -1856,7 +1893,7 @@ def get_points_in_spheres(
all_coords: np.ndarray,
center_coords: np.ndarray,
r: float,
pbc: bool | list[bool] = True,
pbc: bool | list[bool] | tuple[bool, bool, bool] = True,
numerical_tol: float = 1e-8,
lattice: Lattice = None,
return_fcoords: bool = False,
Expand Down
8 changes: 4 additions & 4 deletions pymatgen/core/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ def __init__(
frac_coords = coords # type: ignore

if to_unit_cell:
frac_coords = np.mod(frac_coords, 1)
frac_coords = [np.mod(f, 1) if p else f for p, f in zip(lattice.pbc, frac_coords)]

if not skip_checks:
frac_coords = np.array(frac_coords)
Expand Down Expand Up @@ -485,9 +485,9 @@ def to_unit_cell(self, in_place=False) -> PeriodicSite | None:
"""
Move frac coords to within the unit cell cell.
"""
frac_coords = np.mod(self.frac_coords, 1)
frac_coords = [np.mod(f, 1) if p else f for p, f in zip(self.lattice.pbc, self.frac_coords)]
if in_place:
self.frac_coords = frac_coords
self.frac_coords = np.array(frac_coords)
return None
return PeriodicSite(self.species, frac_coords, self.lattice, properties=self.properties)

Expand All @@ -509,7 +509,7 @@ def is_periodic_image(self, other: PeriodicSite, tolerance: float = 1e-8, check_
if self.species != other.species:
return False

frac_diff = pbc_diff(self.frac_coords, other.frac_coords)
frac_diff = pbc_diff(self.frac_coords, other.frac_coords, self.lattice.pbc)
return np.allclose(frac_diff, [0, 0, 0], atol=tolerance)

def __eq__(self, other):
Expand Down
21 changes: 17 additions & 4 deletions pymatgen/core/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,18 @@ def density(self) -> float:
m = Mass(self.composition.weight, "amu")
return m.to("g") / (self.volume * Length(1, "ang").to("cm") ** 3)

@property
def pbc(self) -> tuple[bool, bool, bool]:
"""
Returns the periodicity of the structure.
"""
return self._lattice.pbc

@property
def is_3d_periodic(self) -> bool:
"""True if the Lattice is periodic in all directions."""
return self._lattice.is_3d_periodic

def get_space_group_info(self, symprec=1e-2, angle_tolerance=5.0) -> tuple[str, int]:
"""
Convenience method to quickly get the spacegroup of a structure.
Expand Down Expand Up @@ -1342,7 +1354,7 @@ def get_neighbor_list(
cart_coords,
site_coords,
r=r,
pbc=np.array([1, 1, 1], dtype=int),
pbc=np.array(self.pbc, dtype=int),
lattice=lattice_matrix,
tol=numerical_tol,
)
Expand Down Expand Up @@ -1661,7 +1673,7 @@ def get_all_neighbors_py(
self.cart_coords,
site_coords,
r=r,
pbc=True,
pbc=self.pbc,
numerical_tol=numerical_tol,
lattice=self.lattice,
)
Expand Down Expand Up @@ -1974,7 +1986,7 @@ def interpolate(

vec = end_coords - start_coords
if pbc:
vec -= np.round(vec)
vec[:, self.pbc] -= np.round(vec[:, self.pbc])
sp = self.species_and_occu
structs = []

Expand Down Expand Up @@ -2251,6 +2263,7 @@ def to_s(x):

outs.append("abc : " + " ".join([to_s(i).rjust(10) for i in self.lattice.abc]))
outs.append("angles: " + " ".join([to_s(i).rjust(10) for i in self.lattice.angles]))
outs.append("pbc : " + " ".join([str(p).rjust(10) for p in self.lattice.pbc]))
if self._charge:
if self._charge >= 0:
outs.append(f"Overall Charge: +{self._charge}")
Expand Down Expand Up @@ -3799,7 +3812,7 @@ def translate_sites(
else:
fcoords = self._lattice.get_fractional_coords(site.coords + vector)
if to_unit_cell:
fcoords = np.mod(fcoords, 1)
fcoords = [np.mod(f, 1) if p else f for p, f in zip(self.lattice.pbc, fcoords)]
self._sites[i].frac_coords = fcoords

def rotate_sites(
Expand Down