Ejemplo n.º 1
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.º 2
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.º 3
0
def _get_nci_aromatic_rings(coords, symbols, ids, aromatic_centers):
    '''
    '''
    cum_ids = np.cumsum(ids)
    print_list, nci = [], []

    for owner, center in aromatic_centers:
        for i, atom in enumerate(coords):

            if i < cum_ids[0]:
                atom_owner = 0
            else:
                atom_owner = next(i for i, n in enumerate(np.cumsum(ids))
                                  if i < n)

            if atom_owner != owner:
                # if this atom belongs to a molecule different than the one that owns the phenyl

                s = ''.join(sorted(['Ph', symbols[i]]))
                if s in nci_dict:

                    threshold, nci_type = nci_dict[s]
                    dist = norm_of(center - atom)

                    if dist < threshold:

                        print_list.append(
                            nci_type + f' ({round(dist, 2)} A, atom {i}/ring)')
                        # string to be printed in log

                        nci.append((nci_type, i, 'ring'))
                        # tuple to be used in identifying the NCI

    # checking phenyl-phenyl pairs
    for i, owner_center in enumerate(aromatic_centers):
        owner1, center1 = owner_center
        for owner2, center2 in aromatic_centers[i + 1:]:
            if owner1 != owner2:
                # if this atom belongs to a molecule different than owner

                threshold, nci_type = nci_dict['PhPh']
                dist = norm_of(center1 - center2)

                if dist < threshold:

                    print_list.append(nci_type +
                                      f' ({round(dist, 2)} A, ring/ring)')
                    # string to be printed in log

                    nci.append((nci_type, 'ring', 'ring'))
                    # tuple to be used in identifying the NCI
    return print_list, nci
Ejemplo n.º 4
0
    def adjust_forces(self, atoms, forces):

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

        if norm_of(direction) > self.d_max:

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

            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.º 5
0
def ase_popt(embedder,
             coords,
             atomnos,
             constrained_indexes=None,
             steps=500,
             targets=None,
             safe=False,
             safe_mask=None,
             traj=None,
             logfunction=None,
             title='temp'):
    '''
    embedder: TSCoDe embedder object
    coords: 
    atomnos: 
    constrained_indexes:
    safe: if True, adds a potential that prevents atoms from scrambling
    safe_mask: bool array, with False for atoms to be excluded when calculating bonds to preserve
    traj: if set to a string, traj is used as a filename for the bending trajectory.
          not only the atoms will be printed, but also all the orbitals and the active pivot.
    '''
    atoms = Atoms(atomnos, positions=coords)
    atoms.calc = get_ase_calc(embedder)
    constraints = []

    if constrained_indexes is not None:
        for i, c in enumerate(constrained_indexes):
            i1, i2 = c
            tgt_dist = norm_of(coords[i1] -
                               coords[i2]) if targets is None else targets[i]
            constraints.append(Spring(i1, i2, tgt_dist))

    if safe:
        constraints.append(
            PreventScramblingConstraint(
                graphize(coords, atomnos, safe_mask),
                atoms,
                double_bond_protection=embedder.options.double_bond_protection,
                fix_angles=embedder.options.fix_angles_in_deformation))

    atoms.set_constraint(constraints)

    t_start_opt = time.perf_counter()
    with LBFGS(atoms, maxstep=0.1, logfile=None, trajectory=traj) as opt:
        opt.run(fmax=0.05, steps=steps)
        iterations = opt.nsteps

    new_structure = atoms.get_positions()
    success = (iterations < 499)

    if logfunction is not None:
        exit_str = 'REFINED' if success else 'MAX ITER'
        logfunction(
            f'    - {title} {exit_str} ({iterations} iterations, {time_to_string(time.perf_counter()-t_start_opt)})'
        )

    energy = atoms.get_total_energy() * 23.06054194532933  #eV to kcal/mol

    return new_structure, energy, success
