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.vectors = self.others - self.coord # vector connecting center to substituent

        if update:
            if orb_dim is None:
                key = self.symbol + ' ' + str(self)
                orb_dim = orb_dim_dict.get(key)
                
                if orb_dim is None:
                    orb_dim = orb_dim_dict['Fallback']
                    print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.')
        
            if mol.sigmatropic[conf]:
                # two p lobes
                p_lobe = norm(np.cross(self.vectors[0], self.vectors[1]))*orb_dim
                self.orb_vecs = np.concatenate(([p_lobe], [-p_lobe]))

            else:
                # lone pair lobe
                self.orb_vecs = np.array([-norm(np.mean([norm(v) for v in self.vectors], axis=0))*orb_dim])

            self.center = self.orb_vecs + self.coord
Ejemplo n.º 2
0
    def adjust_forces(self, atoms, forces):

        direction = atoms.positions[self.i2] - atoms.positions[self.i1]
        # vector connecting atom1 to atom2

        spring_force = self.k * (norm_of(direction) - self.d_eq)
        # absolute spring force (float). Positive if spring is overstretched.

        if not self.tight:
            spring_force = np.clip(spring_force, -50, 50)
            # force is clipped at 50 eV/A

        forces[self.i1] += (norm(direction) * spring_force)
        forces[self.i2] -= (norm(direction) * spring_force)
Ejemplo n.º 3
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.º 4
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.º 5
0
    def adjust_forces(self, atoms, forces):

        # First, assess if we have to move atoms 1 and 2 at all

        sum_of_distances = (norm_of(atoms.positions[self.i1] - self.orb1) +
                            norm_of(atoms.positions[self.i2] - self.orb2) +
                            self.d_eq)

        reactive_atoms_distance = norm_of(atoms.positions[self.i1] -
                                          atoms.positions[self.i2])

        orb_direction = self.orb2 - self.orb1
        # vector connecting orb1 to orb2

        spring_force = self.k * (norm_of(orb_direction) - self.d_eq)
        # absolute spring force (float). Positive if spring is overstretched.

        # spring_force = np.clip(spring_force, -50, 50)
        # # force is clipped at 5 eV/A

        force_direction1 = np.sign(spring_force) * norm(
            np.mean((norm(+orb_direction),
                     norm(self.orb1 - atoms.positions[self.i1])),
                    axis=0))

        force_direction2 = np.sign(spring_force) * norm(
            np.mean((norm(-orb_direction),
                     norm(self.orb2 - atoms.positions[self.i2])),
                    axis=0))

        # versors specifying the direction at which forces act, that is on the
        # bisector of the angle between vector connecting atom to orbital and
        # vector connecting the two orbitals

        if np.abs(sum_of_distances - reactive_atoms_distance) > 0.2:

            forces[self.i1] += (force_direction1 * spring_force)
            forces[self.i2] += (force_direction2 * spring_force)
            # applying harmonic force to each atom, directed toward the other one

        # Now applying to neighbors the force derived by torque, scaled to match the spring_force,
        # but only if atomic orbitals are more than two Angstroms apart. This improves convergence.

        if norm_of(orb_direction) > 2:
            torque1 = np.cross(self.orb1 - atoms.positions[self.i1],
                               force_direction1)
            for i in self.neighbors_of_1:
                forces[i] += norm(
                    np.cross(torque1, atoms.positions[i] -
                             atoms.positions[self.i1])) * spring_force

            torque2 = np.cross(self.orb2 - atoms.positions[self.i2],
                               force_direction2)
            for i in self.neighbors_of_2:
                forces[i] += norm(
                    np.cross(torque2, atoms.positions[i] -
                             atoms.positions[self.i2])) * spring_force
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 init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
        '''
        '''
        self.index = i
        self.symbol = pt[mol.atomnos[i]].symbol
        neighbors_indexes = neighbors(mol.graph, i)
        


        self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indexes]
        self.coord = mol.atomcoords[conf][i]
        self.others = mol.atomcoords[conf][neighbors_indexes]

        self.vectors = self.others - self.coord # vectors connecting reactive atom with neighbors
        self.orb_vec = norm(np.mean(np.array([np.cross(norm(self.vectors[0]), norm(self.vectors[1])),
                                              np.cross(norm(self.vectors[1]), norm(self.vectors[2])),
                                              np.cross(norm(self.vectors[2]), norm(self.vectors[0]))]), axis=0))

        self.orb_vecs = np.vstack((self.orb_vec, -self.orb_vec))

        if update:
            if orb_dim is None:
                key = self.symbol + ' ' + str(self)
                orb_dim = orb_dim_dict.get(key)
                
                if orb_dim is None:
                    orb_dim = orb_dim_dict['Fallback']
                    print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.')
            
            self.center = self.orb_vecs * orb_dim      

            self.center += self.coord
