def setup_plumed(self, timestep, restart): r'''Send commands to PLUMED to make it computation-ready. **Arguments:** timestep The timestep (in au) of the integrator restart Set to a value different from 0 to let PLUMED know that this is a restarted run ''' # Try to load the plumed Python wrapper, quit if not possible try: from plumed import Plumed except: log("Could not import the PLUMED python wrapper!") raise ImportError self.plumed = Plumed(kernel=self.kernel) # Conversion between PLUMED internal units and YAFF internal units # Note that PLUMED output will follow the PLUMED conventions # concerning units self.plumed.cmd("setMDEnergyUnits", 1.0 / kjmol) self.plumed.cmd("setMDLengthUnits", 1.0 / nanometer) self.plumed.cmd("setMDTimeUnits", 1.0 / picosecond) # Initialize the system in PLUMED self.plumed.cmd("setPlumedDat", self.fn) self.plumed.cmd("setNatoms", self.system.natom) self.plumed.cmd("setMDEngine", "YAFF") self.plumed.cmd("setLogFile", self.fn_log) self.plumed.cmd("setTimestep", timestep) self.plumed.cmd("setRestart", restart) self.plumed.cmd("init")
def __call__(self, iterative): r'''When this point is reached, a complete time integration step was finished and PLUMED should be notified about this. ''' if not self.hooked: if log.do_high: log.hline() log("Reinitializing PLUMED") log.hline() if log.do_warning: log.warn("You are using PLUMED as a hook for your integrator. " "If PLUMED adds time-dependent forces (for instance " "when performing metadynamics) there is no energy " "conservation. The conserved quantity reported by " "YAFF is irrelevant in this case.") self.setup_plumed(timestep=iterative.timestep, restart=iterative.counter > 0) self.hooked = True # PLUMED provides a setEnergy command, which should pass the # current potential energy. It seems that this is never used, so we # don't pass anything for the moment. # current_energy = sum([part.energy for part in iterative.ff.parts[:-1] if not isinstance(part, ForcePartPlumed)]) # self.plumed.cmd("setEnergy", current_energy) self.plumedstep = iterative.counter self._internal_compute(None, None) self.plumed.cmd("update")
def generate(cls, system, parameters, **kwargs): """Create a force field for the given system with the given parameters. **Arguments:** system An instance of the System class parameters 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. See the constructor of the :class:`yaff.pes.generator.FFArgs` class for the available optional arguments. This method takes care of setting up the FF object, and configuring all the necessary FF parts. This is a lot easier than creating an FF with the default constructor. Parameters for atom types that are not present in the system, are simply ignored. """ if system.ffatype_ids is None: raise ValueError( 'The generators needs ffatype_ids in the system object.') with log.section('GEN'), timer.section('Generator'): from yaff.pes.generator import apply_generators, FFArgs from yaff.pes.parameters import Parameters if log.do_medium: log('Generating force field from %s' % str(parameters)) if not isinstance(parameters, Parameters): parameters = Parameters.from_file(parameters) ff_args = FFArgs(**kwargs) apply_generators(system, parameters, ff_args) return ForceField(system, ff_args.parts, ff_args.nlist)
def __init__(self, system, part_pair): ''' **Arguments:** system An instance of the ``System`` class. part_pair An instance of the ``PairPot`` class. This force part is only applicable to systems that are 3D periodic. ''' if system.cell.nvec != 3: raise ValueError( 'Tail corrections can only be applied to 3D periodic systems') if part_pair.name in ['pair_ei', 'pair_eidip']: raise ValueError('Tail corrections are divergent for %s' % part_pair.name) super(ForcePartTailCorrection, self).__init__('tailcorr_%s' % (part_pair.name), system) self.ecorr, self.wcorr = part_pair.pair_pot.prepare_tailcorrections( system.natom) self.system = system if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline()
def __init__(self, system, grids): ''' **Arguments:** system An instance of the ``System`` class. grids A dictionary with (ffatype, grid) items. Each grid must be a three-dimensional array with energies. This force part is only applicable to systems that are 3D periodic. ''' if system.cell.nvec != 3: raise ValueError( 'The system must be 3d periodic for the grid term.') for grid in grids.values(): if grid.ndim != 3: raise ValueError('The energy grids must be 3D numpy arrays.') ForcePart.__init__(self, 'grid', system) self.system = system self.grids = grids if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline()
def set_external_conditions(self, T, fugacity): """ **Arguments:** T Temperature fugacity Fugacity. If the species behaves more or less like an ideal gas, this is equal to the pressure. Otherwise, the fugacity can be obtained from the pressure using an equation of state from yaff.pes.eos """ # External conditions assert T>0.0 self.T = T self.beta = 1.0/boltzmann/self.T assert fugacity>=0.0 self.fugacity = fugacity self.conditions_set = True if log.do_medium: with log.section(self.log_name): # log.pressure does not exist yet, what a pity... log("GCMC simulation with T = %s and fugacity = %12.6f bar"%( log.temperature(self.T), self.fugacity/bar))
def __init__(self, system, parts, nlist=None): """ **Arguments:** system An instance of the ``System`` class. parts A list of instances of sublcasses of ``ForcePart``. These are the different types of contributions to the force field, e.g. valence interactions, real-space electrostatics, and so on. **Optional arguments:** nlist A ``NeighborList`` instance. This is required if some items in the parts list use this nlist object. """ ForcePart.__init__(self, 'all', system) self.system = system self.parts = [] self.nlist = nlist self.needs_nlist_update = nlist is not None for part in parts: self.add_part(part) if log.do_medium: with log.section('FFINIT'): log('Force field with %i parts:&%s.' % (len(self.parts), ', '.join(part.name for part in self.parts))) log('Neighborlist present: %s' % (self.nlist is not None))
def get_trajectory_datasets(tgrp, *fields): '''Return a list of new/existing datasets corresponding to the given fields **Arguments:** tgrp The trajectory group fields A list of fields, i.e. pairs of name and row_shape. ''' result = [] for name, row_shape in fields: if name in tgrp: ds = tgrp[name] if ds.shape[1:] != row_shape: raise TypeError('The shape of the existing dataset is not compatible with the new data.') if log.do_medium: log('Found an existing dataset %s in group %s with %i rows.' % (name, tgrp.name, ds.shape[0])) else: if log.do_high: log('Creating new dataset %s with row shape %s' % (name, row_shape)) # Create a new dataset shape = (0,) + row_shape maxshape = (None,) + row_shape ds = tgrp.create_dataset(name, shape, maxshape=maxshape, dtype=float) result.append(ds) return result
def polynomial_roots(self): """ Find the real roots of the polynomial form of the Peng-Robinson equation of state """ a = -(1 - self.B) b = self.A - 2 * self.B - 3 * self.B**2 c = -(self.A * self.B - self.B**2 - self.B**3) Q = (a**2 - 3 * b) / 9 R = (2 * a**3 - 9 * a * b + 27 * c) / 54 M = R**2 - Q**3 if M > 0: S = np.cbrt(-R + np.sqrt(M)) T = np.cbrt(-R - np.sqrt(M)) Z = S + T - a / 3 else: theta = np.arccos(R / np.sqrt(Q**3)) x1 = -2.0 * np.sqrt(Q) * np.cos(theta / 3) - a / 3 x2 = -2.0 * np.sqrt(Q) * np.cos((theta + 2 * np.pi) / 3) - a / 3 x3 = -2.0 * np.sqrt(Q) * np.cos((theta - 2 * np.pi) / 3) - a / 3 solutions = np.array([x1, x2, x3]) solutions = solutions[solutions > 0.0] if self.phase == 'vapour': Z = np.amax(solutions) elif self.phase == 'liquid': Z = np.amin(solutions) else: raise NotImplementedError if log.do_high: log("Found 3 solutions for Z (%f,%f,%f), meaning that two " "phases coexist. Returning Z=%f, corresponding to the " "%s phase" % (x1, x2, x3, Z, self.phase)) return Z
def generate(cls, system, parameters, **kwargs): """Create a force field for the given system with the given parameters. **Arguments:** system An instance of the System class parameters 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. See the constructor of the :class:`yaff.pes.generator.FFArgs` class for the available optional arguments. This method takes care of setting up the FF object, and configuring all the necessary FF parts. This is a lot easier than creating an FF with the default constructor. Parameters for atom types that are not present in the system, are simply ignored. """ if system.ffatype_ids is None: raise ValueError('The generators needs ffatype_ids in the system object.') with log.section('GEN'), timer.section('Generator'): from yaff.pes.generator import apply_generators, FFArgs from yaff.pes.parameters import Parameters if log.do_medium: log('Generating force field from %s' % str(parameters)) if not isinstance(parameters, Parameters): parameters = Parameters.from_file(parameters) ff_args = FFArgs(**kwargs) apply_generators(system, parameters, ff_args) return ForceField(system, ff_args.parts, ff_args.nlist)
def __init__(self, system, parts, nlist=None): """ **Arguments:** system An instance of the ``System`` class. parts A list of instances of sublcasses of ``ForcePart``. These are the different types of contributions to the force field, e.g. valence interactions, real-space electrostatics, and so on. **Optional arguments:** nlist A ``NeighborList`` instance. This is required if some items in the parts list use this nlist object. """ ForcePart.__init__(self, 'all', system) self.system = system self.parts = [] self.nlist = nlist self.needs_nlist_update = nlist is not None for part in parts: self.add_part(part) if log.do_medium: with log.section('FFINIT'): log('Force field with %i parts:&%s.' % ( len(self.parts), ', '.join(part.name for part in self.parts) )) log('Neighborlist present: %s' % (self.nlist is not None))
def __init__(self, system, grids): ''' **Arguments:** system An instance of the ``System`` class. grids A dictionary with (ffatype, grid) items. Each grid must be a three-dimensional array with energies. This force part is only applicable to systems that are 3D periodic. ''' if system.cell.nvec != 3: raise ValueError('The system must be 3d periodic for the grid term.') for grid in grids.itervalues(): if grid.ndim != 3: raise ValueError('The energy grids must be 3D numpy arrays.') ForcePart.__init__(self, 'grid', system) self.system = system self.grids = grids if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline()
def await_header(self): data = self.s.recv(HDRLEN * L_CHAR) if self.verbose: print('RECV', (data,)) with log.section('DRIVER'): log("RECV %s %s" % (data, datetime.now())) return data
def load_hdf5(self, fn, T=None): """ Read information from HDF5 file **Arguments:** fn A HDF5 filename containing a hills group. If this concerns a well- tempered MTD run, the simulation temperature should be provided Otherwise, it will be read from the HDF5 file. """ with h5.File(fn,'r') as f: q0s = f['hills/q0'][:] Ks = f['hills/K'][:] sigmas = f['hills/sigma'][:] tempering = f['hills'].attrs['tempering'] if tempering!=0.0: if T is None: if not 'trajectory/temp' in f: raise ValueError("For a well-tempered MTD run, the temperature " "should be specified or readable from the trajectory/temp " "group in the HDF5 file") T = np.mean(f['trajectory/temp'][:]) if log.do_medium: log("Well-tempered MTD run: T = %s deltaT = %s"%(log.temperature(T), log.temperature(tempering))) if 'hills/periodicities' in f: periodicities = f['hills/periodicities'][:] else: periodicities = None self.set_hills(q0s, Ks, sigmas, tempering=tempering, T=T, periodicities=periodicities)
def update_gmax(self): '''This routine must be called after the attribute self.gmax is modified.''' self.gmax = np.ceil(self.gcut / self.system.cell.gspacings - 0.5).astype(int) if log.do_debug: with log.section('EWALD'): log('gmax a,b,c = %i,%i,%i' % tuple(self.gmax))
def to_file(self, fn): """Write the system to a file **Arguments:** fn The file to write to. Supported formats are: chk Internal human-readable checkpoint format. This format includes all the information of a system object. All data are stored in atomic units. h5 Internal binary checkpoint format. This format includes all the information of a system object. All data are stored in atomic units. xyz A simple file with atomic positions and elements. Coordinates are written in Angstroms. """ if fn.endswith('.chk'): from molmod.io import dump_chk dump_chk( fn, { 'numbers': self.numbers, 'pos': self.pos, 'ffatypes': self.ffatypes, 'ffatype_ids': self.ffatype_ids, 'scopes': self.scopes, 'scope_ids': self.scope_ids, 'bonds': self.bonds, 'rvecs': self.cell.rvecs, 'charges': self.charges, 'radii': self.radii, 'valence_charges': self.valence_charges, 'dipoles': self.dipoles, 'radii2': self.radii2, 'masses': self.masses, }) elif fn.endswith('.h5'): with h5.File(fn, 'w') as f: self.to_hdf5(f) elif fn.endswith('.xyz'): from molmod.io import XYZWriter from molmod.periodic import periodic xyz_writer = XYZWriter(fn, [periodic[n].symbol for n in self.numbers]) xyz_writer.dump(str(self), self.pos) else: raise NotImplementedError( 'The extension of %s does not correspond to any known format.' % fn) if log.do_high: with log.section('SYS'): log('Wrote system to %s.' % fn)
def send_header(self, header): assert len(header) <= 12 header = header.ljust(12, ' ') if self.verbose: print('SEND', (header,)) with log.section('DRIVER'): log("SEND %s %s" % (header, datetime.now())) self.s.send(header)
def g09log_to_hdf5(f, fn_log): """Convert Gaussian09 BOMD log file to Yaff HDF5 format. **Arguments:** f An open and writable HDF5 file. fn_log The name of the Gaussian log file. """ with log.section('G09H5'): if log.do_medium: log('Loading Gaussian 09 file \'%s\' into \'trajectory\' of HDF5 file \'%s\'' % ( fn_log, f.filename )) # First make sure the HDF5 file has a system description that is consistent # with the XYZ file. if 'system' not in f: raise ValueError('The HDF5 file must contain a system group.') if 'numbers' not in f['system']: raise ValueError('The HDF5 file must have a system group with atomic numbers.') natom = f['system/numbers'].shape[0] # Take care of the trajectory group tgrp = get_trajectory_group(f) # Take care of the pos and vel datasets dss = get_trajectory_datasets(tgrp, ('pos', (natom, 3)), ('vel', (natom, 3)), ('frc', (natom, 3)), ('time', (1,)), ('step', (1,)), ('epot', (1,)), ('ekin', (1,)), ('etot', (1,)), ) ds_pos, ds_vel, ds_frc, ds_time, ds_step, ds_epot, ds_ekin, ds_etot = dss # Load frame by frame row = get_last_trajectory_row(dss) for numbers, pos, vel, frc, time, step, epot, ekin, etot in _iter_frames_g09(fn_log): if (numbers != f['system/numbers']).any(): log.warn('The element numbers of the HDF5 and LOG file do not match.') write_to_dataset(ds_pos, pos, row) write_to_dataset(ds_vel, vel, row) write_to_dataset(ds_frc, frc, row) write_to_dataset(ds_time, time, row) write_to_dataset(ds_step, step, row) write_to_dataset(ds_epot, epot, row) write_to_dataset(ds_ekin, ekin, row) write_to_dataset(ds_etot, etot, row) row += 1 # Check number of rows check_trajectory_rows(tgrp, dss, row)
def to_file(self, fn): """Write the system to a file **Arguments:** fn The file to write to. Supported formats are: chk Internal human-readable checkpoint format. This format includes all the information of a system object. All data are stored in atomic units. h5 Internal binary checkpoint format. This format includes all the information of a system object. All data are stored in atomic units. xyz A simple file with atomic positions and elements. Coordinates are written in Angstroms. """ if fn.endswith('.chk'): from molmod.io import dump_chk dump_chk(fn, { 'numbers': self.numbers, 'pos': self.pos, 'ffatypes': self.ffatypes, 'ffatype_ids': self.ffatype_ids, 'scopes': self.scopes, 'scope_ids': self.scope_ids, 'bonds': self.bonds, 'rvecs': self.cell.rvecs, 'charges': self.charges, 'radii': self.radii, 'valence_charges': self.valence_charges, 'dipoles': self.dipoles, 'radii2': self.radii2, 'masses': self.masses, }) elif fn.endswith('.h5'): with h5.File(fn, 'w') as f: self.to_hdf5(f) elif fn.endswith('.xyz'): from molmod.io import XYZWriter from molmod.periodic import periodic xyz_writer = XYZWriter(fn, [periodic[n].symbol for n in self.numbers]) xyz_writer.dump(str(self), self.pos) else: raise NotImplementedError('The extension of %s does not correspond to any known format.' % fn) if log.do_high: with log.section('SYS'): log('Wrote system to %s.' % fn)
def update(self): '''Rebuild or recompute the neighbor lists Based on the changes of the atomic positions or due to calls to ``update_rcut`` and ``update_rmax``, the neighbor lists will be rebuilt from scratch. The heavy computational work is done in low-level C routines. The neighbor lists array is reallocated if needed. The memory allocation is done in Python for convenience. ''' with log.section('NLIST'), timer.section('Nlists'): assert self.rcut > 0 if self._need_rebuild(): # *rebuild* the entire neighborlist if self.system.cell.volume != 0: if self.system.natom / self.system.cell.volume > 10: raise ValueError('Atom density too high') # 1) make an initial status object for the neighbor list algorithm status = nlist_status_init(self.rmax) # The atom index of the first atom in pair is always at least # nlow. The following status initialization avoids searching # for excluded atom pairs in the neighbourlist build status[3] = self.nlow # 2) a loop of consecutive update/allocate calls last_start = 0 while True: done = nlist_build(self.system.pos, self.rcut + self.skin, self.rmax, self.system.cell, status, self.neighs[last_start:], self.nlow, self.nhigh) if done: break last_start = len(self.neighs) new_neighs = np.empty((len(self.neighs) * 3) // 2, dtype=neigh_dtype) new_neighs[:last_start] = self.neighs self.neighs = new_neighs del new_neighs # 3) get the number of neighbors in the list. self.nneigh = nlist_status_finish(status) if log.do_debug: log('Rebuilt, size = %i' % self.nneigh) # 4) store the current state to check in future calls if we # need to do a rebuild or a recompute. self._checkpoint() self.rebuild_next = False else: # just *recompute* the deltas and the distance in the # neighborlist nlist_recompute(self.system.pos, self._pos_old, self.system.cell, self.neighs[:self.nneigh]) if log.do_debug: log('Recomputed')
def __call__(self, iterative): if log.do_medium: if self.time0 is None: self.time0 = time.time() if log.do_medium: log.hline() log('counter Walltime') log.hline() log('%7i %10.1f' % ( iterative.counter, time.time() - self.time0, ))
def _need_rebuild(self): '''Internal method that determines if a rebuild is needed.''' if self.skin <= 0 or self._pos_old is None or self.rebuild_next: return True else: # Compute an upper bound for the maximum relative displacement. disp = np.sqrt(((self.system.pos - self._pos_old)**2).sum(axis=1).max()) disp *= 2*(self.rmax.max()+1) if log.do_debug: log('Maximum relative displacement %s Skin %s' % (log.length(disp), log.length(self.skin))) # Compare with skin parameter return disp >= self.skin
def apply(self, par_table, cpar_table, scale_table, mixing_rules, system, ff_args): # Prepare the atomic parameters amps = np.zeros(system.nffatype, float) bs = np.zeros(system.nffatype, float) for i in xrange(system.nffatype): key = (system.ffatypes[i],) par_list = par_table.get(key, []) if len(par_list) == 0: if log.do_warning: log.warn('No EXPREP parameters found for ffatype %s.' % system.ffatypes[i]) else: amps[i], bs[i] = par_list[0] # Prepare the cross parameters amp_cross = np.zeros((system.nffatype, system.nffatype), float) b_cross = np.zeros((system.nffatype, system.nffatype), float) for i0 in xrange(system.nffatype): for i1 in xrange(i0+1): cpar_list = cpar_table.get((system.ffatypes[i0], system.ffatypes[i1]), []) if len(cpar_list) == 0: if log.do_high: log('No EXPREP cross parameters found for ffatypes %s,%s. Mixing rule will be used' % (system.ffatypes[i0], system.ffatypes[i1])) else: amp_cross[i0,i1], b_cross[i0,i1] = cpar_list[0] if i0 != i1: amp_cross[i1,i0], b_cross[i1,i0] = cpar_list[0] # Prepare the global parameters scalings = Scalings(system, scale_table[1], scale_table[2], scale_table[3]) amp_mix, amp_mix_coeff = mixing_rules['A'] if amp_mix == 0: amp_mix_coeff = 0.0 elif amp_mix == 1: amp_mix_coeff = amp_mix_coeff[0] b_mix, b_mix_coeff = mixing_rules['B'] if b_mix == 0: b_mix_coeff = 0.0 elif b_mix == 1: b_mix_coeff = b_mix_coeff[0] # Get the part. It should not exist yet. part_pair = ff_args.get_part_pair(PairPotExpRep) if part_pair is not None: raise RuntimeError('Internal inconsistency: the EXPREP part should not be present yet.') pair_pot = PairPotExpRep( system.ffatype_ids, amp_cross, b_cross, ff_args.rcut, ff_args.tr, amps, amp_mix, amp_mix_coeff, bs, b_mix, b_mix_coeff, ) nlist = ff_args.get_nlist(system) part_pair = ForcePartPair(system, nlist, scalings, pair_pot) ff_args.parts.append(part_pair)
def __call__(self, mc): if log.do_medium: if self.time0 is None: self.time0 = time.time() if log.do_medium: log.hline() log(' counter %s Walltime' % (mc.log_header())) log.hline() log('%12i %s %10.1f' % ( mc.counter, mc.log(), time.time() - self.time0, ))
def update(self, dx, dg): tmp = dg - np.dot(self.hessian, dx) denom = np.dot(tmp, dx) if abs(denom) > 1e-5 * np.linalg.norm(dx) * np.linalg.norm(tmp): if log.do_debug: log("Updating SR1 Hessian. denom=%10.3e" % denom) self.hessian += np.outer(tmp, tmp) / denom return True else: if log.do_high: log("Skipping SR1 update because denom=%10.3e is not big enough." % denom) return False
def update(self, dx, dg): tmp = dg - np.dot(self.hessian, dx) denom = np.dot(tmp, dx) if abs(denom) > 1e-5*np.linalg.norm(dx)*np.linalg.norm(tmp): if log.do_debug: log('Updating SR1 Hessian. denom=%10.3e' % denom) self.hessian += np.outer(tmp, tmp)/denom return True else: if log.do_high: log('Skipping SR1 update because denom=%10.3e is not big enough.' % denom) return False
def _verify_hooks(self): with log.section('ENSEM'): thermo = None index_thermo = 0 baro = None index_baro = 0 # Look for the presence of a thermostat and/or barostat if hasattr(self.hooks, '__len__'): for index, hook in enumerate(self.hooks): if hook.method == 'thermostat': thermo = hook index_thermo = index elif hook.method == 'barostat': baro = hook index_baro = index elif self.hooks is not None: if self.hooks.method == 'thermostat': thermo = self.hooks elif self.hooks.method == 'barostat': baro = self.hooks # If both are present, delete them and generate TBCombination element if thermo is not None and baro is not None: from yaff.sampling.npt import TBCombination if log.do_warning: log.warn( 'Both thermostat and barostat are present separately and will be merged' ) del self.hooks[max(index_thermo, index_thermo)] del self.hooks[min(index_thermo, index_baro)] self.hooks.append(TBCombination(thermo, baro)) if hasattr(self.hooks, '__len__'): for hook in self.hooks: if hook.name == 'TBCombination': thermo = hook.thermostat baro = hook.barostat elif self.hooks is not None: if self.hooks.name == 'TBCombination': thermo = self.hooks.thermostat baro = self.hooks.barostat if log.do_warning: if thermo is not None: log('Temperature coupling achieved through ' + str(thermo.name) + ' thermostat') if baro is not None: log('Pressure coupling achieved through ' + str(baro.name) + ' barostat')
def make_step(self): # get relevant hessian information evals, evecs = self.hessian.get_spectrum() if log.do_high: log(' lowest eigen value: %7.1e' % evals.min()) log('highest eigen value: %7.1e' % evals.max()) # convert gradient to eigen basis grad_eigen = np.dot(evecs.T, self.g_old) while True: # Find the step with the given radius. If the hessian is positive # definite and the unconstrained step is smaller than the trust # radius, this step is returned delta_eigen = solve_trust_radius(grad_eigen, evals, self.trust_radius) radius = np.linalg.norm(delta_eigen) # convert the step to user basis delta_x = np.dot(evecs, delta_eigen) # compute the function and gradient at the new position x = self.x_old + delta_x f, g = self.fun(x, True) # compute the change in function value delta_f = f - self.f_old # compute the change in norm of the gradient delta_norm_g = np.linalg.norm(g) - np.linalg.norm(self.g_old) # must_shrink is a parameter to control the trust radius must_shrink = False if delta_f > 0: # The function must decrease, if not the trust radius is too big. if log.do_high: log('Function increases.') must_shrink = True if (self.trust_radius < self.small_radius and delta_norm_g > 0): # When the trust radius becomes small, the numerical noise on # the energy may be too large to detect an increase energy. # In that case the norm of the gradient is used instead. if log.do_high: log('Gradient norm increases.') must_shrink = True if must_shrink: self.trust_radius *= 0.5 while self.trust_radius >= radius: self.trust_radius *= 0.5 if self.trust_radius < self.too_small_radius: raise RuntimeError('The trust radius becomes too small. Is the potential energy surface smooth?') else: # If we get here, we are done with the trust radius loop. if log.do_high: log.hline() # It is fine to increase the trust radius a little after a # successful step. if self.trust_radius < self.initial_trust_radius: self.trust_radius *= 2.0 # Return the results of the successful step return x, f, g
def make_step(self): # get relevant hessian information evals, evecs = self.hessian.get_spectrum() if log.do_high: log(" lowest eigen value: %7.1e" % evals.min()) log("highest eigen value: %7.1e" % evals.max()) # convert gradient to eigen basis grad_eigen = np.dot(evecs.T, self.g_old) while True: # Find the step with the given radius. If the hessian is positive # definite and the unconstrained step is smaller than the trust # radius, this step is returned delta_eigen = solve_trust_radius(grad_eigen, evals, self.trust_radius) radius = np.linalg.norm(delta_eigen) # convert the step to user basis delta_x = np.dot(evecs, delta_eigen) # compute the function and gradient at the new position x = self.x_old + delta_x f, g = self.fun(x, True) # compute the change in function value delta_f = f - self.f_old # compute the change in norm of the gradient delta_norm_g = np.linalg.norm(g) - np.linalg.norm(self.g_old) # must_shrink is a parameter to control the trust radius must_shrink = False if delta_f > 0: # The function must decrease, if not the trust radius is too big. if log.do_high: log("Function increases.") must_shrink = True if self.trust_radius < self.small_radius and delta_norm_g > 0: # When the trust radius becomes small, the numerical noise on # the energy may be too large to detect an increase energy. # In that case the norm of the gradient is used instead. if log.do_high: log("Gradient norm increases.") must_shrink = True if must_shrink: self.trust_radius *= 0.5 while self.trust_radius >= radius: self.trust_radius *= 0.5 if self.trust_radius < self.too_small_radius: raise RuntimeError("The trust radius becomes too small. Is the potential energy surface smooth?") else: # If we get here, we are done with the trust radius loop. if log.do_high: log.hline() # It is fine to increase the trust radius a little after a # successful step. if self.trust_radius < self.initial_trust_radius: self.trust_radius *= 2.0 # Return the results of the successful step return x, f, g
def __init__(self, comsystem, scaling=None): ForcePart.__init__(self, 'valence_com', comsystem) #ForcePartValence.__init__(self, system) self.comlist = comsystem.comlist self.gpos = np.zeros((comsystem.gpos_dim, 3), float) self.dlist = DeltaList(self.comlist) self.iclist = InternalCoordinateList(self.dlist) self.vlist = ValenceList(self.iclist) self.scaling = scaling if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline() self.term = None # volume term
def update(self): '''Rebuild or recompute the neighbor lists Based on the changes of the atomic positions or due to calls to ``update_rcut`` and ``update_rmax``, the neighbor lists will be rebuilt from scratch. The heavy computational work is done in low-level C routines. The neighbor lists array is reallocated if needed. The memory allocation is done in Python for convenience. ''' with log.section('NLIST'), timer.section('Nlists'): assert self.rcut > 0 if self._need_rebuild(): # *rebuild* the entire neighborlist if self.system.cell.volume != 0: if self.system.natom/self.system.cell.volume > 10: raise ValueError('Atom density too high') # 1) make an initial status object for the neighbor list algorithm status = nlist_status_init(self.rmax) # 2) a loop of consecutive update/allocate calls last_start = 0 while True: done = nlist_build( self.system.pos, self.rcut + self.skin, self.rmax, self.system.cell, status, self.neighs[last_start:] ) if done: break last_start = len(self.neighs) new_neighs = np.empty((len(self.neighs)*3)/2, dtype=neigh_dtype) new_neighs[:last_start] = self.neighs self.neighs = new_neighs del new_neighs # 3) get the number of neighbors in the list. self.nneigh = nlist_status_finish(status) if log.do_debug: log('Rebuilt, size = %i' % self.nneigh) # 4) store the current state to check in future calls if we # need to do a rebuild or a recompute. self._checkpoint() self.rebuild_next = False else: # just *recompute* the deltas and the distance in the # neighborlist nlist_recompute(self.system.pos, self._pos_old, self.system.cell, self.neighs[:self.nneigh]) if log.do_debug: log('Recomputed')
def __init__(self, system): ''' **Arguments:** system An instance of the ``System`` class. ''' ForcePart.__init__(self, 'valence', system) self.dlist = DeltaList(system) self.iclist = InternalCoordinateList(self.dlist) self.vlist = ValenceList(self.iclist) if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline()
def add_term(self, term): '''Add a new term to the covalent force field. **Arguments:** term An instance of the class :class:`yaff.pes.ff.vlist.ValenceTerm`. In principle, one should add all energy terms before calling the ``compute`` method, but with the current implementation of Yaff, energy terms can be added at any time. (This may change in future.) ''' if log.do_high: with log.section('VTERM'): log('%7i&%s %s' % (self.vlist.nv, term.get_log(), ' '.join(ic.get_log() for ic in term.ics))) self.vlist.add_term(term)
def __init__(self, system, alpha, gcut=0.35, dielectric=1.0, exclude_frame=False, n_frame=0): ''' **Arguments:** system The system to which this interaction applies. alpha The alpha parameter in the Ewald summation method. **Optional arguments:** gcut The cutoff in reciprocal space. dielectric The scalar relative permittivity of the system. exclude_frame A boolean to exclude framework-framework interactions (exclude_frame=True) for efficiency sake in MC simulations. n_frame Number of framework atoms. This parameter is used to exclude framework-framework neighbors when exclude_frame=True. ''' ForcePart.__init__(self, 'ewald_reci', system) if not system.cell.nvec == 3: raise TypeError('The system must have a 3D periodic cell.') if system.charges is None: raise ValueError('The system does not have charges.') self.system = system self.alpha = alpha self.gcut = gcut self.dielectric = dielectric self.update_gmax() self.work = np.empty(system.natom * 2) if exclude_frame == True and n_frame < 0: raise ValueError( 'The number of framework atoms to exclude must be positive.') elif exclude_frame == False: n_frame = 0 self.n_frame = n_frame if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline() log(' alpha: %s' % log.invlength(self.alpha)) log(' gcut: %s' % log.invlength(self.gcut)) log(' relative permittivity: %5.3f' % self.dielectric) log.hline()
def _verify_hooks(self): with log.section('ENSEM'): thermo = None index_thermo = 0 baro = None index_baro = 0 # Look for the presence of a thermostat and/or barostat if hasattr(self.hooks, '__len__'): for index, hook in enumerate(self.hooks): if hook.method == 'thermostat': thermo = hook index_thermo = index elif hook.method == 'barostat': baro = hook index_baro = index elif self.hooks is not None: if self.hooks.method == 'thermostat': thermo = self.hooks elif self.hooks.method == 'barostat': baro = self.hooks # If both are present, delete them and generate TBCombination element if thermo is not None and baro is not None: from yaff.sampling.npt import TBCombination if log.do_warning: log.warn('Both thermostat and barostat are present separately and will be merged') del self.hooks[max(index_thermo, index_thermo)] del self.hooks[min(index_thermo, index_baro)] self.hooks.append(TBCombination(thermo, baro)) if hasattr(self.hooks, '__len__'): for hook in self.hooks: if hook.name == 'TBCombination': thermo = hook.thermostat baro = hook.barostat elif self.hooks is not None: if self.hooks.name == 'TBCombination': thermo = self.hooks.thermostat baro = self.hooks.barostat if log.do_warning: if thermo is not None: log('Temperature coupling achieved through ' + str(thermo.name) + ' thermostat') if baro is not None: log('Pressure coupling achieved through ' + str(baro.name) + ' barostat')
def get_trajectory_group(f): '''Create or return an existing trajectory group **Arguments:** f An open HDF5 File or Group object. ''' if 'trajectory' not in f: if log.do_high: log('Creating new trajectory datagroup in %s.' % f.filename) tgrp = f.create_group('trajectory') else: tgrp = f['trajectory'] if log.do_high: log('Using existing trajectory datagroup in %s.' % f.filename) return tgrp
def add_term(self, term): '''Add a new term to the covalent force field. **Arguments:** term An instance of the class :class:`yaff.pes.ff.vlist.ValenceTerm`. In principle, one should add all energy terms before calling the ``compute`` method, but with the current implementation of Yaff, energy terms can be added at any time. (This may change in future.) ''' if log.do_high: with log.section('VTERM'): log('%7i&%s %s' % (self.vlist.nv, term.get_log(), ' '.join( ic.get_log() for ic in term.ics))) self.vlist.add_term(term)
def set_hills(self, q0s, Ks, sigmas, tempering=0.0, T=None, periodicities=None): # Safety checks assert q0s.shape[1]==self.ncv assert sigmas.shape[0]==self.ncv assert q0s.shape[0]==Ks.shape[0] if tempering != 0.0 and T is None: raise ValueError("For a well-tempered MTD run, the temperature " "has to be specified") self.q0s = q0s self.sigmas = sigmas self.Ks = Ks self.tempering = tempering self.T = T self.periodicities = periodicities if log.do_medium: with log.section("SUMHILL"): log("Found %d collective variables and %d Gaussian hills"%(self.ncv,self.q0s.shape[0]))
def add_term(self, term, use_comlist=False): '''Add a new term to the bias potential. **Arguments:** term An instance of the class :class:`yaff.pes.ff.vlist.ValenceTerm` or an instance of the class :class:`yaff.pes.ff.vias.BiasPotential` **Optional arguments:** use_comlist Boolean indicating whether the comlist should be used for adding this ValenceTerm ''' if isinstance(term, ValenceTerm): if use_comlist: raise NotImplementedError('Cannot use COMList') if self.valence_com is None: raise TypeError( "No COMList was provided when setting up the ForcePartBias" ) self.term_lookup.append((2, self.valence_com.vlist.nv)) # Keep track of the index this term gets in the ValenceList self.valence_com.vlist.add_term(term) else: self.term_lookup.append((1, self.valence.vlist.nv)) # Add to the ValenceList self.valence.vlist.add_term(term) if log.do_high: with log.section('BIAS'): log('%7i&%s %s' % (len(self.terms), term.get_log(), ' '.join(ic.get_log() for ic in term.ics))) elif isinstance(term, BiasPotential): self.term_lookup.append((0, len(self.terms))) if log.do_high: with log.section('BIAS'): log('%7i&%s %s' % (len(self.terms), term.get_log(), ' '.join(cv.get_log() for cv in term.cvs))) else: raise NotImplementedError self.terms.append(term)
def propagate(self): # Update the Hessian assert not self.g is self.g_old assert not self.x is self.x_old hessian_safe = self.hessian.update(self.x - self.x_old, self.g - self.g_old) if not hessian_safe: # Reset the Hessian completely if log.do_high: log("Resetting hessian due to failed update.") self.hessian = SR1HessianModel(len(self.x)) self.trust_radius = self.initial_trust_radius # Move new to old self.x_old = self.x self.f_old = self.f self.g_old = self.g # Compute a step self.x, self.f, self.g = self.make_step() return BaseOptimizer.propagate(self)
def propagate(self): # Update the Hessian assert not self.g is self.g_old assert not self.x is self.x_old hessian_safe = self.hessian.update(self.x - self.x_old, self.g - self.g_old) if not hessian_safe: # Reset the Hessian completely if log.do_high: log('Resetting hessian due to failed update.') self.hessian = SR1HessianModel(len(self.x)) self.trust_radius = self.initial_trust_radius # Move new to old self.x_old = self.x self.f_old = self.f self.g_old = self.g # Compute a step self.x, self.f, self.g = self.make_step() return BaseOptimizer.propagate(self)
def __call__(self, iterative): if log.do_medium: if self.time0 is None: self.time0 = time.time() if log.do_medium: log.hline() log('Cons.Err. =&the root of the ratio of the variance on the conserved quantity and the variance on the kinetic energy.') log('d-rmsd =&the root-mean-square displacement of the atoms.') log('g-rmsd =&the root-mean-square gradient of the energy.') log('counter Cons.Err. Temp d-RMSD g-RMSD Walltime') log.hline() log('%7i %10.5f %s %s %s %10.1f' % ( iterative.counter, iterative.cons_err, log.temperature(iterative.temp), log.length(iterative.rmsd_delta), log.force(iterative.rmsd_gpos), time.time() - self.time0, ))
def __call__(self): """Perform a trial move and calculate the associated energy difference, decide whether it is accepted or not, and update the state of the MC simulation accordingly """ with timer.section("MC %s move" % self.log_name): e = self.compute() p = self.probability(e) if np.random.rand()>p: accepted = False self.reject() else: accepted = True self.mc.energy += e self.accept() if log.do_debug: log("MC %s: N = %d energy difference = %s acceptance probability = %6.2f %% accepted = %s" % (self.__class__.__name__, self.mc.N, log.energy(e), p*100.0, accepted)) return accepted
def __call__(self, iterative): if log.do_medium: if self.time0 is None: self.time0 = time.time() if log.do_medium: log.hline() log('Conv.val. =&the highest ratio of a convergence criterion over its threshold.') log('N =&the number of convergence criteria that is not met.') log('Worst =&the name of the convergence criterion that is worst.') log('counter Conv.val. N Worst Energy Walltime') log.hline() log('%7i % 10.3e %2i %15s %s %10.1f' % ( iterative.counter, iterative.dof.conv_val, iterative.dof.conv_count, iterative.dof.conv_worst, log.energy(iterative.epot), time.time() - self.time0, ))
def from_hdf5(cls, f): '''Create a system from an HDF5 file/group containing a system group **Arguments:** f An open h5.File object with a system group. The system group must at least contain a numbers and pos dataset. ''' sgrp = f['system'] kwargs = { 'numbers': sgrp['numbers'][:], 'pos': sgrp['pos'][:], } for key in 'scopes', 'scope_ids', 'ffatypes', 'ffatype_ids', 'bonds', 'rvecs', 'charges', 'masses': if key in sgrp: kwargs[key] = sgrp[key][:] if log.do_high: log('Read system parameters from %s.' % f.filename) return cls(**kwargs)
def __init__(self, system, comlist=None): ''' Parameters ---------- system An instance of the ``System`` class. comlist An optional layer to derive centers of mass from the atomic positions. These centers of mass are used as input for the first layer, the relative vectors. ''' ForcePart.__init__(self, 'valence', system) self.comlist = comlist self.dlist = DeltaList(system if comlist is None else comlist) self.iclist = InternalCoordinateList(self.dlist) self.vlist = ValenceList(self.iclist) if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline()
def __init__(self, system, alpha, gcut=0.35, dielectric=1.0, exclude_frame=False, n_frame=0): ''' **Arguments:** system The system to which this interaction applies. alpha The alpha parameter in the Ewald summation method. **Optional arguments:** gcut The cutoff in reciprocal space. dielectric The scalar relative permittivity of the system. exclude_frame A boolean to exclude framework-framework interactions (exclude_frame=True) for efficiency sake in MC simulations. n_frame Number of framework atoms. This parameter is used to exclude framework-framework neighbors when exclude_frame=True. ''' ForcePart.__init__(self, 'ewald_reci', system) if not system.cell.nvec == 3: raise TypeError('The system must have a 3D periodic cell.') if system.charges is None: raise ValueError('The system does not have charges.') self.system = system self.alpha = alpha self.gcut = gcut self.dielectric = dielectric self.update_gmax() self.work = np.empty(system.natom*2) if exclude_frame == True and n_frame < 0: raise ValueError('The number of framework atoms to exclude must be positive.') elif exclude_frame == False: n_frame = 0 self.n_frame = n_frame if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline() log(' alpha: %s' % log.invlength(self.alpha)) log(' gcut: %s' % log.invlength(self.gcut)) log(' relative permittivity: %5.3f' % self.dielectric) log.hline()
def _init_log(self): if log.do_medium: log('Unit cell') log.hline() log('Number of periodic dimensions: %i' % self.cell.nvec) lengths, angles = self.cell.parameters names = 'abc' for i in xrange(len(lengths)): log('Cell parameter %5s: %10s' % (names[i], log.length(lengths[i]))) names = 'alpha', 'beta', 'gamma' for i in xrange(len(angles)): log('Cell parameter %5s: %10s' % (names[i], log.angle(angles[i]))) log.hline() log.blank()
def _init_derived_scopes(self): if self.scope_ids is None: if len(self.scopes) != self.natom: raise TypeError('When the scope_ids are derived automatically, the length of the scopes list must match the number of atoms.') lookup = {} scopes = [] self.scope_ids = np.zeros(self.natom, int) for i in xrange(self.natom): scope = self.scopes[i] scope_id = lookup.get(scope) if scope_id is None: scope_id = len(scopes) scopes.append(scope) lookup[scope] = scope_id self.scope_ids[i] = scope_id self.scopes = scopes for scope in self.scopes: check_name(scope) # check the range of the ids if self.scope_ids.min() != 0 or self.scope_ids.max() != len(self.scopes)-1: raise ValueError('The ffatype_ids have incorrect bounds.') if log.do_medium: log('The following scopes are present in the system:') log.hline() log(' Scope ID Number of atoms') log.hline() for scope_id, scope in enumerate(self.scopes): log('%22s %3i %3i' % (scope, scope_id, (self.scope_ids==scope_id).sum())) log.hline() log.blank()
def apply(self, par_table, cpar_table, scale_table, system, ff_args): # Prepare the atomic parameters c6s = np.zeros(system.nffatype, float) bs = np.zeros(system.nffatype, float) vols = np.zeros(system.nffatype, float) for i in xrange(system.nffatype): key = (system.ffatypes[i],) par_list = par_table.get(key, []) if len(par_list) == 0: if log.do_warning: log.warn('No DAMPDISP parameters found for atom %i with fftype %s.' % (i, system.get_ffatype(i))) else: c6s[i], bs[i], vols[i] = par_list[0] # Prepare the cross parameters c6_cross = np.zeros((system.nffatype, system.nffatype), float) b_cross = np.zeros((system.nffatype, system.nffatype), float) for i0 in xrange(system.nffatype): for i1 in xrange(i0+1): cpar_list = cpar_table.get((system.ffatypes[i0], system.ffatypes[i1]), []) if len(cpar_list) == 0: if log.do_high: log('No DAMPDISP cross parameters found for ffatypes %s,%s. Mixing rule will be used' % (system.ffatypes[i0], system.ffatypes[i1])) else: c6_cross[i0,i1], b_cross[i0,i1] = cpar_list[0] if i0 != i1: c6_cross[i1,i0], b_cross[i1,i0] = cpar_list[0] # Prepare the global parameters scalings = Scalings(system, scale_table[1], scale_table[2], scale_table[3]) # Get the part. It should not exist yet. part_pair = ff_args.get_part_pair(PairPotDampDisp) if part_pair is not None: raise RuntimeError('Internal inconsistency: the DAMPDISP part should not be present yet.') pair_pot = PairPotDampDisp(system.ffatype_ids, c6_cross, b_cross, ff_args.rcut, ff_args.tr, c6s, bs, vols) nlist = ff_args.get_nlist(system) part_pair = ForcePartPair(system, nlist, scalings, pair_pot) ff_args.parts.append(part_pair)
def __init__(self, system, alpha, gcut=0.35): ''' **Arguments:** system The system to which this interaction applies. alpha The alpha parameter in the Ewald summation method. gcut The cutoff in reciprocal space. ''' ForcePart.__init__(self, 'ewald_reci', system) if not system.cell.nvec == 3: raise TypeError('The system must have a 3D periodic cell.') if system.charges is None: raise ValueError('The system does not have charges.') if system.dipoles is None: raise ValueError('The system does not have dipoles.') self.system = system self.alpha = alpha self.gcut = gcut self.update_gmax() self.work = np.empty(system.natom*2) if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline() log(' alpha: %s' % log.invlength(self.alpha)) log(' gcut: %s' % log.invlength(self.gcut)) log.hline()
def __init__(self, system, alpha, scalings): ''' **Arguments:** system The system to which this interaction applies. alpha The alpha parameter in the Ewald summation method. scalings A ``Scalings`` object. This object contains all the information about the energy scaling of pairwise contributions that are involved in covalent interactions. See :class:`yaff.pes.scalings.Scalings` for more details. ''' ForcePart.__init__(self, 'ewald_cor', system) if not system.cell.nvec == 3: raise TypeError('The system must have a 3D periodic cell') if system.charges is None: raise ValueError('The system does not have charges.') self.system = system self.alpha = alpha self.scalings = scalings if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline() log(' alpha: %s' % log.invlength(self.alpha)) log(' scalings: %5.3f %5.3f %5.3f' % (scalings.scale1, scalings.scale2, scalings.scale3)) log.hline()
def __init__(self, system, alpha, dielectric=1.0): ''' **Arguments:** system The system to which this interaction applies. alpha The alpha parameter in the Ewald summation method. **Optional arguments:** dielectric The scalar relative permittivity of the system. ''' ForcePart.__init__(self, 'ewald_neut', system) if not system.cell.nvec == 3: raise TypeError('The system must have a 3D periodic cell') if system.charges is None: raise ValueError('The system does not have charges.') self.system = system self.alpha = alpha self.dielectric = dielectric if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline() log(' alpha: %s' % log.invlength(self.alpha)) log(' relative permittivity: %5.3f' % self.dielectric) log.hline()
def __init__(self, system, nlist, scalings, pair_pot): ''' **Arguments:** system The system to which this pairwise interaction applies. nlist A ``NeighborList`` object. This has to be the same as the one passed to the ForceField object that contains this part. scalings A ``Scalings`` object. This object contains all the information about the energy scaling of pairwise contributions that are involved in covalent interactions. See :class:`yaff.pes.scalings.Scalings` for more details. pair_pot An instance of the ``PairPot`` built-in class from :mod:`yaff.pes.ext`. ''' ForcePart.__init__(self, 'pair_%s' % pair_pot.name, system) self.nlist = nlist self.scalings = scalings self.pair_pot = pair_pot self.nlist.request_rcut(pair_pot.rcut) if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline() log(' scalings: %5.3f %5.3f %5.3f' % (scalings.scale1, scalings.scale2, scalings.scale3)) log(' real space cutoff: %s' % log.length(pair_pot.rcut)) tr = pair_pot.get_truncation() if tr is None: log(' truncation: none') else: log(' truncation: %s' % tr.get_log()) self.pair_pot.log() log.hline()
def __init__(self, system, pext): ''' **Arguments:** system An instance of the ``System`` class. pext The external pressure. (Positive will shrink the system.) In case of 2D-PBC, this is the surface tension. In case of 1D, this is the linear strain. This force part is only applicable to systems that are periodic. ''' if system.cell.nvec == 0: raise ValueError('The system must be periodic in order to apply a pressure') ForcePart.__init__(self, 'press', system) self.system = system self.pext = pext if log.do_medium: with log.section('FPINIT'): log('Force part: %s' % self.name) log.hline()