Source code for sarkas.potentials.force_pp

"""
Module for handling Particle-Particle interaction.
"""

from numba import jit
from numba.core.types import float64, int64, Tuple
from numpy import arange, sqrt, zeros, zeros_like


[docs]@jit(nopython=True) def update_0D(pos, vel, p_id, p_mass, box_lengths, rc, potential_matrix, force, measure, rdf_hist): """ Updates particles' accelerations when the cutoff radius :math:`r_c` is half the box's length, :math:`r_c = L/2` For no sub-cell. All ptcls within :math:`r_c = L/2` participate for force calculation. Cost ~ O(N^2) Parameters ---------- force: func Potential and force values. potential_matrix: numpy.ndarray Potential parameters. rc: float Cut-off radius. box_lengths: numpy.ndarray Array of box sides' length. p_mass: numpy.ndarray Mass of each particle. p_id: numpy.ndarray Id of each particle pos: numpy.ndarray Particles' positions. measure : bool Boolean for rdf calculation. rdf_hist : numpy.ndarray Radial Distribution function array. Returns ------- U_s_r : float Short-ranged component of the potential energy of the system. acc_s_r : numpy.ndarray Short-ranged component of the acceleration for the particles. virial_species_tensor : numpy.ndarray Virial term of each particle. \n Shape = (3, 3, pos.shape[0]) """ # L = Lv[0] actual_dimensions = len(box_lengths.nonzero()[0]) Lh = 0.5 * box_lengths N = pos.shape[0] # Number of particles ptcl_pot_energy = zeros(N) # Short-ranges potential energy of each particle acc_s_r = zeros(pos.shape) # Vector of accelerations # Energy current j_e = zeros((N, 3)) # Virial term for the viscosity calculation virial_species_tensor = zeros((3, 3, N)) rdf_nbins = rdf_hist.shape[0] dr_rdf = Lh[:actual_dimensions].prod() ** (1.0 / actual_dimensions) / float(rdf_nbins) for i in range(N): for j in range(i + 1, N): dx = pos[i, 0] - pos[j, 0] dy = pos[i, 1] - pos[j, 1] dz = pos[i, 2] - pos[j, 2] vx = vel[i, 0] + vel[j, 0] vy = vel[i, 1] + vel[j, 1] vz = vel[i, 2] + vel[j, 2] dx2 = box_lengths[0] - dx * (dx >= Lh[0]) + dx * (dx <= -Lh[0]) dy2 = box_lengths[1] - dy * (dy >= Lh[1]) + dy * (dy <= -Lh[1]) dz2 = box_lengths[2] - dz * (dz >= Lh[2]) + dz * (dz <= -Lh[2]) # if dy >= Lh[1]: # dy = Lv[1] - dy # elif dy <= -Lh: # dy = Lv[1] + dy # # if dz >= Lh[2]: # dz = Lv[2] - dz # elif dz <= -Lh: # dz = Lv[2] + dz # Compute distance between particles i and j r = sqrt(dx2 * dx2 + dy2 * dy2 + dz2 * dz2) rdf_bin = int(r / dr_rdf) id_i = p_id[i] id_j = p_id[j] # These definitions are needed due to numba # see https://github.com/numba/numba/issues/5881 if measure and rdf_bin < rdf_nbins: rdf_hist[rdf_bin, id_j, id_j] += 1 if 0.0 < r < rc: mass_i = p_mass[i] mass_j = p_mass[j] p_matrix = potential_matrix[id_i, id_j] # Compute the short-ranged force pot, fr = force(r, p_matrix) fr /= r # Update the acceleration for i particles in each dimension acc_ix = dx2 * fr / mass_i acc_iy = dy2 * fr / mass_i acc_iz = dz2 * fr / mass_i acc_jx = dx2 * fr / mass_j acc_jy = dy2 * fr / mass_j acc_jz = dz2 * fr / mass_j acc_s_r[i, 0] += acc_ix acc_s_r[i, 1] += acc_iy acc_s_r[i, 2] += acc_iz # Apply Newton's 3rd law to update acceleration on j particles acc_s_r[j, 0] -= acc_jx acc_s_r[j, 1] -= acc_jy acc_s_r[j, 2] -= acc_jz # Need to add the same pot to each particle pair. ptcl_pot_energy[i] += 0.5 * pot ptcl_pot_energy[j] += 0.5 * pot # Since we have the info already calculate the virial_species_tensor virial_species_tensor[0, 0, i] += dx2 * dx2 * fr virial_species_tensor[0, 1, i] += dx2 * dy2 * fr virial_species_tensor[0, 2, i] += dx2 * dz2 * fr virial_species_tensor[1, 0, i] += dy2 * dx2 * fr virial_species_tensor[1, 1, i] += dy2 * dy2 * fr virial_species_tensor[1, 2, i] += dy2 * dz2 * fr virial_species_tensor[2, 0, i] += dz2 * dx2 * fr virial_species_tensor[2, 1, i] += dz2 * dy2 * fr virial_species_tensor[2, 2, i] += dz2 * dz2 * fr fij_vij = dx * fr * vx + dy * fr * vy + dz * fr * vz j_e[i, 0] += 0.25 * (vx * pot + dx * fij_vij) j_e[i, 1] += 0.25 * (vy * pot + dy * fij_vij) j_e[i, 2] += 0.25 * (vz * pot + dz * fij_vij) j_e[j, 0] += 0.25 * (vx * pot + dx * fij_vij) j_e[j, 1] += 0.25 * (vy * pot + dy * fij_vij) j_e[j, 2] += 0.25 * (vz * pot + dz * fij_vij) # Add the first term of the energy current for i in range(pos.shape[0]): j_e[i, 0] += (0.5 * p_mass[i] * (vel[i, :] ** 2).sum(axis=-1) + ptcl_pot_energy[i]) * vel[i, 0] j_e[i, 1] += (0.5 * p_mass[i] * (vel[i, :] ** 2).sum(axis=-1) + ptcl_pot_energy[i]) * vel[i, 1] j_e[i, 2] += (0.5 * p_mass[i] * (vel[i, :] ** 2).sum(axis=-1) + ptcl_pot_energy[i]) * vel[i, 2] return ptcl_pot_energy, acc_s_r, virial_species_tensor, j_e
[docs]@jit(nopython=True) def update(pos, vel, p_id, p_mass, box_lengths, rc, potential_matrix, force, measure, rdf_hist): """ Update the force on the particles based on a linked cell-list (LCL) algorithm. Parameters ---------- pos: numpy.ndarray Particles' positions. vel: numpy.ndarray Velocity of each particle. p_id: numpy.ndarray Id of each particle force: func Potential and force values. potential_matrix: numpy.ndarray Potential parameters. rc: float Cut-off radius. box_lengths: numpy.ndarray Array of box sides' length. p_mass: numpy.ndarray Mass of each particle. measure : bool Boolean for rdf calculation. rdf_hist : numpy.ndarray Radial Distribution function array. Returns ------- U_s_r : float Short-ranged component of the potential energy of the system. acc_s_r : numpy.ndarray Short-ranged component of the acceleration for the particles. virial_species_tensor : numpy.ndarray Virial term of each particle. \n Shape = (3, 3, pos.shape[0]) """ # Total number of cells in volume cells_per_dim, cell_lengths = create_cells_array(box_lengths, rc) head, ls_array = create_head_list_arrays(pos, cell_lengths, cells_per_dim) U_s_r, acc_s_r, virial_species_tensor, heat_flux_species_tensor = particles_interaction_loop( pos, vel, p_mass, p_id, potential_matrix, rc, measure, force, rdf_hist, head, ls_array, cells_per_dim, box_lengths ) return U_s_r, acc_s_r, virial_species_tensor, heat_flux_species_tensor
[docs]@jit(nopython=True) def particles_interaction_loop( pos, vel, p_mass, p_id, potential_matrix, rc, measure, force, rdf_hist, head, ls_array, cells_per_dim, box_lengths ): """ Update the force on the particles based on a linked cell-list (LCL) algorithm. Parameters ---------- pos: numpy.ndarray Particles' positions. vel: numpy.ndarray Particles' positions. p_mass: numpy.ndarray Mass of each particle. p_id: numpy.ndarray Id of each particle potential_matrix: numpy.ndarray Potential parameters. rc: float Cut-off radius. measure : bool Boolean for rdf calculation. force: func Potential and force values. rdf_hist : numpy.ndarray Radial Distribution function array. head: numpy.ndarray Head array of the linked cell list algorithm. ls_array: numpy.ndarray List array of the linked cell list algorithm. cells_per_dim: numpy.ndarray Number of cells per dimension. box_lengths: numpy.ndarray Array of box sides' length. Returns ------- ptcl_pot_energy : numpy.ndarray Short-ranged component of the potential energy of each particle. Shape = `tot_num_ptcls`. acc_s_r : numpy.ndarray Short-ranged component of the acceleration for the particles. virial_species_tensor : numpy.ndarray Virial term of each particle. \n Shape = (3, 3, pos.shape[0]) j_e : numpy.ndarray Energy current of each particle. Shape=((3,N) Notes ----- Here the "short-ranged component" refers to the Ewald decomposition of the short and long ranged interactions. See the wikipedia article: https://en.wikipedia.org/wiki/Ewald_summation or "Computer Simulation of Liquids by Allen and Tildesley" for more information. """ # Declare parameters rshift = zeros(3) # Shifts for array flattening acc_s_r = zeros_like(pos) # energy current j_e = zeros((3, potential_matrix.shape[0], potential_matrix.shape[0])) # Virial term for the viscosity calculation virial_species_tensor = zeros((3, 3, potential_matrix.shape[0], potential_matrix.shape[0])) # Initialize ptcl_pot_energy = zeros(pos.shape[0]) # Short-ranges potential energy of each particle # Pair distribution function rdf_nbins = rdf_hist.shape[0] dr_rdf = rc / float(rdf_nbins) d3_min = min(cells_per_dim[2], 1) d3_max = max(cells_per_dim[2], 1) d2_min = min(cells_per_dim[1], 1) d2_max = max(cells_per_dim[1], 1) d1_min = min(cells_per_dim[0], 1) d1_max = max(cells_per_dim[0], 1) # Dev Note: the array neighbors should be used for testing. This array is used to see if all the particles interact # with each other. The array is a NxN matrix initialized to empty. If two particles interact (r < rc) then the # matrix element (p1, p2) will be updated with p2. You can use the same array for checking if the loops go over # every particle in the case of small rc. If two particle see each other than the p1,p2 position is updated to -1. # neighbors = zeros((N, N), dtype=int64) # neighbors.fill(-50) # Loop over all cells in x, y, and z direction for cz in range(d3_max): for cy in range(d2_max): for cx in range(d1_max): # Compute the cell in 3D volume c = cx + cy * cells_per_dim[0] + cz * cells_per_dim[0] * cells_per_dim[1] # Loop over all cell pairs (N-1 and N+1) for cz_N in range(cz - 1, (cz + 2) * d3_min): # if d3_min = 0 -> range( -1, 0). This ensures that when the z-dimension is 0 we only loop once here # z cells # Check periodicity: needed for 0th cell # if cz_N < 0: # cz_shift = cells_per_dim[2] # rshift[2] = -box_lengths[2] # # Check periodicity: needed for Nth cell # elif cz_N >= cells_per_dim[2]: # cz_shift = -cells_per_dim[2] # rshift[2] = box_lengths[2] # else: # cz_shift = 0 # rshift[2] = 0.0 cz_shift = 0 + d3_max * (cz_N < 0) - cells_per_dim[2] * (cz_N >= cells_per_dim[2]) rshift[2] = 0.0 - box_lengths[2] * (cz_N < 0) + box_lengths[2] * (cz_N >= cells_per_dim[2]) # Note: In lower dimension systems (2D, 1D) # cz_shift will be 1, 0, -1. This will cancel later on when cz_N + cz_shift = (-1 + 1, 0 + 0, 1 - 1) # Similarly rshift[2] = 0.0 in all cases since box_lengths[2] == 0 for cy_N in range(cy - 1, (cy + 2) * d2_min): # y cells # Check periodicity # if cy_N < 0: # cy_shift = cells_per_dim[1] # rshift[1] = -box_lengths[1] # elif cy_N >= cells_per_dim[1]: # cy_shift = -cells_per_dim[1] # rshift[1] = box_lengths[1] # else: # cy_shift = 0 # rshift[1] = 0.0 cy_shift = 0 + d2_max * (cy_N < 0) - cells_per_dim[1] * (cy_N >= cells_per_dim[1]) rshift[1] = 0.0 - box_lengths[1] * (cy_N < 0) + box_lengths[1] * (cy_N >= cells_per_dim[1]) for cx_N in range(cx - 1, (cx + 2) * d1_min): # x cells # Check periodicity # if cx_N < 0: # cx_shift = cells_per_dim[0] # rshift[0] = -box_lengths[0] # elif cx_N >= cells_per_dim[0]: # cx_shift = -cells_per_dim[0] # rshift[0] = box_lengths[0] # else: # cx_shift = 0 # rshift[0] = 0.0 cx_shift = 0 + cells_per_dim[0] * (cx_N < 0) - cells_per_dim[0] * (cx_N >= cells_per_dim[0]) rshift[0] = 0.0 - box_lengths[0] * (cx_N < 0) + box_lengths[0] * (cx_N >= cells_per_dim[0]) # Compute the location of the N-th cell based on shifts c_N = ( (cx_N + cx_shift) + (cy_N + cy_shift) * cells_per_dim[0] + (cz_N + cz_shift) * cells_per_dim[0] * cells_per_dim[1] ) i = head[c] # print(cx_N, cy_N, cz_N, "head cell", c, "p1", i) # First compute interaction of head particle with neighboring cell head particles # Then compute interactions of head particle within a specific cell while i >= 0: # Check neighboring head particle interactions j = head[c_N] while j >= 0: # print("cell", c, "p1", i, "cell", c_N, "p2", j) # Only compute particles beyond i-th particle (Newton's 3rd Law) if i < j: # neighbors[i, j] = -1 # print(" rshift", rshift) # Compute the difference in positions for the i-th and j-th particles dx = pos[i, 0] - (pos[j, 0] + rshift[0]) dy = pos[i, 1] - (pos[j, 1] + rshift[1]) dz = pos[i, 2] - (pos[j, 2] + rshift[2]) # print(" distances", dx, dy, dz) vx = vel[i, 0] + vel[j, 0] vy = vel[i, 1] + vel[j, 1] vz = vel[i, 2] + vel[j, 2] # Compute distance between particles i and j r = sqrt(dx**2 + dy**2 + dz**2) rdf_bin = int(r / dr_rdf) id_i = p_id[i] id_j = p_id[j] # These definitions are needed due to numba # see https://github.com/numba/numba/issues/5881 # if measure and rdf_bin < rdf_nbins: rdf_hist[rdf_bin, id_i, id_j] += measure * (rdf_bin < rdf_nbins) # If below the cutoff radius, compute the force if r < rc: p_matrix = potential_matrix[id_i, id_j] # neighbors[i, j] = j # Compute the short-ranged force pot, fr = force(r, p_matrix) fr /= r # Need to add the same pot to each particle pair. ptcl_pot_energy[i] += 0.5 * pot ptcl_pot_energy[j] += 0.5 * pot # Update the acceleration for i particles in each dimension acc_s_r[i, 0] += dx * fr / p_mass[i] acc_s_r[i, 1] += dy * fr / p_mass[i] acc_s_r[i, 2] += dz * fr / p_mass[i] # Apply Newton's 3rd law to update acceleration on j particles acc_s_r[j, 0] -= dx * fr / p_mass[j] acc_s_r[j, 1] -= dy * fr / p_mass[j] acc_s_r[j, 2] -= dz * fr / p_mass[j] # Since we have the info already calculate the virial_species_tensor # This factor is to avoid double counting in the case of same species factor = 0.5 # * (id_i != id_j) + 0.25*( id_i == id_j) virial_species_tensor[0, 0, id_i, id_j] += factor * dx * dx * fr virial_species_tensor[0, 1, id_i, id_j] += factor * dx * dy * fr virial_species_tensor[0, 2, id_i, id_j] += factor * dx * dz * fr virial_species_tensor[1, 0, id_i, id_j] += factor * dy * dx * fr virial_species_tensor[1, 1, id_i, id_j] += factor * dy * dy * fr virial_species_tensor[1, 2, id_i, id_j] += factor * dy * dz * fr virial_species_tensor[2, 0, id_i, id_j] += factor * dz * dx * fr virial_species_tensor[2, 1, id_i, id_j] += factor * dz * dy * fr virial_species_tensor[2, 2, id_i, id_j] += factor * dz * dz * fr # This is where the double counting could happen. virial_species_tensor[0, 0, id_j, id_i] += factor * dx * dx * fr virial_species_tensor[0, 1, id_j, id_i] += factor * dx * dy * fr virial_species_tensor[0, 2, id_j, id_i] += factor * dx * dz * fr virial_species_tensor[1, 0, id_j, id_i] += factor * dy * dx * fr virial_species_tensor[1, 1, id_j, id_i] += factor * dy * dy * fr virial_species_tensor[1, 2, id_j, id_i] += factor * dy * dz * fr virial_species_tensor[2, 0, id_j, id_i] += factor * dz * dx * fr virial_species_tensor[2, 1, id_j, id_i] += factor * dz * dy * fr virial_species_tensor[2, 2, id_j, id_i] += factor * dz * dz * fr fij_vij = dx * fr * vx + dy * fr * vy + dz * fr * vz # For this further factor of 1/2 see eq.(5) in https://doi.org/10.1016/j.cpc.2013.01.008 factor *= 0.5 j_e[0, id_i, id_j] += factor * dx * fij_vij j_e[1, id_i, id_j] += factor * dy * fij_vij j_e[2, id_i, id_j] += factor * dz * fij_vij j_e[0, id_j, id_i] += factor * dx * fij_vij j_e[1, id_j, id_i] += factor * dy * fij_vij j_e[2, id_j, id_i] += factor * dz * fij_vij # Move down list (ls) of particles for cell interactions with a head particle j = ls_array[j] # Check if head particle interacts with other cells i = ls_array[i] # Add the ideal term of the energy current for i in range(pos.shape[0]): id_i = p_id[i] j_e[0, id_i, id_i] += (0.5 * p_mass[i] * (vel[i] ** 2).sum() + ptcl_pot_energy[i]) * vel[i, 0] j_e[1, id_i, id_i] += (0.5 * p_mass[i] * (vel[i] ** 2).sum() + ptcl_pot_energy[i]) * vel[i, 1] j_e[2, id_i, id_i] += (0.5 * p_mass[i] * (vel[i] ** 2).sum() + ptcl_pot_energy[i]) * vel[i, 2] return ptcl_pot_energy, acc_s_r, virial_species_tensor, j_e
[docs]@jit(Tuple((int64[:], float64[:]))(float64[:], float64), nopython=True) def create_cells_array(box_lengths, cutoff): """ Calculate the number of cells per dimension and their lengths. Parameters ---------- box_lengths: numpy.ndarray Length of each box side. cutoff: float Short range potential cutoff Returns ------- cells_per_dim : numpy.ndarray, numba.int32 No. of cells per dimension. There is only 1 cell for the non-dimension. cell_lengths_per_dim: numpy.ndarray, numba.float64 Length of each cell per dimension. """ # actual_dimensions = len(box_lengths.nonzero()[0]) cells_per_dim = zeros(3, dtype=int64) # The number of cells in each dimension. # Note that the branchless programming is to take care of the 1D and 2D case, in which we should have at least 1 cell # so that we can enter the loops below cells_per_dim[0] = int(box_lengths[0] / cutoff) # * (box_lengths[0] > 0.0) + 1 * (actual_dimensions < 1) cells_per_dim[1] = int(box_lengths[1] / cutoff) # * (box_lengths[1] > 0.0) + 1 * (actual_dimensions < 2) cells_per_dim[2] = int(box_lengths[2] / cutoff) # * (box_lengths[2] > 0.0) + 1 * (actual_dimensions < 3) # Branchless programming to avoid the division by zero later on cell_length_per_dim = zeros(3, dtype=float64) cell_length_per_dim[0] = box_lengths[0] / (1 * (cells_per_dim[0] == 0) + cells_per_dim[0]) # avoid division by zero cell_length_per_dim[1] = box_lengths[1] / (1 * (cells_per_dim[1] == 0) + cells_per_dim[1]) # avoid division by zero cell_length_per_dim[2] = box_lengths[2] / (1 * (cells_per_dim[2] == 0) + cells_per_dim[2]) # avoid division by zero return cells_per_dim, cell_length_per_dim
[docs]@jit(Tuple((int64[:], int64[:]))(float64[:, :], float64[:], int64[:]), nopython=True) def create_head_list_arrays(pos, cell_lengths, cells): # Loop over all particles and place them in cells ls = arange(pos.shape[0]) # List of particle indices in a given cell Ncell = cells[cells > 0].prod() head = arange(Ncell) # List of head particles empty = -50 # value for empty list and head arrays head.fill(empty) # Make head list empty until population for i in range(pos.shape[0]): # Determine what cell, in each direction, the i-th particle is in cx = int(pos[i, 0] / (1 * (cell_lengths[0] == 0.0) + cell_lengths[0])) # X cell, avoid division by zero cy = int(pos[i, 1] / (1 * (cell_lengths[1] == 0.0) + cell_lengths[1])) # Y cell, avoid division by zero cz = int(pos[i, 2] / (1 * (cell_lengths[2] == 0.0) + cell_lengths[2])) # Z cell, avoid division by zero # Determine cell in 3D volume for i-th particle c = cx + cy * cells[0] + cz * cells[0] * cells[1] # List of particle indices occupying a given cell ls[i] = head[c] # The last particle found to lie in cell c (head particle) head[c] = i return head, ls
[docs]@jit(nopython=True) def calculate_virial(pos, p_id, box_lengths, rc, potential_matrix, force): """ Update the force on the particles based on a linked cell-list (LCL) algorithm. Parameters ---------- force: tuple, float Potential and force values. potential_matrix: array Potential parameters. rc: float Cut-off radius. box_lengths: array Array of box sides' length. p_id: array Id of each particle pos: array Particles' positions. Returns ------- """ # Declare parameters N = pos.shape[0] # Number of particles rshift = zeros(3) # Shifts for array flattening # Virial term for the viscosity calculation virial_species_tensor = zeros((3, 3, N)) # Total number of cells in volume cells_per_dim, cell_lengths = create_cells_array(box_lengths, rc) head, ls_array = create_head_list_arrays(pos, cell_lengths, cells_per_dim) # Loop over all cells in x, y, and z direction for cx in range(cells_per_dim[0]): for cy in range(cells_per_dim[1]): for cz in range(cells_per_dim[2]): # Compute the cell in 3D volume c = cx + cy * cells_per_dim[0] + cz * cells_per_dim[0] * cells_per_dim[1] # Loop over all cell pairs (N-1 and N+1) for cz_N in range(cz - 1, cz + 2): # z cells # Check periodicity: needed for 0th cell # if cz_N < 0: # cz_shift = cells_per_dim[2] # rshift[2] = -box_lengths[2] # # Check periodicity: needed for Nth cell # elif cz_N >= cells_per_dim[2]: # cz_shift = -cells_per_dim[2] # rshift[2] = box_lengths[2] # else: # cz_shift = 0 # rshift[2] = 0.0 cz_shift = 0 + cells_per_dim[2] * (cz_N < 0) - cells_per_dim[2] * (cz_N >= cells_per_dim[2]) rshift[2] = 0.0 - box_lengths[2] * (cz_N < 0) + box_lengths[2] * (cz_N >= cells_per_dim[2]) for cy_N in range(cy - 1, cy + 2): # y cells # Check periodicity # if cy_N < 0: # cy_shift = cells_per_dim[1] # rshift[1] = -box_lengths[1] # elif cy_N >= cells_per_dim[1]: # cy_shift = -cells_per_dim[1] # rshift[1] = box_lengths[1] # else: # cy_shift = 0 # rshift[1] = 0.0 cy_shift = 0 + cells_per_dim[1] * (cy_N < 0) - cells_per_dim[1] * (cy_N >= cells_per_dim[1]) rshift[1] = 0.0 - box_lengths[1] * (cy_N < 0) + box_lengths[1] * (cy_N >= cells_per_dim[1]) for cx_N in range(cx - 1, cx + 2): # x cells # Check periodicity # if cx_N < 0: # cx_shift = cells_per_dim[0] # rshift[0] = -box_lengths[0] # elif cx_N >= cells_per_dim[0]: # cx_shift = -cells_per_dim[0] # rshift[0] = box_lengths[0] # else: # cx_shift = 0 # rshift[0] = 0.0 cx_shift = 0 + cells_per_dim[0] * (cx_N < 0) - cells_per_dim[0] * (cx_N >= cells_per_dim[0]) rshift[0] = 0.0 - box_lengths[0] * (cx_N < 0) + box_lengths[0] * (cx_N >= cells_per_dim[0]) # Compute the location of the N-th cell based on shifts c_N = ( (cx_N + cx_shift) + (cy_N + cy_shift) * cells_per_dim[0] + (cz_N + cz_shift) * cells_per_dim[0] * cells_per_dim[1] ) i = head[c] # First compute interaction of head particle with neighboring cell head particles # Then compute interactions of head particle within a specific cell while i > 0: # Check neighboring head particle interactions j = head[c_N] while j > 0: # Only compute particles beyond i-th particle (Newton's 3rd Law) if i < j: # Compute the difference in positions for the i-th and j-th particles dx = pos[i, 0] - (pos[j, 0] + rshift[0]) dy = pos[i, 1] - (pos[j, 1] + rshift[1]) dz = pos[i, 2] - (pos[j, 2] + rshift[2]) # Compute distance between particles i and j r = sqrt(dx**2 + dy**2 + dz**2) # If below the cutoff radius, compute the force if r < rc: p_matrix = potential_matrix[p_id[i], p_id[j]] # Compute the short-ranged force pot, fr = force(r, p_matrix) fr /= r virial_species_tensor[0, 0, i] += dx * dx * fr virial_species_tensor[0, 1, i] += dx * dy * fr virial_species_tensor[0, 2, i] += dx * dz * fr virial_species_tensor[1, 0, i] += dy * dx * fr virial_species_tensor[1, 1, i] += dy * dy * fr virial_species_tensor[1, 2, i] += dy * dz * fr virial_species_tensor[2, 0, i] += dz * dx * fr virial_species_tensor[2, 1, i] += dz * dy * fr virial_species_tensor[2, 2, i] += dz * dz * fr # Move down list (ls) of particles for cell interactions with a head particle j = ls_array[j] # Check if head particle interacts with other cells i = ls_array[i] return virial_species_tensor
[docs]@jit(nopython=True) def calculate_heat_flux(pos, vel, p_id, box_lengths, rc, potential_matrix, force): """ Update the force on the particles based on a linked cell-list (LCL) algorithm. Parameters ---------- pos: numpy.ndarray Particles' positions. Shape = ((N,3)). vel: array Velocity of each particle. Shape = ((N,3)). p_id: numpy.ndarray Id of each particle box_lengths: array Array of box sides' length. rc: float Cut-off radius. potential_matrix: array Potential parameters. force: tuple, float Potential and force values. Returns ------- j_e : numpy.ndarray Part of the energy current. Shape = ((3, N)) """ # Declare parameters N = pos.shape[0] # Number of particles rshift = zeros(3) # Shifts for array flattening # energy current j_e = zeros((3, N)) # Total number of cells in volume cells_per_dim, cell_lengths = create_cells_array(box_lengths, rc) head, ls_array = create_head_list_arrays(pos, cell_lengths, cells_per_dim) # Loop over all cells in x, y, and z direction for cx in range(cells_per_dim[0]): for cy in range(cells_per_dim[1]): for cz in range(cells_per_dim[2]): # Compute the cell in 3D volume c = cx + cy * cells_per_dim[0] + cz * cells_per_dim[0] * cells_per_dim[1] # Loop over all cell pairs (N-1 and N+1) for cz_N in range(cz - 1, cz + 2): # z cells # Check periodicity: needed for 0th cell # if cz_N < 0: # cz_shift = cells_per_dim[2] # rshift[2] = -box_lengths[2] # # Check periodicity: needed for Nth cell # elif cz_N >= cells_per_dim[2]: # cz_shift = -cells_per_dim[2] # rshift[2] = box_lengths[2] # else: # cz_shift = 0 # rshift[2] = 0.0 cz_shift = 0 + cells_per_dim[2] * (cz_N < 0) - cells_per_dim[2] * (cz_N >= cells_per_dim[2]) rshift[2] = 0.0 - box_lengths[2] * (cz_N < 0) + box_lengths[2] * (cz_N >= cells_per_dim[2]) for cy_N in range(cy - 1, cy + 2): # y cells # Check periodicity # if cy_N < 0: # cy_shift = cells_per_dim[1] # rshift[1] = -box_lengths[1] # elif cy_N >= cells_per_dim[1]: # cy_shift = -cells_per_dim[1] # rshift[1] = box_lengths[1] # else: # cy_shift = 0 # rshift[1] = 0.0 cy_shift = 0 + cells_per_dim[1] * (cy_N < 0) - cells_per_dim[1] * (cy_N >= cells_per_dim[1]) rshift[1] = 0.0 - box_lengths[1] * (cy_N < 0) + box_lengths[1] * (cy_N >= cells_per_dim[1]) for cx_N in range(cx - 1, cx + 2): # x cells # Check periodicity # if cx_N < 0: # cx_shift = cells_per_dim[0] # rshift[0] = -box_lengths[0] # elif cx_N >= cells_per_dim[0]: # cx_shift = -cells_per_dim[0] # rshift[0] = box_lengths[0] # else: # cx_shift = 0 # rshift[0] = 0.0 cx_shift = 0 + cells_per_dim[0] * (cx_N < 0) - cells_per_dim[0] * (cx_N >= cells_per_dim[0]) rshift[0] = 0.0 - box_lengths[0] * (cx_N < 0) + box_lengths[0] * (cx_N >= cells_per_dim[0]) # Compute the location of the N-th cell based on shifts c_N = ( (cx_N + cx_shift) + (cy_N + cy_shift) * cells_per_dim[0] + (cz_N + cz_shift) * cells_per_dim[0] * cells_per_dim[1] ) i = head[c] # First compute interaction of head particle with neighboring cell head particles # Then compute interactions of head particle within a specific cell while i > 0: # Check neighboring head particle interactions j = head[c_N] while j > 0: # Only compute particles beyond i-th particle (Newton's 3rd Law) if i < j: # Compute the difference in positions for the i-th and j-th particles dx = pos[i, 0] - (pos[j, 0] + rshift[0]) dy = pos[i, 1] - (pos[j, 1] + rshift[1]) dz = pos[i, 2] - (pos[j, 2] + rshift[2]) vx = vel[i, 0] + vel[j, 0] vy = vel[i, 1] + vel[j, 1] vz = vel[i, 2] + vel[j, 2] # Compute distance between particles i and j r = sqrt(dx**2 + dy**2 + dz**2) # If below the cutoff radius, compute the force if r < rc: p_matrix = potential_matrix[p_id[i], p_id[j]] # Compute the short-ranged force uij, fr = force(r, p_matrix) fr /= r fij_vij = dx * fr * vx + dy * fr * vy + dz * fr * vz j_e[0, i] += 0.25 * (vx * uij + dx * fij_vij) j_e[1, i] += 0.25 * (vy * uij + dy * fij_vij) j_e[2, i] += 0.25 * (vz * uij + dz * fij_vij) j_e[0, j] += 0.25 * (vx * uij + dx * fij_vij) j_e[1, j] += 0.25 * (vy * uij + dy * fij_vij) j_e[2, j] += 0.25 * (vz * uij + dz * fij_vij) # Move down list (ls) of particles for cell interactions with a head particle j = ls_array[j] # Check if head particle interacts with other cells i = ls_array[i] return j_e