Exemple #1
0
def test_get_initial_state(tmpdir, starting_conformations):
    """
    Make sure we can correctly build a starting state using the torsiondrive api.
    """
    with tmpdir.as_cwd():
        mol = Ligand.from_file(get_data("ethane.sdf"))
        bond = mol.find_rotatable_bonds()[0]
        dihedral = mol.dihedrals[bond.indices][0]
        tdriver = TorsionDriver(starting_conformations=starting_conformations)
        # make the scan data
        dihedral_data = TorsionScan(torsion=dihedral, scan_range=(-165, 180))
        td_state = tdriver._create_initial_state(molecule=mol,
                                                 dihedral_data=dihedral_data,
                                                 qc_spec=QCOptions())
        assert td_state["dihedrals"] == [
            dihedral,
        ]
        assert td_state["elements"] == [
            atom.atomic_symbol for atom in mol.atoms
        ]
        assert td_state["dihedral_ranges"] == [
            (-165, 180),
        ]
        assert np.allclose((mol.coordinates * constants.ANGS_TO_BOHR),
                           td_state["init_coords"][0])
        # make sure we have tried to generate conformers
        assert len(td_state["init_coords"]) <= tdriver.starting_conformations
Exemple #2
0
def test_full_tdrive(tmpdir, workers, capsys):
    """
    Try and run a full torsiondrive for ethane with a cheap rdkit method.
    """
    with tmpdir.as_cwd():

        ethane = Ligand.from_file(get_data("ethane.sdf"))
        # make the scan data
        bond = ethane.find_rotatable_bonds()[0]
        dihedral = ethane.dihedrals[bond.indices][0]
        dihedral_data = TorsionScan(torsion=dihedral, scan_range=(-165, 180))
        qc_spec = QCOptions(program="rdkit", basis=None, method="uff")
        local_ops = LocalResource(cores=workers, memory=2)
        tdriver = TorsionDriver(
            n_workers=workers,
            grid_spacing=60,
        )
        _ = tdriver.run_torsiondrive(
            molecule=ethane,
            dihedral_data=dihedral_data,
            qc_spec=qc_spec,
            local_options=local_ops,
        )
        captured = capsys.readouterr()
        # make sure a fresh torsiondrive is run
        assert "Starting new torsiondrive" in captured.out
Exemple #3
0
def test_get_new_jobs(ethane_state):
    """
    Make sure that for a given initial state we can get the next jobs to be done.
    """
    tdriver = TorsionDriver()
    new_jobs = tdriver._get_new_jobs(td_state=ethane_state)
    assert "-60" in new_jobs
    assert new_jobs["-60"][0] == pytest.approx([
        -1.44942051524959,
        0.015117815022160003,
        -0.030235630044320005,
        1.44942051524959,
        -0.015117815022160003,
        0.030235630044320005,
        -2.2431058039129903,
        -0.18897268777700002,
        1.8708296089923,
        -2.16562700192442,
        1.78768162637042,
        -0.82203119182995,
        -2.1920831782132,
        -1.5325684978714702,
        -1.18863820611733,
        2.1920831782132,
        1.5306787709937002,
        1.18863820611733,
        2.2431058039129903,
        0.18897268777700002,
        -1.8708296089923,
        2.16562700192442,
        -1.78957135324819,
        0.82014146495218,
    ])
def ethane_state() -> Dict[str, Any]:
    """
    build an initial state for a ethane scan.
    """
    mol = Ligand.from_file(get_data("ethane.sdf"))
    bond = mol.find_rotatable_bonds()[0]
    dihedral = mol.dihedrals[bond.indices][0]
    tdriver = TorsionDriver(grid_spacing=15)
    # make the scan data
    dihedral_data = TorsionScan(torsion=dihedral, scan_range=(-165, 180))
    td_state = tdriver._create_initial_state(molecule=mol,
                                             dihedral_data=dihedral_data)
    return td_state
Exemple #5
0
def ethane_state(tmpdir) -> Dict[str, Any]:
    """
    build an initial state for a ethane scan.
    """
    with tmpdir.as_cwd():
        mol = Ligand.from_file(get_data("ethane.sdf"))
        bond = mol.find_rotatable_bonds()[0]
        dihedral = mol.dihedrals[bond.indices][0]
        tdriver = TorsionDriver(grid_spacing=15)
        # make the scan data
        dihedral_data = TorsionScan(torsion=dihedral, scan_range=(-165, 180))
        qc_spec = QCOptions(program="rdkit", basis=None, method="uff")
        td_state = tdriver._create_initial_state(molecule=mol,
                                                 dihedral_data=dihedral_data,
                                                 qc_spec=qc_spec)
        return td_state
