def make_precon(self, atoms): 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_nearest_neighbour_distance(atoms): """ Estimate nearest neighbour distance r_NN Args: atoms: Atoms object Returns: rNN: float Nearest neighbour distance """ if isinstance(atoms, Filter): atoms = atoms.atoms start_time = time.time() # compute number of neighbours of each atom. If any atom doesn't # have a neighbour we increase the cutoff and try again, until our # cutoff exceeds the size of the sytem r_cut = 1.0 phi = (1.0 + np.sqrt(5.0)) / 2.0 # Golden ratio # cell lengths and angles a, b, c, alpha, beta, gamma = cell_to_cellpar(atoms.cell) extent = [a, b, c] logger.debug('estimate_nearest_neighbour_distance(): extent=%r', extent) while r_cut < 2.0 * max(extent): logger.info( 'estimate_nearest_neighbour_distance(): ' 'calling neighbour_list with r_cut=%.2f A', r_cut) i, j, rij, fixed_atoms = get_neighbours(atoms, r_cut, self_interaction=True) if len(i) != 0: nn_i = np.bincount(i, minlength=len(atoms)) if (nn_i != 0).all(): break r_cut *= phi else: raise RuntimeError('increased r_cut to twice system extent without ' 'finding neighbours for all atoms. This can ' 'happen if your system is too small; try ' 'setting r_cut manually') # maximum of nearest neigbour distances nn_distances = [np.min(rij[i == I]) for I in range(len(atoms))] r_NN = np.max(nn_distances) logger.info('estimate_nearest_neighbour_distance(): got r_NN=%.3f in %s s', r_NN, time.time() - start_time) return r_NN
def estimate_nearest_neighbour_distance(atoms): """ Estimate nearest neighbour distance r_NN Args: atoms: Atoms object Returns: rNN: float Nearest neighbour distance """ if isinstance(atoms, Filter): atoms = atoms.atoms start_time = time.time() # compute number of neighbours of each atom. If any atom doesn't # have a neighbour we increase the cutoff and try again, until our # cutoff exceeds the size of the sytem r_cut = 1.0 phi = (1.0 + np.sqrt(5.0)) / 2.0 # Golden ratio # cell lengths and angles a, b, c, alpha, beta, gamma = cell_to_cellpar(atoms.cell) extent = [a, b, c] logger.debug('estimate_nearest_neighbour_distance(): extent=%r', extent) while r_cut < 2.0 * max(extent): logger.info('estimate_nearest_neighbour_distance(): ' 'calling neighbour_list with r_cut=%.2f A', r_cut) i, j, rij, fixed_atoms = get_neighbours( atoms, r_cut, self_interaction=True) if len(i) != 0: nn_i = np.bincount(i, minlength=len(atoms)) if (nn_i != 0).all(): break r_cut *= phi else: raise RuntimeError('increased r_cut to twice system extent without ' 'finding neighbours for all atoms. This can ' 'happen if your system is too small; try ' 'setting r_cut manually') # maximum of nearest neigbour distances nn_distances = [np.min(rij[i == I]) for I in range(len(atoms))] r_NN = np.max(nn_distances) logger.info('estimate_nearest_neighbour_distance(): got r_NN=%.3f in %s s', r_NN, time.time() - start_time) return r_NN
def solve(self, x): """ Solve the (sparse) linear system P x = y and return y """ start_time = time.time() if self.use_pyamg and have_pyamg: y = self.ml.solve(x, x0=rand(self.P.shape[0]), tol=self.solve_tol, accel='cg', maxiter=300, cycle='W') else: y = spsolve(self.P, x) logger.info('--- Precon applied in %s seconds ---', time.time() - start_time) return y
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 _make_sparse_precon(self, atoms, initial_assembly=False, force_stab=False): """Create a sparse preconditioner matrix based on the passed atoms. Args: atoms: the Atoms object used to create the preconditioner. Returns: A scipy.sparse.csr_matrix object, representing a d*N by d*N matrix (where N is the number of atoms, and d is the value of self.dim). BE AWARE that using numpy.dot() with this object will result in errors/incorrect results - use the .dot method directly on the sparse matrix instead. """ logger.info('creating sparse precon: initial_assembly=%r, ' 'force_stab=%r, apply_positions=%r, apply_cell=%r', initial_assembly, force_stab, self.apply_positions, self.apply_cell) N = len(atoms) start_time = time.time() if self.apply_positions: # compute neighbour list i_list, j_list, rij_list, fixed_atoms = get_neighbours( atoms, self.r_cut) logger.info('--- neighbour list created in %s s ---' % (time.time() - start_time)) row = [] col = [] data = [] # precon is mu_c*identity for cell DoF if isinstance(atoms, Filter): i = N - 3 j = N - 2 k = N - 1 x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2, 3 * k, 3 * k + 1, 3 * k + 2] row.extend(x) col.extend(x) if self.apply_cell: data.extend(np.repeat(self.mu_c, 9)) else: data.extend(np.repeat(self.mu_c, 9)) logger.info('--- computed triplet format in %s s ---' % (time.time() - start_time)) conn = sparse.lil_matrix((N, N), dtype=bool) if self.apply_positions and not initial_assembly: if self.morses is not None: for n in range(len(self.morses)): if self.hessian == 'reduced': i, j, Hx = ff.get_morse_potential_reduced_hessian( atoms, self.morses[n]) elif self.hessian == 'spectral': i, j, Hx = ff.get_morse_potential_hessian( atoms, self.morses[n], spectral=True) else: raise NotImplementedError('Not implemented hessian') x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2] row.extend(np.repeat(x, 6)) col.extend(np.tile(x, 6)) data.extend(Hx.flatten()) conn[i, j] = True conn[j, i] = True if self.bonds is not None: for n in range(len(self.bonds)): if self.hessian == 'reduced': i, j, Hx = ff.get_bond_potential_reduced_hessian( atoms, self.bonds[n], self.morses) elif self.hessian == 'spectral': i, j, Hx = ff.get_bond_potential_hessian( atoms, self.bonds[n], self.morses, spectral=True) else: raise NotImplementedError('Not implemented hessian') x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2] row.extend(np.repeat(x, 6)) col.extend(np.tile(x, 6)) data.extend(Hx.flatten()) conn[i, j] = True conn[j, i] = True if self.angles is not None: for n in range(len(self.angles)): if self.hessian == 'reduced': i, j, k, Hx = ff.get_angle_potential_reduced_hessian( atoms, self.angles[n], self.morses) elif self.hessian == 'spectral': i, j, k, Hx = ff.get_angle_potential_hessian( atoms, self.angles[n], self.morses, spectral=True) else: raise NotImplementedError('Not implemented hessian') x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2, 3 * k, 3 * k + 1, 3 * k + 2] row.extend(np.repeat(x, 9)) col.extend(np.tile(x, 9)) data.extend(Hx.flatten()) conn[i, j] = conn[i, k] = conn[j, k] = True conn[j, i] = conn[k, i] = conn[k, j] = True if self.dihedrals is not None: for n in range(len(self.dihedrals)): if self.hessian == 'reduced': i, j, k, l, Hx = \ ff.get_dihedral_potential_reduced_hessian( atoms, self.dihedrals[n], self.morses) elif self.hessian == 'spectral': i, j, k, l, Hx = ff.get_dihedral_potential_hessian( atoms, self.dihedrals[n], self.morses, spectral=True) else: raise NotImplementedError('Not implemented hessian') x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2, 3 * k, 3 * k + 1, 3 * k + 2, 3 * l, 3 * l + 1, 3 * l + 2] row.extend(np.repeat(x, 12)) col.extend(np.tile(x, 12)) data.extend(Hx.flatten()) conn[i, j] = conn[i, k] = conn[i, l] = conn[ j, k] = conn[j, l] = conn[k, l] = True conn[j, i] = conn[k, i] = conn[l, i] = conn[ k, j] = conn[l, j] = conn[l, k] = True if self.apply_positions: for i, j, rij in zip(i_list, j_list, rij_list): if not conn[i, j]: coeff = self.get_coeff(rij) x = [3 * i, 3 * i + 1, 3 * i + 2] y = [3 * j, 3 * j + 1, 3 * j + 2] row.extend(x + x) col.extend(x + y) data.extend(3 * [-coeff] + 3 * [coeff]) row.extend(range(self.dim * N)) col.extend(range(self.dim * N)) if initial_assembly: data.extend([self.mu * self.c_stab] * self.dim * N) else: data.extend([self.c_stab] * self.dim * N) # create the matrix start_time = time.time() self.P = sparse.csc_matrix( (data, (row, col)), shape=(self.dim * N, self.dim * N)) logger.info('--- created CSC matrix in %s s ---' % (time.time() - start_time)) if not initial_assembly: if len(fixed_atoms) != 0: self.P.tolil() for i in fixed_atoms: self.P[i, :] = 0.0 self.P[:, i] = 0.0 self.P[i, i] = 1.0 self.P = self.P.tocsr() # Create solver if self.use_pyamg and have_pyamg: start_time = time.time() self.ml = smoothed_aggregation_solver( self.P, B=None, strength=('symmetric', {'theta': 0.0}), smooth=( 'jacobi', {'filter': True, 'weighting': 'local'}), improve_candidates=[('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 4}), None, None, None, None, None, None, None, None, None, None, None, None, None, None], aggregate='standard', presmoother=('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 1}), postsmoother=('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 1}), max_levels=15, max_coarse=300, coarse_solver='pinv') logger.info('--- multi grid solver created in %s s ---' % (time.time() - start_time)) return self.P
def _make_sparse_precon(self, atoms, initial_assembly=False, force_stab=False): """ """ start_time = time.time() N = len(atoms) row = [] col = [] data = [] if self.morses is not None: for n in range(len(self.morses)): if self.hessian == 'reduced': i, j, Hx = ff.get_morse_potential_reduced_hessian( atoms, self.morses[n]) elif self.hessian == 'spectral': i, j, Hx = ff.get_morse_potential_hessian( atoms, self.morses[n], spectral=True) else: raise NotImplementedError('Not implemented hessian') x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2] row.extend(np.repeat(x, 6)) col.extend(np.tile(x, 6)) data.extend(Hx.flatten()) if self.bonds is not None: for n in range(len(self.bonds)): if self.hessian == 'reduced': i, j, Hx = ff.get_bond_potential_reduced_hessian( atoms, self.bonds[n], self.morses) elif self.hessian == 'spectral': i, j, Hx = ff.get_bond_potential_hessian( atoms, self.bonds[n], self.morses, spectral=True) else: raise NotImplementedError('Not implemented hessian') x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2] row.extend(np.repeat(x, 6)) col.extend(np.tile(x, 6)) data.extend(Hx.flatten()) if self.angles is not None: for n in range(len(self.angles)): if self.hessian == 'reduced': i, j, k, Hx = ff.get_angle_potential_reduced_hessian( atoms, self.angles[n], self.morses) elif self.hessian == 'spectral': i, j, k, Hx = ff.get_angle_potential_hessian( atoms, self.angles[n], self.morses, spectral=True) else: raise NotImplementedError('Not implemented hessian') x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2, 3 * k, 3 * k + 1, 3 * k + 2] row.extend(np.repeat(x, 9)) col.extend(np.tile(x, 9)) data.extend(Hx.flatten()) if self.dihedrals is not None: for n in range(len(self.dihedrals)): if self.hessian == 'reduced': i, j, k, l, Hx = \ ff.get_dihedral_potential_reduced_hessian( atoms, self.dihedrals[n], self.morses) elif self.hessian == 'spectral': i, j, k, l, Hx = ff.get_dihedral_potential_hessian( atoms, self.dihedrals[n], self.morses, spectral=True) else: raise NotImplementedError('Not implemented hessian') x = [3 * i, 3 * i + 1, 3 * i + 2, 3 * j, 3 * j + 1, 3 * j + 2, 3 * k, 3 * k + 1, 3 * k + 2, 3 * l, 3 * l + 1, 3 * l + 2] row.extend(np.repeat(x, 12)) col.extend(np.tile(x, 12)) data.extend(Hx.flatten()) row.extend(range(self.dim * N)) col.extend(range(self.dim * N)) data.extend([self.c_stab] * self.dim * N) # create the matrix start_time = time.time() self.P = sparse.csc_matrix( (data, (row, col)), shape=(self.dim * N, self.dim * N)) logger.info('--- created CSC matrix in %s s ---' % (time.time() - start_time)) fixed_atoms = [] for constraint in atoms.constraints: if isinstance(constraint, FixAtoms): fixed_atoms.extend(list(constraint.index)) else: raise TypeError( 'only FixAtoms constraints are supported by Precon class') if len(fixed_atoms) != 0: self.P.tolil() for i in fixed_atoms: self.P[i, :] = 0.0 self.P[:, i] = 0.0 self.P[i, i] = 1.0 self.P = self.P.tocsr() logger.info('--- N-dim precon created in %s s ---' % (time.time() - start_time)) # Create solver if self.use_pyamg and have_pyamg: start_time = time.time() self.ml = smoothed_aggregation_solver( self.P, B=None, strength=('symmetric', {'theta': 0.0}), smooth=( 'jacobi', {'filter': True, 'weighting': 'local'}), improve_candidates=[('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 4}), None, None, None, None, None, None, None, None, None, None, None, None, None, None], aggregate='standard', presmoother=('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 1}), postsmoother=('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 1}), max_levels=15, max_coarse=300, coarse_solver='pinv') logger.info('--- multi grid solver created in %s s ---' % (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 __init__(self, r_cut=None, r_NN=None, mu=None, mu_c=None, dim=3, c_stab=0.1, force_stab=False, recalc_mu=False, array_convention='C', use_pyamg=True, solve_tol=1e-8, apply_positions=True, apply_cell=True, estimate_mu_eigmode=False): """Initialise a preconditioner object based on passed parameters. Args: r_cut: float. This is a cut-off radius. The preconditioner matrix will be created by considering pairs of atoms that are within a distance r_cut of each other. For a regular lattice, this is usually taken somewhere between the first- and second-nearest neighbour distance. If r_cut is not provided, default is 2 * r_NN (see below) r_NN: nearest neighbour distance. If not provided, this is calculated from input structure. mu: float energy scale for position degreees of freedom. If `None`, mu is precomputed using finite difference derivatives. mu_c: float energy scale for cell degreees of freedom. Also precomputed if None. estimate_mu_eigmode: If True, estimates mu based on the lowest eigenmodes of unstabilised preconditioner. If False it uses the sine based approach. dim: int; dimensions of the problem c_stab: float. The diagonal of the preconditioner matrix will have a stabilisation constant added, which will be the value of c_stab times mu. force_stab: If True, always add the stabilisation to diagnonal, regardless of the presence of fixed atoms. recalc_mu: if True, the value of mu will be recalculated every time self.make_precon is called. This can be overridden in specific cases with recalc_mu argument in self.make_precon. If recalc_mu is set to True here, the value passed for mu will be irrelevant unless recalc_mu is set False the first time make_precon is called. array_convention: Either 'C' or 'F' for Fortran; this will change the preconditioner to reflect the ordering of the indices in the vector it will operate on. The C convention assumes the vector will be arranged atom-by-atom (ie [x1, y1, z1, x2, ...]) while the F convention assumes it will be arranged component by component (ie [x1, x2, ..., y1, y2, ...]). use_pyamg: use PyAMG to solve P x = y, if available. solve_tol: tolerance used for PyAMG sparse linear solver, if available. apply_positions: if True, apply preconditioner to position DoF apply_cell: if True, apply preconditioner to cell DoF Raises: ValueError for problem with arguments """ self.r_NN = r_NN self.r_cut = r_cut self.mu = mu self.mu_c = mu_c self.estimate_mu_eigmode = estimate_mu_eigmode self.c_stab = c_stab self.force_stab = force_stab self.array_convention = array_convention self.recalc_mu = recalc_mu self.P = None self.old_positions = None if use_pyamg and not have_pyamg: use_pyamg = False logger.warning('use_pyamg=True but PyAMG cannot be imported! ' 'falling back on direct inversion of ' 'preconditioner, may be slow for large systems') self.use_pyamg = use_pyamg self.solve_tol = solve_tol self.apply_positions = apply_positions self.apply_cell = apply_cell if dim < 1: raise ValueError('Dimension must be at least 1') self.dim = dim if not have_matscipy: logger.info('Unable to import Matscipy. Neighbour list ' 'calculations may be very slow.')
def _make_sparse_precon(self, atoms, initial_assembly=False, force_stab=False): """Create a sparse preconditioner matrix based on the passed 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. Note that this function will use self.mu, whatever it is. Args: atoms: the Atoms object used to create the preconditioner. Returns: A scipy.sparse.csr_matrix object, representing a d*N by d*N matrix (where N is the number of atoms, and d is the value of self.dim). BE AWARE that using numpy.dot() with this object will result in errors/incorrect results - use the .dot method directly on the sparse matrix instead. """ logger.info('creating sparse precon: initial_assembly=%r, ' 'force_stab=%r, apply_positions=%r, apply_cell=%r', initial_assembly, force_stab, self.apply_positions, self.apply_cell) N = len(atoms) diag_i = np.arange(N, dtype=int) start_time = time.time() if self.apply_positions: # compute neighbour list i, j, rij, fixed_atoms = get_neighbours(atoms, self.r_cut) logger.info('--- neighbour list created in %s s ---' % (time.time() - start_time)) # compute entries in triplet format: without the constraints start_time = time.time() coeff = self.get_coeff(rij) diag_coeff = np.bincount(i, -coeff, minlength=N).astype(np.float64) if force_stab or len(fixed_atoms) == 0: logger.info('adding stabilisation to preconditioner') diag_coeff += self.mu * self.c_stab else: diag_coeff = np.ones(N) # precon is mu_c*identity for cell DoF if isinstance(atoms, Filter): if self.apply_cell: diag_coeff[-3] = self.mu_c diag_coeff[-2] = self.mu_c diag_coeff[-1] = self.mu_c else: diag_coeff[-3] = 1.0 diag_coeff[-2] = 1.0 diag_coeff[-1] = 1.0 logger.info('--- computed triplet format in %s s ---' % (time.time() - start_time)) if self.apply_positions and not initial_assembly: # apply the constraints start_time = time.time() mask = np.ones(N) mask[fixed_atoms] = 0.0 coeff *= mask[i] * mask[j] diag_coeff[fixed_atoms] = 1.0 logger.info('--- applied fixed_atoms in %s s ---' % (time.time() - start_time)) if self.apply_positions: # remove zeros start_time = time.time() inz = np.nonzero(coeff) i = np.hstack((i[inz], diag_i)) j = np.hstack((j[inz], diag_i)) coeff = np.hstack((coeff[inz], diag_coeff)) logger.info('--- remove zeros in %s s ---' % (time.time() - start_time)) else: i = diag_i j = diag_i coeff = diag_coeff # create the matrix start_time = time.time() csc_P = sparse.csc_matrix((coeff, (i, j)), shape=(N, N)) logger.info('--- created CSC matrix in %s s ---' % (time.time() - start_time)) self.csc_P = csc_P start_time = time.time() if self.dim == 1: self.P = csc_P elif self.array_convention == 'F': csc_P = csc_P.tocsr() self.P = csc_P for i in range(self.dim - 1): self.P = sparse.block_diag((self.P, csc_P)).tocsr() else: # convert back to triplet and read the arrays csc_P = csc_P.tocoo() i = csc_P.row * self.dim j = csc_P.col * self.dim z = csc_P.data # N-dimensionalise, interlaced coordinates I = np.hstack([i + d for d in range(self.dim)]) J = np.hstack([j + d for d in range(self.dim)]) Z = np.hstack([z for d in range(self.dim)]) self.P = sparse.csc_matrix((Z, (I, J)), shape=(self.dim * N, self.dim * N)) self.P = self.P.tocsr() logger.info('--- N-dim precon created in %s s ---' % (time.time() - start_time)) # Create solver if self.use_pyamg and have_pyamg: start_time = time.time() self.ml = smoothed_aggregation_solver( self.P, B=None, strength=('symmetric', {'theta': 0.0}), smooth=( 'jacobi', {'filter': True, 'weighting': 'local'}), improve_candidates=[('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 4}), None, None, None, None, None, None, None, None, None, None, None, None, None, None], aggregate='standard', presmoother=('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 1}), postsmoother=('block_gauss_seidel', {'sweep': 'symmetric', 'iterations': 1}), max_levels=15, max_coarse=300, coarse_solver='pinv') logger.info('--- multi grid solver created in %s s ---' % (time.time() - start_time)) return self.P
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