def _update_rvecs(self, rvecs): self.cell = Cell(rvecs) if self.cell.nvec != 3: raise ValueError( 'RDF can only be computed for 3D periodic systems.') if (2 * self.rcut > self.cell.rspacings * (1 + 2 * self.nimage)).any(): raise ValueError( 'The 2*rcut argument should not exceed any of the cell spacings.' )
def _generate_ffs(self, nguests): for iguest in range(len(self._ffs),nguests): if len(self._ffs)==0: # The very first force field, no guests system = System.create_empty() system.cell = Cell(self.guest.cell.rvecs) elif len(self._ffs)==1: # The first real force field, a single guest system = self.guest else: # Take the system of the lastly generated force field (N-1) guests # and add an additional guest system = self._ffs[-1].system.merge(self.guest) self._ffs.append(self.ff_generator(system, self.guest))
def __init__(self, system, pppm_accuracy=1e-5, fn_log='lammps.log', scalings=np.zeros(6), fn_system='lammps.data', fn_table='lammps.table', triclinic=True, comm=None): ''' **Arguments:** system An instance of the ``System`` class. **Optional Arguments:** scalings Numpy array [6x1] containing the scaling factors for 1-2, 1-3, 1-4 Lennard-Jones and 1-2, 1-3, 1-4 electrostatic interactions. Default: [0.0,0.0,0.0,0.0,0.0,0.0] pppm_accuracy Desired relative error in electrostatic forces Default: 1e-5 fn_log Filename where LAMMPS output is stored. Default: 'lammps.log' fn_system Filename of file containing system information, can be written using the ```write_lammps_data``` method. Default: lammps.data fn_table Filename of file containing tabulated non-bonded potential without charges, can be written using the ```write_lammps_table``` method. Default: lammps.table triclinic Boolean, specify whether a triclinic cell will be used during the simulation. If the cell is orthogonal, set it to False as LAMMPS should run slightly faster. Default: True comm MPI communicator, required if LAMMPS should run in parallel ''' if system.cell.nvec != 3: raise ValueError( 'The system must be 3d periodic for Lammps calculations.') if not os.path.isfile(fn_system): raise ValueError('Could not read file %s' % fn_system) if not os.path.isfile(fn_table): raise ValueError('Could not read file %s' % fn_table) ForcePart.__init__(self, 'lammps', system) self.system = system self.comm = comm self.triclinic = triclinic self.setup_lammps(fn_system, fn_table, pppm_accuracy, fn_log, scalings) # LAMMPS needs cell vectors (ax,0,0), (bx,by,0) and (cx,cy,cz) # This means we need to perform a rotation to switch between Yaff and # LAMMPS coordinates. All information about this rotation is stored # in the variables defined below self.rvecs = np.eye(3) self.cell = Cell(self.rvecs) self.rot = np.zeros((3, 3))
def align_cell(self, lcs=None, swap=True): """Align the unit cell with respect to the Cartesian Axes frame **Optional Arguments:** lcs The linear combinations of the unit cell that must get aligned. This is a 2x3 array, where each row represents a linear combination of cell vectors. The first row is for alignment with the x-axis, second for the z-axis. The default value is:: np.array([ [1, 0, 0], [0, 0, 1], ]) swap By default, the first alignment is done with the z-axis, then with the x-axis. The order is reversed when swap is set to False. The alignment of the first linear combination is always perfect. The alignment of the second linear combination is restricted to a plane. The cell is always made right-handed. The coordinates are also rotated with respect to the origin, but never inverted. The attributes of the system are modified in-place. Note that this method only works on 3D periodic systems. """ from molmod import Rotation, deg # define the target target = np.array([ [1, 0, 0], [0, 0, 1], ]) # default value for linear combination if lcs is None: lcs = target.copy() # The starting values pos = self.pos rvecs = self.cell.rvecs.copy() if rvecs.shape != (3, 3): raise TypeError( 'The align_cell method only supports 3D periodic systems.') # Optionally swap a cell vector if the cell is not right-handed. if np.linalg.det(rvecs) < 0: # Find a reasonable vector to swap... index = rvecs.sum(axis=1).argmin() rvecs[index] *= -1 # Define the source source = np.dot(lcs, rvecs) # Do the swapping if swap: target = target[::-1] source = source[::-1] # auxiliary function def get_angle_axis(t, s): cos = np.dot(s, t) / np.linalg.norm(s) / np.linalg.norm(t) angle = np.arccos(np.clip(cos, -1, 1)) axis = np.cross(s, t) return angle, axis # first alignment angle, axis = get_angle_axis(target[0], source[0]) if np.linalg.norm(axis) > 0: r1 = Rotation.from_properties(angle, axis, False) pos = r1 * pos rvecs = r1 * rvecs source = r1 * source # second alignment # Make sure the source is orthogonal to target[0] s1p = source[1] - target[0] * np.dot(target[0], source[1]) angle, axis = get_angle_axis(target[1], s1p) r2 = Rotation.from_properties(angle, axis, False) pos = r2 * pos rvecs = r2 * rvecs # assign self.pos = pos self.cell = Cell(rvecs)
def __init__(self, numbers, pos, scopes=None, scope_ids=None, ffatypes=None, ffatype_ids=None, bonds=None, rvecs=None, charges=None, radii=None, valence_charges=None, dipoles=None, radii2=None, masses=None): r'''Initialize a System object. **Arguments:** numbers A numpy array with atomic numbers pos A numpy array (N,3) with atomic coordinates in Bohr. **Optional arguments:** scopes A list with scope names scope_ids A list of scope indexes that links each atom with an element of the scopes list. If this argument is not present, while scopes is given, it is assumed that scopes contains a scope name for every atom, i.e. that it is a list with length natom. In that case, it will be converted automatically to a scopes list with only unique name together with a corresponding scope_ids array. ffatypes A list of labels of the force field atom types. ffatype_ids A list of atom type indexes that links each atom with an element of the list ffatypes. If this argument is not present, while ffatypes is given, it is assumed that ffatypes contains an atom type for every element, i.e. that it is a list with length natom. In that case, it will be converted automatically to a short ffatypes list with only unique elements (within each scope) together with a corresponding ffatype_ids array. bonds a numpy array (B,2) with atom indexes (counting starts from zero) to define the chemical bonds. rvecs An array whose rows are the unit cell vectors. At most three rows are allowed, each containing three Cartesian coordinates. charges An array of atomic charges radii An array of atomic radii, :math:`R_{A,c}`, that determine shape of the atomic charge distribution: .. math:: \rho_{A,c}(\mathbf{r}) = \frac{q_A}{\pi^{3/2}R_{A,c}^3} \exp\left( -\frac{|r - \mathbf{R}_A|^2}{R_{A,c}^2} \right) valence_charges In case a point-core + distribute valence charge is used, this vector contains the valence charges. The core charges can be computed by subtracting the valence charges from the net charges. dipoles An array of atomic dipoles radii2 An array of atomic radii, :math:`R_{A,d}`, that determine shape of the atomic dipole distribution: .. math:: \rho_{A,d}(\mathbf{r}) = -2\frac{\mathbf{d}_A \cdot (\mathbf{r} - \mathbf{R}_A)}{ \sqrt{\pi} R_{A,d}^5 }\exp\left( -\frac{|r - \mathbf{R}_A|^2}{R_{A,d}^2} \right) masses The atomic masses (in atomic units, i.e. m_e) Several attributes are derived from the (optional) arguments: * ``cell`` contains the rvecs attribute and is an instance of the ``Cell`` class. * ``neighs1``, ``neighs2`` and ``neighs3`` are dictionaries derived from ``bonds`` that contain atoms that are separated 1, 2 and 3 bonds from a given atom, respectively. This means that i in system.neighs3[j] is ``True`` if there are three bonds between atoms i and j. ''' if len(numbers.shape) != 1: raise ValueError( 'Argument numbers must be a one-dimensional array.') if pos.shape != (len(numbers), 3): raise ValueError( 'The pos array must have Nx3 rows. Mismatch with numbers argument with shape (N,).' ) self.numbers = numbers self.pos = pos self.ffatypes = ffatypes self.ffatype_ids = ffatype_ids self.scopes = scopes self.scope_ids = scope_ids self.bonds = bonds self.cell = Cell(rvecs) self.charges = charges self.radii = radii self.valence_charges = valence_charges self.dipoles = dipoles self.radii2 = radii2 self.masses = masses with log.section('SYS'): # report some stuff self._init_log() # compute some derived attributes self._init_derived()
def from_files(cls, guest, parameters, **kwargs): """Automated setup of GCMC simulation **Arguments:** guest Two types are accepted: (i) the filename of a system file describing one guest molecule, (ii) a System instance of one guest molecule parameters Force-field parameters describing guest-guest and optionally host-guest interaction. Three types are accepted: (i) the filename of the parameter file, which is a text file that adheres to YAFF parameter format, (ii) a list of such filenames, or (iii) an instance of the Parameters class. **Optional arguments:** hooks A list of MCHooks host Two types are accepted: (i) the filename of a system file describing the host system, (ii) a System instance of the host All other keyword arguments are passed to the ForceField constructor See the constructor of the :class:`yaff.pes.generator.FFArgs` class for the available optional arguments. """ # Load the guest System if isinstance(guest, str): guest = System.from_file(guest) assert isinstance(guest, System) # We want to control nlow and nhigh here ourselves, so remove it from the # optional arguments if the user provided it. kwargs.pop('nlow', None) kwargs.pop('nhigh', None) # Rough guess for number of adsorbed guests nguests = kwargs.pop('nguests', 10) # Load the host if it is present as a keyword host = kwargs.pop('host', None) # Extract the hooks hooks = kwargs.pop('hooks', []) # Efficient treatment of reciprocal ewald contribution if not 'reci_ei' in kwargs.keys(): kwargs['reci_ei'] = 'ewald_interaction' if host is not None: if isinstance(host, str): host = System.from_file(host) assert isinstance(host, System) # If the guest molecule is currently an isolated molecule, than put # it in the same periodic box as the host if guest.cell is None or guest.cell.nvec==0: guest.cell = Cell(host.cell.rvecs) # Construct a complex of host and one guest and the corresponding # force field excluding host-host interactions hostguest = host.merge(guest) external_potential = ForceField.generate(hostguest, parameters, nlow=host.natom, nhigh=host.natom, **kwargs) else: external_potential = None # # Compare the energy of the guest, once isolated, once in a periodic box # guest_isolated = guest.subsystem(np.arange(guest.natom)) # guest_isolated.cell = Cell(np.zeros((0,3))) # optional_arguments = {} # for key in kwargs.keys(): # if key=='reci_ei': continue # optional_arguments[key] = kwargs[key] # ff_guest_isolated = ForceField.generate(guest_isolated, parameters, **optional_arguments) # e_isolated = ff_guest_isolated.compute() # guest_periodic = guest.subsystem(np.arange(guest.natom)) # ff_guest_periodic = ForceField.generate(guest_periodic, parameters, **optional_arguments) # e_periodic = ff_guest_periodic.compute() # if np.abs(e_isolated-e_periodic)>1e-4: # if log.do_warning: # log.warn("An interaction energy of %s of the guest with its periodic " # "images was detected. The interaction of a guest with its periodic " # "images will however NOT be taken into account in this simulation. " # "If the energy difference is large compared to k_bT, you should " # "consider using a supercell." % (log.energy(e_isolated-e_periodic))) # By making use of nlow=nhigh, we automatically discard intramolecular energies eguest = 0.0 # Generator of guest-guest force fields, excluding interactions # between the first N-1 guests def ff_generator(system, guest): return ForceField.generate(system, parameters, nlow=max(0,system.natom-guest.natom), nhigh=max(0,system.natom-guest.natom), **kwargs) return cls(guest, ff_generator, external_potential=external_potential, eguest=eguest, hooks=hooks, nguests=nguests)