Ejemplo n.º 6
0
 def _scale_orbs(self, value):
     '''
     Scale each orbital dimension according to value.
     '''
     for c, _ in enumerate(self.atomcoords):
         for index, atom in self.reactive_atoms_classes_dict[c].items():
             orb_dim = norm_of(atom.center[0] - atom.coord)
             atom.init(self,
                       index,
                       update=True,
                       orb_dim=orb_dim * value,
                       conf=c)
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.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.º 8
0
def _get_hydrogen_bonds(coords, atomnos, graph):
    '''
    '''
    output = []
    for i1, (coord1, num1) in enumerate(zip(coords, atomnos)):
        for i2, (coord2,
                 num2) in enumerate(zip(coords[i1 + 1:], atomnos[i1 + 1:])):
            i2 += (i1 + 1)

            if (i1, i2) not in graph.edges and num1 in (1, 7,
                                                        8) and num2 in (1, 7,
                                                                        8):
                tag = ''.join(sorted([pt[num1].symbol, pt[num2].symbol]))

                if tag in nci_dict:
                    thresh, _ = nci_dict[tag]
                    dist = norm_of(coord1 - coord2)

                    if dist < thresh:
                        output.append((i1, i2))
    return output
Ejemplo n.º 9
0
def fitness_check(embedder, coords) -> bool:
    '''
    Returns True if the strucure respects
    the imposed pairings.
    '''
    if hasattr(embedder, 'pairings_dists'):
        for letter, pairing in embedder.pairings_table.items():

            if letter in ('a', 'b', 'c'):
                i1, i2 = pairing
                dist = norm_of(coords[i1] - coords[i2])
                target = embedder.pairings_dists.get(letter)

                if target is not None and abs(target - dist) > 0.05:
                    return False

            else:
                if dist < 2.5:
                    return False

    return True
Ejemplo n.º 10
0
def is_sigmatropic(mol, conf):
    '''
    mol: Hypermolecule object
    conf: conformer index

    A hypermolecule is considered sigmatropic when:
    - has 2 reactive atoms
    - they are of sp2 or analogous types
    - they are connected, or at least one path connecting them
      is made up of atoms that do not make more than three bonds each
    - they are less than 3 A apart (cisoid propenal makes it, transoid does not)

    Used to set the mol.sigmatropic attribute, that affects orbital
    building (p or n lobes) for Ketone and Imine reactive atoms classes.
    '''
    sp2_types = ('Ketone', 'Imine', 'sp2', 'sp', 'bent carbene')
    if len(mol.reactive_indexes) == 2:

        i1, i2 = mol.reactive_indexes
        if norm_of(mol.atomcoords[conf][i1] - mol.atomcoords[conf][i2]) < 3:

            if all([
                    str(r_atom) in sp2_types for r_atom in
                    mol.reactive_atoms_classes_dict[conf].values()
            ]):

                paths = nx.all_simple_paths(mol.graph, i1, i2)

                for path in paths:
                    path = path[1:-1]

                    full_sp2 = True
                    for index in path:
                        if len(neighbors(mol.graph, index)) - 2 > 1:
                            full_sp2 = False
                            break

                    if full_sp2:
                        return True
    return False
Ejemplo n.º 11
0
def _get_nci_atomic_pairs(coords, symbols, constrained_indexes, ids):
    '''
    '''
    print_list = []
    nci = []

    cum_ids = np.cumsum(ids)

    for i1, _ in enumerate(coords):
        # check atomic pairs (O-H, N-H, ...)

        start_of_next_mol = cum_ids[next(i for i, n in enumerate(cum_ids)
                                         if i1 < n)]
        # ensures that we are only taking into account intermolecular NCIs

        for i2, _ in enumerate(coords[start_of_next_mol:]):
            i2 += start_of_next_mol

            if (i1 not in constrained_indexes) and (
                    i2 not in constrained_indexes):
                # ignore atoms involved in constraints

                s = ''.join(sorted([symbols[i1], symbols[i2]]))
                # print(f'Checking pair {i1}/{i2}')

                if s in nci_dict:
                    threshold, nci_type = nci_dict[s]
                    dist = norm_of(coords[i1] - coords[i2])

                    if dist < threshold:

                        print_list.append(
                            nci_type +
                            f' ({round(dist, 2)} A, indexes {i1}/{i2})')
                        # string to be printed in log

                        nci.append((nci_type, i1, i2))
                        # tuple to be used in identifying the NCI

    return print_list, nci
Ejemplo n.º 12
0
def get_double_bonds_indexes(coords, atomnos):
    '''
    Returns a list containing 2-elements tuples
    of indexes involved in any double bond
    '''
    mask = (atomnos != 1)
    numbering = np.arange(len(coords))[mask]
    coords = coords[mask]
    atomnos = atomnos[mask]
    output = []

    for i1, _ in enumerate(coords):
        for i2 in range(i1 + 1, len(coords)):
            dist = norm_of(coords[i1] - coords[i2])
            tag = ''.join(
                sorted([pt[atomnos[i1]].symbol, pt[atomnos[i2]].symbol]))

            threshold = double_bonds_thresholds_dict.get(tag)
            if threshold is not None and dist < threshold:
                output.append((numbering[i1], numbering[i2]))

    return output
