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 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 rotate_dihedral(coords, dihedral, angle, mask=None, indexes_to_be_moved=None): ''' Rotate a molecule around a given bond. Atoms that will move are the ones specified by mask or indexes_to_be_moved. If both are None, only the first index of the dihedral iterable is moved. angle: angle, in degrees ''' i1, i2, i3, _ = dihedral if indexes_to_be_moved is not None: mask = np.array( [i in indexes_to_be_moved for i, _ in enumerate(coords)]) if mask is None: mask = i1 axis = coords[i2] - coords[i3] mat = rot_mat_from_pointer(axis, angle) center = coords[i3] coords[mask] = (mat @ (coords[mask] - center).T).T + center return coords
def rotation_matrix_from_vectors(vec1, vec2): """ Find the rotation matrix that aligns vec1 to vec2 :param vec1: A 3d "source" vector :param vec2: A 3d "destination" vector :return mat: A transform matrix (3x3) which when applied to vec1, aligns it with vec2. """ assert vec1.shape == (3, ) assert vec2.shape == (3, ) a, b = (vec1 / norm_of(vec1)).reshape(3), (vec2 / norm_of(vec2)).reshape(3) v = np.cross(a, b) if norm_of(v) != 0: c = np.dot(a, b) s = norm_of(v) kmat = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) rotation_matrix = np.eye(3) + kmat + kmat.dot(kmat) * ((1 - c) / (s**2)) return rotation_matrix # if the cross product is zero, then vecs must be parallel or perpendicular if norm_of(a + b) == 0: pointer = np.array([0, 0, 1]) return rot_mat_from_pointer(pointer, 180) return np.eye(3)
def string_embed(embedder): ''' return threads: return embedded structures, with position and rotation attributes set, ready to be pumped into embedder.structures. Algorithm used is the "string" algorithm (see docs). ''' assert len(embedder.objects) == 2 embedder.log( f'\n--> Performing string embed ({embedder.candidates} candidates)') conf_number = [len(mol.atomcoords) for mol in embedder.objects] conf_indexes = cartesian_product( *[np.array(range(i)) for i in conf_number]) # (n,2) vectors where the every element is the conformer index for that molecule r_atoms_centers_indexes = cartesian_product(*[ np.array(range(len(mol.get_centers(0)[0]))) for mol in embedder.objects ]) # for two mols with 3 and 2 centers: [[0 0][0 1][1 0][1 1][2 0][2 1]] mol1, mol2 = embedder.objects poses = [] for c1, c2 in conf_indexes: for ai1, ai2 in r_atoms_centers_indexes: for angle in embedder.systematic_angles: ra1 = mol1.get_r_atoms(c1)[0] ra2 = mol2.get_r_atoms(c2)[0] ref_vec = ra1.center[ai1] mol_vec = ra2.center[ai2] mol2.rotation = rotation_matrix_from_vectors(mol_vec, -ref_vec) pointer = mol2.rotation @ mol_vec if angle != 0: delta_rot = rot_mat_from_pointer(pointer, angle) mol2.rotation = delta_rot @ mol2.rotation mol2.position = mol1.rotation @ ref_vec + mol1.position - pointer # coords1 = (mol1.rotation @ mol1.atomcoords[c1].T).T + mol1.position # coords2 = (mol2.rotation @ mol2.atomcoords[c2].T).T + mol2.position # poses.append(np.concatenate((coords1, coords2))) poses.append(get_embed((mol1, mol2), (c1, c2))) embedder.constrained_indexes = _get_string_constrained_indexes( embedder, len(poses)) return np.array(poses)
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 _dock(coords1, coords2, anchors1, anchors2): ''' Return a (n, d1+d2, 3) shaped structure array where: - n is the number of non-compenetrating docked structures - d1 and d2 are coords1 and coords2 first dimension ''' a1_centers, a1_vectors, a1_labels = anchors1 a2_centers, a2_vectors, a2_labels = anchors2 # getting pivots that connect each pair of anchors in a mol pivots1 = vector_cartesian_product(a1_centers, a1_centers) pivots2 = vector_cartesian_product(a2_centers, a2_centers) directions1 = internal_mean( vector_cartesian_product(a1_vectors, a1_vectors)) directions2 = internal_mean( vector_cartesian_product(a2_vectors, a2_vectors)) pivots1_signatures = vector_cartesian_product(a1_labels, a1_labels) pivots2_signatures = vector_cartesian_product(a2_labels, a2_labels) # pivots are paired if they respect this pairing table: # ep/ep, er/er and ar/er are discarded # 0 - electron-poor # 1 - electron-rich # 2 - aromatic signatures_mat = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 1]], dtype=np.int32) ids = (len(coords1), len(coords2)) structures = [] coords1 = np.ascontiguousarray(coords1) coords2 = np.ascontiguousarray(coords2) for i1, (p1, s1) in enumerate(zip(pivots1, pivots1_signatures)): p1 = np.ascontiguousarray(p1) # print(f'pivot {(i1+1)*len(pivots2)}/{len(pivots1)*len(pivots2)}') for i2, (p2, s2) in enumerate(zip(pivots2, pivots2_signatures)): l1 = norm_of(p1[0] - p1[1]) l2 = norm_of(p2[0] - p2[1]) if l1 > 0.1 and l2 > 0.1 and np.abs(l1 - l2) < 2: # do not pair pivots that are: # - starting and ending on the same atom # - too different in length (>2 A) if signatures_mat[s1[0][0]][s2[0][0]]: if signatures_mat[s1[1][0]][s2[1][0]]: # do not pair pivots that do not respect polarity al_mat1 = align_vec_pair( (p2[0] - p2[1], -directions2[i2]), (p1[0] - p1[1], directions1[i1])) # matrix that applied to coords1, aligns them to coords2 # p1 goes to p2 # direction1 goes to -direction2 step_rot_axis = al_mat1 @ (p1[0] - p1[1]) # vector connecting the ends of pivot1 after alignment for angle in np.arange(-90, 90, 20): step_mat1 = rot_mat_from_pointer( step_rot_axis, angle) rot1 = step_mat1 @ al_mat1 pos1 = vec_mean(p2) - rot1 @ vec_mean(p1) new_coords1 = transform_coords(coords1, rot1, pos1) embedded_coords = np.concatenate( (new_coords1, coords2)) if compenetration_check(embedded_coords, ids=ids, thresh=1.5): # structures.append(embedded_coords) ### DEBUG embedded_coords = np.concatenate( (embedded_coords, transform_coords(p1, rot1, pos1), p2)) structures.append(embedded_coords) return structures
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 _adjust_directions(embedder, directions, constrained_indexes, triangle_vectors, pivots, conf_ids): ''' For trimolecular TSs, correct molecules pre-alignment. That is, after the initial estimate based on pivot triangle circocentrum, systematically rotate each molecule around its pivot by fixed increments and look for the arrangement with the smallest deviation from orbital parallel interaction. This optimizes the obtainment of poses with the correct inter-reactive atoms distances. ''' assert directions.shape[0] == 3 mols = deepcopy(embedder.objects) p0, p1, p2 = [end - start for start, end in triangle_vectors] p0_mean, p1_mean, p2_mean = [ np.mean((end, start), axis=0) for start, end in triangle_vectors ] ############### get triangle vertexes 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 v0 = np.concatenate((v0, [0])) v1 = np.concatenate((v1, [0])) v2 = np.concatenate((v2, [0])) ############### set up mols -> pos + rot for i in (0, 1, 2): start, end = triangle_vectors[i] mol_direction = pivots[i].meanpoint - np.mean( embedder.objects[i].atomcoords[conf_ids[i]][ embedder.objects[i].reactive_indexes], axis=0) if np.all(mol_direction == 0.): mol_direction = pivots[i].meanpoint mols[i].rotation = align_vec_pair( np.array([end - start, directions[i]]), np.array([pivots[i].pivot, mol_direction])) mols[i].position = np.mean( triangle_vectors[i], axis=0) - mols[i].rotation @ pivots[i].meanpoint ############### set up pairings between reactive atoms pairings = [[None, None] for _ in constrained_indexes] for i, c in enumerate(constrained_indexes): for m, mol in enumerate(embedder.objects): for index, r_atom in mol.reactive_atoms_classes_dict[0].items( ): if r_atom.cumnum == c[0]: pairings[i][0] = (m, index) if r_atom.cumnum == c[1]: pairings[i][1] = (m, index) r = np.zeros((3, 3), dtype=int) for first, second in pairings: mol_index = first[0] partner_index = second[0] reactive_index = first[1] r[mol_index, partner_index] = reactive_index mol_index = second[0] partner_index = first[0] reactive_index = second[1] r[mol_index, partner_index] = reactive_index # r[0,1] is the reactive_index of molecule 0 that faces molecule 1 and so on # diagonal of r (r[0,0], r[1,1], r[2,2]) is just unused ############### calculate reactive atoms positions mol0, mol1, mol2 = mols a01 = mol0.rotation @ mol0.atomcoords[0][r[0, 1]] + mol0.position a02 = mol0.rotation @ mol0.atomcoords[0][r[0, 2]] + mol0.position a10 = mol1.rotation @ mol1.atomcoords[0][r[1, 0]] + mol1.position a12 = mol1.rotation @ mol1.atomcoords[0][r[1, 2]] + mol1.position a20 = mol2.rotation @ mol2.atomcoords[0][r[2, 0]] + mol2.position a21 = mol2.rotation @ mol2.atomcoords[0][r[2, 1]] + mol2.position ############### explore all angles combinations steps = 6 angle_range = 30 step_angle = 2 * angle_range / steps angles_list = cartesian_product( *[range(steps + 1) for _ in range(3)]) * step_angle - angle_range # Molecules are rotated around the +angle_range/-angle_range range in the given number of steps. # Therefore, the angular resolution between candidates is step_angle (10 degrees) candidates = [] for angles in angles_list: rot0 = rot_mat_from_pointer(p0, angles[0]) new_a01 = rot0 @ a01 new_a02 = rot0 @ a02 d0 = p0_mean - np.mean((new_a01, new_a02), axis=0) rot1 = rot_mat_from_pointer(p1, angles[1]) new_a10 = rot1 @ a10 new_a12 = rot1 @ a12 d1 = p1_mean - np.mean((new_a10, new_a12), axis=0) rot2 = rot_mat_from_pointer(p2, angles[2]) new_a20 = rot2 @ a20 new_a21 = rot2 @ a21 d2 = p2_mean - np.mean((new_a20, new_a21), axis=0) cost = 0 cost += vec_angle(v0 - new_a02, new_a20 - v0) cost += vec_angle(v1 - new_a01, new_a10 - v1) cost += vec_angle(v2 - new_a21, new_a12 - v2) candidates.append((cost, angles, (d0, d1, d2))) ############### choose the one with the best alignment, that is minor cost cost, angles, directions = sorted(candidates, key=lambda x: x[0])[0] return np.array(directions)
def cyclical_embed(embedder): ''' return threads: return embedded structures, with position and rotation attributes set, ready to be pumped into embedder.structures. Algorithm used is the "cyclical" algorithm (see docs). ''' 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 _adjust_directions(embedder, directions, constrained_indexes, triangle_vectors, pivots, conf_ids): ''' For trimolecular TSs, correct molecules pre-alignment. That is, after the initial estimate based on pivot triangle circocentrum, systematically rotate each molecule around its pivot by fixed increments and look for the arrangement with the smallest deviation from orbital parallel interaction. This optimizes the obtainment of poses with the correct inter-reactive atoms distances. ''' assert directions.shape[0] == 3 mols = deepcopy(embedder.objects) p0, p1, p2 = [end - start for start, end in triangle_vectors] p0_mean, p1_mean, p2_mean = [ np.mean((end, start), axis=0) for start, end in triangle_vectors ] ############### get triangle vertexes 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 v0 = np.concatenate((v0, [0])) v1 = np.concatenate((v1, [0])) v2 = np.concatenate((v2, [0])) ############### set up mols -> pos + rot for i in (0, 1, 2): start, end = triangle_vectors[i] mol_direction = pivots[i].meanpoint - np.mean( embedder.objects[i].atomcoords[conf_ids[i]][ embedder.objects[i].reactive_indexes], axis=0) if np.all(mol_direction == 0.): mol_direction = pivots[i].meanpoint mols[i].rotation = align_vec_pair( np.array([end - start, directions[i]]), np.array([pivots[i].pivot, mol_direction])) mols[i].position = np.mean( triangle_vectors[i], axis=0) - mols[i].rotation @ pivots[i].meanpoint ############### set up pairings between reactive atoms pairings = [[None, None] for _ in constrained_indexes] for i, c in enumerate(constrained_indexes): for m, mol in enumerate(embedder.objects): for index, r_atom in mol.reactive_atoms_classes_dict[0].items( ): if r_atom.cumnum == c[0]: pairings[i][0] = (m, index) if r_atom.cumnum == c[1]: pairings[i][1] = (m, index) r = np.zeros((3, 3), dtype=int) for first, second in pairings: mol_index = first[0] partner_index = second[0] reactive_index = first[1] r[mol_index, partner_index] = reactive_index mol_index = second[0] partner_index = first[0] reactive_index = second[1] r[mol_index, partner_index] = reactive_index # r[0,1] is the reactive_index of molecule 0 that faces molecule 1 and so on # diagonal of r (r[0,0], r[1,1], r[2,2]) is just unused ############### calculate reactive atoms positions mol0, mol1, mol2 = mols a01 = mol0.rotation @ mol0.atomcoords[0][r[0, 1]] + mol0.position a02 = mol0.rotation @ mol0.atomcoords[0][r[0, 2]] + mol0.position a10 = mol1.rotation @ mol1.atomcoords[0][r[1, 0]] + mol1.position a12 = mol1.rotation @ mol1.atomcoords[0][r[1, 2]] + mol1.position a20 = mol2.rotation @ mol2.atomcoords[0][r[2, 0]] + mol2.position a21 = mol2.rotation @ mol2.atomcoords[0][r[2, 1]] + mol2.position ############### explore all angles combinations steps = 6 angle_range = 30 step_angle = 2 * angle_range / steps angles_list = cartesian_product( *[range(steps + 1) for _ in range(3)]) * step_angle - angle_range # Molecules are rotated around the +angle_range/-angle_range range in the given number of steps. # Therefore, the angular resolution between candidates is step_angle (10 degrees) candidates = [] for angles in angles_list: rot0 = rot_mat_from_pointer(p0, angles[0]) new_a01 = rot0 @ a01 new_a02 = rot0 @ a02 d0 = p0_mean - np.mean((new_a01, new_a02), axis=0) rot1 = rot_mat_from_pointer(p1, angles[1]) new_a10 = rot1 @ a10 new_a12 = rot1 @ a12 d1 = p1_mean - np.mean((new_a10, new_a12), axis=0) rot2 = rot_mat_from_pointer(p2, angles[2]) new_a20 = rot2 @ a20 new_a21 = rot2 @ a21 d2 = p2_mean - np.mean((new_a20, new_a21), axis=0) cost = 0 cost += vec_angle(v0 - new_a02, new_a20 - v0) cost += vec_angle(v1 - new_a01, new_a10 - v1) cost += vec_angle(v2 - new_a21, new_a12 - v2) candidates.append((cost, angles, (d0, d1, d2))) ############### choose the one with the best alignment, that is minor cost cost, angles, directions = sorted(candidates, key=lambda x: x[0])[0] return np.array(directions) s = f'\n--> Performing {embedder.embed} embed' if len(embedder.objects) == 2: s += f' ({embedder.candidates} candidates)' else: s += f' (ideally {embedder.candidates} candidates, maybe less)' embedder.log(s) if not embedder.options.rigid: embedder.ase_bent_mols_dict = {} # used as molecular cache for ase_bend # keys are tuples with: ((identifier, pivot.index, target_pivot_length), obtained with: # (np.sum(original_mol.atomcoords[0]), tuple(sorted(pivot.index)), round(threshold,3)) if not embedder.options.let: for mol in embedder.objects: if len(mol.atomcoords) > 10: mol.atomcoords = most_diverse_conformers( 10, mol.atomcoords, mol.atomnos) embedder.log( f'Using only the most diverse 10 conformers of molecule {mol.name} (override with LET keyword)' ) # Do not keep more than 10 conformations, unless LET keyword is provided conf_number = [len(mol.atomcoords) for mol in embedder.objects] conf_indexes = cartesian_product( *[np.array(range(i)) for i in conf_number]) poses = [] constrained_indexes = [] for ci, conf_ids in enumerate(conf_indexes): pivots_indexes = cartesian_product(*[ range(len(mol.pivots[conf_ids[i]])) for i, mol in enumerate(embedder.objects) ]) # indexes of pivots in each molecule self.pivots[conf] list. For three mols with 2 pivots each: [[0,0,0], [0,0,1], [0,1,0], ...] for p, pi in enumerate(pivots_indexes): loadbar(p + ci * (len(pivots_indexes)), len(pivots_indexes) * len(conf_indexes), prefix=f'Embedding structures ') pivots = [ embedder.objects[m].pivots[conf_ids[m]][pi[m]] for m, _ in enumerate(embedder.objects) ] # getting the active pivot for each molecule for this run norms = np.linalg.norm(np.array([p.pivot for p in pivots]), axis=1) # getting the pivots norms to feed into the polygonize function if len(norms) == 2: if abs(norms[0] - norms[1]) < 2.5: norms_type = 'digon' else: norms_type = 'impossible_digon' else: if all([ norms[i] < norms[i - 1] + norms[i - 2] for i in (0, 1, 2) ]): norms_type = 'triangle' else: norms_type = 'impossible_triangle' if norms_type in ('triangle', 'digon'): polygon_vectors = polygonize(norms) elif norms_type == 'impossible_triangle': # Accessed if we cannot build a triangle with the given norms. # Try to bend the structure if it was close or just skip this triangle and go on. deltas = [ norms[i] - (norms[i - 1] + norms[i - 2]) for i in range(3) ] rel_delta = max([deltas[i] / norms[i] for i in range(3)]) # s = 'Rejected triangle, delta was %s, %s of side length' % (round(delta, 3), str(round(100*rel_delta, 3)) + ' %') # embedder.log(s, p=False) if rel_delta < 0.2 and not embedder.options.rigid: # correct the molecule structure with the longest # side if the distances are at most 20% off. index = deltas.index(max(deltas)) mol = embedder.objects[index] if not tuple(sorted(mol.reactive_indexes)) in list( mol.graph.edges): # do not try to bend molecules where the two reactive indices are bonded pivot = pivots[index] # ase_view(mol) maxval = norms[index - 1] + norms[index - 2] traj = f'bend_{mol.name}_p{p}_tgt_{round(0.9*maxval, 3)}' if embedder.options.debug else None bent_mol = ase_bend( embedder, mol, conf_ids[index], pivot, 0.9 * maxval, title=f'{mol.rootname} - pivot {p}', traj=traj) embedder.objects[index] = bent_mol try: pivots = [ embedder.objects[m].pivots[conf_ids[m]][pi[m]] for m, _ in enumerate(embedder.objects) ] # updating the active pivot for each molecule for this run except IndexError: raise Exception(( f'The number of pivots for molecule {index} ({bent_mol.name}) most likely decreased during ' + 'its bending, causing this error. Adding the RIGID (and maybe also SHRINK) keyword to the ' + 'input file should solve the issue. I do not think this should ever happen under common ' + 'circumstances, but if it does, it may be reasonable to print a statement on the log, ' + 'discard the bent molecule, and then proceed with the embed. If you see this error, ' + 'please report your input and structures on a GitHub issue. Thank you.' )) norms = np.linalg.norm(np.array( [p.pivot for p in pivots]), axis=1) # updating the pivots norms to feed into the polygonize function try: polygon_vectors = polygonize(norms) # repeating the failed polygon creation. If it fails again, skip these pivots except TriangleError: continue else: continue else: continue else: # norms type == 'impossible_digon', that is sides are too different in length if not embedder.options.rigid: if embedder.embed == 'chelotropic': target_length = min(norms) else: maxgap = 3 # in Angstrom gap = abs(norms[0] - norms[1]) r = 0.3 + 0.5 * (gap / maxgap) r = np.clip(5, 0.5, 0.8) # r is the ratio for calculating target_length based # on the gap that deformations will need to cover. # It ranges from 0.5 to 0.8 and is shifted more toward # the shorter norm as the gap rises. For gaps of more # than maxgap Angstroms, the target length is very close # to the shortest molecule, and only the molecule # with the longest pivot is bent. target_length = min(norms) * r + max(norms) * (1 - r) for i, mol in enumerate(deepcopy(embedder.objects)): if len(mol.reactive_indexes) > 1: # do not try to bend molecules that react with a single atom if tuple(sorted(mol.reactive_indexes)) not in list( mol.graph.edges): # do not try to bend molecules where the two reactive indices are bonded traj = f'bend_{mol.name}_p{p}_tgt_{round(target_length, 3)}' if embedder.options.debug else None bent_mol = ase_bend( embedder, mol, conf_ids[i], pivots[i], target_length, title=f'{mol.rootname} - pivot {p}', traj=traj) # ase_view(bent_mol) embedder.objects[i] = bent_mol # Repeating the previous polygonization steps with the bent molecules pivots = [ embedder.objects[m].pivots[conf_ids[m]][pi[m]] for m, _ in enumerate(embedder.objects) ] # updating the active pivot for each molecule for this run norms = np.linalg.norm(np.array([p.pivot for p in pivots]), axis=1) # updating the pivots norms to feed into the polygonize function polygon_vectors = polygonize(norms) # repeating the failed polygon creation directions = _get_directions(norms) # directions to orient the molecules toward, orthogonal to each vec_pair for v, vecs in enumerate(polygon_vectors): # getting vertexes to embed molecules with and iterating over start/end points ids = _get_cyclical_reactive_indexes(embedder, pivots, v) # get indexes of atoms that face each other if not embedder.pairings_table or all( [pair in ids for pair in embedder.pairings_table.values()]): # ensure that the active arrangement has all the pairings that the user specified if len(embedder.objects) == 3: directions = _adjust_directions( embedder, directions, ids, vecs, pivots, conf_ids) # For trimolecular TSs, the alignment direction previously get is # just a general first approximation that needs to be corrected # for the specific case through another algorithm. for angles in embedder.systematic_angles: for i, vec_pair in enumerate(vecs): # setting molecular positions and rotations (embedding) # i is the molecule index, vecs is a tuple of start and end positions # for the pivot vector start, end = vec_pair angle = angles[i] reactive_coords = embedder.objects[i].atomcoords[ conf_ids[i]][ embedder.objects[i].reactive_indexes] # coordinates for the reactive atoms in this run atomic_pivot_mean = np.mean(reactive_coords, axis=0) # mean position of the atoms active in this run mol_direction = pivots[ i].meanpoint - atomic_pivot_mean if np.all(mol_direction == 0.): mol_direction = pivots[i].meanpoint # log.write(f'mol {i} - improper pivot? Thread {len(threads)-1}\n') # Direction in which the molecule should be oriented, based on the mean of reactive # atom positions and the mean point of the active pivot for the run. # If this vector is too small and gets rounded to zero (as it can happen for # "antrafacial" vectors), we fallback to the vector starting from the molecule # center (mean of atomic positions) and ending in pivot_means[i], so to avoid # numeric errors in the next function. alignment_rotation = align_vec_pair( np.array([end - start, directions[i]]), np.array([pivots[i].pivot, mol_direction])) # this rotation superimposes the molecular orbitals active in this run (pivots[i].pivot # goes to end-start) and also aligns the molecules so that they face each other # (mol_direction goes to directions[i]) if len(reactive_coords) == 2: axis_of_step_rotation = alignment_rotation @ ( reactive_coords[0] - reactive_coords[1]) else: axis_of_step_rotation = alignment_rotation @ pivots[ i].pivot # molecules with two reactive atoms are step-rotated around the line connecting # the reactive atoms, while single reactive atom mols around their active pivot step_rotation = rot_mat_from_pointer( axis_of_step_rotation, angle) # this rotation cycles through all different rotation angles for each molecule center_of_rotation = alignment_rotation @ atomic_pivot_mean # center_of_rotation is the mean point between the reactive atoms so # as to keep the reactive distances constant embedder.objects[ i].rotation = step_rotation @ alignment_rotation # overall rotation for the molecule is given by the matrices product pos = np.mean( vec_pair, axis=0 ) - alignment_rotation @ pivots[i].meanpoint embedder.objects[ i].position = center_of_rotation - step_rotation @ center_of_rotation + pos # overall position is given by superimposing mean of active pivot (connecting orbitals) # to mean of vec_pair (defining the target position - the side of a triangle for three molecules) embedded_structure = get_embed(embedder.objects, conf_ids) if compenetration_check( embedded_structure, ids=embedder.ids, thresh=embedder.options.clash_thresh): poses.append(embedded_structure) constrained_indexes.append(ids) # Save indexes to be constrained later in the optimization step loadbar(1, 1, prefix=f'Embedding structures ') embedder.constrained_indexes = np.array(constrained_indexes) if not poses: s = ( '\n--> Cyclical embed did not find any suitable disposition of molecules.\n' + ' This is probably because one molecule has two reactive centers at a great distance,\n' + ' preventing the other two molecules from forming a closed, cyclical structure.' ) embedder.log(s, p=False) raise ZeroCandidatesError(s) return np.array(poses)