Exemple #6
0
def test_load_old_state(tmpdir, ethane_state, qc_options, scan_range,
                        compatible):
    """
    Make sure we can load and cross-check torsiondrive state files.
    """
    with tmpdir.as_cwd():
        # dump the basic ethane result to file
        td_api.current_state_json_dump(current_state=ethane_state,
                                       jsonfilename="torsiondrive_state.json")
        td = TorsionDriver()
        state = td._load_state(
            qc_spec=qc_options,
            torsion_scan=TorsionScan(ethane_state["dihedrals"][0],
                                     scan_range=scan_range),
        )
        if compatible:
            assert state is not None
        else:
            assert state is None
Exemple #7
0
def test_optimise_grid_point_and_update(tmpdir, ethane_state):
    """
    Try and perform a single grid point optimisation.
    """
    with tmpdir.as_cwd():
        mol = Ligand.from_file(get_data("ethane.sdf"))
        tdriver = TorsionDriver(n_workers=1)
        qc_spec = QCOptions(program="rdkit", basis=None, method="uff")
        local_ops = LocalResource(cores=1, memory=1)
        geo_opt = tdriver._build_geometry_optimiser()
        # get the job inputs
        new_jobs = tdriver._get_new_jobs(td_state=ethane_state)
        coords = new_jobs["-60"][0]
        result = optimise_grid_point(
            geometry_optimiser=geo_opt,
            qc_spec=qc_spec,
            local_options=local_ops,
            molecule=mol,
            coordinates=coords,
            dihedral=ethane_state["dihedrals"][0],
            dihedral_angle=-60,
            job_id=0,
        )
        new_state = tdriver._update_state(
            td_state=ethane_state,
            result_data=[
                result,
            ],
        )
        next_jobs = tdriver._get_new_jobs(td_state=new_state)
        assert "-75" in next_jobs
        assert "-45" in next_jobs
def test_full_tdrive(tmpdir):
    """
    Try and run a full torsiondrive for ethane with a cheap rdkit method.
    """
    with tmpdir.as_cwd():

        ethane = Ligand.from_file(get_data("ethane.sdf"))
        # make the scan data
        bond = ethane.find_rotatable_bonds()[0]
        dihedral = ethane.dihedrals[bond.indices][0]
        dihedral_data = TorsionScan(torsion=dihedral, scan_range=(-165, 180))
        tdriver = TorsionDriver(
            program="rdkit",
            method="uff",
            basis=None,
            memory=2,
            cores=1,
            n_workers=1,
            grid_spacing=60,
        )
        _ = tdriver.run_torsiondrive(molecule=ethane,
                                     dihedral_data=dihedral_data)
Exemple #9
0
def test_initial_state_coords_passed(tmpdir):
    """
    Make sure any seed conformations are used in the initial state
    """
    with tmpdir.as_cwd():
        mol = Ligand.from_file(get_data("ethane.sdf"))
        bond = mol.find_rotatable_bonds()[0]
        dihedral = mol.dihedrals[bond.indices][0]
        tdriver = TorsionDriver()
        # make the scan data
        dihedral_data = TorsionScan(torsion=dihedral, scan_range=(-165, 180))
        # make some mock coords
        coords = [np.random.random(size=(mol.n_atoms, 3)) for _ in range(4)]
        td_state = tdriver._create_initial_state(
            molecule=mol,
            dihedral_data=dihedral_data,
            qc_spec=QCOptions(),
            seed_coordinates=coords,
        )
        assert len(td_state["init_coords"]) == 4
        # make sure they are the same random coords
        for i in range(4):
            assert np.allclose((coords[i] * constants.ANGS_TO_BOHR),
                               td_state["init_coords"][i])
Exemple #10
0
def test_double_dihedral(tmpdir):
    """Test running a molecule with two rotatable bonds."""
    with tmpdir.as_cwd():
        mol = Ligand.from_smiles("CCO", "ethanol")
        # build a scanner with grid spacing 60 and clear out avoided methyl
        tdrive = TorsionDriver(
            program="rdkit",
            method="uff",
            basis=None,
            cores=1,
            memory=1,
            n_workers=1,
            grid_spacing=60,
        )
        t_scan = TorsionScan1D(torsion_driver=tdrive)
        t_scan.clear_avoided_torsions()
        result_mol = t_scan.run(molecule=mol)
        assert len(result_mol.qm_scans) == 2
        # make sure input molecule coords were not changed
        assert np.allclose(mol.coordinates, result_mol.coordinates)