Ejemplo n.º 13
0
def graphize(coords, atomnos, mask=None):
    '''
    :params coords: atomic coordinates as 3D vectors
    :params atomnos: atomic numbers as a list
    :params mask: bool array, with False for atoms
                  to be excluded in the bond evaluation
    :return connectivity graph
    '''

    mask = np.array([True
                     for _ in atomnos], dtype=bool) if mask is None else mask

    matrix = np.zeros((len(coords), len(coords)))
    for i, _ in enumerate(coords):
        for j in range(i, len(coords)):
            if mask[i] and mask[j]:
                if norm_of(coords[i] - coords[j]) < d_min_bond(
                        atomnos[i], atomnos[j]):
                    matrix[i][j] = 1

    graph = nx.from_numpy_matrix(matrix)
    nx.set_node_attributes(graph, dict(enumerate(atomnos)), 'atomnos')

    return graph
Ejemplo n.º 14
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.º 15
0
def xtb_opt(coords,
            atomnos,
            constrained_indexes=None,
            method='GFN2-xTB',
            solvent=None,
            title='temp',
            read_output=True,
            **kwargs):
    '''
    This function writes an XTB .inp file, runs it with the subprocess
    module and reads its output.

    :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 theory level to be used.
    :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.
    '''

    with open(f'{title}.xyz', 'w') as f:
        write_xyz(coords, atomnos, f, title=title)

    s = f'$opt\n   logfile={title}_opt.log\n$end'

    if constrained_indexes is not None:
        s += '\n$constrain\n'
        for a, b in constrained_indexes:
            s += '   distance: %s, %s, %s\n' % (
                a + 1, b + 1, round(norm_of(coords[a] - coords[b]), 5))

    if method.upper() in ('GFN-XTB', 'GFNXTB'):
        s += '\n$gfn\n   method=1\n'

    elif method.upper() in ('GFN2-XTB', 'GFN2XTB'):
        s += '\n$gfn\n   method=2\n'

    s += '\n$end'

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

    flags = '--opt'

    if method in ('GFN-FF', 'GFNFF'):
        flags += ' tight'
        # tighter convergence for GFN-FF works better

        flags += ' --gfnff'
        # declaring the use of FF instead of semiempirical

    if solvent is not None:

        if solvent == 'methanol':
            flags += f' --gbsa methanol'

        else:
            flags += f' --alpb {solvent}'

    elif method.upper() in ('GFN-FF', 'GFNFF'):
        flags += f' --alpb thf'

    try:
        check_call(
            f'xtb --input {title}.inp {title}.xyz {flags} > temp.log 2>&1'.
            split(),
            stdout=DEVNULL,
            stderr=STDOUT)

    except KeyboardInterrupt:
        print('KeyboardInterrupt requested by user. Quitting.')
        quit()

    if read_output:

        try:
            outname = 'xtbopt.xyz'
            opt_coords = read_xyz(outname).atomcoords[0]
            energy = read_xtb_energy(outname)

            clean_directory()
            os.remove(outname)

            for filename in ('gfnff_topo', 'charges', 'wbo', 'xtbrestart',
                             'xtbtopo.mol', '.xtboptok'):
                try:
                    os.remove(filename)
                except FileNotFoundError:
                    pass

            return opt_coords, energy, True

        except FileNotFoundError:
            return None, None, False
