# Copyright (c) 2022, mushroomfire in Beijing Institute of Technology
# This file is from the mdapy project, released under the BSD 3-Clause License.
import taichi as ti
import numpy as np
try:
from tool_function import _check_repeat_cutoff
from replicate import Replicate
from neighbor import Neighbor
except Exception:
from .tool_function import _check_repeat_cutoff
from .replicate import Replicate
from .neighbor import Neighbor
[docs]
@ti.data_oriented
class Calculator:
"""This class is used to calculate the atomic energy and force based on the given embedded atom method
EAM potential. Multi-elements alloy is also supported.
Args:
potential (mp.EAM): A EAM class.
pos (np.ndarray): (:math:`N_p, 3`) particles positions.
boundary (list): boundary conditions, 1 is periodic and 0 is free boundary. Such as [1, 1, 1].
box (np.ndarray): (:math:`3, 2`) or (:math:`4, 3`) system box.
elements_list (list): elements need to be calculated. Such as ['Al', 'Fe'].
init_type_list (np.ndarray): (:math:`N_p`) per atom type.
verlet_list (np.ndarray, optional): (:math:`N_p, max\_neigh`) verlet_list[i, j] means j atom is a neighbor of i atom if j > -1. Defaults to None.
distance_list (np.ndarray, optional): (:math:`N_p, max\_neigh`) distance_list[i, j] means distance between i and j atom. Defaults to None.
neighbor_number (np.ndarray, optional): (:math:`N_p`) neighbor atoms number. Defaults to None.
Outputs:
- **energy** (np.ndarray) - (:math:`N_p`) atomic energy (eV).
- **force** (np.ndarray) - (:math:`N_p, 3`) atomic force (eV/A).
Examples:
>>> import mdapy as mp
>>> mp.init()
>>> potential = mp.EAM("./example/CoNiFeAlCu.eam.alloy") # Read a eam.alloy file.
>>> FCC = mp.LatticeMaker(3.615, 'FCC', 10, 10, 10) # Create a FCC structure
>>> FCC.compute() # Get atom positions
>>> neigh = mp.Neighbor(FCC.pos, FCC.box,
potential.rc, max_neigh=100) # Initialize Neighbor class.
>>> neigh.compute() # Calculate particle neighbor information.
>>> Cal = mp.Calculator(
potential,
FCC.pos,
[1, 1, 1],
FCC.box,
["Al"],
np.ones(FCC.pos.shape[0], dtype=np.int32),
neigh.verlet_list,
neigh.distance_list,
neigh.neighbor_number,
) # Initialize Calculator class.
>>> Cal.compute() # Calculate the atomic energy and force.
>>> Cal.energy # Check the energy.
>>> Cal.force # Check the force.
"""
def __init__(
self,
potential,
pos,
boundary,
box,
elements_list,
init_type_list,
verlet_list=None,
distance_list=None,
neighbor_number=None,
):
self.potential = potential
self.rc = self.potential.rc
repeat = _check_repeat_cutoff(box, boundary, self.rc, 5)
if pos.dtype != np.float64:
pos = pos.astype(np.float64)
if box.dtype != np.float64:
box = box.astype(np.float64)
self.old_N = None
if sum(repeat) == 3:
self.pos = pos
if box.shape == (3, 2):
self.box = np.zeros((4, 3), dtype=box.dtype)
self.box[0, 0], self.box[1, 1], self.box[2, 2] = box[:, 1] - box[:, 0]
self.box[-1] = box[:, 0]
elif box.shape == (4, 3):
self.box = box
self.init_type_list = init_type_list
else:
self.old_N = pos.shape[0]
repli = Replicate(pos, box, *repeat, type_list=init_type_list)
repli.compute()
self.pos = repli.pos
self.box = repli.box
self.init_type_list = repli.type_list
assert self.box[0, 1] == 0
assert self.box[0, 2] == 0
assert self.box[1, 2] == 0
self.box_length = ti.Vector([np.linalg.norm(self.box[i]) for i in range(3)])
self.rec = True
if self.box[1, 0] != 0 or self.box[2, 0] != 0 or self.box[2, 1] != 0:
self.rec = False
self.boundary = ti.Vector([int(boundary[i]) for i in range(3)])
self.elements_list = elements_list
self.verlet_list = verlet_list
self.distance_list = distance_list
self.neighbor_number = neighbor_number
def _get_type_list(self):
assert len(self.elements_list) == len(
np.unique(self.init_type_list)
), "All type must be assigned."
for i in self.elements_list:
assert (
i in self.potential.elements_list
), f"Input element {i} not included in potential."
init_to_now = np.array(
[self.potential.elements_list.index(i) for i in self.elements_list]
)
N = self.pos.shape[0]
type_list = np.zeros(N, dtype=int)
@ti.kernel
def _get_type_list_real(
type_list: ti.types.ndarray(),
N: int,
init_to_now: ti.types.ndarray(),
init_type_list: ti.types.ndarray(),
):
for i in range(N):
type_list[i] = init_to_now[init_type_list[i] - 1] + 1
_get_type_list_real(type_list, N, init_to_now, self.init_type_list)
return type_list
@ti.func
def _pbc_rec(self, rij):
for m in ti.static(range(3)):
if self.boundary[m]:
dx = rij[m]
x_size = self.box_length[m]
h_x_size = x_size * 0.5
if dx > h_x_size:
dx = dx - x_size
if dx <= -h_x_size:
dx = dx + x_size
rij[m] = dx
return rij
@ti.func
def _pbc(self, rij, box: ti.types.ndarray(dtype=ti.math.vec3)) -> ti.math.vec3:
nz = rij[2] / box[2][2]
ny = (rij[1] - nz * box[2][1]) / box[1][1]
nx = (rij[0] - ny * box[1][0] - nz * box[2][0]) / box[0][0]
n = ti.Vector([nx, ny, nz])
for i in ti.static(range(3)):
if self.boundary[i] == 1:
if n[i] > 0.5:
n[i] -= 1
elif n[i] < -0.5:
n[i] += 1
return n[0] * box[0] + n[1] * box[1] + n[2] * box[2]
@ti.kernel
def _compute_energy_force(
self,
box: ti.types.ndarray(dtype=ti.math.vec3),
verlet_list: ti.types.ndarray(),
distance_list: ti.types.ndarray(),
neighbor_number: ti.types.ndarray(),
atype_list: ti.types.ndarray(),
dr: float,
drho: float,
nr: int,
nrho: int,
embedded_data: ti.types.ndarray(),
phi_data: ti.types.ndarray(),
elec_density_data: ti.types.ndarray(),
d_embedded_data: ti.types.ndarray(),
d_phi_data: ti.types.ndarray(),
d_elec_density_data: ti.types.ndarray(),
pos: ti.types.ndarray(dtype=ti.math.vec3),
energy: ti.types.ndarray(),
force: ti.types.ndarray(dtype=ti.math.vec3),
elec_density: ti.types.ndarray(),
d_embedded_rho: ti.types.ndarray(),
):
N = verlet_list.shape[0]
for i in range(N):
i_type = atype_list[i] - 1
for jj in range(neighbor_number[i]):
j = verlet_list[i, jj]
if j > i:
j_type = atype_list[j] - 1
r = distance_list[i, jj]
if r <= self.rc:
rk = r / dr
k = int(rk)
quanzhong = rk - k
if k > nr - 2:
k = nr - 2
pair_enegy = (
quanzhong * phi_data[i_type, j_type, k + 1]
+ (1 - quanzhong) * phi_data[i_type, j_type, k]
)
energy[i] += pair_enegy / 2.0
energy[j] += pair_enegy / 2.0
elec_density[i] += (
quanzhong * elec_density_data[j_type, k + 1]
+ (1 - quanzhong) * elec_density_data[j_type, k]
)
elec_density[j] += (
quanzhong * elec_density_data[i_type, k + 1]
+ (1 - quanzhong) * elec_density_data[i_type, k]
)
for i in range(N):
i_type = atype_list[i] - 1
rk = elec_density[i] / drho
k = int(rk)
quanzhong = rk - k
if k > nrho - 2:
k = nrho - 2
energy[i] += (
quanzhong * embedded_data[i_type, k + 1]
+ (1 - quanzhong) * embedded_data[i_type, k]
)
d_embedded_rho[i] = (
quanzhong * d_embedded_data[i_type, k + 1]
+ (1 - quanzhong) * d_embedded_data[i_type, k]
)
for i in range(N):
i_type = atype_list[i] - 1
for jj in range(neighbor_number[i]):
j = verlet_list[i, jj]
if j > i:
rij = pos[i] - pos[j]
if ti.static(self.rec):
rij = self._pbc_rec(rij)
else:
rij = self._pbc(rij, box)
j_type = atype_list[j] - 1
r = distance_list[i, jj]
if r <= self.rc:
rk = r / dr
k = int(rk)
quanzhong = rk - k
if k > nr - 2:
k = nr - 2
d_pair = (
quanzhong * d_phi_data[i_type, j_type, k + 1]
+ (1 - quanzhong) * d_phi_data[i_type, j_type, k]
)
d_elec_density_i = (
quanzhong * d_elec_density_data[j_type, k + 1]
+ (1 - quanzhong) * d_elec_density_data[j_type, k]
)
d_elec_density_j = (
quanzhong * d_elec_density_data[i_type, k + 1]
+ (1 - quanzhong) * d_elec_density_data[i_type, k]
)
d_pair += (
d_embedded_rho[i] * d_elec_density_i
+ d_embedded_rho[j] * d_elec_density_j
)
force[i] -= d_pair * rij / r
force[j] += d_pair * rij / r
[docs]
def compute(self):
"""Do the real energy and force calculation."""
N = self.pos.shape[0]
self.energy = np.zeros(N)
self.force = np.zeros((N, 3))
elec_density = np.zeros(N)
d_embedded_rho = np.zeros(N)
type_list = self._get_type_list()
if (
self.distance_list is None
or self.verlet_list is None
or self.neighbor_number is None
):
neigh = Neighbor(self.pos, self.box, self.rc, self.boundary)
neigh.compute()
self.distance_list, self.verlet_list, self.neighbor_number = (
neigh.distance_list,
neigh.verlet_list,
neigh.neighbor_number,
)
self._compute_energy_force(
self.box,
self.verlet_list,
self.distance_list,
self.neighbor_number,
type_list,
self.potential.dr,
self.potential.drho,
self.potential.nr,
self.potential.nrho,
self.potential.embedded_data,
self.potential.phi_data,
self.potential.elec_density_data,
self.potential.d_embedded_data,
self.potential.d_phi_data,
self.potential.d_elec_density_data,
self.pos,
self.energy,
self.force,
elec_density,
d_embedded_rho,
)
if self.old_N is not None:
self.energy = np.ascontiguousarray(self.energy[: self.old_N])
self.force = np.ascontiguousarray(self.force[: self.old_N])
if __name__ == "__main__":
from lattice_maker import LatticeMaker
from neighbor import Neighbor
from potential import EAM
from time import time
# ti.init(ti.gpu, device_memory_GB=5.0)
ti.init(ti.cpu)
start = time()
lattice_constant = 4.05
x, y, z = 1, 1, 1
FCC = LatticeMaker(lattice_constant, "FCC", x, y, z)
FCC.compute()
end = time()
print(f"Build {FCC.pos.shape[0]} atoms FCC time: {end-start} s.")
potential = EAM("./example/Al_DFT.eam.alloy")
print(4.05 * 8 / potential.rc)
# start = time()
# neigh = Neighbor(FCC.pos, FCC.box, potential.rc, max_neigh=100)
# neigh.compute()
# end = time()
# print(f"Build neighbor time: {end-start} s.")
start = time()
Cal = Calculator(
potential,
FCC.pos * 0.9,
[1, 1, 1],
FCC.box * 0.9,
["Al"],
np.ones(FCC.pos.shape[0], dtype=np.int32),
)
# neigh.verlet_list,
# neigh.distance_list,
# neigh.neighbor_number,
Cal.compute()
end = time()
print(f"Calculate energy and force time: {end-start} s.")
print("energy:")
print(Cal.energy[:4])
print("force:")
print(Cal.force[:4, :])