def test_generate():
    # Test with a simple molecule
    inchi, xyz = generate_inchi_and_xyz('C')
    assert xyz.startswith('5')
    assert inchi == 'InChI=1S/CH4/h1H4'

    # Test with a molecule that has defined stereochemistry
    inchi, xyz = generate_inchi_and_xyz("C/C=C\\C")
    assert inchi == "InChI=1S/C4H8/c1-3-4-2/h3-4H,1-2H3/b4-3-"

    # Change the stereo chemistry
    inchi_man_iso = Chem.MolToInchi(Chem.MolFromSmiles("C/C=C/C"))
    inchi_iso, xyz_iso = generate_inchi_and_xyz("C/C=C/C")
    assert inchi_man_iso == inchi_iso
    assert inchi != inchi_iso

    # Test with a molecule w/o defined sterochemistry
    inchi_undef, xyz = generate_inchi_and_xyz("CC=CC")
    assert inchi_undef == inchi_man_iso  # Make sure it gets the lower-energy isomer
def test_hash():
    inchi, xyz = generate_inchi_and_xyz('O')
    mol = Molecule.from_data(xyz, 'xyz')
    assert mol.get_hash() != mol.orient_molecule().get_hash()
    assert get_hash(mol) == get_hash(mol.orient_molecule())

    ox_mol = Molecule.from_data(xyz, 'xyz', molecular_charge=1)
    assert ox_mol.molecular_multiplicity != mol.molecular_multiplicity
    assert mol.get_hash() != ox_mol.get_hash()
    assert get_hash(mol) == get_hash(ox_mol.orient_molecule())
def test_subtract_reference_energies():
    # Make a RDKit and QCEngine representation of a molecule
    _, xyz = generate_inchi_and_xyz('C')
    molr = Chem.MolFromSmiles('C')
    molr = Chem.AddHs(molr)
    molq = Molecule.from_data(xyz, 'xyz')

    # Get the desired answer
    my_ref = lookup_reference_energies('small_basis')
    actual = my_ref['C'] + 4 * my_ref['H']

    # Check it works with either RDKit or QCElemental inputs
    assert subtract_reference_energies(0, molr, my_ref) == -actual
    assert subtract_reference_energies(0, molq, my_ref) == -actual
Exemple #4
0
def run_simulation(smiles: str, n_nodes: int, spec_name: str = 'small_basis', solvent: Optional[str] = None)\
        -> Tuple[List[OptimizationResult], List[AtomicResult]]:
    """Run the ionization potential computation

    Args:
        smiles: SMILES string to evaluate
        n_nodes: Number of nodes to use
        spec_name: Name of the quantum chemistry specification
        solvent: Name of the solvent to use
    Returns:
        Relax records for the neutral and ionized geometry
    """
    from moldesign.simulate.functions import generate_inchi_and_xyz, relax_structure, run_single_point
    from moldesign.simulate.specs import get_qcinput_specification
    from moldesign.utils.chemistry import get_baseline_charge

    # Make the initial geometry
    inchi, xyz = generate_inchi_and_xyz(smiles)
    neu_charge = get_baseline_charge(smiles)
    chg_charge = neu_charge + 1

    # Make the compute spec
    compute_config = {'nnodes': n_nodes, 'cores_per_rank': 2, 'ncores': 64}

    # Get the specification and make it more resilient
    spec, code = get_qcinput_specification(spec_name)
    if code == "nwchem":
        spec.keywords["dft__iterations"] = 150
        spec.keywords["geometry__noautoz"] = True

    # Compute the neutral geometry and hessian
    neutral_xyz, _, neutral_relax = relax_structure(xyz, spec, compute_config=compute_config, charge=neu_charge, code=code)

    # Compute the relaxed geometry
    oxidized_xyz, _, oxidized_relax = relax_structure(neutral_xyz, spec, compute_config=compute_config, charge=chg_charge, code=code)

    # If desired, compute the solvent energies
    if solvent is None:
        return [neutral_relax, oxidized_relax], []

    spec, code = get_qcinput_specification(spec_name, solvent=solvent)
    if code == "nwchem":
        spec.keywords["dft__iterations"] = 150
        spec.keywords["geometry__noautoz"] = True

    neutral_solvent = run_single_point(neutral_xyz, 'energy', spec, compute_config=compute_config, charge=neu_charge, code=code)
    charged_solvent = run_single_point(oxidized_xyz, 'energy', spec, compute_config=compute_config, charge=chg_charge, code=code)

    return [neutral_relax, oxidized_relax], [neutral_solvent, charged_solvent]