Exemple #11
0
def test_tdrive_restarts(capsys, ethane_state, tmpdir):
    """
    Make sure that an old torsiondrive is continued when possible from the current state file.
    """
    with tmpdir.as_cwd():
        ethane_state["grid_spacing"] = [
            60,
        ]
        mol = Ligand.from_file(get_data("ethane.sdf"))
        tdriver = TorsionDriver(n_workers=1, grid_spacing=60)
        qc_spec = QCOptions(program="rdkit", basis=None, method="uff")
        local_ops = LocalResource(cores=1, memory=1)
        geo_opt = tdriver._build_geometry_optimiser()
        # get the job inputs
        new_jobs = tdriver._get_new_jobs(td_state=ethane_state)
        coords = new_jobs["-60"][0]
        result = optimise_grid_point(
            geometry_optimiser=geo_opt,
            qc_spec=qc_spec,
            local_options=local_ops,
            molecule=mol,
            coordinates=coords,
            dihedral=ethane_state["dihedrals"][0],
            dihedral_angle=-60,
            job_id=0,
        )
        _ = tdriver._update_state(
            td_state=ethane_state,
            result_data=[
                result,
            ],
        )
        # now start a run and make sure it continues
        _ = tdriver.run_torsiondrive(
            molecule=mol,
            dihedral_data=TorsionScan(torsion=ethane_state["dihedrals"][0],
                                      scan_range=(-165, 180)),
            qc_spec=qc_spec,
            local_options=local_ops,
        )
        capture = capsys.readouterr()
        assert ("Compatible TorsionDrive state found restarting torsiondrive!"
                in capture.out)