Ejemplo n.º 8
0
    def get_anchors(self, mol, conf=0, aromatic=False):
        '''
        mol: a Hypermolecule object
        conf: the conformer index to be used

        returns a 3-tuple of arrays
        
        centers - absolute coordinates of points
        vectors - direction of center relative to its atom
        label - 0: electron-poor, 1: electron-rich, 2: aromatic
        '''

        centers, vectors, labels = [], [], []
        # initializing the lists

        for i, atom in enumerate(mol.atomnos):
            if atom in (7, 8):
                # N and O atoms
                atom_cls = get_atom_type(mol.graph, i)()
                atom_cls.init(mol, i, update=True, orb_dim=1, conf=conf)
                for c, v in zip(atom_cls.center, atom_cls.orb_vecs):
                    centers.append(c)
                    vectors.append(v)
                    labels.append(1)

            elif atom == 1 and any((mol.graph.nodes[n]['atomnos'] in (7, 8)
                                    for n in neighbors(mol.graph, i))):
                # protic H atoms
                atom_cls = get_atom_type(mol.graph, i)()
                atom_cls.init(mol, i, update=True, orb_dim=1, conf=conf)
                for c, v in zip(atom_cls.center, atom_cls.orb_vecs):
                    centers.append(c)
                    vectors.append(v)
                    labels.append(0)

        # looking for aromatic rings

        if aromatic and len(mol.atomnos) > 9:

            for coords_ in get_phenyls(mol.atomcoords[conf], mol.atomnos):

                mean = np.mean(coords_, axis=0)
                # getting the center of the ring

                vec = 1.8 * norm(
                    np.cross(coords_[0] - coords_[1], coords_[1] - coords_[2]))
                # normal vector orthogonal to the ring
                # 1.8 A so that rings will stack at around 3.6 A

                centers.append(mean + vec)
                vectors.append(vec)
                labels.append(2)

                centers.append(mean - vec)
                vectors.append(-vec)
                labels.append(2)

        centers = np.array(centers)
        vectors = np.array(vectors)
        labels = np.array(labels)

        return centers, vectors, labels
