# Copyright (c) 2022-2024, 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 AtomicTemperature:
"""This class is used to calculated an average thermal temperature per atom, wchich is useful at shock
simulations. The temperature of atom :math:`i` is given by:
.. math:: T_i=\\sum^{N^i_{neigh}}_0 m^i_j(v_j^i -v_{COM}^i)^2/(3N^i_{neigh}k_B),
where :math:`N^i_{neigh}` is neighbor atoms number of atom :math:`i`,
:math:`m^i_j` and :math:`v^i_j` are the atomic mass and velocity of neighbor atom :math:`j` of atom :math:`i`,
:math:`k_B` is the Boltzmann constant, :math:`v^i_{COM}` is
the center of mass COM velocity of neighbor of atom :math:`i` and is given by:
.. math:: v^i_{COM}=\\frac{\\sum _0^{N^i_{neigh}}m^i_jv_j^i}{\\sum_0^{N^i_{neigh}} m_j^i}.
Here the neighbor of atom :math:`i` includes itself.
Args:
amass (np.ndarray): (:math:`N_{type}`) atomic mass.
vel (np.ndarray): (:math:`N_p, 3`), atomic velocity.
atype_list (np.ndarray): (:math:`N_p`), atomic type.
rc (float): cutoff distance to average.
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.
distance_list (np.ndarray, optional): (:math:`N_p, max\_neigh`) distance_list[i, j] means distance between i and j atom.
neighbor_number (np.ndarray, optional): (:math:`N_p`) neighbor atoms number.
pos (np.ndarray, optional): (:math:`N_p, 3`) particles positions. Defaults to None.
box (np.ndarray, optional): (:math:`3, 2`) or (:math:`4, 3`) system box. Defaults to None.
boundary (list, optional): boundary conditions, 1 is periodic and 0 is free boundary. Such as [1, 1, 1]. Defaults to None.
units (str, optional): `units <https://docs.lammps.org/units.html>`_ defined in LAMMPS, supports *metal* and *charge*. Defaults to "metal".
Outputs:
- **T** (np.ndarray) - (:math:`N_p`), atomic temperature.
Examples:
>>> import mdapy as mp
>>> mp.init()
>>> 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,
5., max_neigh=50) # Initialize Neighbor class.
>>> neigh.compute() # Calculate particle neighbor information.
>>> def init_vel(N, T, Mass=1.0):
# Generate random velocity at T K.
Boltzmann_Constant = 8.617385e-5
np.random.seed(10086)
x1 = np.random.random(N * 3)
x2 = np.random.random(N * 3)
vel = (
np.sqrt(T * Boltzmann_Constant / Mass)
* np.sqrt(-2 * np.log(x1))
* np.cos(2 * np.pi * x2)
).reshape(N, 3)
vel -= vel.mean(axis=0)
return vel * 100 # A/ps
>>> vel = init_vel(FCC.N, 300.0, 1.0) # Generate random velocity at 300 K.
>>> Temp = AtomicTemperature(
np.array([1.0]),
vel,
np.ones(FCC.N, dtype=int),
5.0,
neigh.verlet_list,
neigh.distance_list,
) # Initilize the temperature class.
>>> Temp.compute() # Do the temperature calculation.
>>> Temp.T.mean() # Average temperature should be close to 300 K.
"""
def __init__(
self,
amass,
vel,
atype_list,
rc,
verlet_list=None,
distance_list=None,
pos=None,
box=None,
boundary=None,
units="metal",
):
self.amass = amass
self.atype_list = atype_list
self.units = units
if self.units == "metal":
self.vel = vel * 100.0
elif self.units == "real":
self.vel = vel * 100000.0
self.verlet_list = verlet_list
self.distance_list = distance_list
self.rc = rc
self.old_N = None
if verlet_list is None or distance_list is None:
assert pos is not None
assert box is not None
assert boundary is not None
repeat = _check_repeat_cutoff(box, boundary, self.rc)
if pos.dtype != np.float64:
pos = pos.astype(np.float64)
if box.dtype != np.float64:
box = box.astype(np.float64)
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
else:
self.old_N = pos.shape[0]
repli = Replicate(pos, box, *repeat, self.atype_list)
repli.compute()
self.pos = repli.pos
self.box = repli.box
self.atype_list = repli.type_list
self.vel = np.vstack([self.vel for _ in range(np.product(repeat))])
assert self.box[0, 1] == 0
assert self.box[0, 2] == 0
assert self.box[1, 2] == 0
self.boundary = [int(boundary[i]) for i in range(3)]
self.N = self.vel.shape[0]
self.T = np.zeros(self.N)
@ti.kernel
def _compute(
self,
verlet_list: ti.types.ndarray(),
distance_list: ti.types.ndarray(),
vel: ti.types.ndarray(),
amass: ti.types.ndarray(),
atype_list: ti.types.ndarray(),
T: ti.types.ndarray(),
):
"""
kb = 8.617333262145e-5 eV / K
kb = 1.380649e−23 J/K
dim = 3.
afu = 6.022140857e23 1/mol
j2e = 6.24150913e18
"""
kb = 1.380649e-23
dim = 3.0
afu = 6.022140857e23
max_neigh = verlet_list.shape[1]
for i in range(self.N):
# obtain v_COM of neighbor of atom_i
v_neigh = ti.Vector([ti.float64(0.0)] * 3)
n_neigh = 0
mass_neigh = ti.float64(0.0)
for j_index in range(max_neigh):
j = verlet_list[i, j_index]
disj = distance_list[i, j_index]
if j > -1:
if j != i and disj <= self.rc:
j_mass = amass[atype_list[j] - 1]
v_neigh += ti.Vector([vel[j, 0], vel[j, 1], vel[j, 2]]) * j_mass
n_neigh += 1
mass_neigh += j_mass
else:
break
v_neigh += ti.Vector([vel[i, 0], vel[i, 1], vel[i, 2]])
n_neigh += 1
mass_neigh += amass[atype_list[i] - 1]
v_mean = v_neigh / mass_neigh
# subtract v_COM
ke_neigh = ti.float64(0.0)
for j_index in range(max_neigh):
j = verlet_list[i, j_index]
disj = distance_list[i, j_index]
if j > -1:
if j != i and disj <= self.rc:
v_j = (
ti.Vector([vel[j, 0], vel[j, 1], vel[j, 2]]) - v_mean
).norm_sqr()
ke_neigh += 0.5 * amass[atype_list[j] - 1] / afu / 1000.0 * v_j
else:
break
ke_neigh += (
0.5
* amass[atype_list[i] - 1]
/ afu
/ 1000.0
* (ti.Vector([vel[i, 0], vel[i, 1], vel[i, 2]]) - v_mean).norm_sqr()
)
# obtain temperature
T[i] = ke_neigh * 2.0 / dim / n_neigh / kb
[docs]
def compute(self):
"""Do the real temperature calculation."""
if self.verlet_list is None or self.distance_list is None:
neigh = Neighbor(self.pos, self.box, self.rc, self.boundary)
neigh.compute()
self.verlet_list, self.distance_list = (
neigh.verlet_list,
neigh.distance_list,
)
self._compute(
self.verlet_list,
self.distance_list,
self.vel,
self.amass,
self.atype_list,
self.T,
)
if self.old_N is not None:
self.T = np.ascontiguousarray(self.T[: self.old_N])
if __name__ == "__main__":
from tool_function import _init_vel
from lattice_maker import LatticeMaker
from neighbor import Neighbor
from time import time
# ti.init(ti.gpu, device_memory_GB=5.0)
ti.init(ti.cpu)
start = time()
lattice_constant = 3.615
x, y, z = (10, 10, 10)
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.")
# start = time()
# neigh = Neighbor(FCC.pos, FCC.box, 5.0)
# neigh.compute()
# end = time()
# print(f"Build neighbor time: {end-start} s.")
vel = _init_vel(FCC.N, 300.0, 1.0)
start = time()
T = AtomicTemperature(
np.array([1.0]),
vel,
np.ones(FCC.pos.shape[0], dtype=int),
5.0,
None,
None,
FCC.pos,
FCC.box,
[1, 1, 1],
)
T.compute()
end = time()
print(f"Calculating T time: {end-start} s.")
print(T.T)
print("Average temperature is", T.T.mean(), "K.")