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.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 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 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 PreventScramblingConstraint(graph, atoms, double_bond_protection=False, fix_angles=False): ''' graph: NetworkX graph of the molecule atoms: ASE atoms object return: FixInternals constraint to apply to ASE calculations ''' angles_deg = None if fix_angles: allpaths = [] for node in graph: allpaths.extend(findPaths(graph, node, 2)) allpaths = {tuple(sorted(path)) for path in allpaths} angles_deg = [] for path in allpaths: angles_deg.append([atoms.get_angle(*path), list(path)]) bonds = [] for bond in [[a, b] for a, b in graph.edges if a != b]: bonds.append([atoms.get_distance(*bond), bond]) dihedrals_deg = None if double_bond_protection: double_bonds = get_double_bonds_indexes(atoms.positions, atoms.get_atomic_numbers()) if double_bonds != []: dihedrals_deg = [] for a, b in double_bonds: n_a = neighbors(graph, a) n_a.remove(b) n_b = neighbors(graph, b) n_b.remove(a) d = [n_a[0], a, b, n_b[0]] dihedrals_deg.append([atoms.get_dihedral(*d), d]) return FixInternals(dihedrals_deg=dihedrals_deg, angles_deg=angles_deg, bonds=bonds, epsilon=1)
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 get_atom_type(graph, index): ''' Returns the appropriate class to represent the atom with the given index on the graph ''' nb = neighbors(graph, index) return atom_type_dict[pt[graph.nodes[index]['atomnos']].symbol + str(len(nb))]
def _is_free(index, graph): ''' Return True if the index specified satisfies all of the following: - Is not a sp2 carbonyl carbon atom - Is not the oxygen atom of an ester - Is not the nitrogen atom of a secondary amide (CONHR) ''' if all((graph.nodes[index]['atomnos'] == 6, is_sp_n(index, graph, 2), 8 in (graph.nodes[n]['atomnos'] for n in neighbors(graph, index)))): return False if is_amide_n(index, graph, mode=1): return False if is_ester_o(index, graph): return False return True
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 ase_bend(embedder, original_mol, conf, pivot, threshold, title='temp', traj=None, check=True): ''' embedder: TSCoDe embedder object original_mol: Hypermolecule object to be bent conf: index of conformation in original_mol to be used pivot: pivot connecting two Hypermolecule orbitals to be approached/distanced threshold: target distance for the specified pivot, in Angstroms title: name to be used for referring to this structure in the embedder log traj: if set to a string, traj+'.traj' is used as a filename for the bending trajectory. not only the atoms will be printed, but also all the orbitals and the active pivot. check: if True, after bending checks that the bent structure did not scramble. If it did, returns the initial molecule. ''' identifier = np.sum(original_mol.atomcoords[conf]) if hasattr(embedder, "ase_bent_mols_dict"): cached = embedder.ase_bent_mols_dict.get( (identifier, tuple(sorted(pivot.index)), round(threshold, 3))) if cached is not None: return cached if traj is not None: from ase.io.trajectory import Trajectory def orbitalized(atoms, orbitals, pivot=None): positions = np.concatenate((atoms.positions, orbitals)) if pivot is not None: positions = np.concatenate( (positions, [pivot.start], [pivot.end])) symbols = list(atoms.numbers) + [0 for _ in orbitals] if pivot is not None: symbols += [9 for _ in range(2)] # Fluorine (9) represents active orbitals new_atoms = Atoms(symbols, positions=positions) return new_atoms try: os.remove(traj) except FileNotFoundError: pass i1, i2 = original_mol.reactive_indexes neighbors_of_1 = neighbors(original_mol.graph, i1) neighbors_of_2 = neighbors(original_mol.graph, i2) mol = deepcopy(original_mol) final_mol = deepcopy(original_mol) for p in mol.pivots[conf]: if p.index == pivot.index: active_pivot = p break dist = norm_of(active_pivot.pivot) atoms = Atoms(mol.atomnos, positions=mol.atomcoords[conf]) atoms.calc = get_ase_calc(embedder) if traj is not None: traj_obj = Trajectory( traj + f'_conf{conf}.traj', mode='a', atoms=orbitalized( atoms, np.vstack([ atom.center for atom in mol.reactive_atoms_classes_dict[0].values() ]), active_pivot)) traj_obj.write() unproductive_iterations = 0 break_reason = 'MAX ITER' t_start = time.perf_counter() for iteration in range(500): atoms.positions = mol.atomcoords[0] orb_memo = { index: norm_of(atom.center[0] - atom.coord) for index, atom in mol.reactive_atoms_classes_dict[0].items() } orb1, orb2 = active_pivot.start, active_pivot.end c1 = OrbitalSpring(i1, i2, orb1, orb2, neighbors_of_1, neighbors_of_2, d_eq=threshold) c2 = PreventScramblingConstraint( mol.graph, atoms, double_bond_protection=embedder.options.double_bond_protection, fix_angles=embedder.options.fix_angles_in_deformation) atoms.set_constraint([ c1, c2, ]) opt = BFGS(atoms, maxstep=0.2, logfile=None, trajectory=None) try: opt.run(fmax=0.5, steps=1) except ValueError: # Shake did not converge break_reason = 'CRASHED' break if traj is not None: traj_obj.atoms = orbitalized( atoms, np.vstack([ atom.center for atom in mol.reactive_atoms_classes_dict[0].values() ])) traj_obj.write() # check if we are stuck if np.max( np.abs( np.linalg.norm(atoms.get_positions() - mol.atomcoords[0], axis=1))) < 0.01: unproductive_iterations += 1 if unproductive_iterations == 10: break_reason = 'STUCK' break else: unproductive_iterations = 0 mol.atomcoords[0] = atoms.get_positions() # Update orbitals and get temp pivots for index, atom in mol.reactive_atoms_classes_dict[0].items(): atom.init(mol, index, update=True, orb_dim=orb_memo[index]) # orbitals positions are calculated based on the conformer we are working on temp_pivots = embedder._get_pivots(mol)[0] for p in temp_pivots: if p.index == pivot.index: active_pivot = p break # print(active_pivot) dist = norm_of(active_pivot.pivot) # print(f'{iteration}. {mol.name} conf {conf}: pivot is {round(dist, 3)} (target {round(threshold, 3)})') if dist - threshold < 0.1: break_reason = 'CONVERGED' break # else: # print('delta is ', round(dist - threshold, 3)) embedder.log( f' {title} - conformer {conf} - {break_reason}{" "*(9-len(break_reason))} ({iteration+1}{" "*(3-len(str(iteration+1)))} iterations, {time_to_string(time.perf_counter()-t_start)})', p=False) if check: if not molecule_check(original_mol.atomcoords[conf], mol.atomcoords[0], mol.atomnos, max_newbonds=1): mol.atomcoords[0] = original_mol.atomcoords[conf] # keep the bent structures only if no scrambling occurred between atoms final_mol.atomcoords[conf] = mol.atomcoords[0] # Now align the ensembles on the new reactive atoms positions reference, *targets = final_mol.atomcoords reference = np.array(reference) targets = np.array(targets) r = reference - np.mean(reference[final_mol.reactive_indexes], axis=0) ts = np.array( [t - np.mean(t[final_mol.reactive_indexes], axis=0) for t in targets]) output = [] output.append(r) for target in ts: matrix = kabsch(r, target) output.append([matrix @ vector for vector in target]) final_mol.atomcoords = np.array(output) # Update orbitals and pivots for conf_, _ in enumerate(final_mol.atomcoords): for index, atom in final_mol.reactive_atoms_classes_dict[conf_].items( ): atom.init(final_mol, index, update=True, orb_dim=orb_memo[index]) embedder._set_pivots(final_mol) # add result to cache (if we have it) so we avoid recomputing it if hasattr(embedder, "ase_bent_mols_dict"): embedder.ase_bent_mols_dict[(identifier, tuple(sorted(pivot.index)), round(threshold, 3))] = final_mol clean_directory() return final_mol
def _is_nondummy(i, root, graph) -> bool: ''' Checks that a molecular rotation along the dihedral angle (*, root, i, *) is non-dummy, that is the atom at index i, in the direction opposite to the one leading to root, has different substituents. i.e. methyl and tBu rotations return False. Thought to eliminate methyl/tert-butyl-type rotations. ''' if graph.nodes[i]['atomnos'] not in (6, 7): return True # for now, we only discard rotations around carbon # and nitrogen atoms, like methyl/tert-butyl/triphenyl # and flat symmetrical rings like phenyl, N-pyrrolyl... G = deepcopy(graph) nb = neighbors(G, i) nb.remove(root) if len(nb) == 1: if len(neighbors(G, nb[0])) == 2: return False # if node i has two bonds only (one with root and one with a) # and the other atom (a) has two bonds only (one with i) # the rotation is considered dummy: some other rotation # will account for its freedom (i.e. alkynes, hydrogen bonds) for n in nb: G.remove_edge(i, n) subgraphs_nodes = [ _set for _set in nx.connected_components(G) if not root in _set ] if len(subgraphs_nodes) == 1: for n in nb: G.add_edge(i, n) # restore i-neighbor bonds removed previously subgraph = G.subgraph(list(subgraphs_nodes[0]) + [i]) cycles = [l for l in nx.cycle_basis(subgraph, root=i) if len(l) != 1] # get all cycles that involve i, if any if cycles: # in this case, i is part of a cycle and root is exocyclic # and we will take care of this in a separate function return _is_cyclical_nondummy(G, cycles[0]) return True # if not, the torsion is likely to be rotable # (tetramethylguanidyl alanine C(β)-N bond) subgraphs = [nx.subgraph(G, s) for s in subgraphs_nodes] for sub in subgraphs[1:]: if not nx.is_isomorphic( subgraphs[0], sub, node_match=lambda n1, n2: n1['atomnos'] == n2['atomnos']): return True # Care should be taken because chiral centers are not taken into account: a rotation # involving an index where substituents only differ by stereochemistry, and where a # rotation is not an element of symmetry of the subsystem, the rotation is discarded # even if it would be meaningful to keep it. return False
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])