Пример #1
0
    def launch_qc(self):
        # Get the next task
        entry: _PriorityEntry = self.task_queue.get()
        sim_info = entry.item

        # Determine which level we are running
        method = sim_info['method']
        inchi = sim_info['inchi']
        self.logger.info(f'Running a {method} computation using {inchi}')
        self.already_ran[method].add(inchi)
        if self.single_fidelity:
            # Baseline mode: Run both in one shot
            self.search_space.remove(inchi)
            self.queues.send_inputs(inchi, task_info=sim_info,
                                    method='compute_adiabatic_one_shot', keep_inputs=True,
                                    topic='simulate')
        elif method == 'low_fidelity':
            self.search_space.remove(inchi)  # We've started to gather data for it
            self.queues.send_inputs(inchi, task_info=sim_info,
                                    method='compute_vertical', keep_inputs=True,
                                    topic='simulate')
        elif method == 'high_fidelity':
            xyz = sim_info['xyz']
            init_charge = get_baseline_charge(inchi)
            self.queues.send_inputs(xyz, init_charge, task_info=sim_info,
                                    method='compute_adiabatic', keep_inputs=True,
                                    topic='simulate')
        else:
            raise ValueError(f'Method "{method}" not recognized')
Пример #2
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]
Пример #3
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]
Пример #4
0
    def from_charge(cls, charge: int, mol_string: str) -> 'OxidationState':
        """Get the oxidation from the charge

        Args:
            charge: Charge state
            mol_string: SMILES or InChI string of the molecule.
                Oxidation states are relative to the formal charge of this molecule
        Returns:
            Name of the charge state
        """

        net_charge = charge - get_baseline_charge(mol_string)
        if net_charge == 0:
            return OxidationState.NEUTRAL
        elif net_charge == 1:
            return OxidationState.OXIDIZED
        elif net_charge == -1:
            return OxidationState.REDUCED
        else:
            raise ValueError(
                f'Unrecognized charge state, {charge}, for {mol_string}')
Пример #5
0
def compute_adiabatic_one_shot(smiles: str, solvent: Optional[str] = None, dilation_factor: float = 1) \
        -> Tuple[List[OptimizationResult], List[AtomicResult]]:
    """Compute the adiabatic energy in one shot

    Args:
        smiles: SMILES string of molecule to evaluate
        solvent: Name of solvent to use when computing IP
        dilation_factor: A factor by which to expand the runtime of the simulation
            Used to emulate longer simulations without spending CPU cycles
    """

    # Run the vertical
    vert_relax, vert_spe = _run_with_delay(_compute_vertical,
                                           (smiles, solvent), dilation_factor)

    # Run the adiabatic
    init_charge = get_baseline_charge(smiles)
    xyz = vert_relax[0].final_molecule.to_string('xyz')
    amb_relax, amb_spe = _run_with_delay(_compute_adiabatic,
                                         (xyz, init_charge, solvent),
                                         dilation_factor)
    return vert_relax + amb_relax, vert_spe + amb_spe
Пример #6
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]
Пример #7
0
def test_charge():
    assert get_baseline_charge('O') == 0
    assert get_baseline_charge('[NH4+]') == 1
    assert get_baseline_charge('Fc1c(F)c1=[F+]') == 1
Пример #8
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
Пример #9
0
    def launch_qc(self):
        # Get the next task
        entry: _PriorityEntry = self.task_queue.get()
        sim_info = entry.item

        # Determine which level we are running
        method = sim_info['method']
        inchi = sim_info['inchi']

        # Get some basic information on the molecule
        record = self.database.get_molecule_record(inchi=inchi)
        neutral_charge = get_baseline_charge(inchi)
        redox_charge = neutral_charge + (1 if self.oxidize else -1)

        self.logger.info(f'Running {method} computation(s) for {inchi}')
        self.already_ran[method].add(inchi)
        if method == 'compute_vertical':
            self.yet_unevaluated.remove(inchi)  # We've started to gather data for it

            # Check if we need to run the neutral relaxation
            if self.target_recipe.geometry_level in record.data and \
                    OxidationState.NEUTRAL in record.data[self.target_recipe.geometry_level]:
                # If already have the neutral, just submit the vertical
                neutral_xyz = record.data[self.target_recipe.geometry_level][OxidationState.NEUTRAL].xyz
                self.queues.send_inputs(neutral_xyz, redox_charge, None, self.target_recipe.geometry_level,
                                        method='compute_single_point', task_info=sim_info,
                                        topic='simulate', keep_inputs=True)
            else:
                self.queues.send_inputs(inchi, self.oxidize, task_info=sim_info,
                                        method='compute_vertical', keep_inputs=True,
                                        topic='simulate')
        elif method == 'compute_adiabatic':
            xyz = sim_info['xyz']
            self.queues.send_inputs(xyz, neutral_charge, self.oxidize, task_info=sim_info,
                                    method='compute_adiabatic', keep_inputs=True,
                                    topic='simulate')
        elif method == 'compute_single_point':
            # Determine which computations we need to run, defined by the geometry and the charge
            to_run: List[Tuple[str, int, Optional[str]]] = []
            geom_level_data = record.data[self.target_recipe.geometry_level]
            for (ox_state, charge) in zip((OxidationState.NEUTRAL, self.oxidation_state),
                                       (neutral_charge, redox_charge)):
                # See if we've done the computation in vacuum
                if self.target_recipe.energy_level not in geom_level_data[ox_state].total_energy[ox_state]:
                    to_run.append((geom_level_data[ox_state].xyz, charge, None))

                # Check if we need the computation in solvent
                if self.solvent is not None and \
                        (ox_state not in geom_level_data[ox_state].total_energy_in_solvent or
                         self.solvent not in geom_level_data[ox_state].total_energy_in_solvent[ox_state] or 
                         self.target_recipe.energy_level not in geom_level_data[ox_state].total_energy_in_solvent[ox_state][self.solvent]):
                    to_run.append((geom_level_data[ox_state].xyz, charge, self.solvent))
            self.logger.info(f'Submitting {len(to_run)} single point computations to finish high-fidelity')

            # Send the single points one after each other
            first = True
            for run_args in to_run:
                # We need to request more resources after the first submission
                if first:
                    first = False
                else:
                    self.rec.acquire('simulation', self.nodes_per_qc)
                self.queues.send_inputs(*run_args, self.target_recipe.energy_level, task_info=sim_info,
                                        method='compute_single_point', keep_inputs=True,
                                        topic='simulate')
        else:
            raise ValueError(f'Method "{method}" not recognized')