Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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])
Ejemplo n.º 11
0
    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)
Ejemplo n.º 12
0
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)