Exemple #5
0
def _run_simulation(smiles: str, solvent: Optional[str], spec_name: str = 'xtb')\
        -> Tuple[List[OptimizationResult], List[AtomicResult]]:
    """Run the ionization potential computation

    Args:
        smiles: SMILES string to evaluate
        solvent: Name of the solvent
        spec: Quantum chemistry specification for the molecule
    Returns:
        Relax records for the neutral and ionized geometry
    """
    from moldesign.simulate.functions import generate_inchi_and_xyz, relax_structure, run_single_point
    from moldesign.simulate.specs import get_qcinput_specification
    from moldesign.utils.chemistry import get_baseline_charge
    from qcelemental.models import DriverEnum

    # Make the initial geometry
    inchi, xyz = generate_inchi_and_xyz(smiles)
    init_charge = get_baseline_charge(smiles)

    # Get the specification and make it more resilient
    spec, code = get_qcinput_specification(spec_name)

    # Compute the geometries
    neutral_xyz, _, neutral_relax = relax_structure(xyz,
                                                    spec,
                                                    charge=init_charge,
                                                    code=code)
    oxidized_xyz, _, oxidized_relax = relax_structure(neutral_xyz,
                                                      spec,
                                                      charge=init_charge + 1,
                                                      code=code)

    # Perform the solvation energy computations, if desired
    if solvent is None:
        return [neutral_relax, oxidized_relax], []

    solv_spec, code = get_qcinput_specification(spec_name, solvent=solvent)
    neutral_solv = run_single_point(neutral_xyz,
                                    DriverEnum.energy,
                                    solv_spec,
                                    charge=init_charge,
                                    code=code)
    oxidized_solv = run_single_point(oxidized_xyz,
                                     DriverEnum.energy,
                                     solv_spec,
                                     charge=init_charge + 1,
                                     code=code)
    return [neutral_relax, oxidized_relax], [neutral_solv, oxidized_solv]
Exemple #6
0
def run_simulation(
        smiles: str,
        n_nodes: int) -> Tuple[List[OptimizationResult], List[AtomicResult]]:
    """Run the ionization potential computation

    Args:
        smiles: SMILES string to evaluate
        n_nodes: Number of nodes to use
    Returns:
        Relax records for the neutral and ionized geometry
    """
    from moldesign.simulate.functions import generate_inchi_and_xyz, relax_structure, run_single_point
    from moldesign.simulate.specs import get_qcinput_specification
    from qcelemental.models import DriverEnum

    # Make the initial geometry
    inchi, xyz = generate_inchi_and_xyz(smiles)

    # Make the compute spec
    compute_config = {'nnodes': n_nodes, 'cores_per_rank': 2}

    # Get the specification and make it more resilient
    spec, code = get_qcinput_specification('small_basis')
    if code == "nwchem":
        spec.keywords["dft__iterations"] = 150
        spec.keywords["geometry__noautoz"] = True

    # Compute the neutral geometry and hessian
    neutral_xyz, _, neutral_relax = relax_structure(
        xyz, spec, compute_config=compute_config, charge=0, code=code)
    neutral_hessian = run_single_point(neutral_xyz,
                                       DriverEnum.hessian,
                                       spec,
                                       charge=0,
                                       compute_config=compute_config,
                                       code=code)

    # Compute the relaxed geometry
    oxidized_xyz, _, oxidized_relax = relax_structure(
        neutral_xyz, spec, compute_config=compute_config, charge=1, code=code)
    oxidized_hessian = run_single_point(oxidized_xyz,
                                        DriverEnum.hessian,
                                        spec,
                                        charge=1,
                                        compute_config=compute_config,
                                        code=code)
    return [neutral_relax, oxidized_relax], [neutral_hessian, oxidized_hessian]
Exemple #7
0
def test_cyclopropyl():
    smiles = 'Oc1c[cH+]1'
    inchi, xyz = generate_inchi_and_xyz(smiles, special_cases=True)

    # Make sure the initial ring is indeed buckled
    atoms = next(simple_read_xyz(StringIO(xyz), slice(None)))

    def _is_coplanar(x: np.ndarray):
        y = x[:3, :] - x[-1, :]
        return np.linalg.det(y) < 1e-4

    assert not _is_coplanar(atoms.positions[:4, :])

    # Attempt to flatten it back out
    xyz = fix_cyclopropenyl(xyz, 'Oc1c[cH+]1')
    atoms = next(simple_read_xyz(StringIO(xyz), slice(None)))
    with open('test.xyz', 'w') as fp:
        fp.write(xyz)
    assert _is_coplanar(atoms.positions[:4, :])