Ejemplo n.º 9
0
def mopac_opt(coords,
              atomnos,
              constrained_indexes=None,
              method='PM7',
              solvent=None,
              title='temp',
              read_output=True,
              **kwargs):
    '''
    This function writes a MOPAC .mop input, runs it with the subprocess
    module and reads its output. Coordinates used are mixed
    (cartesian and internal) to be able to constrain the reactive atoms
    distances specified in constrained_indexes.

    :params coords: array of shape (n,3) with cartesian coordinates for atoms
    :params atomnos: array of atomic numbers for atoms
    :params constrained_indexes: array of shape (n,2), with the indexes
                                 of atomic pairs to be constrained
    :params method: string, specifiyng the first line of keywords for the MOPAC input file.
    :params title: string, used as a file name and job title for the mopac input file.
    :params read_output: Whether to read the output file and return anything.
    '''

    constrained_indexes_list = constrained_indexes.ravel(
    ) if constrained_indexes is not None else []
    constrained_indexes = constrained_indexes if constrained_indexes is not None else []

    if solvent is not None:
        method += ' ' + get_solvent_line(solvent, 'MOPAC', method)

    order = []
    s = [method + '\n' + title + '\n\n']
    for i, num in enumerate(atomnos):
        if i not in constrained_indexes:
            order.append(i)
            s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[num].symbol,
                                                   coords[i][0], coords[i][1],
                                                   coords[i][2]))

    free_indexes = list(
        set(range(len(atomnos))) - set(constrained_indexes_list))
    # print('free indexes are', free_indexes, '\n')

    if len(constrained_indexes_list) == len(set(constrained_indexes_list)):
        # block pairs of atoms if no atom is involved in more than one distance constrain

        for a, b in constrained_indexes:

            order.append(b)
            order.append(a)

            c, d = np.random.choice(free_indexes, 2)
            while c == d:
                c, d = np.random.choice(free_indexes, 2)
            # indexes of reference atoms, from unconstraind atoms set

            dist = norm_of(coords[a] - coords[b])  # in Angstrom
            # print(f'DIST - {dist} - between {a} {b}')

            angle = vec_angle(norm(coords[a] - coords[b]),
                              norm(coords[c] - coords[b]))
            # print(f'ANGLE - {angle} - between {a} {b} {c}')

            d_angle = dihedral([coords[a], coords[b], coords[c], coords[d]])
            d_angle += 360 if d_angle < 0 else 0
            # print(f'D_ANGLE - {d_angle} - between {a} {b} {c} {d}')

            list_len = len(s)
            s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[atomnos[b]].symbol,
                                                   coords[b][0], coords[b][1],
                                                   coords[b][2]))
            s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format(
                pt[atomnos[a]].symbol, dist, angle, d_angle, list_len,
                free_indexes.index(c) + 1,
                free_indexes.index(d) + 1))
            # print(f'Blocked bond between mopac ids {list_len} {list_len+1}\n')

    elif len(set(constrained_indexes_list)) == 3:
        # three atoms, the central bound to the other two
        # OTHERS[0]: cartesian
        # CENTRAL: internal (self, others[0], two random)
        # OTHERS[1]: internal (self, central, two random)

        central = max(set(constrained_indexes_list),
                      key=lambda x: list(constrained_indexes_list).count(x))
        # index of the atom that is constrained to two other

        others = list(set(constrained_indexes_list) - {central})

        # OTHERS[0]

        order.append(others[0])
        s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[atomnos[others[0]]].symbol,
                                               coords[others[0]][0],
                                               coords[others[0]][1],
                                               coords[others[0]][2]))
        # first atom is placed in cartesian coordinates, the other two have a distance constraint and are expressed in internal coordinates

        #CENTRAL

        order.append(central)
        c, d = np.random.choice(free_indexes, 2)
        while c == d:
            c, d = np.random.choice(free_indexes, 2)
        # indexes of reference atoms, from unconstraind atoms set

        dist = norm_of(coords[central] - coords[others[0]])  # in Angstrom

        angle = vec_angle(norm(coords[central] - coords[others[0]]),
                          norm(coords[others[0]] - coords[c]))

        d_angle = dihedral(
            [coords[central], coords[others[0]], coords[c], coords[d]])
        d_angle += 360 if d_angle < 0 else 0

        list_len = len(s)
        s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format(
            pt[atomnos[central]].symbol, dist, angle, d_angle, list_len - 1,
            free_indexes.index(c) + 1,
            free_indexes.index(d) + 1))

        #OTHERS[1]

        order.append(others[1])
        c1, d1 = np.random.choice(free_indexes, 2)
        while c1 == d1:
            c1, d1 = np.random.choice(free_indexes, 2)
        # indexes of reference atoms, from unconstraind atoms set

        dist1 = norm_of(coords[others[1]] - coords[central])  # in Angstrom

        angle1 = np.arccos(
            norm(coords[others[1]] - coords[central])
            @ norm(coords[others[1]] - coords[c1])) * 180 / np.pi  # in degrees

        d_angle1 = dihedral(
            [coords[others[1]], coords[central], coords[c1], coords[d1]])
        d_angle1 += 360 if d_angle < 0 else 0

        list_len = len(s)
        s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format(
            pt[atomnos[others[1]]].symbol, dist1, angle1, d_angle1,
            list_len - 1,
            free_indexes.index(c1) + 1,
            free_indexes.index(d1) + 1))

    else:
        raise NotImplementedError(
            'The constraints provided for MOPAC optimization are not yet supported'
        )

    s = ''.join(s)
    with open(f'{title}.mop', 'w') as f:
        f.write(s)

    try:
        check_call(f'{COMMANDS["MOPAC"]} {title}.mop'.split(),
                   stdout=DEVNULL,
                   stderr=STDOUT)
    except KeyboardInterrupt:
        print('KeyboardInterrupt requested by user. Quitting.')
        quit()

    os.remove(f'{title}.mop')
    # delete input, we do not need it anymore

    if read_output:

        inv_order = [order.index(i) for i, _ in enumerate(order)]
        # undoing the atomic scramble that was needed by the mopac input requirements

        opt_coords, energy, success = read_mop_out(f'{title}.out')
        os.remove(f'{title}.out')

        opt_coords = scramble(opt_coords,
                              inv_order) if opt_coords is not None else coords
        # If opt_coords is None, that is if TS seeking crashed,
        # sets opt_coords to the old coords. If not, unscrambles
        # coordinates read from mopac output.

        return opt_coords, energy, success
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]

        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.º 11
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.º 12
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.º 13
0
    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))
