import numpy as np
import numba
import math
from numba import cuda
from .colarray import colarray
from ..simulation_boxes import Orthorhombic, LeesEdwards
from .topology import Topology, replicate_topologies
from ..simulation.get_default_compute_flags import get_default_compute_flags
# IO
import h5py
import gzip
# TODO: add possibility of "with ... as conf:" TypeError: 'Configuration' object does not support the context manager protocol
[docs]
class Configuration:
""" The configuration class
Store particle vectors (positions, velocities, forces) and scalars (energy, virial, mass ...).
Also store particle type, image coordinates, and the simulation box.
Parameters
----------
D : int
Spatial dimension for the configuration.
N : int, optional
Number of particles.
If not set, this will be determined the first time particle data is written to the configuration.
compute_flags : dict, optional
Dictionary specifying which quantities to compute (volume, energies, stresses, etc.).
If `None` the defaults are processed, see :func:`~gamdpy.get_default_compute_flags`:.
Examples
--------
>>> import gamdpy as gp
>>> conf = gp.Configuration(D=3, N=1000)
>>> print(conf.vector_columns) # Print names of vector columns
['r', 'v', 'f']
>>> print(conf.scalar_columns) # Print names of scalar columns
['U', 'W', 'K', 'm']
>>> print(conf['r'].shape) # Vectors are stored as (N, D) numpy arrays
(1000, 3)
>>> print(conf['m'].shape) # Scalars are stored as (N,) numpy arrays
(1000,)
Data can be accessed via string keys (similar to dataframes in pandas):
>>> conf['r'] = np.ones((1000, 3))
>>> conf['v'] = 2 # Broadcast by numpy to correct shape
>>> print(conf['r'] + 0.01*conf['v'])
[[1.02 1.02 1.02]
[1.02 1.02 1.02]
[1.02 1.02 1.02]
...
[1.02 1.02 1.02]
[1.02 1.02 1.02]
[1.02 1.02 1.02]]
A configuration can be specified without setting the number particles, N.
In that case N is determined the first time the particle data is written to the configuration:
>>> import numpy as np
>>> conf = gp.Configuration(D=3)
>>> conf['r'] = np.zeros((400, 3))
>>> print(conf['r'].shape)
(400, 3)
The ``compute_flags`` paramter can be used if there should be allocated memory for storing volume data (or other data).
>>> configuration = gp.Configuration(D=3, compute_flags={'Vol':True})
The default values can be seen with :func:`~gamdpy.get_default_compute_flags`:
>>> gp.get_default_compute_flags()
{'U': True, 'W': True, 'K': True, 'lapU': False, 'Fsq': False, 'stresses': False, 'Vol': False, 'Ptot': False}
"""
scalar_parameters = ['m']
scalar_computables_interactions = ['U', 'W', 'lapU']
scalar_computables_integrator = ['K', 'Fsq']
scalar_decriptions = {'m': 'Particle mass.',
'U': 'Potential energy.',
'W': 'Virial.',
'lapU': 'Laplace(U).',
'K': 'Kinetic energy.',
'Fsq': 'Squared length of force vector.',
}
def __init__(self, D: int, N: int = None, type_names=None, compute_flags=None, ftype=np.float32, itype=np.int32) -> None:
self.D = D
self.N = N
self.type_names = type_names
self.index_from_type_name = {}
if type_names:
for index, type_name in enumerate(type_names):
self.index_from_type_name[type_name] = index
self.compute_flags = get_default_compute_flags()
if compute_flags != None:
# only keys present in the default are processed
for k in compute_flags:
if k in self.compute_flags:
self.compute_flags[k] = compute_flags[k]
else:
raise ValueError('Unknown key in compute_flags:%s' %k)
self.vector_columns = ['r', 'v', 'f'] # Should be user modifiable
if self.compute_flags['stresses']:
if self.D > 4:
raise ValueError("compute_flags['stresses'] should not be set for D>4")
self.vector_columns += ['sx', 'sy', 'sz','sw'][:self.D]
self.num_cscalars = 0
self.sid = {}
self.scalar_columns = []
sid_index = 0
for label in self.scalar_computables_interactions:
if self.compute_flags[label]:
self.sid[label] = sid_index
self.scalar_columns.append(label)
sid_index += 1
self.num_cscalars += 1
for label in self.scalar_computables_integrator:
if self.compute_flags[label]:
self.sid[label] = sid_index
self.scalar_columns.append(label)
sid_index += 1
for label in self.scalar_parameters:
self.sid[label] = sid_index
self.scalar_columns.append(label)
sid_index += 1
self.simbox = None
self.topology = Topology()
self.ptype_function = self.make_ptype_function()
self.ftype = ftype
self.itype = itype
if self.N != None:
self.__allocate_arrays()
def __allocate_arrays(self):
self.vectors = colarray(self.vector_columns, size=(self.N, self.D), dtype=self.ftype)
self.scalars = np.zeros((self.N, len(self.scalar_columns)), dtype=self.ftype)
self.r_im = np.zeros((self.N, self.D), dtype=self.itype) # Move to vectors
self.ptype = np.zeros(self.N, dtype=self.itype) # Move to scalars
return
def __repr__(self):
return f'Configuration(D={self.D}, N={self.N}, compute_flags={self.compute_flags})'
def __code__(self):
import sys
np.set_printoptions(threshold=sys.maxsize)
code_str = "# Define configuration class\n"
code_str += f"from gamdpy import Configuration\n"
code_str += f"configuration = Configuration(D={self.D}, N={self.N}, compute_flags={self.compute_flags})\n"
# Following part needs to be done with a read function from the .h5
for key in self.vector_columns:
code_str += f"configuration['{key}'] = {self[key]}\n"
for key in self.scalar_columns:
code_str += f"configuration['{key}'] = {self[key]}\n"
return code_str
def __str__(self):
if self.N == None:
return f'{self.D} dimensional configuration. Particles not yet assigned.'
str = f'{self.N} particles in {self.D} dimensions. Number density (atomic): {self.N/self.get_volume():.3f}'
num_types = np.max(self.ptype)+1
if num_types==1:
str += '. Single component. '
else:
str += f'. {num_types} components with fractions '
for ptype in range(num_types):
str += f'{np.mean(self.ptype==ptype):.3f}, '
str += '\nCurrent scalar data per particle:'
for key in self.sid:
str += f'\n{key+",":5} {np.mean(self.scalars[:,self.sid[key]]):.3f}'
if key in self.scalar_decriptions:
str += '\t' + self.scalar_decriptions[key]
return str
def __setitem__(self, key, data):
if self.N is None: # First time setting particle data, so allocate arrays
if type(data) != np.ndarray:
raise (TypeError)(
'Number of particles, N, not determined yet, so assignment needs to be with a numpy array')
self.N = data.shape[0]
self.__allocate_arrays()
if key in self.vector_columns:
self.__set_vector(key, data)
return
if key in self.scalar_columns:
self.__set_scalar(key, data)
return
raise ValueError(f'Unknown key {key}. Vectors: {self.vector_columns}, Scalars: {self.scalar_columns}')
def __set_vector(self, key: str, data: np.ndarray) -> None:
""" Set new vector data """
if type(data) == np.ndarray: # Allow for possibility of using scalar float, which is then broadcast by numpy
N, D = data.shape
if N != self.N:
raise ValueError(f'Inconsistent number of particles, {N} <> {self.N}')
if D != self.D:
raise ValueError(f'Inconsistent number of dimensions, {D} <> {self.D}')
self.vectors[key] = data
return
def __set_scalar(self, key: str, data) -> None:
""" Set new scalar data """
if type(data) == np.ndarray: # Allow for possibility of using scalar float, which is then broadcast by numpy
N, = data.shape
if N != self.N:
raise ValueError(f'Inconsistent number of particles, {N} <> {self.N}')
self.scalars[:, self.sid[key]] = data
return
def __getitem__(self, key):
if key in self.vector_columns:
return self.vectors[key]
if key in self.scalar_columns:
return self.scalars[:, self.sid[key]]
raise ValueError(f'Unknown key {key}. Vectors: {self.vector_columns}, Scalars: {self.scalar_columns}')
[docs]
def copy_to_device(self) -> None:
""" Copy all data from host to device memory (CPU to GPU)."""
self.d_vectors = cuda.to_device(self.vectors.array)
self.d_scalars = cuda.to_device(self.scalars)
self.d_r_im = cuda.to_device(self.r_im)
self.d_ptype = cuda.to_device(self.ptype)
self.simbox.copy_to_device()
return
[docs]
def copy_to_host(self) -> None:
""" Copy all data from device to host memory (GPU to CPU)."""
self.vectors.array = self.d_vectors.copy_to_host()
self.scalars = self.d_scalars.copy_to_host()
self.r_im = self.d_r_im.copy_to_host()
self.ptype = self.d_ptype.copy_to_host()
self.simbox.copy_to_host()
return
def make_ptype_function(self) -> callable:
def ptype_function(pid, ptype_array):
ptype = ptype_array[pid] # Default: read from ptype_array
return ptype
return ptype_function
[docs]
def get_potential_energy(self) -> float:
""" Get total potential energy of the configuration """
return float(np.sum(self['U']))
[docs]
def get_volume(self) -> float:
""" Get volume of simulation box associated with configuration """
return self.simbox.get_volume()
[docs]
def set_kinetic_temperature(self, temperature: float, ndofs=None) -> None:
""" Rescale velocities so magnitude correspond to a given temperature.
Parameters
----------
temperature : float
Temperature after rescaling of velocities.
ndofs : int, optional
Degrees of freedom. If not provided, ndofs = D*(N-1)
Raises
------
ValueError
If the current temperature is zero (velocities are zero).
"""
if ndofs is None:
ndofs = self.D * (self.N - 1)
T_ = np.sum(np.dot(self['m'], np.sum(self['v'] ** 2, axis=1))) / ndofs
if T_ == 0:
raise ValueError('Cannot rescale velocities when all equal to zero')
self['v'] *= (temperature / T_) ** 0.5
[docs]
def randomize_velocities(self, temperature: float, seed=None, ndofs=None) -> None:
""" Randomize velocities according to a given temperature.
Parameters
----------
temperature : float
Temperature to randomize velocities by. If <= 0, set all velocities to zero.
seed : int, optional
Seed for random number generator
ndofs : int, optional
Number of degrees of freedom
Raises
------
ValueError
If spatial dimention (D) is None
ValueError
If any mass is zero. Set masses before randomizing velocities.
"""
if self.D is None:
raise ValueError('Cannot randomize velocities. Start by assigning positions.')
masses = self['m']
if np.any(masses == 0):
raise ValueError('Cannot randomize velocities when any mass is zero')
if temperature > 0.0:
self['v'] = generate_random_velocities(self.N, self.D, T=temperature, seed=seed, m=self['m'])
# rescale to get the kinetic temperature exactly right
self.set_kinetic_temperature(temperature=temperature, ndofs=ndofs)
else:
self['v'] = np.zeros((self.N, self.D), np.float32)
[docs]
def make_lattice(self, unit_cell: dict, cells: list, rho: float = None, ptype_unit_cell: list = None) -> None:
""" Generate a lattice configuration
The lattice is constructed by replicating the unit cell in all directions.
Unit cell is a dictonary with `fractional_coordinates` for particles, and
the `lattice_constants` as a list of unit cell lengths in all directions.
The simulation box is :class:`~gamdpy.Orthorhombic`.
Unit cells directories are available in :obj:`gamdpy.unit_cells`.
Parameters
----------
unit_cell : dict
Dictionary with `fractional_coordinates` for particles and `lattice_constants`
cells : list[int]
Number of unit cells in each direction
rho : float
Number density
ptype_unit_cell : list
Types of the particles in the unit cell
Example
-------
>>> import gamdpy as gp
>>> conf = gp.Configuration(D=3)
>>> conf.make_lattice(gp.unit_cells.FCC, cells=[8, 8, 8], rho=1.0)
>>> print(gp.unit_cells.FCC) # Example of a unit cell dict
{'fractional_coordinates': [[0.0, 0.0, 0.0], [0.5, 0.5, 0.0], [0.5, 0.0, 0.5], [0.0, 0.5, 0.5]], 'lattice_constants': [1.0, 1.0, 1.0]}
"""
from .make_lattice import make_lattice
lattice = make_lattice(unit_cell=unit_cell, cells=cells, rho=rho, ptype_unit_cell=ptype_unit_cell)
self['r'] = lattice["positions"]
self.simbox = Orthorhombic(self.D, lattice["box_vector"])
if "ptype" in lattice:
self.ptype = lattice["ptype"]
return
[docs]
def make_positions(self, N, rho: float) -> None:
""" Generate particle positions in D dimensions.
Positions are generated in a simple cubic configuration in D dimensions.
Takes the number of particles N and the density rho as inputs.
The simulation box type is :class:`~gamdpy.Orthorhombic` and cubic.
Parameters
----------
N : int
Number of particles
rho : float
Number density of particles
Example
-------
>>> import gamdpy as gp
>>> configuration = gp.Configuration(D=3)
>>> configuration.make_positions(N=1000, rho=1.2)
"""
D = self.D
part_per_line = np.ceil(pow(N, 1./D))
box_length = pow(N/rho, 1./D)
box_vector = np.array(D*[box_length])
index = 0
x = [] # empty list
# This loop places particles in a simple cubic configuration
# The first particle is in D*[0]
while index < N:
dcurrent = D - 1
i_d = D*[float(0)]
i_d[dcurrent] = (index / pow(part_per_line, dcurrent))
rest = index % (pow(part_per_line, dcurrent))
while dcurrent != 0:
dcurrent = dcurrent - 1
i_d[dcurrent] = (rest/pow(part_per_line,dcurrent))
rest = index % (pow(part_per_line, dcurrent))
x.append(i_d)
index = index + 1
pos = np.array(x)
# Centering the array
dcurrent = 1
remove = 0
while dcurrent < D:
remove += D**(D-dcurrent)
pos[:, dcurrent] -= remove/N
dcurrent = dcurrent + 1
pos -= np.array(D*[int(0.5*part_per_line)]) # center cube at 0
# Scaling for density
pos *= box_length/part_per_line
# Saving to Configuration object
self['r'] = pos
self.simbox = Orthorhombic(self.D, box_vector)
# Check all particles are in the box (-L/2, L/2)
assert np.any(np.abs(pos))<0.5*box_length
return
[docs]
def atomic_scale(self, density: float) -> None:
""" Scale atom positions and simulation box to a given density.
Parameters
----------
density : float
Number density of particles after scaling.
"""
actual_rho = self.N / self.get_volume()
scale_factor = (actual_rho / density)**(1/3)
self.vectors['r'] *= scale_factor
self.simbox.scale(scale_factor)
[docs]
def save(self, output: h5py.File, group_name: str, mode: str="w",
update_ptype: bool=True, include_topology: bool=True, use_topology_link: bool=False, verbose: bool=True) -> None:
""" Write a configuration to a HDF5 file
Parameters
----------
configuration : ~gamdpy.Configuration
a gamdpy configuration object
output : h5py.File
h5 file
group_name : str
name of the group which will be created in the h5 and in which
the configuration will be saved
mode: str
default value is "w" and corresponds to replacing existing dataset
include_topology : bool
Boolean flag indicating whether the topology of the configuration should be included
use_topology_link : bool
Boolean flag indicating the topology should just be a soft link to that of the initial configuration (if present)
verbose : bool
Boolean flag indicating whether messages should be written
Example
-------
>>> import os
>>> import h5py
>>> import gamdpy as gp
>>> conf = gp.Configuration(D=3)
>>> conf.make_positions(N=10, rho=1.0)
>>> conf.save(output=h5py.File("final.h5", "w"), group_name="configuration", mode="w")
>>> os.remove("final.h5") # Removes file (for doctests)
>>> with h5py.File("manyconfs.h5", "a") as fout:
... conf.save(output=fout, group_name="restarts/restart0000", mode="w")
... conf.save(output=fout, group_name="restarts/restart0001", mode="w")
... conf.save(output=fout, group_name="restarts/restart0002", mode="w")
>>> os.remove("manyconfs.h5") # Removes file (for doctests)
"""
# Sanity:
#print(f"output {isinstance(output, h5py.File)} {output}")
#print(f"group_name {isinstance(group_name, str)} {group_name}")
# Creating group group_name in h5 root
if group_name in output.keys() and mode=="w":
if verbose:
print(f"{group_name} already present in h5 root, replacing it")
del output[f'{group_name}']
output.create_group(group_name)
# Checks if group group_name exists in case mode="append"
elif group_name not in output.keys() and mode=="a":
output.create_group(group_name)
elif group_name not in output.keys() and mode=="w":
output.create_group(group_name)
elif group_name in output.keys() and mode=="a":
if verbose:
print(f"append data to {group_name} in h5 root")
else:
raise ValueError("Unexpected combination of input in save method of Configuration")
# Save attributes of group group_name
# output[group_name].attrs['simbox'] = self.simbox.get_lengths()
# Saving vectors separately
#output[group_name].create_dataset('r', data=self['r'], dtype=np.float32)
#output[f"{group_name}/r"].attrs['simbox'] = self.simbox.get_lengths()
#output[group_name].create_dataset('v', data=self['v'], dtype=np.float32)
#output[group_name].create_dataset('f', data=self['f'], dtype=np.float32)
# Saving vectors all together
#output[group_name].create_dataset('vectors', data=np.hstack([self['r'], self['v'], self['f']]), dtype=np.float32)
output[group_name].create_dataset('vectors', data=self.vectors.array, dtype=np.float32)
output[f"{group_name}/vectors"].attrs['vector_columns'] = self.vector_columns
# For ptype decide to save new array every time or link to the one in initial_configuration
if update_ptype:
output[group_name].create_dataset('ptype', data=self.ptype, dtype=np.int32)
else:
#layout = h5py.VirtualLayout(shape=(1,self.N), dtype=np.int32)
#layout[0] = h5py.VirtualSource(output['/initial_configuration/ptype'])
layout = h5py.VirtualLayout(shape=(self.ptype.shape), dtype=np.int32)
layout[:] = h5py.VirtualSource(output['/initial_configuration/ptype'])
output.create_virtual_dataset(f'{group_name}/ptype', layout, fillvalue=0)
# Saving other things
#output[group_name].create_dataset('m', data=self['m'], dtype=np.float32) # included in scalars
output[group_name].create_dataset('r_im', data=self.r_im, dtype=np.int32)
output[group_name].create_dataset('scalars', data=self.scalars, dtype=np.float32)
output[f"{group_name}/scalars"].attrs['scalar_columns'] = self.scalar_columns
# save simulation box
output[group_name].attrs['simbox_name'] = self.simbox.get_name()
output[group_name].attrs['simbox_data'] = self.simbox.data_array
# For topology decide to save new array every time or link to the one in initial_configuration
if include_topology:
if use_topology_link:
if not output['/initial_configuration/topology']:
raise KeyError("Group '/initial_configuration/topology' not found")
output[f'{group_name}/topology'] = h5py.SoftLink('/initial_configuration/topology')
else:
output[group_name].create_group('topology')
self.topology.save(output[f'{group_name}/topology'])
# The following is equivalent to overloading in c++ : https://stackoverflow.com/questions/12179271/meaning-of-classmethod-and-staticmethod-for-beginner
# cls stands for class, in this case the Configuration class
[docs]
@classmethod
def from_h5(cls, h5file: h5py.File, group_name: str, reset_images: bool=False, compute_flags: bool=None, include_topology: bool=True) -> "Configuration":
""" Read a configuration from an open HDF5 file identified by group-name
Parameters
----------
h5file : HDF5 File
open HDF5 file object, as returned by h5py.File()
group_name : str
string corresponding to a key in the h5py.File containing a saved gamdpy configuration
reset_images : bool
if True set the images to zero (default False)
compute_flags : bool
NOTE: still to be developed, should be possible to define compute flags from dictionary
compute_flags defining what will be stored in the configuration (default None)
include_topology : bool
if True then read also the topology from the file (default True)
Returns
-------
configuration : ~gamdpy.Configuration
a gamdpy configuration object
Example
-------
>>> import gamdpy as gp
>>> output_file = h5py.File('examples/Data/LJ_r0.973_T0.70_toread.h5')
>>> conf = Configuration.from_h5(output_file, 'restarts/restart0000')
>>> print(conf.D, conf.N, conf['r'][0]) # Print number of dimensions D, number of particles N and position of first particle
3 2048 [-6.407801 -6.407801 -6.407801]
"""
h5_vector_columns = h5file[group_name]['vectors'].attrs['vector_columns']
h5_scalar_columns = h5file[group_name]['scalars'].attrs['scalar_columns']
h5_vec_col_dict = {value: index for index, value in enumerate(h5_vector_columns)}
h5_sca_col_dict = {value: index for index, value in enumerate(h5_scalar_columns)}
h5_vectors_array = h5file[group_name]['vectors'][:]
h5_scalars_array = h5file[group_name]['scalars'][:]
simbox_name = h5file[group_name].attrs['simbox_name']
simbox_data = h5file[group_name].attrs['simbox_data']
_, N, D = h5_vectors_array.shape
configuration = cls(D=D, N=N, compute_flags=compute_flags)
configuration.ptype = h5file[group_name]['ptype'][:]
conf_vec_col_dict = {value: index for index, value in enumerate(configuration.vector_columns)}
conf_sca_col_dict = {value: index for index, value in enumerate(configuration.scalar_columns)}
# copy scalars where present in both places
for label in h5_scalar_columns:
if label in configuration.scalar_columns:
configuration.scalars[:, conf_sca_col_dict[label]] = h5_scalars_array[:,h5_sca_col_dict[label]]
# copy vectors where present in both places
for label in h5_vector_columns:
if label in configuration.vector_columns:
configuration.vectors.array[conf_vec_col_dict[label],:,:] = h5_vectors_array[h5_vec_col_dict[label],:,:]
#configuration.scalars = h5_scalars_array
#configuration.vectors.array = h5_vectors_array
if simbox_name == 'Orthorhombic':
configuration.simbox = Orthorhombic(D, simbox_data)
elif simbox_name == 'LeesEdwards':
box_shift_image = {True:0, False: int(simbox_data[D+1])} [reset_images]
configuration.simbox = LeesEdwards(D, simbox_data[:D], simbox_data[D], box_shift_image)
else:
raise ValueError('simbox_name %s not recognized in group %s' % (simbox_name, group_name))
if reset_images:
configuration.r_im = np.zeros((N, D), dtype=np.int32)
else:
configuration.r_im = h5file[group_name]['r_im'][:]
# Read topology
if include_topology:
if 'topology' in h5file[group_name]:
configuration.topology.from_h5(h5file[group_name]['topology'])
return configuration
# Helper functions
def generate_random_velocities(N, D, T, seed, m=1, dtype=np.float32):
""" Generate random velocities according to a given temperature. """
v = np.zeros((N, D), dtype=dtype)
# default value of seed is None and random.seed(None) has no effect
np.random.seed(seed)
for k in range(D):
# to cover the case that m is a 1D array of length N, need to
# generate one column at a time, passing the initial zeros as the
# mean to avoid problems with inferring the correct shape
v[:, k] = np.random.normal(v[:, k], (T / m) ** 0.5)
CM_drift = np.mean(m * v[:, k]) / np.mean(m)
v[:, k] -= CM_drift
return dtype(v)
@numba.njit
def generate_fcc_positions(nx, ny, nz, rho, dtype=np.float32):
# This function is not recommended to use, and should be considered deprecated
# raise DeprecationWarning('Use Configuration.make_lattice() instead')
D = 3
conf = np.zeros((nx * ny * nz * 4, D), dtype=dtype)
count = 0
for ix in range(nx):
for iy in range(ny):
for iz in range(nz):
conf[count + 0, :] = [ix + 0.25, iy + 0.25, iz + 0.25]
conf[count + 1, :] = [ix + 0.75, iy + 0.75, iz + 0.25]
conf[count + 2, :] = [ix + 0.75, iy + 0.25, iz + 0.75]
conf[count + 3, :] = [ix + 0.25, iy + 0.75, iz + 0.75]
count += 4
for k in range(D):
conf[:, k] -= np.mean(conf[:, k]) # put sample in the middle of the box
sim_box = np.array((nx, ny, nz), dtype=dtype)
rho_initial = 4.0
scale_factor = dtype((rho_initial / rho) ** (1 / D))
return conf * scale_factor, sim_box * scale_factor
def make_configuration_fcc(nx, ny, nz, rho, N=None):
"""
Generate Configuration for particle positions and simbox of a FCC lattice with a given density
(nx x ny x nz unit cells),
and default types ('0') and masses ('1.')
If N is given, only N particles will be in the configuration
(needs to be equal to or smaller than number of particle in generated crystal)
"""
# This function is not recommended to use, and should be considered deprecated
# raise DeprecationWarning('Use Configuration.make_lattice() instead')
positions, simbox_data = generate_fcc_positions(nx, ny, nz, rho)
N_, D = positions.shape
if N == None:
N = N_
else:
if N > N_:
raise ValueError(
f'N ({N}) needs to be equal to or smaller than number of particle in generated crystal ({N_})')
scale_factor = (N / N_) ** (1 / 3)
positions *= scale_factor
simbox_data *= scale_factor
configuration = Configuration(D=3)
configuration['r'] = positions[:N, :]
configuration.simbox = Orthorhombic(D, simbox_data)
configuration['m'] = np.ones(N, dtype=np.float32) # Set masses
configuration.ptype = np.zeros(N, dtype=np.int32) # Set types
return configuration
def replicate_molecules(molecule_dicts, num_molecules_each_type_list, safety_distance, random_rotations=True, compute_flags=None):
""" Construct a configuration containing different molecules, with the numbers of each type specified
Parameters:
moleculde_dicts (list): A list of dictionaries, each of which contains keys "positions", "particle_types", "masses" and "topology", whose values are corresponding lists of data for that molecule
num_molecules_each_type_list (list): A list of integers, specifying how many molecules of each type are to be included
safety_distance (float): A length to be added in all directions to the size of the bounding box to be used for each molecule when placing them initially on a lattice
random_rotation (Bool): Whether the x,y,z coordinates in each molecule should be randomly permutated to give a simple randomization of orientations.
Returns:
configuration (Configuration): the resulting configuration with all molecules replicated and including the corresponding replicated topology
"""
D = 3
num_molecule_types = len(molecule_dicts)
total_num_particles = 0
total_num_molecules = 0
mol_types = []
positions_array_list = []
cell_length_list = []
size_molecule_type_list = []
# unpack the list of molecule dictionaries and make lists of positions, particle_types, masses and topologies
mol_topology_list = []
mol_positions_list = []
particle_type_list = []
masses_list = []
for idx in range(num_molecule_types):
mol_positions_list.append(molecule_dicts[idx]["positions"])
particle_type_list.append(molecule_dicts[idx]["particle_types"])
masses_list.append(molecule_dicts[idx]["masses"])
mol_topology_list.append(molecule_dicts[idx]["topology"])
# tally the total numbers of particles and molecules, make shifted arrays of possitions for each molecule type
for idx in range(num_molecule_types):
num_mol_this_type = num_molecules_each_type_list[idx]
total_num_particles += len(mol_positions_list[idx]) * num_mol_this_type
total_num_molecules += num_mol_this_type
mol_types += [idx] * num_mol_this_type
positions_array = np.array(mol_positions_list[idx])
positions_array -= np.min(positions_array, axis=0)
positions_array_list.append(positions_array)
size_molecule_type_list.append( len(mol_positions_list[idx]) )
cell_length = np.max(positions_array) + safety_distance
cell_length_list.append(cell_length)
# shuffle molecule types randomly
np.random.shuffle(mol_types)
configuration = Configuration(D=D, N=total_num_particles, compute_flags=compute_flags)
configuration.topology = replicate_topologies(mol_topology_list, num_molecules_each_type_list, mol_types, size_molecule_type_list)
max_cell_length = max(cell_length_list)
# make a cubic box big enough to hold the total number of molecules
num_cells_axis = math.ceil(total_num_molecules**(1/3))
simbox_data = np.ones(D) * (num_cells_axis * max_cell_length)
configuration.simbox = Orthorhombic(D, simbox_data)
mol_count = 0
particle_count = 0
for ix in range(num_cells_axis):
for iy in range(num_cells_axis):
for iz in range(num_cells_axis):
if mol_count < total_num_molecules:
# add a molecule
this_mol_type = mol_types[mol_count]
particles_this_molecule = size_molecule_type_list[this_mol_type]
arr = np.arange(D)
if random_rotations:
np.random.shuffle(arr)
configuration['r'][particle_count:particle_count+particles_this_molecule,0] = positions_array_list[this_mol_type][:,arr[0]] + ix*max_cell_length
configuration['r'][particle_count:particle_count+particles_this_molecule,1] = positions_array_list[this_mol_type][:,arr[1]] + iy*max_cell_length
configuration['r'][particle_count:particle_count+particles_this_molecule,2] = positions_array_list[this_mol_type][:,arr[2]] + iz*max_cell_length
configuration['m'][particle_count:particle_count+particles_this_molecule] = masses_list[this_mol_type]
configuration.ptype[particle_count:particle_count+particles_this_molecule] = particle_type_list[this_mol_type]
particle_count += particles_this_molecule
mol_count += 1
assert mol_count == total_num_molecules
assert particle_count == total_num_particles
for i in range(configuration.D):
configuration['r'][:,i] -= configuration.simbox.get_lengths()[i]/2
return configuration