def make_precon(self, atoms, recalc_mu=None): if self.r_NN is None: self.r_NN = estimate_nearest_neighbour_distance(atoms) if self.r_cut is None: # This is the first time this function has been called, and no # cutoff radius has been specified, so calculate it automatically. self.r_cut = 2.0 * self.r_NN elif self.r_cut < self.r_NN: warning = ('WARNING: r_cut (%.2f) < r_NN (%.2f), ' 'increasing to 1.1*r_NN = %.2f' % (self.r_cut, self.r_NN, 1.1 * self.r_NN)) logger.info(warning) print(warning) self.r_cut = 1.1 * self.r_NN if recalc_mu is None: # The caller has not specified whether or not to recalculate mu, # so the Precon's setting is used. recalc_mu = self.recalc_mu if self.mu is None: # Regardless of what the caller has specified, if we don't # currently have a value of mu, then we need one. recalc_mu = True if recalc_mu: self.estimate_mu(atoms) if self.P is not None: real_atoms = atoms if isinstance(atoms, Filter): real_atoms = atoms.atoms if self.old_positions is None: self.old_positions = wrap_positions(real_atoms.positions, real_atoms.cell) displacement = wrap_positions(real_atoms.positions, real_atoms.cell) - self.old_positions self.old_positions = real_atoms.get_positions() max_abs_displacement = abs(displacement).max() logger.info('max(abs(displacements)) = %.2f A (%.2f r_NN)', max_abs_displacement, max_abs_displacement / self.r_NN) if max_abs_displacement < 0.5 * self.r_NN: return self.P start_time = time.time() # Create the preconditioner: self._make_sparse_precon(atoms, force_stab=self.force_stab) logger.info('--- Precon created in %s seconds ---', time.time() - start_time) return self.P
def estimate_mu(self, atoms, H=None): r"""Estimate optimal preconditioner coefficient \mu \mu is estimated from a numerical solution of [dE(p+v) - dE(p)] \cdot v = \mu < P1 v, v > with perturbation v(x,y,z) = H P_lowest_nonzero_eigvec(x, y, z) or v(x,y,z) = H (sin(x / Lx), sin(y / Ly), sin(z / Lz)) After the optimal \mu is found, self.mu will be set to its value. If `atoms` is an instance of Filter an additional \mu_c will be computed for the cell degrees of freedom . Args: atoms: Atoms object for initial system H: 3x3 array or None Magnitude of deformation to apply. Default is 1e-2*rNN*np.eye(3) Returns: mu : float mu_c : float or None """ if self.dim != 3: raise ValueError('Automatic calculation of mu only possible for ' 'three-dimensional preconditioners. Try setting ' 'mu manually instead.') if self.r_NN is None: self.r_NN = estimate_nearest_neighbour_distance(atoms) # deformation matrix, default is diagonal if H is None: H = 1e-2 * self.r_NN * np.eye(3) # compute perturbation p = atoms.get_positions() if self.estimate_mu_eigmode: self.mu = 1.0 self.mu_c = 1.0 c_stab = self.c_stab self.c_stab = 0.0 if isinstance(atoms, Filter): n = len(atoms.atoms) else: n = len(atoms) P0 = self._make_sparse_precon(atoms, initial_assembly=True)[:3 * n, :3 * n] eigvals, eigvecs = sparse.linalg.eigsh(P0, k=4, which='SM') #print('estimate_mu(): lowest 4 eigvals = %f %f %f %f' # % (eigvals[0], eigvals[1], eigvals[2], eigvals[3])) # check eigenvalues if any(eigvals[0:3] > 1e-6): raise ValueError('First 3 eigenvalues of preconditioner matrix' 'do not correspond to translational modes.') elif eigvals[3] < 1e-6: raise ValueError('Fourth smallest eigenvalue of ' 'preconditioner matrix ' 'is too small, increase r_cut.') x = np.zeros(n) for i in range(n): x[i] = eigvecs[:, 3][3 * i] x = x / np.linalg.norm(x) if x[0] < 0: x = -x v = np.zeros(3 * len(atoms)) for i in range(n): v[3 * i] = x[i] v[3 * i + 1] = x[i] v[3 * i + 2] = x[i] v = v / np.linalg.norm(v) v = v.reshape((-1, 3)) self.c_stab = c_stab else: Lx, Ly, Lz = [p[:, i].max() - p[:, i].min() for i in range(3)] #print('estimate_mu(): Lx=%.1f Ly=%.1f Lz=%.1f' % (Lx, Ly, Lz)) x, y, z = p.T # sine_vr = [np.sin(x/Lx), np.sin(y/Ly), np.sin(z/Lz)], but we need # to take into account the possibility that one of Lx/Ly/Lz is # zero. sine_vr = [x, y, z] for i, L in enumerate([Lx, Ly, Lz]): if L == 0: warnings.warn( 'Cell length L[%d] == 0. Setting H[%d,%d] = 0.' % (i, i, i)) H[i, i] = 0.0 else: sine_vr[i] = np.sin(sine_vr[i] / L) v = np.dot(H, sine_vr).T natoms = len(atoms) if isinstance(atoms, Filter): natoms = len(atoms.atoms) eps = H / self.r_NN v[natoms:, :] = eps v1 = v.reshape(-1) # compute LHS dE_p = -atoms.get_forces().reshape(-1) atoms_v = atoms.copy() atoms_v.calc = atoms.calc if isinstance(atoms, Filter): atoms_v = atoms.__class__(atoms_v) if hasattr(atoms, 'constant_volume'): atoms_v.constant_volume = atoms.constant_volume atoms_v.set_positions(p + v) dE_p_plus_v = -atoms_v.get_forces().reshape(-1) # compute left hand side LHS = (dE_p_plus_v - dE_p) * v1 # assemble P with \mu = 1 self.mu = 1.0 self.mu_c = 1.0 P1 = self._make_sparse_precon(atoms, initial_assembly=True) # compute right hand side RHS = P1.dot(v1) * v1 # use partial sums to compute separate mu for positions and cell DoFs self.mu = longsum(LHS[:3 * natoms]) / longsum(RHS[:3 * natoms]) if self.mu < 1.0: warnings.warn('mu (%.3f) < 1.0, capping at mu=1.0' % self.mu) self.mu = 1.0 if isinstance(atoms, Filter): self.mu_c = longsum(LHS[3 * natoms:]) / longsum(RHS[3 * natoms:]) if self.mu_c < 1.0: print( 'mu_c (%.3f) < 1.0, capping at mu_c=1.0' % self.mu_c) self.mu_c = 1.0 print('estimate_mu(): mu=%r, mu_c=%r' % (self.mu, self.mu_c)) self.P = None # force a rebuild with new mu (there may be fixed atoms) return (self.mu, self.mu_c)
def make_precon(self, atoms, recalc_mu=None): """Create a preconditioner matrix based on the passed set of atoms. Creates a general-purpose preconditioner for use with optimization algorithms, based on examining distances between pairs of atoms in the lattice. The matrix will be stored in the attribute self.P and returned. Args: atoms: the Atoms object used to create the preconditioner. Can also recalc_mu: if True, self.mu (and self.mu_c for variable cell) will be recalculated by calling self.estimate_mu(atoms) before the preconditioner matrix is created. If False, self.mu will be calculated only if it does not currently have a value (ie, the first time this function is called). Returns: A two-element tuple: P: A sparse scipy csr_matrix. BE AWARE that using numpy.dot() with sparse matrices will result in errors/incorrect results - use the .dot method directly on the matrix instead. """ if self.r_NN is None: self.r_NN = estimate_nearest_neighbour_distance(atoms) if self.r_cut is None: # This is the first time this function has been called, and no # cutoff radius has been specified, so calculate it automatically. self.r_cut = 2.0 * self.r_NN elif self.r_cut < self.r_NN: warning = ('WARNING: r_cut (%.2f) < r_NN (%.2f), ' 'increasing to 1.1*r_NN = %.2f' % (self.r_cut, self.r_NN, 1.1 * self.r_NN)) warnings.warn(warning) self.r_cut = 1.1 * self.r_NN if recalc_mu is None: # The caller has not specified whether or not to recalculate mu, # so the Precon's setting is used. recalc_mu = self.recalc_mu if self.mu is None: # Regardless of what the caller has specified, if we don't # currently have a value of mu, then we need one. recalc_mu = True if recalc_mu: self.estimate_mu(atoms) if self.P is not None: real_atoms = atoms if isinstance(atoms, Filter): real_atoms = atoms.atoms if self.old_positions is None: self.old_positions = wrap_positions(real_atoms.positions, real_atoms.cell) displacement = wrap_positions(real_atoms.positions, real_atoms.cell) - self.old_positions self.old_positions = real_atoms.get_positions() max_abs_displacement = abs(displacement).max() #print('max(abs(displacements)) = %.2f A (%.2f r_NN)' % # (max_abs_displacement, max_abs_displacement / self.r_NN)) if max_abs_displacement < 0.5 * self.r_NN: return self.P #start_time = time.time() # Create the preconditioner: self._make_sparse_precon(atoms, force_stab=self.force_stab) #print('--- Precon created in %s seconds ---' % # (time.time() - start_time)) return self.P
def estimate_mu(self, atoms, H=None): """ Estimate optimal preconditioner coefficient \mu \mu is estimated from a numerical solution of [dE(p+v) - dE(p)] \cdot v = \mu < P1 v, v > with perturbation v(x,y,z) = H P_lowest_nonzero_eigvec(x, y, z) or v(x,y,z) = H (sin(x / Lx), sin(y / Ly), sin(z / Lz)) After the optimal \mu is found, self.mu will be set to its value. If `atoms` is an instance of Filter an additional \mu_c will be computed for the cell degrees of freedom . Args: atoms: Atoms object for initial system H: 3x3 array or None Magnitude of deformation to apply. Default is 1e-2*rNN*np.eye(3) Returns: mu : float mu_c : float or None """ if self.dim != 3: raise ValueError('Automatic calculation of mu only possible for ' 'three-dimensional preconditioners. Try setting ' 'mu manually instead.') if self.r_NN is None: self.r_NN = estimate_nearest_neighbour_distance(atoms) # deformation matrix, default is diagonal if H is None: H = 1e-2 * self.r_NN * np.eye(3) # compute perturbation p = atoms.get_positions() if self.estimate_mu_eigmode: self.mu = 1.0 self.mu_c = 1.0 c_stab = self.c_stab self.c_stab = 0.0 if isinstance(atoms, Filter): n = len(atoms.atoms) else: n = len(atoms) P0 = self._make_sparse_precon(atoms, initial_assembly=True)[:3 * n, :3 * n] eigvals, eigvecs = sparse.linalg.eigsh(P0, k=4, which='SM') logger.debug('estimate_mu(): lowest 4 eigvals = %f %f %f %f' % (eigvals[0], eigvals[1], eigvals[2], eigvals[3])) # check eigenvalues if any(eigvals[0:3] > 1e-6): raise ValueError('First 3 eigenvalues of preconditioner matrix' 'do not correspond to translational modes.') elif eigvals[3] < 1e-6: raise ValueError('Fourth smallest eigenvalue of ' 'preconditioner matrix ' 'is too small, increase r_cut.') x = np.zeros(n) for i in range(n): x[i] = eigvecs[:, 3][3 * i] x = x / np.linalg.norm(x) if x[0] < 0: x = -x v = np.zeros(3 * len(atoms)) for i in range(n): v[3 * i] = x[i] v[3 * i + 1] = x[i] v[3 * i + 2] = x[i] v = v / np.linalg.norm(v) v = v.reshape((-1, 3)) self.c_stab = c_stab else: Lx, Ly, Lz = [p[:, i].max() - p[:, i].min() for i in range(3)] logger.debug('estimate_mu(): Lx=%.1f Ly=%.1f Lz=%.1f', Lx, Ly, Lz) x, y, z = p.T # sine_vr = [np.sin(x/Lx), np.sin(y/Ly), np.sin(z/Lz)], but we need # to take into account the possibility that one of Lx/Ly/Lz is # zero. sine_vr = [x, y, z] for i, L in enumerate([Lx, Ly, Lz]): if L == 0: logger.warning( 'Cell length L[%d] == 0. Setting H[%d,%d] = 0.' % (i, i, i)) H[i, i] = 0.0 else: sine_vr[i] = np.sin(sine_vr[i] / L) v = np.dot(H, sine_vr).T natoms = len(atoms) if isinstance(atoms, Filter): natoms = len(atoms.atoms) eps = H / self.r_NN v[natoms:, :] = eps v1 = v.reshape(-1) # compute LHS dE_p = -atoms.get_forces().reshape(-1) atoms_v = atoms.copy() atoms_v.set_calculator(atoms.get_calculator()) if isinstance(atoms, Filter): atoms_v = atoms.__class__(atoms_v) if hasattr(atoms, 'constant_volume'): atoms_v.constant_volume = atoms.constant_volume atoms_v.set_positions(p + v) dE_p_plus_v = -atoms_v.get_forces().reshape(-1) # compute left hand side LHS = (dE_p_plus_v - dE_p) * v1 # assemble P with \mu = 1 self.mu = 1.0 self.mu_c = 1.0 P1 = self._make_sparse_precon(atoms, initial_assembly=True) # compute right hand side RHS = P1.dot(v1) * v1 # use partial sums to compute separate mu for positions and cell DoFs self.mu = longsum(LHS[:3 * natoms]) / longsum(RHS[:3 * natoms]) if self.mu < 1.0: logger.info('mu (%.3f) < 1.0, capping at mu=1.0', self.mu) self.mu = 1.0 if isinstance(atoms, Filter): self.mu_c = longsum(LHS[3 * natoms:]) / longsum(RHS[3 * natoms:]) if self.mu_c < 1.0: logger.info( 'mu_c (%.3f) < 1.0, capping at mu_c=1.0', self.mu_c) self.mu_c = 1.0 logger.info('estimate_mu(): mu=%r, mu_c=%r', self.mu, self.mu_c) self.P = None # force a rebuild with new mu (there may be fixed atoms) return (self.mu, self.mu_c)
def make_precon(self, atoms, recalc_mu=None): """Create a preconditioner matrix based on the passed set of atoms. Creates a general-purpose preconditioner for use with optimization algorithms, based on examining distances between pairs of atoms in the lattice. The matrix will be stored in the attribute self.P and returned. Args: atoms: the Atoms object used to create the preconditioner. Can also recalc_mu: if True, self.mu (and self.mu_c for variable cell) will be recalculated by calling self.estimate_mu(atoms) before the preconditioner matrix is created. If False, self.mu will be calculated only if it does not currently have a value (ie, the first time this function is called). Returns: A two-element tuple: P: A sparse scipy csr_matrix. BE AWARE that using numpy.dot() with sparse matrices will result in errors/incorrect results - use the .dot method directly on the matrix instead. """ if self.r_NN is None: self.r_NN = estimate_nearest_neighbour_distance(atoms) if self.r_cut is None: # This is the first time this function has been called, and no # cutoff radius has been specified, so calculate it automatically. self.r_cut = 2.0 * self.r_NN elif self.r_cut < self.r_NN: warning = ('WARNING: r_cut (%.2f) < r_NN (%.2f), ' 'increasing to 1.1*r_NN = %.2f' % (self.r_cut, self.r_NN, 1.1 * self.r_NN)) logger.info(warning) print(warning) self.r_cut = 1.1 * self.r_NN if recalc_mu is None: # The caller has not specified whether or not to recalculate mu, # so the Precon's setting is used. recalc_mu = self.recalc_mu if self.mu is None: # Regardless of what the caller has specified, if we don't # currently have a value of mu, then we need one. recalc_mu = True if recalc_mu: self.estimate_mu(atoms) if self.P is not None: real_atoms = atoms if isinstance(atoms, Filter): real_atoms = atoms.atoms if self.old_positions is None: self.old_positions = wrap_positions(real_atoms.positions, real_atoms.cell) displacement = wrap_positions(real_atoms.positions, real_atoms.cell) - self.old_positions self.old_positions = real_atoms.get_positions() max_abs_displacement = abs(displacement).max() logger.info('max(abs(displacements)) = %.2f A (%.2f r_NN)', max_abs_displacement, max_abs_displacement / self.r_NN) if max_abs_displacement < 0.5 * self.r_NN: return self.P start_time = time.time() # Create the preconditioner: self._make_sparse_precon(atoms, force_stab=self.force_stab) logger.info('--- Precon created in %s seconds ---', time.time() - start_time) return self.P
rij: The distances between atoms i and {j}. """ sij = np.dot(cell_ih, (qi - qj).T) # column vectors needed sij -= np.rint(sij) dij = np.dot(cell_h, sij).T # back to i-pi shape rij = np.linalg.norm(dij, axis=1) return dij, rij atoms = read(options.inputfile, format=options.formatfile) N = len(atoms) A = options.A r_NN = estimate_nearest_neighbour_distance(atoms) r_cut = 2.0 * r_NN coordinates = atoms.get_positions() cell_h = atoms.get_cell()[:] cell_ih = atoms.get_reciprocal_cell()[:] mu = 1.0 hessian = np.zeros(shape=(3 * N, 3 * N)) for i in range(N - 1): qi = coordinates[i].reshape(1, 3) qj = coordinates[i + 1:].reshape(-1, 3) dij, rij = vector_separation(cell_h, cell_ih, qi, qj) coeff = -mu * np.exp(-A * (rij / r_NN - 1)) mask = np.array(rij >= r_cut) coeff[mask] = 0
def estimate_mu(self, structure, fixed_frame, parameters): """Estimate scaling parameter mu for Expoential preconditioner scheme. For more implementation detail see Packwood et. al: A universal preconditioner for simulating condensed phase materials, J. Chem. Phys. 144, 164109 (2016). https://aip.scitation.org/doi/full/10.1063/1.4947024 and https://wiki.fysik.dtu.dk/ase/ase/optimize.html First reads the parameters file and checks if the estimation of mu is necessary. Then Estimates mu with default parameters of r_cut=2*r_NN, where r_NN is estimated nearest neighbour distance. Parameter A=3.0 set to default value as was mentioned in the paper. Args: structure {GenSec structure}: structure object fixed_frame {GenSec fixed frame}: fixed frame object parameters {JSON} : Parameters from file Returns: float: Scaling parameter mu """ # Figure out for which atoms Exp is applicapble precons_parameters = { "mol": parameters["calculator"]["preconditioner"]["mol"]["precon"], "fixed_frame": parameters["calculator"]["preconditioner"]["fixed_frame"] ["precon"], "mol-mol": parameters["calculator"]["preconditioner"]["mol-mol"]["precon"], "mol-fixed_frame": parameters["calculator"]["preconditioner"]["mol-fixed_frame"] ["precon"], } precons_parameters_init = { "mol": parameters["calculator"]["preconditioner"]["mol"]["initial"], "fixed_frame": parameters["calculator"]["preconditioner"]["fixed_frame"] ["initial"], "mol-mol": parameters["calculator"]["preconditioner"]["mol-mol"]["initial"], "mol-fixed_frame": parameters["calculator"]["preconditioner"]["mol-fixed_frame"] ["initial"], } precons_parameters_update = { "mol": parameters["calculator"]["preconditioner"]["mol"]["update"], "fixed_frame": parameters["calculator"]["preconditioner"]["fixed_frame"] ["update"], "mol-mol": parameters["calculator"]["preconditioner"]["mol-mol"]["update"], "mol-fixed_frame": parameters["calculator"]["preconditioner"]["mol-fixed_frame"] ["update"], } need_for_exp = False for i in range(len(list(precons_parameters.values()))): if list(precons_parameters.values())[i] == "Exp": if (list(precons_parameters_init.values())[i] or list(precons_parameters_update.values())[i]): need_for_exp = True mu = 1.0 if need_for_exp: if len(structure.molecules) > 1: a0 = structure.molecules[0].copy() for i in range(1, len(structure.molecules)): a0 += structure.molecules[i] else: a0 = structure.molecules[0] if hasattr(fixed_frame, "fixed_frame"): all_atoms = a0 + fixed_frame.fixed_frame else: all_atoms = a0 atoms = all_atoms.copy() atoms.set_calculator(self.calculator) self.set_constrains(atoms, parameters) r_NN = estimate_nearest_neighbour_distance(atoms) try: mu = Exp(r_cut=2.0 * r_NN, A=3.0).estimate_mu(atoms)[0] except: print("Something is wrong!") return mu