Exemple #12
0
class TorsionScan1D(StageBase):
    """
    A 1D torsion scanner.

    Note:
        By default this will scan all rotatable bonds not involving methyl or amine terminal groups.
    """

    special_torsions: List[TargetTorsion] = Field(
        [],
        description=
        "A list of special target torsions to be scanned and their scan range.",
    )
    default_scan_range: Tuple[int, int] = Field(
        (-165, 180),
        description=
        "The default scan range for any torsions not covered by a special rule.",
    )
    avoided_torsions: List[AvoidedTorsion] = Field(
        [
            AvoidedTorsion(smirks="[*:1]-[CH3:2]"),
            AvoidedTorsion(smirks="[*:1]-[NH2:2]"),
        ],
        description="The list of torsion patterns that should be avoided.",
    )
    torsion_driver: TorsionDriver = Field(
        TorsionDriver(),
        description=
        "The torsion drive engine used to compute the reference data.",
    )
    type: Literal["TorsionScan1D"] = "TorsionScan1D"

    def start_message(self, **kwargs) -> str:
        return f"Performing QM-constrained optimisation with Torsiondrive and {kwargs['qc_spec'].program}"

    def finish_message(self, **kwargs) -> str:
        return "Torsiondrive finished and QM results saved."

    @classmethod
    def is_available(cls) -> bool:
        """
        Make sure geometric and torsiondrive are installed.
        """
        geo = which_import(
            "geometric",
            return_bool=True,
            raise_error=True,
            raise_msg=
            "Please install via `conda install geometric -c conda-forge`.",
        )
        tdrive = which_import(
            "torsiondrive",
            return_bool=True,
            raise_error=True,
            raise_msg=
            "Please install via `conda install torsiondrive -c conda-forge`.",
        )
        engine = which_import(
            "qcengine",
            return_bool=True,
            raise_error=True,
            raise_msg=
            "Please install via `conda install qcengine -c conda-forge`.",
        )
        return geo and tdrive and engine

    def run(self, molecule: "Ligand", **kwargs) -> "Ligand":
        """
        Run any possible torsiondrives for this molecule given the list of allowed and disallowed torsion patterns.

        Note:
            This function just validates the molecule and builds a list of torsions to scan before passing to the main method.
        Important:
            We work with a copy of the input molecule as we change the coordinates throughout the calculation.
        """

        molecule.qm_scans = []

        # work with a copy as we change coordinates from the qm a lot!
        drive_mol = copy.deepcopy(molecule)
        # first find all rotatable bonds, while removing the unwanted scans
        bonds = drive_mol.find_rotatable_bonds(smirks_to_remove=[
            torsion.smirks for torsion in self.avoided_torsions
        ])
        if bonds is None:
            print("No rotatable bonds found to scan!")
            return molecule

        # remove symmetry duplicates
        bonds = self._get_symmetry_unique_bonds(molecule=drive_mol,
                                                bonds=bonds)

        torsion_scans = []
        for bond in bonds:
            # get the scan range and a torsion for the bond
            torsion = find_heavy_torsion(molecule=drive_mol, bond=bond)
            scan_range = self._get_scan_range(molecule=drive_mol, bond=bond)
            torsion_scans.append(
                TorsionScan(torsion=torsion, scan_range=scan_range))

        result_mol = self._run_torsion_drives(
            molecule=drive_mol,
            torsion_scans=torsion_scans,
            qc_spec=kwargs["qc_spec"],
            local_options=kwargs["local_options"],
        )
        # make sure we preserve the input coords
        result_mol.coordinates = copy.deepcopy(molecule.coordinates)
        # make sure we have all of the scans we expect
        assert len(result_mol.qm_scans) == len(bonds)
        return result_mol

    def _get_symmetry_unique_bonds(self, molecule: "Ligand",
                                   bonds: List["Bond"]) -> List["Bond"]:
        """
        For a list of central torsion bonds deduplicate the list by bond symmetry types.
        """
        atom_types = molecule.atom_types
        unique_bonds = {}
        for bond in bonds:
            bond_type = f"{atom_types[bond.atom1_index]}-{atom_types[bond.atom2_index]}"
            if bond_type not in unique_bonds and bond_type[::
                                                           -1] not in unique_bonds:
                unique_bonds[bond_type] = bond
        return list(unique_bonds.values())

    def _get_scan_range(self, molecule: "Ligand",
                        bond: "Bond") -> Tuple[int, int]:
        """
        Get the scan range for the target bond based on the allowed substructure list.

        Note:
            We loop over the list of targets checking each meaning that the last match in the list will be applied to substructure.
            So generic matches should be placed at the start of the list with more specific ones at the end.
        """
        scan_range = self.default_scan_range
        for target_torsion in self.special_torsions:
            matches = molecule.get_smarts_matches(smirks=target_torsion.smirks)
            for match in matches:
                if len(match) == 4:
                    atoms = match[1:3]
                else:
                    atoms = match

                if set(atoms) == set(bond.indices):
                    scan_range = target_torsion.scan_range

        return scan_range

    def _run_torsion_drives(
        self,
        molecule: "Ligand",
        torsion_scans: List[TorsionScan],
        qc_spec: QCOptions,
        local_options: LocalResource,
    ) -> "Ligand":
        """
        Run the list of validated torsion drives.

        Note:
            We do not change the initial coordinates passed at this point.

        Args:
            molecule: The molecule to be scanned.
            torsion_scans: A list of TorsionScan jobs to perform detailing the dihedral and the scan range.

        Returns:
            The updated molecule object with the scan results.
        """
        for torsion_scan in torsion_scans:
            # make a folder and move into to run the calculation
            folder = "SCAN_"
            folder += "_".join([str(t) for t in torsion_scan.torsion])
            with folder_setup(folder):
                print(
                    f"Running scan for dihedral: {torsion_scan.torsion} with range: {torsion_scan.scan_range}"
                )
                result_mol = self.torsion_driver.run_torsiondrive(
                    molecule=molecule,
                    dihedral_data=torsion_scan,
                    qc_spec=qc_spec,
                    local_options=local_options,
                )

        return result_mol

    def add_special_torsion(
        self, smirks: str, scan_range: Tuple[int, int] = (-165, 180)) -> None:
        """
        Add a new allowed torsion scan.

        Args:
            smirks: The smirks pattern that should be used to identify the torsion.
            scan_range: The scan range for this type of dihedral.
        """
        target = TargetTorsion(smirks=smirks, scan_range=scan_range)
        self.special_torsions.append(target)

    def clear_special_torsions(self) -> None:
        """Remove all allowed torsion scans."""
        self.special_torsions = []

    def add_avoided_torsion(self, smirks: str) -> None:
        """
        Add a new torsion pattern to avoid scanning.

        Args:
            smirks: The valid smirks pattern which describes a torsion not to be scanned.
        """
        torsion = AvoidedTorsion(smirks=smirks)
        self.avoided_torsions.append(torsion)

    def clear_avoided_torsions(self) -> None:
        """Remove all avoided torsions."""
        self.avoided_torsions = []