Ejemplo n.º 14
0
def opt_linear_scan(embedder,
                    coords,
                    atomnos,
                    scan_indexes,
                    constrained_indexes,
                    step_size=0.02,
                    safe=False,
                    title='temp',
                    logfile=None,
                    xyztraj=None):
    '''
    Runs a linear scan along the specified linear coordinate.
    The highest energy structure that passes sanity checks is returned.

    embedder
    coords
    atomnos
    scan_indexes
    constrained_indexes
    step_size
    safe
    title
    logfile
    xyztraj
    '''
    assert [i in constrained_indexes.ravel() for i in scan_indexes]

    i1, i2 = scan_indexes
    far_thr = 2 * sum([pt[atomnos[i]].covalent_radius for i in scan_indexes])
    t_start = time.perf_counter()
    total_iter = 0

    _, energy, _ = optimize(
        coords,
        atomnos,
        embedder.options.calculator,
        embedder.options.theory_level,
        constrained_indexes=constrained_indexes,
        mols_graphs=embedder.graphs,
        procs=embedder.options.procs,
        max_newbonds=embedder.options.max_newbonds,
    )

    direction = coords[i1] - coords[i2]
    base_dist = norm_of(direction)
    energies, geometries = [energy], [coords]

    for sign in (1, -1):
        # getting closer for sign == 1, further apart for -1
        active_coords = deepcopy(coords)
        dist = base_dist

        if scan_peak_present(energies):
            break

        for iterations in range(75):

            if safe:  # use ASE optimization function - more reliable, but locks all interatomic dists

                targets = [
                    norm_of(active_coords[a] - active_coords[b]) - step_size if
                    (a in scan_indexes and b in scan_indexes) else
                    norm_of(active_coords[a] - active_coords[b])
                    for a, b in constrained_indexes
                ]

                active_coords, energy, success = ase_popt(
                    embedder,
                    active_coords,
                    atomnos,
                    constrained_indexes,
                    targets=targets,
                    safe=True,
                )

            else:  # use faster raw optimization function, might scramble more often than the ASE one

                active_coords[i2] += sign * norm(direction) * step_size
                active_coords, energy, success = optimize(
                    active_coords,
                    atomnos,
                    embedder.options.calculator,
                    embedder.options.theory_level,
                    constrained_indexes=constrained_indexes,
                    mols_graphs=embedder.graphs,
                    procs=embedder.options.procs,
                    max_newbonds=embedder.options.max_newbonds,
                )

            if not success:
                if logfile is not None and iterations == 0:
                    logfile.write(f'    - {title} CRASHED at first step\n')

                if embedder.options.debug:
                    with open(title + '_SCRAMBLED.xyz', 'a') as f:
                        write_xyz(
                            active_coords,
                            atomnos,
                            f,
                            title=title +
                            (f' d({i1}-{i2}) = {round(dist, 3)} A, Rel. E = {round(energy-energies[0], 3)} kcal/mol'
                             ))

                break

            direction = active_coords[i1] - active_coords[i2]
            dist = norm_of(direction)

            total_iter += 1
            geometries.append(active_coords)
            energies.append(energy)

            if xyztraj is not None:
                with open(xyztraj, 'a') as f:
                    write_xyz(
                        active_coords,
                        atomnos,
                        f,
                        title=title +
                        (f' d({i1}-{i2}) = {round(dist, 3)} A, Rel. E = {round(energy-energies[0], 3)} kcal/mol'
                         ))

            if (dist < 1.2
                    and sign == 1) or (dist > far_thr and sign
                                       == -1) or (scan_peak_present(energies)):
                break

    distances = [norm_of(g[i1] - g[i2]) for g in geometries]
    best_distance = distances[energies.index(max(energies))]

    distances_delta = [abs(d - best_distance) for d in distances]
    closest_geom = geometries[distances_delta.index(min(distances_delta))]
    closest_dist = distances[distances_delta.index(min(distances_delta))]

    direction = closest_geom[i1] - closest_geom[i2]
    closest_geom[i1] += norm(direction) * (best_distance - closest_dist)

    final_geom, final_energy, _ = optimize(
        closest_geom,
        atomnos,
        embedder.options.calculator,
        embedder.options.theory_level,
        constrained_indexes=constrained_indexes,
        mols_graphs=embedder.graphs,
        procs=embedder.options.procs,
        max_newbonds=embedder.options.max_newbonds,
        check=False,
    )

    if embedder.options.debug:

        if embedder.options.debug:
            with open(xyztraj, 'a') as f:
                write_xyz(
                    active_coords,
                    atomnos,
                    f,
                    title=title +
                    (f' FINAL - d({i1}-{i2}) = {round(norm_of(final_geom[i1]-final_geom[i2]), 3)} A,'
                     f' Rel. E = {round(final_energy-energies[0], 3)} kcal/mol'
                     ))

        import matplotlib.pyplot as plt

        plt.figure()

        distances = [norm_of(geom[i1] - geom[i2]) for geom in geometries]
        distances, sorted_energies = zip(
            *sorted(zip(distances, energies), key=lambda x: x[0]))

        plt.plot(distances, [s - energies[0] for s in sorted_energies],
                 '-o',
                 color='tab:red',
                 label=f'Linear SCAN ({i1}-{i2})',
                 linewidth=3,
                 alpha=0.5)

        plt.plot(
            norm_of(coords[i1] - coords[i2]),
            0,
            marker='o',
            color='tab:blue',
            label='Starting point (0 kcal/mol)',
            markersize=5,
        )

        plt.plot(best_distance,
                 final_energy - energies[0],
                 marker='o',
                 color='black',
                 label='Interpolated best distance, actual energy',
                 markersize=5)

        plt.legend()
        plt.title(title)
        plt.xlabel(f'Interatomic distance {tuple(scan_indexes)}')
        plt.ylabel('Energy Rel. to starting point (kcal/mol)')
        plt.savefig(f'{title.replace(" ", "_")}_plt.svg')

    if logfile is not None:
        logfile.write(
            f'    - {title} COMPLETED {total_iter} steps ({time_to_string(time.perf_counter()-t_start)})\n'
        )

    return final_geom, final_energy, True