Ejemplo n.º 16
0
def xtb_metadyn_augmentation(coords,
                             atomnos,
                             constrained_indexes=None,
                             new_structures: int = 5,
                             title=0,
                             debug=False):
    '''
    Runs a metadynamics simulation (MTD) through
    the XTB program to obtain new conformations.
    The GFN-FF force field is used.
    '''
    with open(f'temp.xyz', 'w') as f:
        write_xyz(coords, atomnos, f, title='temp')

    s = ('$md\n'
         '   time=%s\n' % (new_structures) + '   step=1\n'
         '   temp=300\n'
         '$end\n'
         '$metadyn\n'
         '   save=%s\n' % (new_structures) + '$end')

    if constrained_indexes is not None:
        s += '\n$constrain\n'
        for a, b in constrained_indexes:
            s += '   distance: %s, %s, %s\n' % (
                a + 1, b + 1, round(norm_of(coords[a] - coords[b]), 5))

    s = ''.join(s)
    with open(f'temp.inp', 'w') as f:
        f.write(s)

    try:
        check_call(
            f'xtb --md --input temp.inp temp.xyz --gfnff > Structure{title}_MTD.log 2>&1'
            .split(),
            stdout=DEVNULL,
            stderr=STDOUT)

    except KeyboardInterrupt:
        print('KeyboardInterrupt requested by user. Quitting.')
        quit()

    structures = [coords]
    for n in range(1, new_structures):
        name = 'scoord.' + str(n)
        structures.append(parse_xtb_out(name))
        os.remove(name)

    for filename in ('gfnff_topo', 'xtbmdoc', 'mdrestart'):
        try:
            os.remove(filename)
        except FileNotFoundError:
            pass

    # if debug:
    os.rename('xtb.trj', f'Structure{title}_MTD_traj.xyz')

    # else:
    #     os.remove('xtb.traj')

    structures = np.array(structures)

    return structures
