def ks_objective_function(density_in, params, precond=None): """ The Kohn-Sham objective function, or residual map, R[rho] = F[rho] - rho == 0? Root find with repeated calls to this map to get solution to the KS equations. """ # Construct Hamiltonian hamiltonian = construct_hamiltonian(params, density_in) # Solve H psi = E psi eigenvalues, eigenvectors = np.linalg.eigh(hamiltonian) eigenvectors = normalise_function(params, eigenvectors[:, 0:]) # Extract lowest lying num_particles eigenfunctions and normalise wavefunctions_ks = eigenvectors[:, 0:params.num_particles] wavefunctions_ks[:, 0:params.num_particles] = normalise_function( params, wavefunctions_ks[:, 0:params.num_particles]) # Calculate the output density density_out = calculate_density_ks(params, wavefunctions_ks) # Residual map residual = density_out - density_in # Print error print(np.sum(abs(residual))) if precond is None: return residual else: return np.dot(np.linalg.inv(precond), residual)
def evolution_objective_function(v_ks,params,wavefunctions_ks,density_reference,objective_type,evolution_type): r""" The objective function for root finding and optimisation algorithms, defined with some method of evolving psi(t) --> psi(t+dt). """ # Evolve the wavefunctions according to the given scheme if (evolution_type == 'CN'): wavefunctions_ks = crank_nicolson_step(params,v_ks,wavefunctions_ks) elif (evolution_type == 'expm'): wavefunctions_ks = expm_step(params,v_ks,wavefunctions_ks) else: raise RuntimeError('Invalid time evolution method specified') wavefunctions_ks[:,0:params.num_electrons] = normalise_function(params,wavefunctions_ks[:,0:params.num_electrons]) # Compute evolved density from the evolved wavefunctions density_ks = calculate_density_ks(params, wavefunctions_ks[:,:]) # Error in the KS density away from the reference density error = norm(params,density_ks[:] - density_reference[:],'C2') # Return a particular output that defines the objective to be minimised if (objective_type == 'root'): return density_ks - density_reference elif (objective_type == 'opt'): return error else: raise RuntimeError('Not a valid type for the objective function output')
def ks_objective_function(density_in, params): """ The KS objective function or `oracle': queried to take numerical derivatives (Jacobian) in Newton's method. """ # Construct Hamiltonian hamiltonian = construct_hamiltonian(params, density_in) # Solve H psi = E psi eigenvalues, eigenvectors = np.linalg.eigh(hamiltonian) eigenvectors = normalise_function(params, eigenvectors[:, 0:]) # Extract lowest lying num_particles eigenfunctions and normalise wavefunctions_ks = eigenvectors[:, 0:params.num_particles] wavefunctions_ks[:, 0:params.num_particles] = normalise_function( params, wavefunctions_ks[:, 0:params.num_particles]) # Calculate the output density density_out = calculate_density_ks(params, wavefunctions_ks) # Residual map residual = density_out - density_in return residual
def crank_nicolson_evolve(params,wavefunctions_ks,v_ks,density_ks): r""" Computes time-dependent wavefunctions and density from a given initial wavefunction and v_ks """ for i in range(1,params.Ntime): wavefunctions_ks[i,:,:] = crank_nicolson_step(params,v_ks[i,:],wavefunctions_ks[i-1,:,:]) density_ks[i,:] = calculate_density_ks(params, wavefunctions_ks[i,:,:]) return density_ks
def initial_guess_density_nonint(params): r""" Initial guess as the solution to the 'non-interacting' problem """ # Independent Hamiltonian hamiltonian = construct_hamiltonian_independent(params) # Solve H psi = E psi eigenvalues, eigenvectors = linalg.eigh(hamiltonian) # Extract lowest lying num_particles eigenfunctions and normalise wavefunctions_ks = eigenvectors[:, 0:params.num_particles] wavefunctions_ks[:, 0:params.num_particles] = normalise_function( params, wavefunctions_ks[:, 0:params.num_particles]) # Calculate the corresponding output density matrix density = calculate_density_ks(params, wavefunctions_ks) return density
def generate_ks_potential(params,density_reference): r""" Reverse engineers a reference density to product a Kohn-Sham potential :param params: input parameters object :param density_reference: ndarray, the reference density, axes [time,space] :return: density_ks, v_ks, wavefunctions_ks: ndarray, the optimised density, Kohn-Sham potential and wavefunctions """ # Deal with ground state before time-dependence # Init variables v_ks = np.zeros((params.Ntime,params.Nspace)) density_ks = np.zeros((params.Ntime,params.Nspace)) wavefunctions_ks = np.zeros((params.Ntime,params.Nspace,params.num_electrons), dtype=complex) # Initial guess for the Kohn-Sham potential v_ks[0,:] = params.v_ext v_ks[1:,:] = params.v_ext + params.v_pert # Compute the ground-state Kohn-Sham potential i, error = 0, 1 while(error > 5e-10): # Construct Hamiltonian for a given Kohn-Sham potential hamiltonian = construct_H(params,v_ks[0,:]) # Find eigenvectors and eigenvalues of this Hamiltonian eigenenergies_ks, eigenfunctions_ks = sp.linalg.eigh(hamiltonian) eigenfunctions_ks = eigenfunctions_ks.real # Store the lowest N_electron eigenfunctions as wavefunctions wavefunctions_ks[0,:,0:params.num_electrons] = eigenfunctions_ks[:,0:params.num_electrons] # Normalise the wavefunctions w.r.t the continous L2 norm wavefunctions_ks[0,:,0:params.num_electrons] = normalise_function(params, wavefunctions_ks[0,:,0:params.num_electrons]) # Construct KS density density_ks[0,:] = calculate_density_ks(params, wavefunctions_ks[0,:,:]) # Error in the KS density away from the reference density error = norm(params,density_ks[0,:] - density_reference[0,:],'MAE') # Update the KS potential with a steepest descent scheme v_ks[0,:] -= 0.01*(density_reference[0,:] - density_ks[0,:]) / density_reference[0,:] #v_ks[0,:] -= density_reference[0,:]**0.05 - density_ks[0,:]**0.05 print('Error = {0} after {1} iterations'.format(error,i), end='\r') i += 1 print('Final error in the ground state KS density is {0} after {1} iterations'.format(error,i)) print(' ') # Compute the ground-state potential using a scipy optimiser opt_info = root(groundstate_objective_function, v_ks[0, :], args=(params, wavefunctions_ks[0,:,:], density_reference[0,:], ), method='hybr', tol=1e-16) # Output v_ks v_ks[0,:] = opt_info.x # Compute the corresponding wavefunctions, density, and error hamiltonian = construct_H(params, v_ks[0, :]) eigenenergies_ks, eigenfunctions_ks = sp.linalg.eigh(hamiltonian) wavefunctions_ks[0,:,0:params.num_electrons] = normalise_function(params,eigenfunctions_ks[:,0:params.num_electrons]) density_ks[0, :] = calculate_density_ks(params, wavefunctions_ks[0,:,:]) error = norm(params, density_ks[0, :] - density_reference[0, :], 'MAE') print('Final root finder error = {0} after {1} function evaluations. Status: {2}'.format(error,opt_info.nfev,opt_info.success)) print(' ') # Now optimise the time-dependent KS potential for i in range(1,params.Ntime): # Find the v_ks that minimises the specified objective function opt_info = root(evolution_objective_function,v_ks[i,:],args=(params,wavefunctions_ks[i-1,:,:],density_reference[i,:], 'root', 'expm'), method='hybr', tol=1e-16) # Final (optimal) Kohn-Sham potential at time step i v_ks[i,:] = opt_info.x # Compute the evolved wavefunctions given the optimal Kohn-Sham potential if params.time_step_method == 'CN': wavefunctions_ks[i,:,:] = crank_nicolson_step(params,v_ks[i,:],wavefunctions_ks[i-1,:,:]) elif params.time_step_method == 'expm': wavefunctions_ks[i,:,:] = expm_step(params,v_ks[i,:],wavefunctions_ks[i-1,:,:]) # Final Kohn-Sham density density_ks[i,:] = calculate_density_ks(params, wavefunctions_ks[i,:,:]) # Final error in the Kohn-Sham density away from the reference density error = norm(params, density_ks[i,:] - density_reference[i,:], 'MAE') print('Final error in KS density is {0} at time {1} after {2} iterations'.format(error, round(params.time_grid[i],3), opt_info.nfev), end='\r') print(' ') print(' ') return density_ks, v_ks, wavefunctions_ks
def minimise_energy_dft(params): """ Minimises the Kohn-Sham energy functional by solving the Kohn-Sham equations H[rho] psi = E psi. Returns density, wavefunction, and ground state energy """ # Array that will store SCF iterative densities and residuals history_of_densities_in = np.zeros((params.history_length, params.Nspace)) history_of_densities_out = np.zeros((params.history_length, params.Nspace)) history_of_residuals = np.zeros((params.history_length, params.Nspace)) density_differences = np.zeros((params.history_length, params.Nspace)) residual_differences = np.zeros((params.history_length, params.Nspace)) # Generate initial guess density (sum weighted Gaussians) density_in = initial_guess_density_nonint(params) # Construct the independent part of the hamiltonian, i.e. KE + v_external hamiltonian_independent = construct_hamiltonian_independent(params) # Optionally use scipy's rootfinder #opt_info = sp.optimize.root(ks_objective_function, density_in, args=(params, precond), method='anderson', tol=1e-13) # SCF loop i, error = 0, 1 while error > params.tol_ks: # Iteration number modulus history length i_mod = i % params.history_length i_mod_prev = (i - 1) % params.history_length hamiltonian = update_hamiltonian(params, hamiltonian_independent, density_in) # Solve H psi = E psi eigenvalues, eigenvectors = linalg.eigh(hamiltonian) eigenvectors = normalise_function(params, eigenvectors[:, 0:]) # Extract lowest lying num_particles eigenfunctions and normalise wavefunctions_ks = eigenvectors[:, 0:params.num_particles] wavefunctions_ks[:, 0:params.num_particles] = normalise_function( params, wavefunctions_ks[:, 0:params.num_particles]) # Calculate the output density density_out = calculate_density_ks(params, wavefunctions_ks) # Calculate total energy total_energy = evaluate_energy_functional(params, wavefunctions_ks, density_out) # L1 error between input and output densities error = np.sum(abs(density_in - density_out) * params.dx) print('SCF error = {0} at iteration {1} with energy {2}'.format( error, i, total_energy)) # Store densities/residuals within the iterative history data history_of_densities_in[i_mod, :] = density_in history_of_densities_out[i_mod, :] = density_out history_of_residuals[i_mod, :] = density_out - density_in if i == 0: # Damped linear step for the first iteration density_in = density_in - params.step_length * (density_in - density_out) elif i > 0: # Store more iterative history data... density_differences[i_mod_prev] = history_of_densities_in[ i_mod] - history_of_densities_in[i_mod_prev] residual_differences[i_mod_prev] = history_of_residuals[ i_mod] - history_of_residuals[i_mod_prev] # Perform Pulay step using the iterative history data #density_in = pulay_mixing(params, density_differences, residual_differences, # history_of_residuals[i_mod], history_of_densities_in[i_mod], i) density_in = newton_mixing(params, history_of_densities_in[i_mod], history_of_residuals[i_mod], eigenvectors, eigenvalues, method='adler-wiser') i += 1 # Plot linear response functions of the converged system eigenvalues, eigenvectors = linalg.eigh(hamiltonian) eigenvectors = normalise_function(params, eigenvectors[:, 0:]) susceptibility = calculate_susceptibility(params, eigenvectors, eigenvalues) eigval, eigvec = np.linalg.eigh(susceptibility) for i in range(len(eigval)): plt.plot(eigvec[:, i]) plt.show() plt.clf() dielectric = calculate_dielectric(params, density_out, susceptibility) plt.imshow(susceptibility.real, origin='lower') plt.colorbar() plt.show() return wavefunctions_ks, total_energy, density_out
def minimise_energy_hf(params): # Calculate number of particles num_atoms = len(params.species) num_particles = 0 elements = element_charges(params) for i in range(0, len(params.species)): num_particles += elements[params.species[i]] params.num_electrons = num_particles # Generate initial guess density (sum weighted Gaussians) density_in = initial_guess_density(params) # SCF loop i, error = 0, 1 while error > 1e-10: # Iteration number modulus history length i_mod = i % params.history_length i_mod_prev = (i - 1) % params.history_length # Construct Hamiltonian hamiltonian = construct_ks_hamiltonian(params, density_in) # Solve H psi = E psi eigenvalues, eigenvectors = sp.linalg.eigh(hamiltonian) # Extract lowest lying num_particles eigenfunctions and normalise wavefunctions_ks = eigenvectors[:, 0:num_particles] wavefunctions_ks[:, 0:num_particles] = normalise_function( params, wavefunctions_ks[:, 0:num_particles]) # Calculate the output density density_out = calculate_density_ks(params, wavefunctions_ks) # Calculate total energy energy = calculate_total_energy(params, eigenvalues[0:num_particles], density_out) # L1 error between input and output densities error = np.sum(abs(density_in - density_out) * params.dx_dft) print('SCF error = {0} at iteration {1} with energy {2}'.format( error, i, energy)) # Store densities/residuals within the iterative history data history_of_densities_in[i_mod, :] = density_in history_of_densities_out[i_mod, :] = density_out history_of_residuals[i_mod, :] = density_out - density_in if i == 0: # Damped linear step for the first iteration density_in = density_in - params.step_length * (density_in - density_out) elif i > 0: # Store more iterative history data... density_differences[i_mod_prev] = history_of_densities_in[ i_mod] - history_of_densities_in[i_mod_prev] residual_differences[i_mod_prev] = history_of_residuals[ i_mod] - history_of_residuals[i_mod_prev] # Perform Pulay step using the iterative history data density_in = pulay_mixing_kresse(params, density_differences, residual_differences, history_of_residuals[i_mod], history_of_densities_in[i_mod], i) i += 1