Ejemplo n.º 15
0
def get_reagent(embedder,
                coords,
                atomnos,
                ids,
                constrained_indexes,
                method='PM7'):
    '''
    Part of the automatic NEB implementation.
    Returns a structure that presumably is the association reaction reagent.
    ([cyclo]additions reactions in mind)
    '''

    opt_func = opt_funcs_dict[embedder.options.calculator]

    bond_factor = 1.5
    # multiple of sum of covalent radii for two atoms.
    # Putting reactive atoms at this times their bonding
    # distance and performing a constrained optimization
    # is the way to get a good guess for reagents structure.

    if len(ids) == 2:

        mol1_center = np.mean([coords[a] for a, _ in constrained_indexes],
                              axis=0)
        mol2_center = np.mean([coords[b] for _, b in constrained_indexes],
                              axis=0)
        motion = norm(mol2_center - mol1_center)
        # norm of the motion that, when applied to mol1,
        # superimposes its reactive centers to the ones of mol2

        threshold_dists = [
            bond_factor *
            (pt[atomnos[a]].covalent_radius + pt[atomnos[b]].covalent_radius)
            for a, b in constrained_indexes
        ]

        reactive_dists = [
            norm_of(coords[a] - coords[b]) for a, b in constrained_indexes
        ]
        # distances between reactive atoms

        coords[:ids[0]] -= norm(motion) * (np.mean(threshold_dists) -
                                           np.mean(reactive_dists))
        # move reactive atoms away from each other just enough

        coords, _, _ = opt_func(coords,
                                atomnos,
                                constrained_indexes=constrained_indexes,
                                method=method)
        # optimize the structure but keeping the reactive atoms distanced

        return coords

    # trimolecular TSs: the approach is to bring the first pair of reactive
    # atoms apart just enough to get a good approximation for reagents

    index_to_be_moved = constrained_indexes[0, 0]
    reference = constrained_indexes[0, 1]
    moving_molecule_index = next(i for i, n in enumerate(np.cumsum(ids))
                                 if index_to_be_moved < n)
    bounds = [0] + [n + 1 for n in np.cumsum(ids)]
    moving_molecule_slice = slice(bounds[moving_molecule_index],
                                  bounds[moving_molecule_index + 1])
    threshold_dist = bond_factor * (
        pt[atomnos[constrained_indexes[0, 0]]].covalent_radius +
        pt[atomnos[constrained_indexes[0, 1]]].covalent_radius)

    motion = (coords[reference] - coords[index_to_be_moved])
    # vector from the atom to be moved to the target reactive atom

    displacement = norm(motion) * (threshold_dist - norm_of(motion))
    # vector to be applied to the reactive atom to push it far just enough

    for i, atom in enumerate(coords[moving_molecule_slice]):
        dist = norm_of(atom - coords[index_to_be_moved])
        # for any atom in the molecule, distance from the reactive atom

        coords[moving_molecule_slice][i] -= displacement * np.exp(-0.5 * dist)
        # the closer they are to the reactive atom, the further they are moved

    coords, _, _ = opt_func(coords,
                            atomnos,
                            constrained_indexes=np.array(
                                [constrained_indexes[0]]),
                            method=method)
    # when all atoms are moved, optimize the geometry with only the first of the previous constraints

    newcoords, _, _ = opt_func(coords, atomnos, method=method)
    # finally, when structures are close enough, do a free optimization to get the reaction product

    new_reactive_dist = norm_of(newcoords[constrained_indexes[0, 0]] -
                                newcoords[constrained_indexes[0, 0]])

    if new_reactive_dist > threshold_dist:
        # return the freely optimized structure only if the reagents did not approached back each other
        # during the optimization, otherwise return the last coords, where partners were further away
        return newcoords

    return coords
