def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None: ''' ''' self.index = i self.symbol = pt[mol.atomnos[i]].symbol neighbors_indexes = neighbors(mol.graph, i) self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes] self.coord = mol.atomcoords[conf][i] self.others = mol.atomcoords[conf][neighbors_indexes] self.vectors = self.others - self.coord # vector connecting center to substituent if update: if orb_dim is None: key = self.symbol + ' ' + str(self) orb_dim = orb_dim_dict.get(key) if orb_dim is None: orb_dim = orb_dim_dict['Fallback'] print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.') if mol.sigmatropic[conf]: # two p lobes p_lobe = norm(np.cross(self.vectors[0], self.vectors[1]))*orb_dim self.orb_vecs = np.concatenate(([p_lobe], [-p_lobe])) else: # lone pair lobe self.orb_vecs = np.array([-norm(np.mean([norm(v) for v in self.vectors], axis=0))*orb_dim]) self.center = self.orb_vecs + self.coord
def adjust_forces(self, atoms, forces): direction = atoms.positions[self.i2] - atoms.positions[self.i1] # vector connecting atom1 to atom2 spring_force = self.k * (norm_of(direction) - self.d_eq) # absolute spring force (float). Positive if spring is overstretched. if not self.tight: spring_force = np.clip(spring_force, -50, 50) # force is clipped at 50 eV/A forces[self.i1] += (norm(direction) * spring_force) forces[self.i2] -= (norm(direction) * spring_force)
def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None: self.index = i self.symbol = pt[mol.atomnos[i]].symbol neighbors_indexes = neighbors(mol.graph, i) self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes] self.coord = mol.atomcoords[conf][i] self.others = mol.atomcoords[conf][neighbors_indexes] self.vectors = self.others - self.coord # vectors connecting reactive atom with neighbors v1 = self.vectors[0] # v1 connects first bonded atom to the metal itself neighbor_of_neighbor_index = neighbors(mol.graph, neighbors_indexes[0])[0] v2 = mol.atomcoords[conf][neighbor_of_neighbor_index] - self.coord # v2 connects first neighbor of the first neighbor to the metal itself self.orb_vec = norm(rot_mat_from_pointer(np.cross(v1, v2), 120) @ v1) # setting the pointer (orb_vec) so that orbitals are oriented correctly # (Lithium enolate in mind) steps = 4 # number of total orbitals self.orb_vecs = np.array([rot_mat_from_pointer(v1, angle) @ self.orb_vec for angle in range(0,360,int(360/steps))]) if update: if orb_dim is None: orb_dim = orb_dim_dict[str(self)] self.center = (self.orb_vecs * orb_dim) + self.coord
def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None: ''' ''' self.index = i self.symbol = pt[mol.atomnos[i]].symbol neighbors_indexes = neighbors(mol.graph, i) self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes] self.coord = mol.atomcoords[conf][i] self.others = mol.atomcoords[conf][neighbors_indexes] self.orb_vecs = self.others - self.coord # vectors connecting center to each of the two substituents if update: if orb_dim is None: key = self.symbol + ' ' + str(self) orb_dim = orb_dim_dict.get(key) if orb_dim is None: orb_dim = orb_dim_dict['Fallback'] print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.') self.orb_vecs = orb_dim * np.array([norm(v) for v in self.orb_vecs]) # making both vectors a fixed, defined length orb_mat = rot_mat_from_pointer(np.mean(self.orb_vecs, axis=0), 90) @ rot_mat_from_pointer(np.cross(self.orb_vecs[0], self.orb_vecs[1]), 180) # self.orb_vecs = np.array([orb_mat @ v for v in self.orb_vecs]) self.orb_vecs = (orb_mat @ self.orb_vecs.T).T self.center = self.orb_vecs + self.coord
def adjust_forces(self, atoms, forces): # First, assess if we have to move atoms 1 and 2 at all sum_of_distances = (norm_of(atoms.positions[self.i1] - self.orb1) + norm_of(atoms.positions[self.i2] - self.orb2) + self.d_eq) reactive_atoms_distance = norm_of(atoms.positions[self.i1] - atoms.positions[self.i2]) orb_direction = self.orb2 - self.orb1 # vector connecting orb1 to orb2 spring_force = self.k * (norm_of(orb_direction) - self.d_eq) # absolute spring force (float). Positive if spring is overstretched. # spring_force = np.clip(spring_force, -50, 50) # # force is clipped at 5 eV/A force_direction1 = np.sign(spring_force) * norm( np.mean((norm(+orb_direction), norm(self.orb1 - atoms.positions[self.i1])), axis=0)) force_direction2 = np.sign(spring_force) * norm( np.mean((norm(-orb_direction), norm(self.orb2 - atoms.positions[self.i2])), axis=0)) # versors specifying the direction at which forces act, that is on the # bisector of the angle between vector connecting atom to orbital and # vector connecting the two orbitals if np.abs(sum_of_distances - reactive_atoms_distance) > 0.2: forces[self.i1] += (force_direction1 * spring_force) forces[self.i2] += (force_direction2 * spring_force) # applying harmonic force to each atom, directed toward the other one # Now applying to neighbors the force derived by torque, scaled to match the spring_force, # but only if atomic orbitals are more than two Angstroms apart. This improves convergence. if norm_of(orb_direction) > 2: torque1 = np.cross(self.orb1 - atoms.positions[self.i1], force_direction1) for i in self.neighbors_of_1: forces[i] += norm( np.cross(torque1, atoms.positions[i] - atoms.positions[self.i1])) * spring_force torque2 = np.cross(self.orb2 - atoms.positions[self.i2], force_direction2) for i in self.neighbors_of_2: forces[i] += norm( np.cross(torque2, atoms.positions[i] - atoms.positions[self.i2])) * spring_force
def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None: ''' ''' self.index = i self.symbol = pt[mol.atomnos[i]].symbol neighbors_indexes = neighbors(mol.graph, i) self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes] self.coord = mol.atomcoords[conf][i] self.other = mol.atomcoords[conf][neighbors_indexes][0] if not mol.sp3_sigmastar: self.orb_vecs = np.array([norm(self.coord - self.other)]) else: other_reactive_indexes = list(mol.reactive_indexes) other_reactive_indexes.remove(i) for index in other_reactive_indexes: if index in neighbors_indexes: parnter_index = index break # obtain the reference partner index partner = mol.atomcoords[conf][parnter_index] pivot = norm(partner - self.coord) neighbors_of_partner = neighbors(mol.graph, parnter_index) neighbors_of_partner.remove(i) orb_vec = norm(mol.atomcoords[conf][neighbors_of_partner[0]] - partner) orb_vec = orb_vec - orb_vec @ pivot * pivot steps = 3 # number of total orbitals self.orb_vecs = np.array([rot_mat_from_pointer(pivot, angle+60) @ orb_vec for angle in range(0,360,int(360/steps))]) # orbitals are staggered in relation to sp3 substituents self.orb_vers = norm(self.orb_vecs[0]) if update: if orb_dim is None: key = self.symbol + ' ' + str(self) orb_dim = orb_dim_dict.get(key) if orb_dim is None: orb_dim = norm_of(self.coord - self.other) print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using the bonding distance ({round(orb_dim, 3)} A).') self.center = orb_dim * self.orb_vecs + self.coord
def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None: ''' ''' self.index = i self.symbol = pt[mol.atomnos[i]].symbol neighbors_indexes = neighbors(mol.graph, i) self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes] self.coord = mol.atomcoords[conf][i] self.others = mol.atomcoords[conf][neighbors_indexes] self.vectors = self.others - self.coord # vectors connecting reactive atom with neighbors self.orb_vec = norm(np.mean(np.array([np.cross(norm(self.vectors[0]), norm(self.vectors[1])), np.cross(norm(self.vectors[1]), norm(self.vectors[2])), np.cross(norm(self.vectors[2]), norm(self.vectors[0]))]), axis=0)) self.orb_vecs = np.vstack((self.orb_vec, -self.orb_vec)) if update: if orb_dim is None: key = self.symbol + ' ' + str(self) orb_dim = orb_dim_dict.get(key) if orb_dim is None: orb_dim = orb_dim_dict['Fallback'] print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.') self.center = self.orb_vecs * orb_dim self.center += self.coord
def get_anchors(self, mol, conf=0, aromatic=False): ''' mol: a Hypermolecule object conf: the conformer index to be used returns a 3-tuple of arrays centers - absolute coordinates of points vectors - direction of center relative to its atom label - 0: electron-poor, 1: electron-rich, 2: aromatic ''' centers, vectors, labels = [], [], [] # initializing the lists for i, atom in enumerate(mol.atomnos): if atom in (7, 8): # N and O atoms atom_cls = get_atom_type(mol.graph, i)() atom_cls.init(mol, i, update=True, orb_dim=1, conf=conf) for c, v in zip(atom_cls.center, atom_cls.orb_vecs): centers.append(c) vectors.append(v) labels.append(1) elif atom == 1 and any((mol.graph.nodes[n]['atomnos'] in (7, 8) for n in neighbors(mol.graph, i))): # protic H atoms atom_cls = get_atom_type(mol.graph, i)() atom_cls.init(mol, i, update=True, orb_dim=1, conf=conf) for c, v in zip(atom_cls.center, atom_cls.orb_vecs): centers.append(c) vectors.append(v) labels.append(0) # looking for aromatic rings if aromatic and len(mol.atomnos) > 9: for coords_ in get_phenyls(mol.atomcoords[conf], mol.atomnos): mean = np.mean(coords_, axis=0) # getting the center of the ring vec = 1.8 * norm( np.cross(coords_[0] - coords_[1], coords_[1] - coords_[2])) # normal vector orthogonal to the ring # 1.8 A so that rings will stack at around 3.6 A centers.append(mean + vec) vectors.append(vec) labels.append(2) centers.append(mean - vec) vectors.append(-vec) labels.append(2) centers = np.array(centers) vectors = np.array(vectors) labels = np.array(labels) return centers, vectors, labels
def mopac_opt(coords, atomnos, constrained_indexes=None, method='PM7', solvent=None, title='temp', read_output=True, **kwargs): ''' This function writes a MOPAC .mop input, runs it with the subprocess module and reads its output. Coordinates used are mixed (cartesian and internal) to be able to constrain the reactive atoms distances specified in constrained_indexes. :params coords: array of shape (n,3) with cartesian coordinates for atoms :params atomnos: array of atomic numbers for atoms :params constrained_indexes: array of shape (n,2), with the indexes of atomic pairs to be constrained :params method: string, specifiyng the first line of keywords for the MOPAC input file. :params title: string, used as a file name and job title for the mopac input file. :params read_output: Whether to read the output file and return anything. ''' constrained_indexes_list = constrained_indexes.ravel( ) if constrained_indexes is not None else [] constrained_indexes = constrained_indexes if constrained_indexes is not None else [] if solvent is not None: method += ' ' + get_solvent_line(solvent, 'MOPAC', method) order = [] s = [method + '\n' + title + '\n\n'] for i, num in enumerate(atomnos): if i not in constrained_indexes: order.append(i) s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[num].symbol, coords[i][0], coords[i][1], coords[i][2])) free_indexes = list( set(range(len(atomnos))) - set(constrained_indexes_list)) # print('free indexes are', free_indexes, '\n') if len(constrained_indexes_list) == len(set(constrained_indexes_list)): # block pairs of atoms if no atom is involved in more than one distance constrain for a, b in constrained_indexes: order.append(b) order.append(a) c, d = np.random.choice(free_indexes, 2) while c == d: c, d = np.random.choice(free_indexes, 2) # indexes of reference atoms, from unconstraind atoms set dist = norm_of(coords[a] - coords[b]) # in Angstrom # print(f'DIST - {dist} - between {a} {b}') angle = vec_angle(norm(coords[a] - coords[b]), norm(coords[c] - coords[b])) # print(f'ANGLE - {angle} - between {a} {b} {c}') d_angle = dihedral([coords[a], coords[b], coords[c], coords[d]]) d_angle += 360 if d_angle < 0 else 0 # print(f'D_ANGLE - {d_angle} - between {a} {b} {c} {d}') list_len = len(s) s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[atomnos[b]].symbol, coords[b][0], coords[b][1], coords[b][2])) s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format( pt[atomnos[a]].symbol, dist, angle, d_angle, list_len, free_indexes.index(c) + 1, free_indexes.index(d) + 1)) # print(f'Blocked bond between mopac ids {list_len} {list_len+1}\n') elif len(set(constrained_indexes_list)) == 3: # three atoms, the central bound to the other two # OTHERS[0]: cartesian # CENTRAL: internal (self, others[0], two random) # OTHERS[1]: internal (self, central, two random) central = max(set(constrained_indexes_list), key=lambda x: list(constrained_indexes_list).count(x)) # index of the atom that is constrained to two other others = list(set(constrained_indexes_list) - {central}) # OTHERS[0] order.append(others[0]) s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[atomnos[others[0]]].symbol, coords[others[0]][0], coords[others[0]][1], coords[others[0]][2])) # first atom is placed in cartesian coordinates, the other two have a distance constraint and are expressed in internal coordinates #CENTRAL order.append(central) c, d = np.random.choice(free_indexes, 2) while c == d: c, d = np.random.choice(free_indexes, 2) # indexes of reference atoms, from unconstraind atoms set dist = norm_of(coords[central] - coords[others[0]]) # in Angstrom angle = vec_angle(norm(coords[central] - coords[others[0]]), norm(coords[others[0]] - coords[c])) d_angle = dihedral( [coords[central], coords[others[0]], coords[c], coords[d]]) d_angle += 360 if d_angle < 0 else 0 list_len = len(s) s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format( pt[atomnos[central]].symbol, dist, angle, d_angle, list_len - 1, free_indexes.index(c) + 1, free_indexes.index(d) + 1)) #OTHERS[1] order.append(others[1]) c1, d1 = np.random.choice(free_indexes, 2) while c1 == d1: c1, d1 = np.random.choice(free_indexes, 2) # indexes of reference atoms, from unconstraind atoms set dist1 = norm_of(coords[others[1]] - coords[central]) # in Angstrom angle1 = np.arccos( norm(coords[others[1]] - coords[central]) @ norm(coords[others[1]] - coords[c1])) * 180 / np.pi # in degrees d_angle1 = dihedral( [coords[others[1]], coords[central], coords[c1], coords[d1]]) d_angle1 += 360 if d_angle < 0 else 0 list_len = len(s) s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format( pt[atomnos[others[1]]].symbol, dist1, angle1, d_angle1, list_len - 1, free_indexes.index(c1) + 1, free_indexes.index(d1) + 1)) else: raise NotImplementedError( 'The constraints provided for MOPAC optimization are not yet supported' ) s = ''.join(s) with open(f'{title}.mop', 'w') as f: f.write(s) try: check_call(f'{COMMANDS["MOPAC"]} {title}.mop'.split(), stdout=DEVNULL, stderr=STDOUT) except KeyboardInterrupt: print('KeyboardInterrupt requested by user. Quitting.') quit() os.remove(f'{title}.mop') # delete input, we do not need it anymore if read_output: inv_order = [order.index(i) for i, _ in enumerate(order)] # undoing the atomic scramble that was needed by the mopac input requirements opt_coords, energy, success = read_mop_out(f'{title}.out') os.remove(f'{title}.out') opt_coords = scramble(opt_coords, inv_order) if opt_coords is not None else coords # If opt_coords is None, that is if TS seeking crashed, # sets opt_coords to the old coords. If not, unscrambles # coordinates read from mopac output. return opt_coords, energy, success
def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None: self.index = i self.symbol = pt[mol.atomnos[i]].symbol neighbors_indexes = neighbors(mol.graph, i) self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes] self.coord = mol.atomcoords[conf][i] self.others = mol.atomcoords[conf][neighbors_indexes] self.vectors = self.others - self.coord # vector connecting center to substituent angle = vec_angle(norm(self.others[0] - self.coord), norm(self.others[1] - self.coord)) if np.abs(angle - 180) < 5: self.type = 'sp' else: self.type = 'bent carbene' self.allene = False self.ketene = False if self.type == 'sp' and all([s == 'C' for s in self.neighbors_symbols]): neighbors_of_neighbors_indexes = (neighbors(mol.graph, neighbors_indexes[0]), neighbors(mol.graph, neighbors_indexes[1])) neighbors_of_neighbors_indexes[0].remove(i) neighbors_of_neighbors_indexes[1].remove(i) if (len(side1) == len(side2) == 2 for side1, side2 in neighbors_of_neighbors_indexes): self.allene = True elif self.type == 'sp' and sorted(self.neighbors_symbols) in (['C', 'O'], ['C', 'S']): self.ketene = True neighbors_of_neighbors_indexes = (neighbors(mol.graph, neighbors_indexes[0]), neighbors(mol.graph, neighbors_indexes[1])) neighbors_of_neighbors_indexes[0].remove(i) neighbors_of_neighbors_indexes[1].remove(i) if len(neighbors_of_neighbors_indexes[0]) == 2: substituent = mol.atomcoords[conf][neighbors_of_neighbors_indexes[0][0]] ketene_atom = mol.atomcoords[conf][neighbors_indexes[0]] self.ketene_ref = substituent - ketene_atom elif len(neighbors_of_neighbors_indexes[1]) == 2: substituent = mol.atomcoords[conf][neighbors_of_neighbors_indexes[1][0]] ketene_atom = mol.atomcoords[conf][neighbors_indexes[1]] self.ketene_ref = substituent - ketene_atom else: self.ketene = False if update: if orb_dim is None: key = self.symbol + ' ' + self.type orb_dim = orb_dim_dict.get(key) if orb_dim is None: orb_dim = orb_dim_dict['Fallback'] print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.') if self.type == 'sp': v = np.random.rand(3) pivot1 = v - ((v @ norm(self.vectors[0])) * self.vectors[0]) if self.allene or self.ketene: # if we have an allene or ketene, pivot1 is aligned to # one substituent so that the resulting positions # for the four orbital centers make chemical sense. axis = norm(self.others[0] - self.others[1]) # versor connecting reactive atom neighbors if self.allene: ref = (mol.atomcoords[conf][neighbors_of_neighbors_indexes[0][0]] - mol.atomcoords[conf][neighbors_indexes[0]]) else: ref = self.ketene_ref pivot1 = ref - ref @ axis * axis # projection of ref orthogonal to axis (vector rejection) pivot2 = norm(np.cross(pivot1, self.vectors[0])) self.orb_vecs = np.array([rot_mat_from_pointer(pivot2, 90) @ rot_mat_from_pointer(pivot1, angle) @ norm(self.vectors[0]) for angle in (0, 90, 180, 270)]) * orb_dim self.center = self.orb_vecs + self.coord # four vectors defining the position of the four orbital lobes centers else: # bent carbene case: three centers, sp2+p self.orb_vecs = np.array([-norm(np.mean([norm(v) for v in self.vectors], axis=0))*orb_dim]) # one sp2 center first p_vec = np.cross(norm(self.vectors[0]), norm(self.vectors[1])) p_vecs = np.array([norm(p_vec)*orb_dim, -norm(p_vec)*orb_dim]) self.orb_vecs = np.concatenate((self.orb_vecs, p_vecs)) # adding two p centers self.center = self.orb_vecs + self.coord
def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None: ''' ''' self.index = i self.symbol = pt[mol.atomnos[i]].symbol neighbors_indexes = neighbors(mol.graph, i) self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes] self.coord = mol.atomcoords[conf][i] self.other = mol.atomcoords[conf][neighbors_indexes][0] self.vector = self.other - self.coord # vector connecting center to substituent if update: if orb_dim is None: key = self.symbol + ' ' + str(self) orb_dim = orb_dim_dict.get(key) if orb_dim is None: orb_dim = orb_dim_dict['Fallback'] print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.') neighbors_of_neighbor_indexes = neighbors(mol.graph, neighbors_indexes[0]) neighbors_of_neighbor_indexes.remove(i) self.vector = norm(self.vector)*orb_dim if len(neighbors_of_neighbor_indexes) == 2: # if it is a normal ketone (or an enolate), n orbital lobes must be coplanar with # atoms connecting to ketone C atom, or p lobes must be placed accordingly a1 = mol.atomcoords[conf][neighbors_of_neighbor_indexes[0]] a2 = mol.atomcoords[conf][neighbors_of_neighbor_indexes[1]] pivot = norm(np.cross(a1 - self.coord, a2 - self.coord)) if mol.sigmatropic[conf]: # two p lobes self.center = np.concatenate(([pivot*orb_dim], [-pivot*orb_dim])) else: #two n lobes self.center = np.array([rot_mat_from_pointer(pivot, angle) @ self.vector for angle in (120,240)]) elif len(neighbors_of_neighbor_indexes) in (1, 3): # ketene or alkoxide ketene_sub_indexes = neighbors(mol.graph, neighbors_of_neighbor_indexes[0]) ketene_sub_indexes.remove(neighbors_indexes[0]) ketene_sub_coords = mol.atomcoords[conf][ketene_sub_indexes[0]] n_o_n_coords = mol.atomcoords[conf][neighbors_of_neighbor_indexes[0]] # vector connecting ketene R with C (O=C=C(R)R) v = (ketene_sub_coords - n_o_n_coords) # this vector is orthogonal to the ketene O=C=C and coplanar with the ketene pointer = v - ((v @ norm(self.vector)) * self.vector) pointer = norm(pointer) * orb_dim self.center = np.array([rot_mat_from_pointer(self.vector, 90*step) @ pointer for step in range(4)]) self.orb_vecs = np.array([norm(center) for center in self.center]) # unit vectors connecting reactive atom coord with orbital centers self.center += self.coord
def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None: self.index = i self.symbol = pt[mol.atomnos[i]].symbol neighbors_indexes = neighbors(mol.graph, i) self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes] self.coord = mol.atomcoords[conf][i] self.others = mol.atomcoords[conf][neighbors_indexes] if not mol.sp3_sigmastar: if not hasattr(self, 'leaving_group_index'): self.leaving_group_index = None if len([atom for atom in self.neighbors_symbols if atom in ['O', 'N', 'Cl', 'Br', 'I']]) == 1: # if we can tell where is the leaving group self.leaving_group_coords = self.others[self.neighbors_symbols.index([atom for atom in self.neighbors_symbols if atom in ['O', 'Cl', 'Br', 'I']][0])] elif len([atom for atom in self.neighbors_symbols if atom not in ['H']]) == 1: # if no clear leaving group but we only have one atom != H self.leaving_group_coords = self.others[self.neighbors_symbols.index([atom for atom in self.neighbors_symbols if atom not in ['H']][0])] else: # if we cannot infer, ask user if we didn't have already try: self.leaving_group_coords = self._set_leaving_group(mol, neighbors_indexes) except Exception: # if something goes wrong, we fallback to command line input for reactive atom index collection if self.leaving_group_index is None: while True: self.leaving_group_index = input(f'Please insert the index of the leaving group atom bonded to the sp3 reactive atom (index {self.index}) of molecule {mol.rootname} : ') if self.leaving_group_index == '' or self.leaving_group_index.lower().islower(): pass elif int(self.leaving_group_index) in neighbors_indexes: self.leaving_group_index = int(self.leaving_group_index) break else: print(f'Atom {self.leaving_group_index} is not bonded to the sp3 center with index {self.index}.') self.leaving_group_coords = self.others[neighbors_indexes.index(self.leaving_group_index)] self.orb_vecs = np.array([self.coord - self.leaving_group_coords]) self.orb_vers = norm(self.orb_vecs[0]) else: # Sigma bond type other_reactive_indexes = list(mol.reactive_indexes) other_reactive_indexes.remove(i) for index in other_reactive_indexes: if index in neighbors_indexes: parnter_index = index break # obtain the reference partner index pivot = norm(mol.atomcoords[conf][parnter_index] - self.coord) other_neighbors = deepcopy(neighbors_indexes) other_neighbors.remove(parnter_index) orb_vec = norm(mol.atomcoords[conf][other_neighbors[0]] - self.coord) orb_vec = orb_vec - orb_vec @ pivot * pivot steps = 3 # number of total orbitals self.orb_vecs = np.array([rot_mat_from_pointer(pivot, angle+60) @ orb_vec for angle in range(0,360,int(360/steps))]) # orbitals are staggered in relation to sp3 substituents self.orb_vers = norm(self.orb_vecs[0]) if update: if orb_dim is None: key = self.symbol + ' ' + str(self) orb_dim = orb_dim_dict.get(key) if orb_dim is None: orb_dim = orb_dim_dict['Fallback'] print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.') self.center = np.array([orb_dim * norm(vec) + self.coord for vec in self.orb_vecs])
def _get_directions(norms): ''' Returns two or three vectors specifying the direction in which each molecule should be aligned in the cyclical TS, pointing towards the center of the polygon. ''' assert len(norms) in (2, 3) if len(norms) == 2: return np.array([[0, 1, 0], [0, -1, 0]]) vertexes = np.zeros((3, 2)) vertexes[1] = np.array([norms[0], 0]) a = np.power(norms[0], 2) b = np.power(norms[1], 2) c = np.power(norms[2], 2) x = (a - b + c) / (2 * a**0.5) y = (c - x**2)**0.5 vertexes[2] = np.array([x, y]) # similar to the code from polygonize, to get the active triangle # but without the orientation specified in the polygonize function a = vertexes[1, 0] # first point, x b = vertexes[2, 0] # second point, x c = vertexes[2, 1] # second point, y x = a / 2 y = (b**2 + c**2 - a * b) / (2 * c) cc = np.array([x, y]) # 2D coordinates of the triangle circocenter v0, v1, v2 = vertexes meanpoint1 = np.mean((v0, v1), axis=0) meanpoint2 = np.mean((v1, v2), axis=0) meanpoint3 = np.mean((v2, v0), axis=0) dir1 = cc - meanpoint1 dir2 = cc - meanpoint2 dir3 = cc - meanpoint3 # 2D direction versors connecting center of side with circumcenter. # Now we need to understand if we want these or their negative if np.any([np.all(d == 0) for d in (dir1, dir2, dir3)]): # We have a right triangle. To aviod numerical # errors, a small perturbation is made. # This should not happen, but just in case... norms[0] += 1e-5 dir1, dir2, dir3 = [t[:-1] for t in _get_directions(norms)] angle0_obtuse = (vec_angle(v1 - v0, v2 - v0) > 90) angle1_obtuse = (vec_angle(v0 - v1, v2 - v1) > 90) angle2_obtuse = (vec_angle(v0 - v2, v1 - v2) > 90) dir1 = -dir1 if angle2_obtuse else dir1 dir2 = -dir2 if angle0_obtuse else dir2 dir3 = -dir3 if angle1_obtuse else dir3 # invert the versors sign if circumcenter is # one angle is obtuse, because then # circumcenter is outside the triangle dir1 = norm(np.concatenate((dir1, [0]))) dir2 = norm(np.concatenate((dir2, [0]))) dir3 = norm(np.concatenate((dir3, [0]))) return np.vstack((dir1, dir2, dir3))
def opt_linear_scan(embedder, coords, atomnos, scan_indexes, constrained_indexes, step_size=0.02, safe=False, title='temp', logfile=None, xyztraj=None): ''' Runs a linear scan along the specified linear coordinate. The highest energy structure that passes sanity checks is returned. embedder coords atomnos scan_indexes constrained_indexes step_size safe title logfile xyztraj ''' assert [i in constrained_indexes.ravel() for i in scan_indexes] i1, i2 = scan_indexes far_thr = 2 * sum([pt[atomnos[i]].covalent_radius for i in scan_indexes]) t_start = time.perf_counter() total_iter = 0 _, energy, _ = optimize( coords, atomnos, embedder.options.calculator, embedder.options.theory_level, constrained_indexes=constrained_indexes, mols_graphs=embedder.graphs, procs=embedder.options.procs, max_newbonds=embedder.options.max_newbonds, ) direction = coords[i1] - coords[i2] base_dist = norm_of(direction) energies, geometries = [energy], [coords] for sign in (1, -1): # getting closer for sign == 1, further apart for -1 active_coords = deepcopy(coords) dist = base_dist if scan_peak_present(energies): break for iterations in range(75): if safe: # use ASE optimization function - more reliable, but locks all interatomic dists targets = [ norm_of(active_coords[a] - active_coords[b]) - step_size if (a in scan_indexes and b in scan_indexes) else norm_of(active_coords[a] - active_coords[b]) for a, b in constrained_indexes ] active_coords, energy, success = ase_popt( embedder, active_coords, atomnos, constrained_indexes, targets=targets, safe=True, ) else: # use faster raw optimization function, might scramble more often than the ASE one active_coords[i2] += sign * norm(direction) * step_size active_coords, energy, success = optimize( active_coords, atomnos, embedder.options.calculator, embedder.options.theory_level, constrained_indexes=constrained_indexes, mols_graphs=embedder.graphs, procs=embedder.options.procs, max_newbonds=embedder.options.max_newbonds, ) if not success: if logfile is not None and iterations == 0: logfile.write(f' - {title} CRASHED at first step\n') if embedder.options.debug: with open(title + '_SCRAMBLED.xyz', 'a') as f: write_xyz( active_coords, atomnos, f, title=title + (f' d({i1}-{i2}) = {round(dist, 3)} A, Rel. E = {round(energy-energies[0], 3)} kcal/mol' )) break direction = active_coords[i1] - active_coords[i2] dist = norm_of(direction) total_iter += 1 geometries.append(active_coords) energies.append(energy) if xyztraj is not None: with open(xyztraj, 'a') as f: write_xyz( active_coords, atomnos, f, title=title + (f' d({i1}-{i2}) = {round(dist, 3)} A, Rel. E = {round(energy-energies[0], 3)} kcal/mol' )) if (dist < 1.2 and sign == 1) or (dist > far_thr and sign == -1) or (scan_peak_present(energies)): break distances = [norm_of(g[i1] - g[i2]) for g in geometries] best_distance = distances[energies.index(max(energies))] distances_delta = [abs(d - best_distance) for d in distances] closest_geom = geometries[distances_delta.index(min(distances_delta))] closest_dist = distances[distances_delta.index(min(distances_delta))] direction = closest_geom[i1] - closest_geom[i2] closest_geom[i1] += norm(direction) * (best_distance - closest_dist) final_geom, final_energy, _ = optimize( closest_geom, atomnos, embedder.options.calculator, embedder.options.theory_level, constrained_indexes=constrained_indexes, mols_graphs=embedder.graphs, procs=embedder.options.procs, max_newbonds=embedder.options.max_newbonds, check=False, ) if embedder.options.debug: if embedder.options.debug: with open(xyztraj, 'a') as f: write_xyz( active_coords, atomnos, f, title=title + (f' FINAL - d({i1}-{i2}) = {round(norm_of(final_geom[i1]-final_geom[i2]), 3)} A,' f' Rel. E = {round(final_energy-energies[0], 3)} kcal/mol' )) import matplotlib.pyplot as plt plt.figure() distances = [norm_of(geom[i1] - geom[i2]) for geom in geometries] distances, sorted_energies = zip( *sorted(zip(distances, energies), key=lambda x: x[0])) plt.plot(distances, [s - energies[0] for s in sorted_energies], '-o', color='tab:red', label=f'Linear SCAN ({i1}-{i2})', linewidth=3, alpha=0.5) plt.plot( norm_of(coords[i1] - coords[i2]), 0, marker='o', color='tab:blue', label='Starting point (0 kcal/mol)', markersize=5, ) plt.plot(best_distance, final_energy - energies[0], marker='o', color='black', label='Interpolated best distance, actual energy', markersize=5) plt.legend() plt.title(title) plt.xlabel(f'Interatomic distance {tuple(scan_indexes)}') plt.ylabel('Energy Rel. to starting point (kcal/mol)') plt.savefig(f'{title.replace(" ", "_")}_plt.svg') if logfile is not None: logfile.write( f' - {title} COMPLETED {total_iter} steps ({time_to_string(time.perf_counter()-t_start)})\n' ) return final_geom, final_energy, True
def get_reagent(embedder, coords, atomnos, ids, constrained_indexes, method='PM7'): ''' Part of the automatic NEB implementation. Returns a structure that presumably is the association reaction reagent. ([cyclo]additions reactions in mind) ''' opt_func = opt_funcs_dict[embedder.options.calculator] bond_factor = 1.5 # multiple of sum of covalent radii for two atoms. # Putting reactive atoms at this times their bonding # distance and performing a constrained optimization # is the way to get a good guess for reagents structure. if len(ids) == 2: mol1_center = np.mean([coords[a] for a, _ in constrained_indexes], axis=0) mol2_center = np.mean([coords[b] for _, b in constrained_indexes], axis=0) motion = norm(mol2_center - mol1_center) # norm of the motion that, when applied to mol1, # superimposes its reactive centers to the ones of mol2 threshold_dists = [ bond_factor * (pt[atomnos[a]].covalent_radius + pt[atomnos[b]].covalent_radius) for a, b in constrained_indexes ] reactive_dists = [ norm_of(coords[a] - coords[b]) for a, b in constrained_indexes ] # distances between reactive atoms coords[:ids[0]] -= norm(motion) * (np.mean(threshold_dists) - np.mean(reactive_dists)) # move reactive atoms away from each other just enough coords, _, _ = opt_func(coords, atomnos, constrained_indexes=constrained_indexes, method=method) # optimize the structure but keeping the reactive atoms distanced return coords # trimolecular TSs: the approach is to bring the first pair of reactive # atoms apart just enough to get a good approximation for reagents index_to_be_moved = constrained_indexes[0, 0] reference = constrained_indexes[0, 1] moving_molecule_index = next(i for i, n in enumerate(np.cumsum(ids)) if index_to_be_moved < n) bounds = [0] + [n + 1 for n in np.cumsum(ids)] moving_molecule_slice = slice(bounds[moving_molecule_index], bounds[moving_molecule_index + 1]) threshold_dist = bond_factor * ( pt[atomnos[constrained_indexes[0, 0]]].covalent_radius + pt[atomnos[constrained_indexes[0, 1]]].covalent_radius) motion = (coords[reference] - coords[index_to_be_moved]) # vector from the atom to be moved to the target reactive atom displacement = norm(motion) * (threshold_dist - norm_of(motion)) # vector to be applied to the reactive atom to push it far just enough for i, atom in enumerate(coords[moving_molecule_slice]): dist = norm_of(atom - coords[index_to_be_moved]) # for any atom in the molecule, distance from the reactive atom coords[moving_molecule_slice][i] -= displacement * np.exp(-0.5 * dist) # the closer they are to the reactive atom, the further they are moved coords, _, _ = opt_func(coords, atomnos, constrained_indexes=np.array( [constrained_indexes[0]]), method=method) # when all atoms are moved, optimize the geometry with only the first of the previous constraints newcoords, _, _ = opt_func(coords, atomnos, method=method) # finally, when structures are close enough, do a free optimization to get the reaction product new_reactive_dist = norm_of(newcoords[constrained_indexes[0, 0]] - newcoords[constrained_indexes[0, 0]]) if new_reactive_dist > threshold_dist: # return the freely optimized structure only if the reagents did not approached back each other # during the optimization, otherwise return the last coords, where partners were further away return newcoords return coords
def get_product(embedder, coords, atomnos, ids, constrained_indexes, method='PM7'): ''' Part of the automatic NEB implementation. Returns a structure that presumably is the association reaction product ([cyclo]additions reactions in mind) ''' opt_func = opt_funcs_dict[embedder.options.calculator] bond_factor = 1.2 # multiple of sum of covalent radii for two atoms. # If two atoms are closer than this times their sum # of c_radii, they are considered to converge to # products when their geometry is optimized. step_size = 0.1 # in Angstroms if len(ids) == 2: mol1_center = np.mean([coords[a] for a, _ in constrained_indexes], axis=0) mol2_center = np.mean([coords[b] for _, b in constrained_indexes], axis=0) motion = norm(mol2_center - mol1_center) # norm of the motion that, when applied to mol1, # superimposes its reactive atoms to the ones of mol2 threshold_dists = [ bond_factor * (pt[atomnos[a]].covalent_radius + pt[atomnos[b]].covalent_radius) for a, b in constrained_indexes ] reactive_dists = [ norm_of(coords[a] - coords[b]) for a, b in constrained_indexes ] # distances between reactive atoms while not np.all([ reactive_dists[i] < threshold_dists[i] for i, _ in enumerate(constrained_indexes) ]): # print('Reactive distances are', reactive_dists) coords[:ids[0]] += motion * step_size coords, _, _ = opt_func(coords, atomnos, constrained_indexes, method=method) reactive_dists = [ norm_of(coords[a] - coords[b]) for a, b in constrained_indexes ] newcoords, _, _ = opt_func(coords, atomnos, method=method) # finally, when structures are close enough, do a free optimization to get the reaction product new_reactive_dists = [ norm_of(newcoords[a] - newcoords[b]) for a, b in constrained_indexes ] if np.all([ new_reactive_dists[i] < threshold_dists[i] for i, _ in enumerate(constrained_indexes) ]): # return the freely optimized structure only if the reagents did not repel each other # during the optimization, otherwise return the last coords, where partners were close return newcoords return coords # trimolecular TSs: the approach is to bring the first pair of reactive # atoms closer until optimization bounds the molecules together index_to_be_moved = constrained_indexes[0, 0] reference = constrained_indexes[0, 1] moving_molecule_index = next(i for i, n in enumerate(np.cumsum(ids)) if index_to_be_moved < n) bounds = [0] + [n + 1 for n in np.cumsum(ids)] moving_molecule_slice = slice(bounds[moving_molecule_index], bounds[moving_molecule_index + 1]) threshold_dist = bond_factor * ( pt[atomnos[constrained_indexes[0, 0]]].covalent_radius + pt[atomnos[constrained_indexes[0, 1]]].covalent_radius) motion = (coords[reference] - coords[index_to_be_moved]) # vector from the atom to be moved to the target reactive atom while norm_of(motion) > threshold_dist: # check if the reactive atoms are sufficiently close to converge to products for i, atom in enumerate(coords[moving_molecule_slice]): dist = norm_of(atom - coords[index_to_be_moved]) # for any atom in the molecule, distance from the reactive atom atom_step = step_size * np.exp(-0.5 * dist) coords[moving_molecule_slice][i] += norm(motion) * atom_step # the more they are close, the more they are moved # print('Reactive dist -', norm_of(motion)) coords, _, _ = opt_func(coords, atomnos, constrained_indexes, method=method) # when all atoms are moved, optimize the geometry with the previous constraints motion = (coords[reference] - coords[index_to_be_moved]) newcoords, _, _ = opt_func(coords, atomnos, method=method) # finally, when structures are close enough, do a free optimization to get the reaction product new_reactive_dist = norm_of(newcoords[constrained_indexes[0, 0]] - newcoords[constrained_indexes[0, 0]]) if new_reactive_dist < threshold_dist: # return the freely optimized structure only if the reagents did not repel each other # during the optimization, otherwise return the last coords, where partners were close return newcoords return coords