"""Utilities for neighbor list calculations."""
import torch
[docs]
def complete_cell(cell: torch.Tensor, pbc: torch.Tensor) -> torch.Tensor:
"""Fill zero-length lattice vectors on non-periodic axes, like ASE's complete_cell.
Slabs, wires and molecules built by ASE (e.g. ``graphene()``, ``nanotube()``,
``molecule()``) leave the vectors of their non-periodic axes at zero. These are
replaced by unit vectors orthogonal to the others, keeping the cell right-handed.
Image shifts along non-periodic axes are always zero, so the filled vectors never
enter interatomic displacements.
Args:
cell: Row-vector cells [n_systems, 3, 3]
pbc: Periodic boundary conditions [n_systems, 3]
Returns:
Cells [n_systems, 3, 3] with every non-periodic zero vector filled in.
"""
missing = (cell == 0).all(dim=-1) & ~pbc.to(torch.bool)
if not missing.any():
return cell
n_missing = missing.sum(dim=-1)[:, None, None]
eye = torch.eye(3, dtype=cell.dtype, device=cell.device)
# One missing vector: the unit normal of the other two
normal = torch.linalg.cross(cell.roll(-1, dims=1), cell.roll(-2, dims=1), dim=-1)
normal = normal / torch.linalg.vector_norm(normal, dim=-1, keepdim=True).clamp_min(
1e-30
)
# Two missing vectors: an orthonormal pair (u, v x u) orthogonal to the present v
present = (cell * (~missing).unsqueeze(-1)).sum(dim=1)
v = present / torch.linalg.vector_norm(present, dim=-1, keepdim=True).clamp_min(1e-30)
helper = torch.where((v[:, 0].abs() > 0.9).unsqueeze(-1), eye[1], eye[0])
u = helper - (helper * v).sum(dim=-1, keepdim=True) * v
u = u / torch.linalg.vector_norm(u, dim=-1, keepdim=True).clamp_min(1e-30)
w = torch.linalg.cross(v, u, dim=-1)
present_idx = (~missing).to(torch.int64).argmax(dim=-1, keepdim=True)
offset = (torch.arange(3, device=cell.device) - present_idx) % 3
pair = torch.where((offset == 1).unsqueeze(-1), u.unsqueeze(1), w.unsqueeze(1))
filled = torch.where(n_missing == 1, normal, pair)
filled = torch.where(n_missing == 3, eye.expand_as(cell), filled)
return torch.where(missing.unsqueeze(-1), filled, cell)