Ejemplo n.º 17
0
def ase_bend(embedder,
             original_mol,
             conf,
             pivot,
             threshold,
             title='temp',
             traj=None,
             check=True):
    '''
    embedder: TSCoDe embedder object
    original_mol: Hypermolecule object to be bent
    conf: index of conformation in original_mol to be used
    pivot: pivot connecting two Hypermolecule orbitals to be approached/distanced
    threshold: target distance for the specified pivot, in Angstroms
    title: name to be used for referring to this structure in the embedder log
    traj: if set to a string, traj+'.traj' is used as a filename for the bending trajectory.
          not only the atoms will be printed, but also all the orbitals and the active pivot.
    check: if True, after bending checks that the bent structure did not scramble.
           If it did, returns the initial molecule.
    '''

    identifier = np.sum(original_mol.atomcoords[conf])

    if hasattr(embedder, "ase_bent_mols_dict"):
        cached = embedder.ase_bent_mols_dict.get(
            (identifier, tuple(sorted(pivot.index)), round(threshold, 3)))
        if cached is not None:
            return cached

    if traj is not None:

        from ase.io.trajectory import Trajectory

        def orbitalized(atoms, orbitals, pivot=None):
            positions = np.concatenate((atoms.positions, orbitals))

            if pivot is not None:
                positions = np.concatenate(
                    (positions, [pivot.start], [pivot.end]))

            symbols = list(atoms.numbers) + [0 for _ in orbitals]

            if pivot is not None:
                symbols += [9 for _ in range(2)]
            # Fluorine (9) represents active orbitals

            new_atoms = Atoms(symbols, positions=positions)
            return new_atoms

        try:
            os.remove(traj)
        except FileNotFoundError:
            pass

    i1, i2 = original_mol.reactive_indexes

    neighbors_of_1 = neighbors(original_mol.graph, i1)
    neighbors_of_2 = neighbors(original_mol.graph, i2)

    mol = deepcopy(original_mol)
    final_mol = deepcopy(original_mol)

    for p in mol.pivots[conf]:
        if p.index == pivot.index:
            active_pivot = p
            break

    dist = norm_of(active_pivot.pivot)

    atoms = Atoms(mol.atomnos, positions=mol.atomcoords[conf])

    atoms.calc = get_ase_calc(embedder)

    if traj is not None:
        traj_obj = Trajectory(
            traj + f'_conf{conf}.traj',
            mode='a',
            atoms=orbitalized(
                atoms,
                np.vstack([
                    atom.center
                    for atom in mol.reactive_atoms_classes_dict[0].values()
                ]), active_pivot))
        traj_obj.write()

    unproductive_iterations = 0
    break_reason = 'MAX ITER'
    t_start = time.perf_counter()

    for iteration in range(500):

        atoms.positions = mol.atomcoords[0]

        orb_memo = {
            index: norm_of(atom.center[0] - atom.coord)
            for index, atom in mol.reactive_atoms_classes_dict[0].items()
        }

        orb1, orb2 = active_pivot.start, active_pivot.end

        c1 = OrbitalSpring(i1,
                           i2,
                           orb1,
                           orb2,
                           neighbors_of_1,
                           neighbors_of_2,
                           d_eq=threshold)

        c2 = PreventScramblingConstraint(
            mol.graph,
            atoms,
            double_bond_protection=embedder.options.double_bond_protection,
            fix_angles=embedder.options.fix_angles_in_deformation)

        atoms.set_constraint([
            c1,
            c2,
        ])

        opt = BFGS(atoms, maxstep=0.2, logfile=None, trajectory=None)

        try:
            opt.run(fmax=0.5, steps=1)
        except ValueError:
            # Shake did not converge
            break_reason = 'CRASHED'
            break

        if traj is not None:
            traj_obj.atoms = orbitalized(
                atoms,
                np.vstack([
                    atom.center
                    for atom in mol.reactive_atoms_classes_dict[0].values()
                ]))
            traj_obj.write()

        # check if we are stuck
        if np.max(
                np.abs(
                    np.linalg.norm(atoms.get_positions() - mol.atomcoords[0],
                                   axis=1))) < 0.01:
            unproductive_iterations += 1

            if unproductive_iterations == 10:
                break_reason = 'STUCK'
                break

        else:
            unproductive_iterations = 0

        mol.atomcoords[0] = atoms.get_positions()

        # Update orbitals and get temp pivots
        for index, atom in mol.reactive_atoms_classes_dict[0].items():
            atom.init(mol, index, update=True, orb_dim=orb_memo[index])
            # orbitals positions are calculated based on the conformer we are working on

        temp_pivots = embedder._get_pivots(mol)[0]

        for p in temp_pivots:
            if p.index == pivot.index:
                active_pivot = p
                break
        # print(active_pivot)

        dist = norm_of(active_pivot.pivot)
        # print(f'{iteration}. {mol.name} conf {conf}: pivot is {round(dist, 3)} (target {round(threshold, 3)})')

        if dist - threshold < 0.1:
            break_reason = 'CONVERGED'
            break
        # else:
        # print('delta is ', round(dist - threshold, 3))

    embedder.log(
        f'    {title} - conformer {conf} - {break_reason}{" "*(9-len(break_reason))} ({iteration+1}{" "*(3-len(str(iteration+1)))} iterations, {time_to_string(time.perf_counter()-t_start)})',
        p=False)

    if check:
        if not molecule_check(original_mol.atomcoords[conf],
                              mol.atomcoords[0],
                              mol.atomnos,
                              max_newbonds=1):
            mol.atomcoords[0] = original_mol.atomcoords[conf]
        # keep the bent structures only if no scrambling occurred between atoms

    final_mol.atomcoords[conf] = mol.atomcoords[0]

    # Now align the ensembles on the new reactive atoms positions

    reference, *targets = final_mol.atomcoords
    reference = np.array(reference)
    targets = np.array(targets)

    r = reference - np.mean(reference[final_mol.reactive_indexes], axis=0)
    ts = np.array(
        [t - np.mean(t[final_mol.reactive_indexes], axis=0) for t in targets])

    output = []
    output.append(r)
    for target in ts:
        matrix = kabsch(r, target)
        output.append([matrix @ vector for vector in target])

    final_mol.atomcoords = np.array(output)

    # Update orbitals and pivots
    for conf_, _ in enumerate(final_mol.atomcoords):
        for index, atom in final_mol.reactive_atoms_classes_dict[conf_].items(
        ):
            atom.init(final_mol, index, update=True, orb_dim=orb_memo[index])

    embedder._set_pivots(final_mol)

    # add result to cache (if we have it) so we avoid recomputing it
    if hasattr(embedder, "ase_bent_mols_dict"):
        embedder.ase_bent_mols_dict[(identifier, tuple(sorted(pivot.index)),
                                     round(threshold, 3))] = final_mol

    clean_directory()

    return final_mol