Exemple #8
0
def run_simulation(smiles: str, n_nodes: int,
                   mode: str) -> Union[OptimizationResult, AtomicResult]:
    """Run a single-point or relaxation computation computation

    Args:
        smiles: SMILES string to evaluate
        n_nodes: Number of nodes to use
        mode: What computation to perform: single, gradient, hessian, relax
    Returns:
        Result of the energy computation
    """
    from moldesign.simulate.functions import generate_inchi_and_xyz, relax_structure, run_single_point
    from moldesign.simulate.specs import get_qcinput_specification
    from qcelemental.models import DriverEnum

    # Make the initial geometry
    inchi, xyz = generate_inchi_and_xyz(smiles)

    # Make the compute spec
    compute_config = {'nnodes': n_nodes, 'cores_per_rank': 2}

    # Get the specification and make it more resilient
    spec, code = get_qcinput_specification('small_basis')
    if code == "nwchem":
        spec.keywords["dft__iterations"] = 150
        spec.keywords["geometry__noautoz"] = True

    # Compute the neutral geometry and hessian
    if mode == 'relax':
        _, _, neutral_relax = relax_structure(xyz,
                                              spec,
                                              compute_config=compute_config,
                                              charge=0,
                                              code=code)
        return neutral_relax
    else:
        return run_single_point(xyz,
                                mode,
                                spec,
                                charge=0,
                                compute_config=compute_config,
                                code=code)
Exemple #9
0
def molecules() -> List[str]:
    return [generate_inchi_and_xyz(x)[1] for x in ['C', 'CC', 'O']]
Exemple #10
0
def compute_vertical(smiles: str, oxidize: bool, solvent: Optional[str] = None,
                     spec_name: str = 'small_basis', n_nodes: int = 2) \
        -> Tuple[List[OptimizationResult], List[AtomicResult]]:
    """Perform the initial ionization potential computation of the vertical

    First relaxes the structure and then runs a single-point energy at the

    Args:
        smiles: SMILES string to evaluate
        oxidize: Whether to perform an oxidation or reduction
        solvent: Name of the solvent, if desired. Runs on the neutral geometry after relaxation
        spec_name: Quantum chemistry specification for the molecule
        n_nodes: Number of nodes per computation
    Returns:
        - Relax records for the neutral
        - Single point energy in oxidized or reduced state
    """

    # Make the initial geometry
    inchi, xyz = generate_inchi_and_xyz(smiles)
    init_charge = get_baseline_charge(smiles)

    # Make the compute spec
    compute_config = {'nnodes': n_nodes, 'cores_per_rank': 2}

    # Get the specification and make it more resilient
    spec, code = get_qcinput_specification(spec_name)
    if code == "nwchem":
        spec.keywords['dft__convergence__energy'] = 1e-7
        spec.keywords['dft__convergence__fast'] = True
        spec.keywords["dft__iterations"] = 150
        spec.keywords["driver__maxiter"] = 150
        spec.keywords["geometry__noautoz"] = True

        # Make a repeatably-named scratch directory.
        #  We cannot base it off a hash of the input file,
        #  because the XYZ file generator is stochastic.
        runhash = hashlib.sha256(
            f'{smiles}_{oxidize}_{spec_name}'.encode()).hexdigest()[:12]
        spec.extras["scratch_name"] = f'nwc_{runhash}'
        spec.extras["allow_restarts"] = True

    # Compute the geometries
    neutral_xyz, _, neutral_relax = relax_structure(
        xyz,
        spec,
        charge=init_charge,
        code=code,
        compute_config=compute_config)

    # Perform the single-point energy for the ionized geometry
    new_charge = init_charge + 1 if oxidize else init_charge - 1
    oxid_spe = run_single_point(neutral_xyz,
                                DriverEnum.energy,
                                spec,
                                charge=new_charge,
                                code=code,
                                compute_config=compute_config)

    # If desired, submit a solvent computation as well
    if solvent is None:
        return [neutral_relax], [oxid_spe]

    spec, code = get_qcinput_specification(spec_name, solvent)
    if code == "nwchem":
        # Reduce the accuracy needed to 1e-7
        spec.keywords['dft__convergence__energy'] = 1e-7
        spec.keywords['dft__convergence__fast'] = True
        spec.keywords["dft__iterations"] = 150
        spec.keywords["geometry__noautoz"] = True

        # Make sure to allow restarting
        spec.extras["allow_restarts"] = True
    solv_spe = run_single_point(neutral_xyz,
                                DriverEnum.energy,
                                spec,
                                charge=init_charge,
                                code=code,
                                compute_config=compute_config)
    return [neutral_relax], [oxid_spe, solv_spe]