Ejemplo n.º 16
0
def get_product(embedder,
                coords,
                atomnos,
                ids,
                constrained_indexes,
                method='PM7'):
    '''
    Part of the automatic NEB implementation.
    Returns a structure that presumably is the association reaction product
    ([cyclo]additions reactions in mind)
    '''

    opt_func = opt_funcs_dict[embedder.options.calculator]

    bond_factor = 1.2
    # multiple of sum of covalent radii for two atoms.
    # If two atoms are closer than this times their sum
    # of c_radii, they are considered to converge to
    # products when their geometry is optimized.

    step_size = 0.1
    # in Angstroms

    if len(ids) == 2:

        mol1_center = np.mean([coords[a] for a, _ in constrained_indexes],
                              axis=0)
        mol2_center = np.mean([coords[b] for _, b in constrained_indexes],
                              axis=0)
        motion = norm(mol2_center - mol1_center)
        # norm of the motion that, when applied to mol1,
        # superimposes its reactive atoms to the ones of mol2

        threshold_dists = [
            bond_factor *
            (pt[atomnos[a]].covalent_radius + pt[atomnos[b]].covalent_radius)
            for a, b in constrained_indexes
        ]

        reactive_dists = [
            norm_of(coords[a] - coords[b]) for a, b in constrained_indexes
        ]
        # distances between reactive atoms

        while not np.all([
                reactive_dists[i] < threshold_dists[i]
                for i, _ in enumerate(constrained_indexes)
        ]):
            # print('Reactive distances are', reactive_dists)

            coords[:ids[0]] += motion * step_size

            coords, _, _ = opt_func(coords,
                                    atomnos,
                                    constrained_indexes,
                                    method=method)

            reactive_dists = [
                norm_of(coords[a] - coords[b]) for a, b in constrained_indexes
            ]

        newcoords, _, _ = opt_func(coords, atomnos, method=method)
        # finally, when structures are close enough, do a free optimization to get the reaction product

        new_reactive_dists = [
            norm_of(newcoords[a] - newcoords[b])
            for a, b in constrained_indexes
        ]

        if np.all([
                new_reactive_dists[i] < threshold_dists[i]
                for i, _ in enumerate(constrained_indexes)
        ]):
            # return the freely optimized structure only if the reagents did not repel each other
            # during the optimization, otherwise return the last coords, where partners were close
            return newcoords

        return coords

    # trimolecular TSs: the approach is to bring the first pair of reactive
    # atoms closer until optimization bounds the molecules together

    index_to_be_moved = constrained_indexes[0, 0]
    reference = constrained_indexes[0, 1]
    moving_molecule_index = next(i for i, n in enumerate(np.cumsum(ids))
                                 if index_to_be_moved < n)
    bounds = [0] + [n + 1 for n in np.cumsum(ids)]
    moving_molecule_slice = slice(bounds[moving_molecule_index],
                                  bounds[moving_molecule_index + 1])
    threshold_dist = bond_factor * (
        pt[atomnos[constrained_indexes[0, 0]]].covalent_radius +
        pt[atomnos[constrained_indexes[0, 1]]].covalent_radius)

    motion = (coords[reference] - coords[index_to_be_moved])
    # vector from the atom to be moved to the target reactive atom

    while norm_of(motion) > threshold_dist:
        # check if the reactive atoms are sufficiently close to converge to products

        for i, atom in enumerate(coords[moving_molecule_slice]):
            dist = norm_of(atom - coords[index_to_be_moved])
            # for any atom in the molecule, distance from the reactive atom

            atom_step = step_size * np.exp(-0.5 * dist)
            coords[moving_molecule_slice][i] += norm(motion) * atom_step
            # the more they are close, the more they are moved

        # print('Reactive dist -', norm_of(motion))
        coords, _, _ = opt_func(coords,
                                atomnos,
                                constrained_indexes,
                                method=method)
        # when all atoms are moved, optimize the geometry with the previous constraints

        motion = (coords[reference] - coords[index_to_be_moved])

    newcoords, _, _ = opt_func(coords, atomnos, method=method)
    # finally, when structures are close enough, do a free optimization to get the reaction product

    new_reactive_dist = norm_of(newcoords[constrained_indexes[0, 0]] -
                                newcoords[constrained_indexes[0, 0]])

    if new_reactive_dist < threshold_dist:
        # return the freely optimized structure only if the reagents did not repel each other
        # during the optimization, otherwise return the last coords, where partners were close
        return newcoords

    return coords