Ejemplo n.º 18
0
def scan_operator(filename, embedder):
    '''
    '''
    embedder.t_start_run = time.perf_counter()
    mol = embedder.objects[0]
    assert len(mol.atomcoords
               ) == 1, 'The scan> operator works on a single .xyz geometry.'
    assert len(mol.reactive_indexes
               ) == 2, 'The scan> operator needs two reactive indexes ' + (
                   f'({len(mol.reactive_indexes)} were provided)')

    import matplotlib.pyplot as plt

    from tscode.algebra import norm_of
    from tscode.ase_manipulations import ase_popt
    from tscode.pt import pt

    i1, i2 = mol.reactive_indexes
    coords = mol.atomcoords[0]
    # shorthands for clearer code

    embedder.log(
        f'--> Performing a distance scan approaching on indexes {i1} ' +
        f'and {i2}.\nTheory level is {embedder.options.theory_level} ' +
        f'via {embedder.options.calculator}')

    d = norm_of(coords[i1] - coords[i2])
    # getting the start distance between scan indexes and start energy

    dists, energies, structures = [], [], []
    # creating a dictionary that will hold results
    # and the structure output list

    step = -0.05
    # defining the step magnitude, in Angstroms

    s1, s2 = mol.atomnos[[i1, i2]]
    smallest_d = 0.8 * (pt[s1].covalent_radius + pt[s2].covalent_radius)
    max_iterations = round((d - smallest_d) / abs(step))
    # defining the maximum number of iterations,
    # so that atoms are never forced closer than
    # a proportionally small distance between those two atoms.

    for i in range(max_iterations):

        coords, energy, _ = ase_popt(
            embedder,
            coords,
            mol.atomnos,
            constrained_indexes=np.array([mol.reactive_indexes]),
            targets=(d, ),
            title=f'Step {i+1}/{max_iterations} - d={round(d, 2)} A -',
            logfunction=embedder.log,
            traj=f'{mol.title}_scanpoint_{i+1}.traj'
            if embedder.options.debug else None,
        )
        # optimizing the structure with a spring constraint

        if i == 0:
            e_0 = energy

        energies.append(energy - e_0)
        dists.append(d)
        structures.append(coords)
        # saving the structure, distance and relative energy

        d += step
        # modify the target distance and reiterate

    ### Start the plotting sequence

    plt.figure()
    plt.plot(
        dists,
        energies,
        color='tab:red',
        label='Scan energy',
        linewidth=3,
    )

    # e_max = max(energies)
    id_max = get_scan_peak_index(energies)
    e_max = energies[id_max]

    # id_max = energies.index(e_max)
    d_opt = dists[id_max]

    plt.plot(
        d_opt,
        e_max,
        color='gold',
        label='Energy maximum (TS guess)',
        marker='o',
        markersize=3,
    )

    title = mol.name + ' distance scan'
    plt.legend()
    plt.title(title)
    plt.xlabel(f'Indexes {i1}-{i2} distance (A)')
    plt.gca().invert_xaxis()
    plt.ylabel('Rel. E. (kcal/mol)')
    plt.savefig(f'{title.replace(" ", "_")}_plt.svg')

    ### Start structure writing

    with open(f'{mol.name[:-4]}_scan.xyz', 'w') as f:
        for i, (s, d, e) in enumerate(zip(structures, dists, energies)):
            write_xyz(
                s,
                mol.atomnos,
                f,
                title=f'Scan point {i+1}/{len(structures)} ' +
                f'- d({i1}-{i2}) = {round(d, 3)} A - Rel. E = {round(e, 3)} kcal/mol'
            )
    # print all scan structures

    with open(f'{mol.name[:-4]}_scan_max.xyz', 'w') as f:
        s = structures[id_max]
        d = dists[id_max]
        write_xyz(
            s,
            mol.atomnos,
            f,
            title=f'Scan point {id_max+1}/{len(structures)} ' +
            f'- d({i1}-{i2}) = {round(d, 3)} A - Rel. E = {round(e_max, 3)} kcal/mol'
        )
    # print the maximum on another file for convienience

    embedder.log(
        f'\n--> Written {len(structures)} structures to {mol.name[:-4]}_scan.xyz'
    )
    embedder.log(
        f'\n--> Written energy maximum to {mol.name[:-4]}_scan_max.xyz')