Exemple #11
0
    def get_required_calculations(self, record: MoleculeData,
                                  oxidation_state: OxidationState,
                                  previous_level: Optional['RedoxEnergyRecipe'] = None) \
            -> List[Tuple[AccuracyLevel, str, int, Optional[str], bool]]:
        """List the required computations to complete this recipe given the current information about a molecule

        If this method returns relaxation calculations, then there may be more yet to perform after those complete
        before one can evaluate the redox potential at the desired level of accuracy.
        Calculations that use those input geometries may be needed

        Args:
            record: Information available about the molecule
            oxidation_state: Oxidation state for the redox computation
            previous_level: Previous level of accuracy, used to determine a starting point for relaxations
        Returns:
            List of required computations as tuples of
                (level of accuracy,
                 input XYZ structure,
                 charge state,
                 solvent,
                 whether to relax)
            All computations can be performed in parallel
        """

        # Required computations
        required = []

        # Get the neutral and oxidized charges
        neutral_charge = get_baseline_charge(record.identifier['inchi'])
        charged_charge = neutral_charge + (-1 if oxidation_state
                                           == OxidationState.REDUCED else 1)

        # Determine the starting point for relaxations, if required
        #  We'll use geometries from the previous level of fidelity with priority over those at the current level
        #   and procedurally-generated ones last
        if previous_level is not None and previous_level.geometry_level in record.data:
            neutral_start = record.data[previous_level.geometry_level][
                OxidationState.NEUTRAL].xyz
            if oxidation_state in record.data[previous_level.geometry_level]:
                charged_start = record.data[
                    previous_level.geometry_level][oxidation_state].xyz
            else:
                charged_start = neutral_start
        elif self.geometry_level not in record.data:
            neutral_start = charged_start = generate_inchi_and_xyz(
                record.identifier['inchi'])[1]
        else:
            neutral_start = record.data[self.geometry_level][
                OxidationState.NEUTRAL].xyz
            if oxidation_state in record.data[self.geometry_level]:
                charged_start = record.data[
                    self.geometry_level][oxidation_state].xyz
            else:
                charged_start = neutral_start

        # Determine if any relaxations are needed
        geom_level = self.geometry_level
        if geom_level not in record.data or OxidationState.NEUTRAL not in record.data[
                geom_level]:
            required.append(
                (geom_level, neutral_start, neutral_charge, None, True))
        if self.adiabatic and (geom_level not in record.data or oxidation_state
                               not in record.data[geom_level]):
            required.append(
                (geom_level, charged_start, charged_charge, None, True))

        # If any relaxations are triggered, then return the list now
        if len(required) > 0:
            return required

        # Determine if any single-point calculations are required
        neutral_geom_data = record.data[geom_level][OxidationState.NEUTRAL]
        if self.adiabatic:
            charged_geom_data = record.data[geom_level][oxidation_state]
        else:
            charged_geom_data = neutral_geom_data

        for state, data, chg in zip([OxidationState.NEUTRAL, oxidation_state],
                                    [neutral_geom_data, charged_geom_data],
                                    [neutral_charge, charged_charge]):
            if self.energy_level not in data.total_energy.get(state, {}):
                required.append(
                    (self.energy_level, data.xyz, chg, None, False))
            if self.solvent is not None and (
                    self.solvent not in data.total_energy_in_solvent.get(
                        state, {}) or self.solvation_level
                    not in data.total_energy_in_solvent[state][self.solvent]):
                required.append(
                    (self.energy_level, data.xyz, chg, self.solvent, False))

        return required
Exemple #12
0
def training_set() -> Tuple[List[str], List[float]]:
    xyzs = [generate_inchi_and_xyz(x)[1] for x in ['C', 'CC']]
    return xyzs, [1., 2.]
"""Generate the data used by the tests"""

from moldesign.simulate.functions import generate_inchi_and_xyz, relax_structure, run_single_point
from moldesign.simulate.specs import get_qcinput_specification

if __name__ == "__main__":
    # Make a water molecule
    inchi, xyz = generate_inchi_and_xyz('O')

    # Generate the neutral geometry with XTB
    xtb_spec, xtb = get_qcinput_specification("xtb")
    xtb_neutral_xyz, _, record = relax_structure(xyz, xtb_spec, code=xtb)
    with open('records/xtb-neutral.json', 'w') as fp:
        print(record.json(), file=fp)

    # Compute the vertical oxidation potential
    record = run_single_point(xtb_neutral_xyz,
                              "energy",
                              xtb_spec,
                              code=xtb,
                              charge=1)
    with open('records/xtb-neutral_xtb-oxidized-energy.json', 'w') as fp:
        print(record.json(), file=fp)

    # Compute the adiabatic oxidation potential
    xtb_oxidized_xyz, _, record = relax_structure(xtb_neutral_xyz,
                                                  xtb_spec,
                                                  code=xtb,
                                                  charge=1)
    with open('records/xtb-oxidized.json', 'w') as fp:
        print(record.json(), file=fp)