def _load_xyzb(self, file): ''' Method used to load xyz files file - string representing the path to the xyz file returns nothing, modifies self ''' self.name = file.split('/')[-1].strip('.xyzb').capitalize() self.name = self.name.split('\\')[-1].strip('.xyzb').capitalize() coords = list( np.loadtxt(file, skiprows=2, usecols=(1, 2, 3), dtype=float)) elements = list(np.loadtxt(file, skiprows=2, usecols=0, dtype=str)) if 'VEC1' in elements: i = elements.index('VEC1') self.repeat_vector = coords[i] del (elements[i]) del (coords[i]) self.atoms = [] for r in range(self.repeat): for e, c in zip(elements, coords): if r == 0: self.atoms.append(Atom(e, c)) else: if not type(self.repeat_vector) is np.ndarray: utils.message('Error: Please supply repeat vector.', colour='red') else: self.atoms.append(Atom(e, c + (r) * self.repeat_vector)) self._mol_load_finish()
def evaluate(self, p, silent=False): if not silent: utils.message(f'Evaluating molecular orbital of {self.molecule.name} with energy {self.energy} eV') dens = np.zeros(p.size//3) for ao, w in zip(self.aos, self.weights): dens += w * ao.evaluate(p) return dens
def draw_electrostatic_potential(self, molecule, points=50000, colour_map=cmap.ElectroStat()): if not hasattr(molecule, '_elec_stat_pos'): utils.message( f'Calculating electrostatic potential of {molecule.name} ...') samples = 50 * points rang = np.amax([np.abs(atom.coords) for atom in molecule.atoms]) + 4 x, y, z = ( (np.random.randint(-rang * 10000, rang * 10000, size=samples) / 10000), (np.random.randint(-rang * 10000, rang * 10000, size=samples) / 10000), (np.random.randint(-rang * 10000, rang * 10000, size=samples) / 10000)) d = molecule.electrostatic_potential(np.asarray( (x, y, z)).T).flatten() index = abs(d).argsort()[::-1] d = np.maximum(d, np.mean(d) * 4) colours = colour_map[d].T x, y, z, colours = x[index][0:points], y[index][0:points], z[ index][0:points], colours[index][0:points] molecule._elec_stat_pos, molecule._elec_stat_colours = np.asarray( (x, y, z)).T, colours self.draw_pixels(molecule._elec_stat_pos, colour_array=molecule._elec_stat_colours)
def _mol_load_finish(self): ''' Method that is called by both xyz and pubchem loading of molecules ''' utils.message(f'Succesfully loaded {self.name}.', 'green') self.center() self.set_bonds() self._load_basis_set()
def load_basis(self): ''' Method that loads the basis set for the atoms in the molecule. If the basis set does not exist in the database, we will download the basis set from https://www.basissetexchange.org/ using their API ''' basis_type = self.basis_type.replace('*', '@') bsf_path = os.getcwd()+rf'\Basis_Sets\{basis_type}.bsf' if not os.path.exists(bsf_path): utils.message(f'Error: Basis set {self.basis_type} not found, downloading ...', colour='red') import requests response = requests.get("http://basissetexchange.org" + f'/api/basis/{self.basis_type}/format/json') if response: utils.message(f'Succesfully obtained basis set file', colour='green') with open(bsf_path, 'w+') as f: f.write(response.text) self.load_basis() else: utils.message(f'Error: Failed to obtain basis set file', colour='red') else: utils.message(f'Succesfully loaded {self.basis_type}', colour='green') with open(bsf_path, 'r') as f: # self.params = json.load(f)['elements'][str(self.atom.atomic_number)]['electron_shells'] self.params = json.load(f)['elements']
def _load_from_pubchem(self, name): ''' Method used to load data from pubchem name - name of compound to search returns nothing ''' import pubchempy as pcp try: name = int(name) except: pass record_type = '3d' mol = pcp.get_compounds(name, ('name', 'cid')[type(name) is int], record_type=record_type) if len(mol) == 0: utils.message( 'Error: Could not find 3d structure of {name}... Attempting to find 2d structure...', 'red') record_type = '2d' mol = pcp.get_compounds(name, ('name', 'cid')[type(name) is int], record_type=record_type) if len(mol) == 0: utils.message('Error: No structural data found for {name}.', 'red') else: mol = mol[0] coords = np.asarray([[a.x, a.y, a.z] for a in mol.atoms]) coords = np.where(coords == None, 0, coords).astype(float) elements = np.asarray([a.element for a in mol.atoms]) self.name = name.capitalize() self.atoms = [] [ self.atoms.append(Atom(elements[i], coords[i])) for i in range(len(coords)) ] if record_type == '3d': self.save_to_xyz(os.getcwd() + rf'\Molecules\{name.lower()}.xyz') self._mol_load_finish()
def save(self, file=None, comment='generated via python\n', filetype='xyz'): ''' Method that saves the molecule to a file file - string path to new file ''' if file == None: file = os.getcwd() + rf'\Molecules\{self.name.lower()}.{filetype}' else: filetype = file.split('.')[-1] if not file.endswith('.' + filetype): file += '.' + filetype if not comment.endswith('\n'): comment += '\n' if filetype == 'xyz': #standard xyz format with open(file, 'w+') as f: f.write(f'{self.natoms}\n') f.write(comment) for a in self.atoms: f.write( f'{a.symbol: <2} \t {a.coords[0]: >8.5f} \t {a.coords[1]: >8.5f} \t {a.coords[2]: >8.5f}\n' ) elif filetype == 'xyzb': #xyz format plus bond information with open(file, 'w+') as f: f.write(f'{self.natoms}\n') f.write(comment) for a in self.atoms: f.write( f'{a.symbol: <2} \t {a.coords[0]: >8.5f} \t {a.coords[1]: >8.5f} \t {a.coords[2]: >8.5f}\n' ) f.write('\n') for a1, a2 in self.get_unique_bonds(): f.write( f'{self.atoms.index(a1)} \t {self.atoms.index(a2)} \t {a1.bond_orders[a2]}\n' ) utils.message(f'Saved molecule to {file}') return file
def save_to_xyz(self, file='', comment='generated via python\n'): ''' Method that saves the molecule to a file file - string path to new file ''' if file == '': file = os.getcwd() + rf'\Molecules\{self.name.lower()}.xyz' if not comment.endswith('\n'): comment += '\n' with open(file, 'w+') as f: f.write(f'{self.natoms}\n') f.write(comment) for a in self.atoms: f.write( f'{a.symbol} {a.coords[0]:.5f} {a.coords[1]:.5f} {a.coords[2]:.5f}\n' ) utils.message(f'Saved molecule to {file}')
def _load_mol(self, molecule_file): #get the file extension: if '.' in molecule_file: name, filetype = molecule_file.split('.') else: utils.message(f'Searching pubchem for {molecule_file}...') self._load_from_pubchem(molecule_file) return if filetype == 'pcp': utils.message(f'Searching pubchem for {name}...') self._load_from_pubchem(name) return else: #check if file exists: if not os.path.exists(molecule_file): utils.message( f'Error: File {molecule_file} does not exist. Searching pubchem ...', colour='red') self._load_from_pubchem(molecule_file) return else: if filetype == 'xyz': self._load_xyz(molecule_file) return if filetype == 'xyzb': self._load_xyzb(molecule_file) return
def extended_huckel(molecule, K=1.75): ''' Function that performs the extended huckel method Returns energies and mo's ''' utils.message(f'Performing Extended Huckel-Method for {molecule.name}.') aos = molecule.basis.atomic_orbitals dim = len(aos) H = np.zeros((dim, dim)) for i in range(dim): H[i,i] = -aos[i].atom.ionisation_energy[sum(aos[i].cardinality)] for i in range(dim): for j in range(i+1, dim): H[i,j] = H[j,i] = K * overlap_integral(aos[i], aos[j]) * (H[i,i] + H[j,j])/2 energies, weights = np.linalg.eigh(H) molecule.molecular_orbitals = [] molecule.molecular_orbitals = sorted(molecule.molecular_orbitals, key=lambda x: x.energy) for energy, weight in zip(energies, weights.T): molecule.molecular_orbitals.append(MolecularOrbital(molecule, aos, weight, energy)) utils.message(f'{len(energies)} molecular orbitals found.') utils.message(f'Highest and lowest energies: {max(energies)}, {min(energies)} eV')
def pre_render_densities(self, orbitals, points=50000, colour_map=cmap.BlueRed(posneg_mode=True)): utils.message( f'Pre-rendering {len(orbitals)} orbitals ({points} points):') for i, orbital in enumerate(orbitals): utils.message( f' Progress: {i+1}/{len(orbitals)} = {round((i+1)/len(orbitals)*100,2)}%' ) samples = 10 * points ranges = orbital.ranges x, y, z = ((np.random.randint( ranges[0] * 10000, ranges[1] * 10000, size=samples) / 10000), ( np.random.randint( ranges[2] * 10000, ranges[3] * 10000, size=samples) / 10000), (np.random.randint( ranges[4] * 10000, ranges[5] * 10000, size=samples) / 10000)) d = orbital.evaluate(np.asarray((x, y, z)), True).flatten() index = abs(d**2).argsort()[::-1] colours = colour_map[d].T x, y, z, colours = x[index][0:points], y[index][0:points], z[ index][0:points], colours[index][0:points] dens_pos = self.rotate( np.asarray((x, y, z)).T, orbital.molecule.rotation) self._dens_pos[orbital], self._dens_colours[ orbital] = dens_pos, colours orbital.molecule._dens_pos[ orbital], orbital.molecule._dens_colours[ orbital] = dens_pos, colours utils.message( f'Orbitals prepared. Please use Screen3D.draw_density() to display the orbitals.' )
def MOTD(): utils.ff_print_source(False) utils.ff_use_colours(fancy_format_colours) utils.ff_print_time(False) os.system('color 07') features = [ 'Loading molecules from xyz files or alternatively download from pubchem.', 'Visualising molecules as a ball-and-stick model or as a wireframe model.', 'Algorithms for guessing atom types, bonds, and bond orders.', 'Support for STO-nG basis set for atomic/molecular orbital visualtions and calculations.', 'Crude extended-Hückel method for calculation of molecular orbitals.' ] planned_features = [ 'Improvements to extended-Hückel and molecular integrals.', 'Marching cubes algorithm to visualise orbitals as iso-volumes instead of current random-dot style.' ] utils.message(f'Welcome, this project so far supports the following:', colour='blue') [utils.message(f'-- {f}', colour='blue') for f in features] utils.message(f'Planned features:', colour='blue') [utils.message(f'-- {f}', colour='blue') for f in planned_features] print()
updt = 0 time = 0 rot = np.array([0., 0.]) zoom = 0 pg.key.set_repeat() run = True mo_numb = 0 draw_dens = False camera_range = max(max([a.coords[0] for a in mol.atoms]), max([a.coords[1] for a in mol.atoms]), max([a.coords[2] for a in mol.atoms])) + 10 screen.camera_position = np.asarray((0, 0, camera_range)) utils.message( 'Please press ENTER to toggle orbital display. Use arrow-keys to switch between orbitals.' ) utils.message('Hold CTRL and use mouse to rotate and move molecule.') ##################### mols = [mol] if randomize_structure: for a1, a2 in mol.get_rotatable_bonds(): mol.rotate_bond(a1, a2, np.random.random() * 2 * 1 * 3.14 - 1 * 3.14) mol.shake(0.5) mol.center() if minimize_structure: mols, energies = minimizer.minimize(mol, 'uff',
import modules.utils as utils utils.ff_verbosity(0) utils.message(('a', 'b', 'c'), (1, 6, 9))
def guess_bond_orders(self): ''' Method that guesses the bond orders of the molecule. Current strategy: - Sort elements from low valence to high valence (H < O < N < C, etc..) and loops over the elements. - Collect every atom of the element and checks its bond saturation. - If the atom is not saturated, loop over the atoms it is bonded to. - Check the saturation of the bonded atom. If the bonded atom is also not saturated, increase the bond order to that bond. - Terminate the loop if the current atom is saturated. ''' self.reset_bond_orders() #sort the elements by valence valences = list(self.max_valence.items()) valences = sorted(valences, key=lambda x: x[1]) for el, _ in valences: #get all atoms of element el atoms = self.get_by_element(el).copy() if self.guess_bond_order_iters > 0: np.random.shuffle(atoms) else: atoms = sorted(atoms, key=lambda x: x.hybridisation) #loop over the atoms for a in atoms: #check if a is saturated if a.is_unsaturated(): #if not, get its neighbours neighbours = np.copy(self.get_bonded_atoms(a)) # np.random.shuffle(neighbours) neighbours = sorted(neighbours, key=lambda x: a.distance_to(x)) neighbour_hybrids = [x.hybridisation for x in neighbours] #loop over the neighbours if a.hybridisation == 'sp' and 'sp' in neighbour_hybrids: x = neighbour_hybrids.index('sp') a.bond_orders[neighbours[x]] = 3 neighbour.bond_orders[a] = 3 if a.is_unsaturated(): for neighbour in neighbours: #check if the neighbour is also unsaturated and whether a has #become saturated in the meantime if neighbour.is_unsaturated() and a.is_unsaturated( ): a.bond_orders[neighbour] += 1 neighbour.bond_orders[a] += 1 #give warnings if necessary mbo = sum([a.is_saturated() for a in self.get_by_element('C')]) - len( self.get_by_element('C')) if self._warning_level == 2: if mbo < 0: utils.message( f'Error: Bond order guessing was not succesful. Unsaturated atoms: {abs(mbo)} (iteration {self.guess_bond_order_iters}).', 'red') else: utils.message( f'Bond order guessing succesful after {self.guess_bond_order_iters+1} iterations', 'green') elif self._warning_level == 1: if mbo == 0: utils.message( f'Bond order guessing succesful after {self.guess_bond_order_iters+1} iteration{"s"*(self.guess_bond_order_iters>0)}.', 'green') elif self.guess_bond_order_iters == self.natoms: utils.message( f'Bond order guessing was not succesful. Unsaturated atoms: {abs(mbo)}.', 'red') if mbo < 0 and self.guess_bond_order_iters < 5 * self.natoms: self.guess_bond_order_iters += 1 self.guess_bond_orders()
def get_energy_from_coords(self, coords, morse_potential=True, use_torsions=True): molecule = self.molecule atoms = molecule.atoms n = len(atoms) for c, a in zip(coords[:n * 3].reshape((n, 3)), atoms): a.coords = c if use_torsions: for t, a in zip(coords[n * 3:], molecule.get_rotatable_bonds()): self.mol.rotate_bond(*a, t) utils.message('ATOM TYPES', 1) utils.message(f'IDX | TYPE | RING', 1) for i, a in enumerate(atoms): utils.message(f'{i: <3} | {self.get_atom_type(a): <5} | {a.ring}', 1) #### E = E_r + E_theta + E_phi + E_ohm + E_vdm + E_el ## E_r: utils.message('BOND STRETCH ENERGY', 2) utils.message('ATOM 1 | ATOM 2 | BO | BOND LEN | IDEAL LEN | ENERGY', 2) E_r = 0 for a1, a2 in molecule.get_unique_bonds(): #some parameters for a1 e1 = self.get_atom_type(a1) r1 = self.valence_bond[e1] chi1 = a1.electro_negativity Z1 = self.effective_charge[e1] #some parameters for a2 e2 = self.get_atom_type(a2) r2 = self.valence_bond[e2] chi2 = a2.electro_negativity Z2 = self.effective_charge[e2] #dist between a1 and a2 r = a1.distance_to(a2) #bo between a1 and a2 n = a1.bond_orders[a2] if a1.ring == a2.ring == 'AR': n = 1.5 if morse_potential: # r12 = r1 + r2 - (r1*r2*(sqrt(chi1) - sqrt(chi2))**2)/(chi1*r1 + chi2*r2) r12 = r1 + r2 - 0.1332 * (r1 + r2) * log(n) + r1 * r2 * ( sqrt(chi1) - sqrt(chi2))**2 / (chi1 * r1 + chi2 * r2) k12 = 664.12 * Z1 * Z2 / r12**3 D12 = 70 * n alpha = sqrt(k12 / (2 * D12)) E_ri = D12 * (exp(-alpha * (r - r12)) - 1)**2 else: rBO = -0.1332 * (r1 + r2) * log(n) rEN = r1 * r2 * (sqrt(chi1) - sqrt(chi2))**2 / (chi1 * r1 + chi2 * r2) r12 = r1 + r2 + rBO + rEN k12 = 664.12 * Z1 * Z2 / r12**3 E_ri = .5 * k12 * (r - r12)**2 E_r += E_ri utils.message( f'{e1: <6} | {e2: <6} | {n: <3.1f} | {r: <8.3f} | {r12: <8.3f} | {E_ri: <.3f}', 2) ## E_theta: E_theta = 0 for a1, a2, a3, theta in molecule.get_unique_bond_angles(): e1 = self.get_atom_type(a1) e2 = self.get_atom_type(a2) e3 = self.get_atom_type(a3) Z1 = self.effective_charge[e1] Z2 = self.effective_charge[e2] Z3 = self.effective_charge[e3] r12, r23, r13 = a1.distance_to(a2), a2.distance_to( a3), a1.distance_to(a3) beta = 664.12 / (r12 * r23) t0 = self.valence_angle[e2] * pi / 180 K123 = beta * Z1 * Z3 / r13**5 * r12 * r23 * (3 * r12 * r23 * (1 - cos(t0)**2) - r13**2 * cos(t0)) C2 = 1 / (4 * sin(t0)**2) C1 = -4 * C2 * cos(t0) C0 = C2 * (2 * cos(t0)**2 + 1) E_theta_i = K123 * (C0 + C1 * cos(theta) + C2 * cos(2 * theta)) E_theta += E_theta_i #E_phi: utils.message('BOND STRETCH ENERGY', 2) utils.message( 'ATOM 1 | ATOM 2 | ATOM 3 | ATOM 4 | TORSION | IDEAL TORS | ENERGY', 2) E_phi = 0 for a1, a2, a3, a4, phi in molecule.get_unique_torsion_angles(): e1 = self.get_atom_type(a1) e2 = self.get_atom_type(a2) e3 = self.get_atom_type(a3) e4 = self.get_atom_type(a4) Vbarr = 1 if a2.hybridisation == a3.hybridisation == 3: V2, V3 = self.sp3_torsional_barrier_params[ e2], self.sp3_torsional_barrier_params[e3] Vbarr = sqrt(V2 * V3) n = 3 phi0 = pi, pi / 3 #two different phi0 possible if (a2.hybridisation == 3 and a3.hybridisation == 2) or (a2.hybridisation == 2 and a3.hybridisation == 3): Vbarr = 1 n = 6 phi0 = 0, 0 if a2.hybridisation == a3.hybridisation == 2: U2 = self.sp2_torsional_barrier_params[ e2] # period starts at 1 U3 = self.sp2_torsional_barrier_params[e3] Vbarr = 5 * sqrt( U2 * U3) * (1 + 4.18 * log(a2.bond_orders[a3])) n = 2 phi0 = pi, 1.047198 #since two differen phi0 are possible, calculate both and return highest energy E_phi1 = 0.5 * Vbarr * (1 - cos(n * phi0[0]) * cos(n * phi)) E_phi2 = 0.5 * Vbarr * (1 - cos(n * phi0[1]) * cos(n * phi)) E_phi_i = min(E_phi1, E_phi2) E_phi += E_phi_i torsion = divmod(phi, phi0[0])[1] if phi0[0] is not 0 else 0 utils.message( f'{e1: <6} | {e2: <6} | {e3: <6} | {e4: <6} | {torsion: <7.3f} | {phi0[0]: <10.3f} | {E_phi_i: <.3f}', 2) #E_vdw: utils.message('VAN DER WAALS ENERGY', 2) utils.message('ATOM 1 | ATOM 2 | BOND LEN | FORCE CONST | ENERGY', 2) E_vdw = 0 for a1, a2 in molecule.get_unique_atom_pairs( ): #cutoff is at 2 angstrom if a1.bond_dist_to(a2) > 2: e1 = self.get_atom_type(a1) e2 = self.get_atom_type(a2) D1 = self.nonbond_energy[e1] D2 = self.nonbond_energy[e2] D12 = sqrt(D1 * D2) x = a1.distance_to(a2) x1 = self.nonbond_distance[e1] x2 = self.nonbond_distance[e2] x12 = .5 * (x1 + x2) E_vdw_i = D12 * (-2 * (x12 / x)**6 + (x12 / x)**12) E_vdw += E_vdw_i utils.message( f'{e1: <6} | {e2: <6} | {x: <8.3f} | {D12: <11.3f} | {E_vdw_i: <.3f}', 2) utils.message( f'TOTAL BOND STRETCHING ENERGY = {round(E_r*4.2,3)} kJ/mol', 1) utils.message( f'TOTAL ANGLE BENDING ENERGY = {round(E_theta*4.2,3)} kJ/mol', 1) utils.message(f'TOTAL TORSIONAL ENERGY = {round(E_phi*4.2,3)} kJ/mol', 1) utils.message( f'TOTAL VAN DER WAAL\'S ENERGY = {round(E_vdw*4.2,3)} kJ/mol', 1) utils.message( f'TOTAL ENERGY = {round((E_r + E_theta + E_phi + E_vdw)*4.2,3)} kJ/mol', 1) return E_r + E_theta + E_phi + E_vdw
def perform_md(mol, ff='uff', run_time=.5e-12, time_step=.5e-15, bath_temp=273, sample_freq=5, temp_coupling_strength=0): ''' Function to perform molecular dynamics using the leap frog algorithm ''' utils.ff_block_source('uff.get_energy') if ff == 'uff': ff = uff.ForceField() atoms = mol.atoms l = len(atoms) masses = np.expand_dims(np.asarray([a.mass for a in atoms]), 1) #initialize velocities velocities = np.random.normal(loc=0.5, scale=0.5, size=(l, 3, 12)) velocities = np.sum(velocities, axis=2) - 6 velocities *= np.sqrt(k * bath_temp / masses) velocities *= sqrt(bath_temp / calculate_temp(velocities, masses)) utils.message( f'Starting molecular dynamics simulation for molecule {mol.name} with the {ff.name} forcefield.' ) utils.message( f'Run Time: {run_time:.2e} s; Time Step: {time_step:.2e} s; Temperature: {bath_temp} K', 1) mol = copy.deepcopy(mol) mols = [mol] energies = [ff.get_energy(mol)] time = 0 i = 0 while time < run_time: forces = get_forces(mol, 1e-6, ff).reshape((l, 3)) velocities += forces / masses * time_step temp = calculate_temp(velocities, masses) temp_couple_scaling = sqrt(1 + temp_coupling_strength * (bath_temp / temp - 1)) velocities *= temp_couple_scaling if i % sample_freq == 0: mol.center() mols.append(copy.deepcopy(mol)) energies.append(ff.get_energy(mol)) utils.message(( f'Current Time: {time:.2e} s with ENERGY = {energies[-1]:.2f} kcal/mol and TEMPERATURE = {temp:.2f} K', f'Current Time: {time:.2e} s with ENERGY = {energies[-1]:.2f} kcal/mol and TEMPERATURE = {temp:.2f} K\nCOORDINATES (angstrom):\n{mol}\n\nVELOCITIES (angstrom/s):\n{velocities}\n' ), (1, 2)) for j, a in enumerate(mol.atoms): a.coords += velocities[j] * time_step time += time_step i += 1 utils.ff_unblock_source('uff.get_energy') return mols, energies
def __init__(self, element, coords): self.coords = coords self.bonds = [] self.bond_orders = {} self.selected = False try: el = pt.elements[element] except: try: el = pt.elements.symbol(element) except: try: el = pt.elements.name(element) except: utils.message('Error: Could not parse element {element}.', 'red') self.ionisation_energy = np.genfromtxt('modules\\ionisation_energies', usecols=(1, 2), missing_values='', delimiter=';')[el.number] self.symbol = el.symbol self.name = el.name self.atomic_number = el.number self.mass = el.mass self.radius = el.covalent_radius self.electro_negativity = { 1: 2.2, 2: 0, 3: 0.98, 4: 1.57, 5: 2.04, 6: 2.55, 7: 3.04, 8: 3.44, 9: 3.98, 10: 0, 11: 0.93, 12: 1.31, 13: 1.61, 14: 1.9, 15: 2.19, 16: 2.58, 17: 3.16, 18: 0, 19: 0.82, 20: 1, 21: 1.36, 22: 1.54, 23: 1.63, 24: 1.66, 25: 1.55, 26: 1.83, 27: 1.88, 28: 1.91, 29: 1.9, 30: 1.65, 31: 1.81, 32: 2.01, 33: 2.18, 34: 2.55, 35: 2.96, 36: 3, 37: 0.82, 38: 0.95, 39: 1.22, 40: 1.33, 41: 1.6, 42: 2.16, 43: 1.9, 44: 2.2, 45: 2.28, 46: 2.2, 47: 1.93, 48: 1.69, 49: 1.78, 50: 1.96, 51: 2.05, 52: 2.1, 53: 2.66, 54: 2.6, 55: 0.79, 56: 0.89, 57: 1.1, 58: 1.12, 59: 1.13, 60: 1.14, 61: 1.13, 62: 1.17, 63: 1.2, 64: 1.2, 65: 1.22, 66: 1.23, 67: 1.24, 68: 1.24, 69: 1.25, 70: 1.1, 71: 1.27, 72: 1.3, 73: 1.5, 74: 2.36, 75: 1.9, 76: 2.2, 77: 2.2, 78: 2.28, 79: 2.54, 80: 2, 81: 1.62, 82: 2.33, 83: 2.02, 84: 2, 85: 2.2, 86: 0, 87: 0.7, 88: 0.89, 89: 1.1, 90: 1.3, 91: 1.5, 92: 1.38, 93: 1.36, 94: 1.28, 95: 1.3, 96: 1.3, 97: 1.3, 98: 1.3, 99: 1.3, 100: 1.3, 101: 1.3, 102: 1.3, 103: 00, 104: 0, 105: 0, 106: 0, 107: 0, 108: 0, 109: 0, 110: 0, 111: 0, 112: 0, 113: 0, 114: 0, 115: 0, 116: 0, 117: 0, 118: 0, }[self.atomic_number] try: self.GMP_electro_negativity = { 1: 0.89, 3: 0.97, 4: 1.47, 5: 1.6, 6: 2, 7: 2.61, 8: 3.15, 9: 3.98, 11: 1.01, 12: 1.23, 13: 1.47, 14: 1.58, 15: 1.96, 16: 2.35, 17: 2.74, 19: 0.91, 20: 1.04, 21: 1.2, 22: 1.32, 23: 1.45, 24: 1.56, 25: 1.6, 26: 1.64, 27: 1.7, 28: 1.75, 29: 1.75, 30: 1.66, 31: 1.82, 32: 1.51, 33: 2.23, 34: 2.51, 35: 2.58, 37: 0.89, 38: 0.99, 39: 1.11, 40: 1.22, 41: 1.23, 42: 1.3, 44: 1.42, 45: 1.54, 46: 1.35, 47: 1.42, 48: 1.46, 49: 1.49, 50: 1.72, 51: 1.72, 52: 2.72, 53: 2.38, 55: 0.86, 56: 0.97, 57: 1.08, 58: 1.08, 59: 1.07, 60: 1.07, 62: 1.07, 63: 1.01, 64: 1.11, 65: 1.1, 66: 1.1, 67: 1.1, 68: 1.11, 69: 1.11, 70: 1.06, 71: 1.14, 72: 1.23, 73: 1.33, 74: 1.4, 75: 1.46, 77: 1.55, 80: 1.44, 81: 1.44, 82: 1.55, 83: 1.67, 90: 1.11, 92: 1.22, }[self.atomic_number] except: self.GMP_electro_negativity = 0 #get period of atom if self.atomic_number in (1, 2): self.period = 1 if self.atomic_number in range(3, 11): self.period = 2 if self.atomic_number in range(11, 19): self.period = 3 if self.atomic_number in range(19, 37): self.period = 4 if self.atomic_number in range(37, 55): self.period = 5 if self.atomic_number in range(55, 87): self.period = 6 try: self.colour = self.draw_colour = { 'C': (34, 34, 34), 'H': (255, 255, 255), 'O': (255, 22, 0), 'N': (22, 33, 255), 'S': (225, 225, 48), 'Ca': (61, 255, 0), 'Fe': (221, 119, 0), 'Mg': (0, 119, 0), 'P': (255, 153, 0), 'Cl': (31, 240, 31), 'Na': (119, 0, 255), }[self.symbol] except: utils.message( 'Error: No default colour found for {self.symbol}. Defaulting to (0,0,0).', 'red') self.colour = (0, 0, 0) try: self.max_valence = { 'C': 4, 'H': 1, 'O': 2, 'N': 3, 'Mg': 2, 'P': 6, 'Cl': 1, 'S': 6, 'Na': 1, 'Fe': 2, }[self.symbol] except: utils.message( 'Error: No max_valence found for {self.symbol}. Defaulting to 1.', 'red') self.max_valence = 1
def minimize(self, method='sd', max_steps=1500, step_factor=4e-4, sample_freq=10, use_torsions=True, converge_thresh=8e-2): ''' Method that performs the energy minimization of self.mol method: str - specify method (steepest descent 'sd', conjugate gradient 'cg') max_steps: int - specify the maximum number of iterations allowed for minimization sample_freq: int - specify the frequency with which a snapshot is taken (in iterations) use_torsions: bool - specify whether to use rotation around bonds as coordinates in minimization returns tuple of lists containing molecule objects and energies ''' assert (max_steps > 0) assert (step_factor > 0) assert (sample_freq > 0) mol = self._mol utils.message(( f'Starting geometry optimisation for molecule {mol.name} with {self._ff.name} using steepest descent.', f'Starting geometry optimisation for molecule {mol.name} with {self._ff.name} using steepest descent.\nINITIAL COORDINATES (angstrom):\n{mol}' ), (0, 1)) utils.message( f'Max. Steps: {max_steps}; Step-Factor: {step_factor:.2e}; Convergence Thresh.: {converge_thresh:.2e}; Apply to Bond Rotation: {use_torsions}', 1) self._ff.molecule = mol energy = self._ff.get_energy mols = [copy.deepcopy(mol)] energies = [energy(mol)] if method == 'sd': gradient = nd.Gradient(self._ff.get_energy_from_coords) #get the coordinates: coords = [a.coords for a in mol.atoms] #cartesian coordinates of the atoms if use_torsions: coords += [0 for _ in mol.get_rotatable_bonds()] #torsions coords = np.asarray(coords).flatten().astype( float) #flatten vector #start minimization for i in range(max_steps): #calculate gradients and change coords forces = -gradient(coords) * step_factor coords += forces converged = np.all(np.absolute(forces) < converge_thresh) #sample mol if (i + 1) % sample_freq == 0 or converged: mols.append(copy.deepcopy(mol)) energies.append(energy(mol)) if converged: break utils.message(( f'Current Step: {i+1} with ENERGY = {energies[-1]:.6f} kcal/mol', f'Current Step: {i+1} with ENERGY = {energies[-1]:.6f} kcal/mol\nCOORDINATES (angstrom):\n{mol}\n\nFORCES (kcal/mol/angstrom):\n{forces}\n' ), (1, 2)) #done if i < max_steps - 1: utils.message(( f'Molecule optimization succesful after {i+1} steps with ENERGY = {energy(mol):.6f} kcal/mol', f'Molecule optimization succesful after {i+1} steps with ENERGY = {energy(mol):.6f} kcal/mol\nCOORDINATES (angstrom):\n\n{mol}\n\nFORCES (kcal/mol/angstrom):\n\n{forces}\n' ), (0, 2), colour='green') else: utils.message(( f'Molecule optimization failed after {i+1} steps with ENERGY = {energy(mol):.6f} kcal/mol', f'Molecule optimization failed after {i+1} steps with ENERGY = {energy(mol):.6f} kcal/mol\nCOORDINATES (angstrom):\n\n{mol}\n\nFORCES (kcal/mol/angstrom):\n\n{forces}\n' ), (0, 2), colour='red') return mols, energies
def minimize(mol, ff='uff', max_steps=1500, converge_thresh=8e-2, step_factor=4e-4, sample_freq=10, use_torsions=True, max_step_size=0.3, method='sd', fix_torsion=False): ''' Energy minimization method that attempts to optimize the structureof a molecule using a given force field (must have a get_energy() method). The method used is the steepest descent method, which follows the negative energy gradient with respect to the coordinates of the molecule multiplied by step_factor. Every sample_freq iterations a snapshot is taken. ''' assert (max_steps > 0) assert (converge_thresh > 0) assert (step_factor > 0) assert (sample_freq > 0) if ff == 'uff': ff = uff.ForceField() utils.message( f'Starting geometry optimisation for molecule {mol.name} with {ff.name} using steepest descent.' ) utils.message( f'Max. Steps: {max_steps}; Step-Factor: {step_factor:.2e}; Max. Step Size: {max_step_size}; Converge Thresh.: {converge_thresh:.2e}; Apply to Torsion: {use_torsions}', 1) mol = copy.deepcopy(mol) mols = [mol] energies = [ff.get_energy(mol)] energy = 0 if method == 'sd': for i in range(max_steps): forces = get_forces(mol, 1e-6, ff, use_torsions=use_torsions) prev_energy = energy energy = ff.get_energy(mol) converged = np.all(np.absolute(forces) < converge_thresh ) or abs(energy - prev_energy) < converge_thresh if (i + 1) % sample_freq == 0 or converged: mol.center() mols.append(copy.deepcopy(mol)) energies.append(ff.get_energy(mol)) if converged: break utils.message(( f'Current Step: {i+1} with ENERGY = {energies[-1]:.6f} kcal/mol', f'Current Step: {i+1} with ENERGY = {energies[-1]:.6f} kcal/mol\nCOORDINATES (angstrom):\n\n{mol}\n\nFORCES (kcal/mol/angstrom):\n\n{forces}\n' ), (0, 2)) if use_torsions: for j, a in enumerate(mol.atoms): a.coords += (forces[3 * j:3 * j + 3] * step_factor) for k, t in enumerate(list(mol.get_rotatable_bonds())): mol.rotate_bond( t[0], t[1], step_factor * forces[3 * len(mol.atoms) + k]) else: for j, a in enumerate(mol.atoms): a.coords += forces[3 * j:3 * j + 3] * step_factor if type(fix_torsion) is float: mol.set_torsion_angle(mol.atoms[3], mol.atoms[1], mol.atoms[0], mol.atoms[2], fix_torsion) if i < max_steps - 1: utils.message(( f'Molecule optimization succesful after {i+1} steps with ENERGY = {ff.get_energy(mol):.6f} kcal/mol', f'Molecule optimization succesful after {i+1} steps with ENERGY = {ff.get_energy(mol):.6f} kcal/mol\nCOORDINATES (angstrom):\n\n{mol}\n\nFORCES (kcal/mol/angstrom):\n\n{forces}' ), (0, 2), colour='green') else: utils.message(( f'Molecule optimization failed after {i+1} steps with ENERGY = {ff.get_energy(mol):.6f} kcal/mol', f'Molecule optimization failed after {i+1} steps with ENERGY = {ff.get_energy(mol):.6f} kcal/mol\nCOORDINATES (angstrom):\n\n{mol}\n\nFORCES (kcal/mol/angstrom):\n\n{forces}' ), (0, 2), colour='red') return mols, energies if method == 'cg': #first step is the same as steepest descent: forces = get_forces(mol, 1e-6, ff, use_torsions=use_torsions) step = forces * step_factor prev_step = step if use_torsions: for j, a in enumerate(mol.atoms): a.coords += (step[3 * j:3 * j + 3]) for k, t in enumerate(list(mol.get_rotatable_bonds())): mol.rotate_bond(t[0], t[1], step[3 * len(mol.atoms) + k]) else: for j, a in enumerate(mol.atoms): a.coords += step[3 * j:3 * j + 3] #next steps: for i in range(max_steps): forces = get_forces(mol, 1e-6, ff, use_torsions=use_torsions) gamma = np.dot(-forces, -forces) / np.dot(prev_step, prev_step) step = forces * gamma * prev_step prev_step = step if (i + 1) % sample_freq == 0 or np.all( np.absolute(forces) < converge_thresh): mol.center() mols.append(copy.deepcopy(mol)) energies.append(ff.get_energy(mol)) if np.all(np.absolute(forces) < converge_thresh): break utils.message(( f'Current Step: {i+1} with ENERGY = {energies[-1]:.6f} kcal/mol', f'Current Step: {i+1} with ENERGY = {energies[-1]:.6f} kcal/mol\nCOORDINATES (angstrom):\n\n{mol}\n\nFORCES (kcal/mol/angstrom):\n\n{forces}' ), (1, 2)) if use_torsions: for j, a in enumerate(mol.atoms): a.coords += (step[3 * j:3 * j + 3]) for k, t in enumerate(list(mol.get_rotatable_bonds())): mol.rotate_bond(t[0], t[1], step[3 * len(mol.atoms) + k]) else: for j, a in enumerate(mol.atoms): a.coords += step[3 * j:3 * j + 3] return mols, energies