Ejemplo n.º 19
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
Ejemplo n.º 20
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.º 21
0
    def _set_pivots(self, mol):
        '''
        params mol: Hypermolecule class
        (Cyclical embed) Function that sets the mol.pivots attribute, that is a list
        containing each vector connecting two orbitals on different atoms or on the
        same atom (for single-reactive atom molecules in chelotropic embedding)
        '''
        mol.pivots = self._get_pivots(mol)

        for c, _ in enumerate(mol.atomcoords):

            if len(mol.pivots[c]) == 2:
                # reactive atoms have one and two centers,
                # respectively. Apply bridging carboxylic acid correction.
                symbols = [
                    atom.symbol
                    for atom in mol.reactive_atoms_classes_dict[c].values()
                ]
                if 'H' in symbols:
                    if ('O' in symbols) or ('S' in symbols):
                        if max([
                                norm_of(p.pivot) /
                                self.options.shrink_multiplier
                                for p in mol.pivots[c]
                        ]) < 4.5:
                            class_types = [
                                str(atom) for atom in
                                mol.reactive_atoms_classes_dict[c].values()
                            ]
                            if 'Single Bond' in class_types and 'Ketone' in class_types:
                                # if we have a bridging acid, remove the longest of the two pivots,
                                # as it would lead to weird structures
                                norms = np.linalg.norm(
                                    [p.pivot for p in mol.pivots[c]], axis=1)
                                for sample in norms:
                                    to_keep = [i for i in norms if sample >= i]
                                    if len(to_keep) == 1:
                                        mask = np.array(
                                            [i in to_keep for i in norms])
                                        mol.pivots[c] = mol.pivots[c][mask]
                                        break

            if self.options.suprafacial:
                if len(mol.pivots[c]) == 4:
                    # reactive atoms have two centers each.
                    # Applying suprafacial correction, only keeping
                    # the shorter two, as they should be the suprafacial ones
                    norms = np.linalg.norm([p.pivot for p in mol.pivots[c]],
                                           axis=1)
                    for sample in norms:
                        to_keep = [i for i in norms if sample >= i]
                        if len(to_keep) == 2:
                            mask = np.array([i in to_keep for i in norms])
                            mol.pivots[c] = mol.pivots[c][mask]
                            break

            if mol.sp3_sigmastar:
                pivots_lengths = [
                    norm_of(pivot.pivot) for pivot in mol.pivots[c]
                ]
                shortest_length = min(pivots_lengths)
                mask = np.array([(i - shortest_length) < 1e-5
                                 for i in pivots_lengths])
                mol.pivots[c] = mol.pivots[c][mask]
Ejemplo n.º 22
0
    def _compute_hypermolecule(self):
        '''
        '''

        self.energies = [0 for _ in self.atomcoords]

        self.hypermolecule_atomnos = []
        clusters = {i: {}
                    for i, _ in enumerate(self.atomnos)
                    }  # {atom_index:{cluster_number:[position,times_found]}}
        for i, atom_number in enumerate(self.atomnos):
            atoms_arrangement = [conformer[i] for conformer in self.atomcoords]
            cluster_number = 0
            clusters[i][cluster_number] = [
                atoms_arrangement[0], 1
            ]  # first structure has rel E = 0 so its weight is surely 1
            self.hypermolecule_atomnos.append(atom_number)
            radii = pt[atom_number].covalent_radius
            for j, atom in enumerate(atoms_arrangement[1:]):

                weight = np.exp(-self.energies[j + 1] * 503.2475342795285 /
                                self.T)
                # print(f'Atom {i} in conf {j+1} weight is {weight} - rel. E was {self.energies[j+1]}')

                for cluster_number, reference in deepcopy(clusters[i]).items():
                    if norm_of(atom - reference[0]) < radii:
                        clusters[i][cluster_number][1] += weight
                    else:
                        clusters[i][max(clusters[i].keys()) +
                                    1] = [atom, weight]
                        self.hypermolecule_atomnos.append(atom_number)

        self.weights = [[] for _ in self.atomnos]
        self.hypermolecule = []

        for i, _ in enumerate(self.atomnos):
            for _, data in clusters[i].items():
                self.weights[i].append(data[1])
                self.hypermolecule.append(data[0])

        def flatten(array):
            out = []

            def rec(l):
                for e in l:
                    if type(e) in [list, np.ndarray]:
                        rec(e)
                    else:
                        out.append(float(e))

            rec(array)
            return out

        self.hypermolecule = np.asarray(self.hypermolecule)
        self.weights = np.array(self.weights).flatten()
        self.weights = np.array(
            [weights / np.sum(weights) for weights in self.weights])
        self.weights = flatten(self.weights)

        self.dimensions = (max([coord[0] for coord in self.hypermolecule]) -
                           min([coord[0] for coord in self.hypermolecule]),
                           max([coord[1] for coord in self.hypermolecule]) -
                           min([coord[1] for coord in self.hypermolecule]),
                           max([coord[2] for coord in self.hypermolecule]) -
                           min([coord[2] for coord in self.hypermolecule]))
Ejemplo n.º 23
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.º 24
0
    def _setup(self, p=True):
        '''
        Setting embed type and calculating the number of conformation combinations based on embed type
        '''

        if 'prune>' in self.options.operators or self.options.noembed:
            self.embed == 'prune'

            # If the run is a prune>/NOEMBED one, the self.embed
            # attribute is set in advance by the self._set_options
            # function through the OptionSetter class
            return

        if all([len(mol.reactive_indexes) == 0 for mol in self.objects]):
            self.embed = None
            # Flag the embed type as None if no reactive indexes are
            # provided (and the run is not a prune> one)
            return

        if len(self.objects) == 1:
            # embed must be either monomolecular or dihedral

            mol = self.objects[0]

            if len(mol.reactive_indexes) == 4:
                self.embed = 'dihedral'
                if self.options.kcal_thresh is None:
                    # set to 5 if user did not specify a value
                    self.options.kcal_thresh = 5

                if self.options.pruning_thresh is None:
                    # set to 0.2 if user did not specify a value
                    self.options.pruning_thresh = 0.2

                return

            if len(mol.reactive_indexes) == 2:

                self.embed = 'monomolecular'
                mol.compute_orbitals()
                self._set_pivots(mol)

                self.options.only_refined = True
                self.options.fix_angles_in_deformation = True
                # These are required: otherwise, extreme bending could scramble molecules

            else:
                self.embed = 'error'
                # if none of the previous, the program had trouble recognizing the embed to carry.

                return

        elif len(self.objects) in (2, 3):
            # Setting embed type and calculating the number of conformation combinations based on embed type

            cyclical = all([
                len(molecule.reactive_indexes) == 2
                for molecule in self.objects
            ])
            chelotropic = sorted([
                len(molecule.reactive_indexes) for molecule in self.objects
            ]) == [1, 2]
            string = all([
                len(molecule.reactive_indexes) == 1
                for molecule in self.objects
            ]) and len(self.objects) == 2

            if cyclical or chelotropic:

                if cyclical:
                    self.embed = 'cyclical'
                else:
                    self.embed = 'chelotropic'
                    for mol in self.objects:
                        mol.compute_orbitals()
                        for c, _ in enumerate(mol.atomcoords):
                            for index, atom in mol.reactive_atoms_classes_dict[
                                    c].items():
                                orb_dim = norm_of(atom.center[0] - atom.coord)
                                atom.init(mol,
                                          index,
                                          update=True,
                                          orb_dim=orb_dim + 0.2)
                    # Slightly enlarging orbitals for chelotropic embeds, or they will
                    # be generated a tad too close to each other for how the cyclical embed works

                self.options.rotation_steps = 9

                if hasattr(self.options, 'custom_rotation_steps'):
                    # if user specified a custom value, use it.
                    self.options.rotation_steps = self.options.custom_rotation_steps

                self.systematic_angles = cartesian_product(*[range(self.options.rotation_steps+1) for _ in self.objects]) \
                            * 2*self.options.rotation_range/self.options.rotation_steps - self.options.rotation_range

                for molecule in self.objects:
                    self._set_pivots(molecule)

            elif string:

                self.embed = 'string'
                self.options.rotation_steps = 24

                for mol in self.objects:
                    mol.compute_orbitals()

                if hasattr(self.options, 'custom_rotation_steps'):
                    # if user specified a custom value, use it.
                    self.options.rotation_steps = self.options.custom_rotation_steps

                self.systematic_angles = [
                    n * 360 / self.options.rotation_steps
                    for n in range(self.options.rotation_steps)
                ]

            else:
                raise InputError((
                    'Bad input - The only molecular configurations accepted are:\n'
                    '1) One molecule with two reactive centers (monomolecular embed)\n'
                    '2) One molecule with four indexes (dihedral embed)\n'
                    '3) Two or three molecules with two reactive centers each (cyclical embed)\n'
                    '4) Two molecules with one reactive center each (string embed)\n'
                    '5) Two molecules, one with a single reactive center and the other with two (chelotropic embed)'
                ))

            self._set_reactive_atoms_cumnums()
            # appending to each reactive atom the cumulative
            # number indexing in the TS context

        else:
            raise InputError(
                'Bad input - too many/few molecules specified (one to three required).'
            )

        if self.options.shrink:
            for molecule in self.objects:
                molecule._scale_orbs(self.options.shrink_multiplier)
                self._set_pivots(molecule)
            self.options.only_refined = True
        # SHRINK - scale orbitals and rebuild pivots

        if self.options.pruning_thresh is None:
            self.options.pruning_thresh = 1

            if sum(self.ids) < 50:
                self.options.pruning_thresh = 0.5
            # small molecules need smaller RMSD threshold

        self.candidates = self._get_number_of_candidates()

        if p:
            self.log(
                f'--> Setup performed correctly. {self.candidates} candidates will be generated.\n'
            )
Ejemplo n.º 25
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