From 8adb2a309b6187891039dfd7b05ba34f04c0abc6 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Wed, 30 Sep 2020 20:08:41 -0400 Subject: [PATCH] Refactor Chemistry Drivers --- qiskit/chemistry/__init__.py | 3 + .../molecular_ground_state_energy.py | 8 +- qiskit/chemistry/drivers/__init__.py | 12 +- qiskit/chemistry/drivers/base_driver.py | 23 ++ .../drivers/fcidumpd/fcidumpdriver.py | 7 +- .../drivers/gaussiand/gaussian_log_driver.py | 5 +- .../drivers/gaussiand/gaussiandriver.py | 36 +- qiskit/chemistry/drivers/hdf5d/hdf5driver.py | 13 +- .../{_basedriver.py => integrals_driver.py} | 17 +- qiskit/chemistry/drivers/psi4d/psi4driver.py | 37 +- .../drivers/pyquanted/pyquantedriver.py | 64 ++- .../chemistry/drivers/pyscfd/pyscfdriver.py | 69 +++- qiskit/chemistry/molecule.py | 386 ++++++++++++++++++ test/chemistry/test_driver_gaussian.py | 21 +- test/chemistry/test_driver_psi4.py | 21 +- test/chemistry/test_driver_pyquante.py | 21 +- test/chemistry/test_driver_pyscf.py | 21 +- 17 files changed, 673 insertions(+), 91 deletions(-) create mode 100644 qiskit/chemistry/drivers/base_driver.py rename qiskit/chemistry/drivers/{_basedriver.py => integrals_driver.py} (75%) create mode 100644 qiskit/chemistry/molecule.py diff --git a/qiskit/chemistry/__init__.py b/qiskit/chemistry/__init__.py index 9b62687e0f..38af32855d 100644 --- a/qiskit/chemistry/__init__.py +++ b/qiskit/chemistry/__init__.py @@ -137,6 +137,7 @@ BosonicOperator FermionicOperator + Molecule QMolecule MP2Info @@ -155,6 +156,7 @@ """ from .qiskit_chemistry_error import QiskitChemistryError +from .molecule import Molecule from .qmolecule import QMolecule from .bosonic_operator import BosonicOperator from .fermionic_operator import FermionicOperator @@ -163,6 +165,7 @@ set_qiskit_chemistry_logging) __all__ = ['QiskitChemistryError', + 'Molecule', 'QMolecule', 'BosonicOperator', 'FermionicOperator', diff --git a/qiskit/chemistry/applications/molecular_ground_state_energy.py b/qiskit/chemistry/applications/molecular_ground_state_energy.py index 15a55c23b3..bda92b0295 100644 --- a/qiskit/chemistry/applications/molecular_ground_state_energy.py +++ b/qiskit/chemistry/applications/molecular_ground_state_energy.py @@ -23,14 +23,14 @@ from qiskit.chemistry.components.variational_forms import UCCSD from qiskit.chemistry.core import (Hamiltonian, TransformationType, QubitMappingType, ChemistryOperator, MolecularGroundStateResult) -from qiskit.chemistry.drivers import BaseDriver +from qiskit.chemistry.drivers import IntegralsDriver class MolecularGroundStateEnergy: """ Molecular ground state energy chemistry application """ def __init__(self, - driver: BaseDriver, + driver: IntegralsDriver, solver: Optional[MinimumEigensolver] = None, transformation: TransformationType = TransformationType.FULL, qubit_mapping: QubitMappingType = QubitMappingType.PARITY, @@ -68,12 +68,12 @@ def __init__(self, self._z2symmetry_reduction = z2symmetry_reduction @property - def driver(self) -> BaseDriver: + def driver(self) -> IntegralsDriver: """ Returns chemistry driver """ return self._driver @driver.setter - def driver(self, driver: BaseDriver) -> None: + def driver(self, driver: IntegralsDriver) -> None: self._driver = driver @property diff --git a/qiskit/chemistry/drivers/__init__.py b/qiskit/chemistry/drivers/__init__.py index 42738472ba..c07788a009 100644 --- a/qiskit/chemistry/drivers/__init__.py +++ b/qiskit/chemistry/drivers/__init__.py @@ -51,6 +51,7 @@ :nosignatures: BaseDriver + IntegralsDriver Driver Common ============= @@ -59,8 +60,8 @@ :toctree: ../stubs/ :nosignatures: - UnitsType HFMethodType + UnitsType BasisType InitialGuess @@ -111,7 +112,9 @@ """ -from ._basedriver import BaseDriver, UnitsType, HFMethodType + +from .base_driver import BaseDriver +from .integrals_driver import IntegralsDriver, UnitsType, HFMethodType from .fcidumpd import FCIDumpDriver from .gaussiand import GaussianDriver, GaussianLogDriver, GaussianLogResult from .hdf5d import HDF5Driver @@ -119,9 +122,10 @@ from .pyquanted import PyQuanteDriver, BasisType from .pyscfd import PySCFDriver, InitialGuess -__all__ = ['BaseDriver', +__all__ = ['HFMethodType', + 'BaseDriver', + 'IntegralsDriver', 'UnitsType', - 'HFMethodType', 'FCIDumpDriver', 'GaussianDriver', 'GaussianLogDriver', diff --git a/qiskit/chemistry/drivers/base_driver.py b/qiskit/chemistry/drivers/base_driver.py new file mode 100644 index 0000000000..9bde2d84bf --- /dev/null +++ b/qiskit/chemistry/drivers/base_driver.py @@ -0,0 +1,23 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +This module implements the abstract base class for driver modules. +""" + +from abc import ABC + + +class BaseDriver(ABC): + """ + Base class for Qiskit's chemistry drivers. + """ diff --git a/qiskit/chemistry/drivers/fcidumpd/fcidumpdriver.py b/qiskit/chemistry/drivers/fcidumpd/fcidumpdriver.py index 5058cbaf7c..e1c7311a40 100644 --- a/qiskit/chemistry/drivers/fcidumpd/fcidumpdriver.py +++ b/qiskit/chemistry/drivers/fcidumpd/fcidumpdriver.py @@ -13,8 +13,9 @@ """FCIDump Driver.""" from typing import List, Optional -from qiskit.chemistry.drivers import BaseDriver -from qiskit.chemistry import QiskitChemistryError, QMolecule +from ..base_driver import BaseDriver +from ...qiskit_chemistry_error import QiskitChemistryError +from ...qmolecule import QMolecule from .dumper import dump from .parser import parse @@ -70,7 +71,7 @@ def run(self) -> QMolecule: q_mol.nuclear_repulsion_energy = fcidump_data.get('ecore', None) q_mol.num_orbitals = fcidump_data.get('NORB') q_mol.multiplicity = fcidump_data.get('MS2', 0) + 1 - q_mol.charge = 0 # ensures QMolecule.log() works + q_mol.molecular_charge = 0 # ensures QMolecule.log() works q_mol.num_beta = (fcidump_data.get('NELEC') - (q_mol.multiplicity - 1)) // 2 q_mol.num_alpha = fcidump_data.get('NELEC') - q_mol.num_beta if self.atoms is not None: diff --git a/qiskit/chemistry/drivers/gaussiand/gaussian_log_driver.py b/qiskit/chemistry/drivers/gaussiand/gaussian_log_driver.py index 4ec281dd35..3bd8efa5b8 100644 --- a/qiskit/chemistry/drivers/gaussiand/gaussian_log_driver.py +++ b/qiskit/chemistry/drivers/gaussiand/gaussian_log_driver.py @@ -15,14 +15,15 @@ from typing import Union, List import logging -from qiskit.chemistry import QiskitChemistryError +from ..base_driver import BaseDriver +from ...qiskit_chemistry_error import QiskitChemistryError from .gaussian_utils import check_valid, run_g16 from .gaussian_log_result import GaussianLogResult logger = logging.getLogger(__name__) -class GaussianLogDriver: +class GaussianLogDriver(BaseDriver): """ Gaussian™ 16 log driver. Qiskit chemistry driver using the Gaussian™ 16 program that provides the log diff --git a/qiskit/chemistry/drivers/gaussiand/gaussiandriver.py b/qiskit/chemistry/drivers/gaussiand/gaussiandriver.py index fdcd5214ae..f6d05786d7 100644 --- a/qiskit/chemistry/drivers/gaussiand/gaussiandriver.py +++ b/qiskit/chemistry/drivers/gaussiand/gaussiandriver.py @@ -19,14 +19,16 @@ import os import tempfile import numpy as np -from qiskit.chemistry import QMolecule, QiskitChemistryError -from qiskit.chemistry.drivers import BaseDriver +from ..integrals_driver import IntegralsDriver +from ...qiskit_chemistry_error import QiskitChemistryError +from ...molecule import Molecule +from ...qmolecule import QMolecule from .gaussian_utils import check_valid, run_g16 logger = logging.getLogger(__name__) -class GaussianDriver(BaseDriver): +class GaussianDriver(IntegralsDriver): """ Qiskit chemistry driver using the Gaussian™ 16 program. @@ -40,17 +42,20 @@ class GaussianDriver(BaseDriver): """ def __init__(self, - config: Union[str, List[str]] = + config: Union[str, List[str], Molecule] = '# rhf/sto-3g scf(conventional)\n\n' 'h2 molecule\n\n0 1\nH 0.0 0.0 0.0\nH 0.0 0.0 0.735\n\n') -> None: """ Args: - config: A molecular configuration conforming to Gaussian™ 16 format + config: A molecular configuration conforming to Gaussian™ 16 format or + a molecule object Raises: QiskitChemistryError: Invalid Input """ GaussianDriver._check_valid() - if not isinstance(config, list) and not isinstance(config, str): + if not isinstance(config, str) and \ + not isinstance(config, list) and \ + not isinstance(config, Molecule): raise QiskitChemistryError("Invalid input for Gaussian Driver '{}'".format(config)) if isinstance(config, list): @@ -63,8 +68,23 @@ def __init__(self, def _check_valid(): check_valid() + @staticmethod + def _from_molecule_to_str(mol: Molecule) -> str: + cfg1 = '# {}/{} scf(conventional)\n\n'.format( + mol.hf_method, mol.basis_set) + name = ''.join([name for (name, _) in mol.geometry]) + geom = '\n'.join([name + ' ' + ' '.join(map(str, coord)) + for (name, coord) in mol.geometry]) + cfg2 = '{} molecule\n\n{} {}\n{}\n\n'.format( + name, mol.charge, mol.multiplicity, geom) + return cfg1 + cfg2 + def run(self) -> QMolecule: - cfg = self._config + if isinstance(self._config, Molecule): + cfg = GaussianDriver._from_molecule_to_str(self._config) + else: + cfg = self._config + while not cfg.endswith('\n\n'): cfg += '\n' @@ -93,7 +113,7 @@ def run(self) -> QMolecule: logger.warning("Failed to remove MatrixElement file %s", fname) q_mol.origin_driver_name = 'GAUSSIAN' - q_mol.origin_driver_config = self._config + q_mol.origin_driver_config = cfg return q_mol @staticmethod diff --git a/qiskit/chemistry/drivers/hdf5d/hdf5driver.py b/qiskit/chemistry/drivers/hdf5d/hdf5driver.py index 1d2a092210..3d5f1eed64 100644 --- a/qiskit/chemistry/drivers/hdf5d/hdf5driver.py +++ b/qiskit/chemistry/drivers/hdf5d/hdf5driver.py @@ -14,8 +14,8 @@ import os import logging -from qiskit.chemistry.drivers import BaseDriver -from qiskit.chemistry import QMolecule +from ..base_driver import BaseDriver +from ...qmolecule import QMolecule logger = logging.getLogger(__name__) @@ -48,6 +48,15 @@ def work_path(self, new_work_path): self._work_path = new_work_path def run(self) -> QMolecule: + """ + Runs driver to produce a QMolecule output. + + Returns: + A QMolecule containing the molecular data. + + Raises: + LookupError: file not found. + """ hdf5_file = self._hdf5_input if self.work_path is not None and not os.path.isabs(hdf5_file): hdf5_file = os.path.abspath(os.path.join(self.work_path, hdf5_file)) diff --git a/qiskit/chemistry/drivers/_basedriver.py b/qiskit/chemistry/drivers/integrals_driver.py similarity index 75% rename from qiskit/chemistry/drivers/_basedriver.py rename to qiskit/chemistry/drivers/integrals_driver.py index b9134975d3..d52fe602f7 100644 --- a/qiskit/chemistry/drivers/_basedriver.py +++ b/qiskit/chemistry/drivers/integrals_driver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2020. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -11,13 +11,14 @@ # that they have been altered from the originals. """ -This module implements the abstract base class for driver modules. +This module implements the abstract base class for integral driver modules. """ -from abc import ABC, abstractmethod +from abc import abstractmethod from enum import Enum -from qiskit.chemistry import QMolecule +from ..qmolecule import QMolecule +from .base_driver import BaseDriver class UnitsType(Enum): @@ -33,15 +34,11 @@ class HFMethodType(Enum): UHF = 'uhf' -class BaseDriver(ABC): +class IntegralsDriver(BaseDriver): """ - Base class for Qiskit's chemistry drivers. + Base class for Qiskit's chemistry integral drivers. """ - @abstractmethod - def __init__(self): - pass - @abstractmethod def run(self) -> QMolecule: """ diff --git a/qiskit/chemistry/drivers/psi4d/psi4driver.py b/qiskit/chemistry/drivers/psi4d/psi4driver.py index 07cc1e0494..6458504e71 100644 --- a/qiskit/chemistry/drivers/psi4d/psi4driver.py +++ b/qiskit/chemistry/drivers/psi4d/psi4driver.py @@ -19,8 +19,10 @@ import logging import sys from shutil import which -from qiskit.chemistry import QMolecule, QiskitChemistryError -from qiskit.chemistry.drivers import BaseDriver +from ..integrals_driver import IntegralsDriver +from ...qiskit_chemistry_error import QiskitChemistryError +from ...molecule import Molecule +from ...qmolecule import QMolecule logger = logging.getLogger(__name__) @@ -29,7 +31,7 @@ PSI4_APP = which(PSI4) -class PSI4Driver(BaseDriver): +class PSI4Driver(IntegralsDriver): """ Qiskit chemistry driver using the PSI4 program. @@ -37,17 +39,20 @@ class PSI4Driver(BaseDriver): """ def __init__(self, - config: Union[str, List[str]] = + config: Union[str, List[str], Molecule] = 'molecule h2 {\n 0 1\n H 0.0 0.0 0.0\n H 0.0 0.0 0.735\n}\n\n' 'set {\n basis sto-3g\n scf_type pk\n reference rhf\n') -> None: """ Args: - config: A molecular configuration conforming to PSI4 format + config: A molecular configuration conforming to PSI4 format or + a molecule object Raises: QiskitChemistryError: Invalid Input """ self._check_valid() - if not isinstance(config, list) and not isinstance(config, str): + if not isinstance(config, str) and \ + not isinstance(config, list) and \ + not isinstance(config, Molecule): raise QiskitChemistryError("Invalid input for PSI4 Driver '{}'".format(config)) if isinstance(config, list): @@ -61,14 +66,30 @@ def _check_valid(): if PSI4_APP is None: raise QiskitChemistryError("Could not locate {}".format(PSI4)) + @staticmethod + def _from_molecule_to_str(mol: Molecule) -> str: + name = ''.join([name for (name, _) in mol.geometry]) + geom = '\n'.join([name + ' ' + ' '.join(map(str, coord)) + for (name, coord) in mol.geometry]) + cfg1 = 'molecule {} {{\n {} {}\n {}\nno_com\nno_reorient\n}}\n\n'.format( + name, mol.charge, mol.multiplicity, geom) + cfg2 = 'set {{\n basis {}\n scf_type pk\n reference {}\n}}'.format( + mol.basis_set, mol.hf_method) + return cfg1 + cfg2 + def run(self) -> QMolecule: + if isinstance(self._config, Molecule): + cfg = PSI4Driver._from_molecule_to_str(self._config) + else: + cfg = self._config + psi4d_directory = os.path.dirname(os.path.realpath(__file__)) template_file = psi4d_directory + '/_template.txt' qiskit_chemistry_directory = os.path.abspath(os.path.join(psi4d_directory, '../..')) molecule = QMolecule() - input_text = self._config + '\n' + input_text = cfg + '\n' input_text += 'import sys\n' syspath = '[\'' + qiskit_chemistry_directory + '\',\'' + '\',\''.join(sys.path) + '\']' @@ -116,7 +137,7 @@ def run(self) -> QMolecule: # remove internal file _q_molecule.remove_file() _q_molecule.origin_driver_name = 'PSI4' - _q_molecule.origin_driver_config = self._config + _q_molecule.origin_driver_config = cfg return _q_molecule @staticmethod diff --git a/qiskit/chemistry/drivers/pyquanted/pyquantedriver.py b/qiskit/chemistry/drivers/pyquanted/pyquantedriver.py index da57a47cf4..4b6b5b321c 100644 --- a/qiskit/chemistry/drivers/pyquanted/pyquantedriver.py +++ b/qiskit/chemistry/drivers/pyquanted/pyquantedriver.py @@ -12,14 +12,16 @@ """ PyQuante Driver """ -from typing import Union, List +from typing import Union, List, cast import importlib from enum import Enum import logging from qiskit.aqua.utils.validation import validate_min -from qiskit.chemistry.drivers import BaseDriver, UnitsType, HFMethodType -from qiskit.chemistry import QiskitChemistryError, QMolecule -from qiskit.chemistry.drivers.pyquanted.integrals import compute_integrals +from ..integrals_driver import IntegralsDriver, UnitsType, HFMethodType +from ...qiskit_chemistry_error import QiskitChemistryError +from ...molecule import Molecule +from ...qmolecule import QMolecule +from .integrals import compute_integrals logger = logging.getLogger(__name__) @@ -31,7 +33,7 @@ class BasisType(Enum): B631GSS = '6-31g**' -class PyQuanteDriver(BaseDriver): +class PyQuanteDriver(IntegralsDriver): """ Qiskit chemistry driver using the PyQuante2 library. @@ -39,7 +41,8 @@ class PyQuanteDriver(BaseDriver): """ def __init__(self, - atoms: Union[str, List[str]] = 'H 0.0 0.0 0.0; H 0.0 0.0 0.735', + atoms: Union[str, List[str], Molecule] = + 'H 0.0 0.0 0.0; H 0.0 0.0 0.735', units: UnitsType = UnitsType.ANGSTROM, charge: int = 0, multiplicity: int = 1, @@ -67,21 +70,28 @@ def __init__(self, hf_method = hf_method.value validate_min('maxiters', maxiters, 1) self._check_valid() - if not isinstance(atoms, list) and not isinstance(atoms, str): + if not isinstance(atoms, str) and \ + not isinstance(atoms, list) and \ + not isinstance(atoms, Molecule): raise QiskitChemistryError("Invalid atom input for PYQUANTE Driver '{}'".format(atoms)) if isinstance(atoms, list): atoms = ';'.join(atoms) - else: + elif isinstance(atoms, str): atoms = atoms.replace('\n', ';') + elif isinstance(atoms, Molecule): + if atoms.basis_set is None: + atoms.basis_set = cast(str, basis) + if atoms.hf_method is None: + atoms.hf_method = cast(str, hf_method) super().__init__() self._atoms = atoms self._units = units self._charge = charge self._multiplicity = multiplicity - self._basis = basis - self._hf_method = hf_method + self._basis = cast(str, basis) + self._hf_method = cast(str, hf_method) self._tol = tol self._maxiters = maxiters @@ -99,22 +109,36 @@ def _check_valid(): raise QiskitChemistryError(err_msg) def run(self) -> QMolecule: - q_mol = compute_integrals(atoms=self._atoms, + if isinstance(self._atoms, Molecule): + atoms = ';'.join([name + ' ' + ' '.join(map(str, coord)) + for (name, coord) in self._atoms.geometry]) + charge = self._atoms.charge + multiplicity = self._atoms.multiplicity + basis = self._atoms.basis_set + hf_method = self._atoms.hf_method + else: + atoms = self._atoms + charge = self._charge + multiplicity = self._multiplicity + basis = self._basis + hf_method = self._hf_method + + q_mol = compute_integrals(atoms=atoms, units=self._units, - charge=self._charge, - multiplicity=self._multiplicity, - basis=self._basis, - hf_method=self._hf_method, + charge=charge, + multiplicity=multiplicity, + basis=basis, + hf_method=hf_method, tol=self._tol, maxiters=self._maxiters) q_mol.origin_driver_name = 'PYQUANTE' - cfg = ['atoms={}'.format(self._atoms), + cfg = ['atoms={}'.format(atoms), 'units={}'.format(self._units), - 'charge={}'.format(self._charge), - 'multiplicity={}'.format(self._multiplicity), - 'basis={}'.format(self._basis), - 'hf_method={}'.format(self._hf_method), + 'charge={}'.format(charge), + 'multiplicity={}'.format(multiplicity), + 'basis={}'.format(basis), + 'hf_method={}'.format(hf_method), 'tol={}'.format(self._tol), 'maxiters={}'.format(self._maxiters), ''] diff --git a/qiskit/chemistry/drivers/pyscfd/pyscfdriver.py b/qiskit/chemistry/drivers/pyscfd/pyscfdriver.py index e047fb8699..0933467a0f 100644 --- a/qiskit/chemistry/drivers/pyscfd/pyscfdriver.py +++ b/qiskit/chemistry/drivers/pyscfd/pyscfdriver.py @@ -12,14 +12,16 @@ """ PYSCF Driver """ -from typing import Optional, Union, List +from typing import Optional, Union, List, cast import importlib from enum import Enum import logging from qiskit.aqua.utils.validation import validate_min -from qiskit.chemistry.drivers import BaseDriver, UnitsType, HFMethodType -from qiskit.chemistry import QiskitChemistryError, QMolecule -from qiskit.chemistry.drivers.pyscfd.integrals import compute_integrals +from ..integrals_driver import IntegralsDriver, UnitsType, HFMethodType +from ...qiskit_chemistry_error import QiskitChemistryError +from ...molecule import Molecule +from ...qmolecule import QMolecule +from .integrals import compute_integrals logger = logging.getLogger(__name__) @@ -32,7 +34,7 @@ class InitialGuess(Enum): ATOM = 'atom' -class PySCFDriver(BaseDriver): +class PySCFDriver(IntegralsDriver): """ Qiskit chemistry driver using the PySCF library. @@ -40,7 +42,8 @@ class PySCFDriver(BaseDriver): """ def __init__(self, - atom: Union[str, List[str]] = 'H 0.0 0.0 0.0; H 0.0 0.0 0.735', + atom: Union[str, List[str], Molecule] = + 'H 0.0 0.0 0.0; H 0.0 0.0 0.735', unit: UnitsType = UnitsType.ANGSTROM, charge: int = 0, spin: int = 0, @@ -68,17 +71,25 @@ def __init__(self, QiskitChemistryError: Invalid Input """ self._check_valid() - if not isinstance(atom, list) and not isinstance(atom, str): + if not isinstance(atom, str) and \ + not isinstance(atom, list) and \ + not isinstance(atom, Molecule): raise QiskitChemistryError("Invalid atom input for PYSCF Driver '{}'".format(atom)) + unit = unit.value + hf_method = hf_method.value + init_guess = init_guess.value + if isinstance(atom, list): atom = ';'.join(atom) - else: + elif isinstance(atom, str): atom = atom.replace('\n', ';') + elif isinstance(atom, Molecule): + if atom.basis_set is None: + atom.basis_set = basis + if atom.hf_method is None: + atom.hf_method = cast(str, hf_method) - unit = unit.value - hf_method = hf_method.value - init_guess = init_guess.value validate_min('max_cycle', max_cycle, 1) super().__init__() self._atom = atom @@ -86,7 +97,7 @@ def __init__(self, self._charge = charge self._spin = spin self._basis = basis - self._hf_method = hf_method + self._hf_method = cast(str, hf_method) self._conv_tol = conv_tol self._max_cycle = max_cycle self._init_guess = init_guess @@ -106,24 +117,38 @@ def _check_valid(): raise QiskitChemistryError(err_msg) def run(self) -> QMolecule: - q_mol = compute_integrals(atom=self._atom, + if isinstance(self._atom, Molecule): + atom = ';'.join([name + ' ' + ' '.join(map(str, coord)) + for (name, coord) in self._atom.geometry]) + charge = self._atom.charge + spin = self._atom.multiplicity + basis = self._atom.basis_set + hf_method = self._atom.hf_method + else: + atom = self._atom + charge = self._charge + spin = self._spin + basis = self._basis + hf_method = self._hf_method + + q_mol = compute_integrals(atom=atom, unit=self._unit, - charge=self._charge, - spin=self._spin, - basis=self._basis, - hf_method=self._hf_method, + charge=charge, + spin=spin, + basis=basis, + hf_method=hf_method, conv_tol=self._conv_tol, max_cycle=self._max_cycle, init_guess=self._init_guess, max_memory=self._max_memory) q_mol.origin_driver_name = 'PYSCF' - cfg = ['atom={}'.format(self._atom), + cfg = ['atom={}'.format(atom), 'unit={}'.format(self._unit), - 'charge={}'.format(self._charge), - 'spin={}'.format(self._spin), - 'basis={}'.format(self._basis), - 'hf_method={}'.format(self._hf_method), + 'charge={}'.format(charge), + 'spin={}'.format(spin), + 'basis={}'.format(basis), + 'hf_method={}'.format(hf_method), 'conv_tol={}'.format(self._conv_tol), 'max_cycle={}'.format(self._max_cycle), 'init_guess={}'.format(self._init_guess), diff --git a/qiskit/chemistry/molecule.py b/qiskit/chemistry/molecule.py new file mode 100644 index 0000000000..6c08da0e24 --- /dev/null +++ b/qiskit/chemistry/molecule.py @@ -0,0 +1,386 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +This module implements an interface for a generic molecule. +It defines the composing atoms (with properties like masses, and nuclear spin), +and allows for changing the molecular geometry through given degrees of freedom +(e.g. bond-stretching, angle-bending, etc.). +""" + +from typing import Callable, Tuple, List, Optional +import copy + +import numpy as np +import scipy.linalg + + +class Molecule: + """ + Molecule class + """ + + def __init__(self, + geometry: List[Tuple[str, List[float]]], + multiplicity: int, + charge: int, + degrees_of_freedom: Optional[List[Callable]] = None, + masses: Optional[List[float]] = None + ) -> None: + """ + Constructor. + + Args: + geometry: 2d list containing atom string names + to generate PySCF molecule strings as keys and list of 3 + floats representing Cartesian coordinates as values, + in units of **Angstroms**. + multiplicity: multiplicity + charge: charge + degrees_of_freedom: List of functions taking a + perturbation value and geometry and returns a perturbed + geometry. Helper functions for typical perturbations are + provided and can be used by the form + itertools.partial(Molecule.stretching_potential, + {'atom_pair': (1, 2)) + to specify the desired degree of freedom. + masses: masses + + Raises: + ValueError: Invalid input. + + """ + self._geometry = geometry + self._degrees_of_freedom = degrees_of_freedom + self._multiplicity = multiplicity + self._charge = charge + + if masses is not None and not len(masses) == len(self._geometry): + raise ValueError( + 'Length of masses must match length of geometries, ' + 'found {} and {} respectively'.format( + len(masses), + len(self._geometry) + ) + ) + + self._masses = masses + self._basis_set = None # type: Optional[str] + self._hf_method = None # type: Optional[str] + + @classmethod + def __distance_modifier(cls, + function: Callable[[float, float], float], + parameter: float, + geometry: List[Tuple[str, List[float]]], + atom_pair: Tuple[int, int]) -> List[Tuple[str, List[float]]]: + """ + Args: + function: a function of two parameters (current distance, + extra parameter) returning the new distance + parameter: The extra parameter of the function above. + geometry: The initial geometry to perturb. + atom_pair: A tuple with two integers, indexing + which atoms from the starting geometry should be moved + apart. **Atom1 is moved away from Atom2, while Atom2 + remains stationary.** + + Returns: + end geometry + """ + a_1, a_2 = atom_pair + starting_coord1 = np.array(geometry[a_1][1]) + coord2 = np.array(geometry[a_2][1]) + + starting_distance_vector = starting_coord1 - coord2 + starting_l2distance = np.linalg.norm(starting_distance_vector) + new_l2distance = function(starting_l2distance, parameter) + new_distance_vector = starting_distance_vector * ( + new_l2distance / starting_l2distance + ) + new_coord1 = coord2 + new_distance_vector + + ending_geometry = copy.deepcopy(geometry) + ending_geometry[a_1][1] = new_coord1.tolist() # type: ignore + return ending_geometry + + @classmethod + def absolute_distance(cls, + distance: float, + geometry: List[Tuple[str, List[float]]], + atom_pair: Tuple[int, int]) -> List[Tuple[str, List[float]]]: + """ + Args: + distance: The (new) distance between the two atoms. + geometry: The initial geometry to perturb. + atom_pair: A tuple with two integers, + indexing which atoms from the starting geometry should be + moved apart. **Atom1 is moved away (at the given distance) + from Atom2, while Atom2 remains stationary.** + + Returns: + end geometry + """ + + def func(curr_dist, extra): # pylint: disable=unused-argument + return extra + + return cls.__distance_modifier(func, distance, geometry, atom_pair) + + @classmethod + def absolute_stretching(cls, + perturbation: float, + geometry: List[Tuple[str, List[float]]], + atom_pair: Tuple[int, int]) -> List[Tuple[str, List[float]]]: + """ + Args: + perturbation: The magnitude of the stretch. + (New distance = stretch + old distance) + geometry: The initial geometry to perturb. + atom_pair: A tuple with two integers, + indexing which atoms from the starting geometry should be + stretched apart. **Atom1 is stretched away from Atom2, while + Atom2 remains stationary.** + + Returns: + end geometry + """ + + def func(curr_dist, extra): + return curr_dist + extra + + return cls.__distance_modifier(func, perturbation, geometry, + atom_pair) + + @classmethod + def relative_stretching(cls, + perturbation: float, + geometry: List[Tuple[str, List[float]]], + atom_pair: Tuple[int, int]) -> List[Tuple[str, List[float]]]: + """ + Args: + perturbation: The magnitude of the stretch. + (New distance = stretch * old distance) + geometry: The initial geometry to perturb. + atom_pair: A tuple with two integers, indexing which + atoms from the starting geometry should be stretched apart. + **Atom1 is stretched away from Atom2, while Atom2 remains + stationary.** + + Returns: + end geometry + """ + + def func(curr_dist, extra): + return curr_dist * extra + + return cls.__distance_modifier(func, perturbation, geometry, + atom_pair) + + @classmethod + def __bend_modifier(cls, + function: Callable[[float, float], float], + parameter: float, + geometry: List[Tuple[str, List[float]]], + atom_trio: Tuple[int, int, int]) -> List[Tuple[str, List[float]]]: + """ + Args: + function: a function of two parameters (current angle, + extra parameter) returning the new angle + parameter: The extra parameter of the function above. + geometry: The initial geometry to perturb. + atom_trio: A tuple with three integers, indexing + which atoms from the starting geometry should be bent apart. + **Atom1 is bent *away* from Atom3 by an angle whose vertex + is Atom2, while Atom2 and Atom3 remain stationary.** + + Returns: + end geometry + """ + a_1, a_2, a_3 = atom_trio + starting_coord1 = np.array(geometry[a_1][1]) + coord2 = np.array(geometry[a_2][1]) + coord3 = np.array(geometry[a_3][1]) + + distance_vec1to2 = starting_coord1 - coord2 + distance_vec3to2 = coord3 - coord2 + rot_axis = np.cross(distance_vec1to2, distance_vec3to2) + # If atoms are linear, choose the rotation direction randomly, + # but still along the correct plane + # Maybe this is a bad idea if we end up here on some + # existing bending path. + # It'd be good to fix this later to remember the axis in some way. + if np.linalg.norm(rot_axis) == 0: + nudged_vec = copy.deepcopy(distance_vec1to2) + nudged_vec[0] += .01 + rot_axis = np.cross(nudged_vec, distance_vec3to2) + rot_unit_axis = rot_axis / np.linalg.norm(rot_axis) + starting_angle = np.arcsin( + np.linalg.norm(rot_axis) / ( + np.linalg.norm(distance_vec1to2) + * np.linalg.norm(distance_vec3to2) + ) + ) + new_angle = function(starting_angle, parameter) + perturbation = new_angle - starting_angle + rot_matrix = scipy.linalg.expm( + np.cross( + np.eye(3), + rot_unit_axis * + perturbation)) + new_coord1 = rot_matrix @ starting_coord1 + + ending_geometry = copy.deepcopy(geometry) + ending_geometry[a_1][1] = new_coord1.tolist() # type: ignore + return ending_geometry + + @classmethod + def absolute_angle(cls, + angle: float, + geometry: List[Tuple[str, List[float]]], + atom_trio: Tuple[int, int, int]) -> List[Tuple[str, List[float]]]: + """ + Args: + angle: The magnitude of the perturbation in **radians**. + **Positive bend is always in the direction toward Atom3.** + the direction of increasing the starting angle.** + geometry: The initial geometry to perturb. + atom_trio: A tuple with three integers, indexing + which atoms from the starting geometry should be bent apart. + **Atom1 is bent *away* from Atom3 by an angle whose vertex + is Atom2 and equal to **angle**, while Atom2 and Atom3 + remain stationary.** + + Returns: + end geometry + """ + + def func(curr_angle, extra): # pylint: disable=unused-argument + return extra + + return cls.__bend_modifier(func, angle, geometry, atom_trio) + + @classmethod + def absolute_bending(cls, + bend: float, + geometry: List[Tuple[str, List[float]]], + atom_trio: Tuple[int, int, int]) -> List[Tuple[str, List[float]]]: + """ + Args: + bend: The magnitude of the perturbation in **radians**. + **Positive bend is always in the direction toward Atom3.** + the direction of increasing the starting angle.** + geometry: The initial geometry to perturb. + atom_trio: A tuple with three integers, indexing + which atoms from the starting geometry should be bent apart. + **Atom1 is bent *away* from Atom3 by an angle whose vertex + is Atom2 and equal to the initial angle **plus** bend, + while Atom2 and Atom3 remain stationary.** + + Returns: + end geometry + """ + + def func(curr_angle, extra): + return curr_angle + extra + + return cls.__bend_modifier(func, bend, geometry, atom_trio) + + @classmethod + def relative_bending(cls, + bend: float, + geometry: List[Tuple[str, List[float]]], + atom_trio: Tuple[int, int, int]) -> List[Tuple[str, List[float]]]: + """ + Args: + bend: The magnitude of the perturbation in **radians**. + **Positive bend is always in the direction toward Atom3.** + the direction of increasing the starting angle.** + geometry: The initial geometry to perturb. + atom_trio: A tuple with three integers, + indexing which atoms from the starting geometry + should be bent apart. **Atom1 is bent *away* from Atom3 + by an angle whose vertex is Atom2 and equal to the initial + angle **times** bend, while Atom2 and Atom3 + remain stationary.** + + Returns: + end geometry + """ + + def func(curr_angle, extra): + return curr_angle * extra + + return cls.__bend_modifier(func, bend, geometry, atom_trio) + + def get_perturbed_geom(self, + perturbations: Optional[List[float]] = None) \ + -> List[Tuple[str, List[float]]]: + """ get perturbed geometry """ + if not perturbations or not self._degrees_of_freedom: + return self._geometry + geometry = copy.deepcopy(self._geometry) + for per, dof in zip(perturbations, self._degrees_of_freedom): + geometry = dof(per, geometry) + return geometry + + @property + def geometry(self) -> List[Tuple[str, List[float]]]: + """ return geometry """ + return self._geometry + + @classmethod + def get_geometry_str(cls, + geometry: List[Tuple[str, List[float]]]) -> str: + """ get geometry string """ + return '; '.join([name + ' ' + ', '.join(map(str, coord)) + for (name, coord) in geometry]) + + @property + def geometry_str(self) -> str: + """ return geometry string """ + return Molecule.get_geometry_str(self.geometry) + + @property + def basis_set(self) -> Optional[str]: + """ return basis set """ + return self._basis_set + + @basis_set.setter + def basis_set(self, value: str) -> None: + """ set basis set """ + self._basis_set = value + + @property + def hf_method(self) -> Optional[str]: + """ return hf method """ + return self._hf_method + + @hf_method.setter + def hf_method(self, value: str) -> None: + """ set hf method """ + self._hf_method = value + + @property + def masses(self) -> Optional[List[float]]: + """ return masses """ + return self._masses + + @property + def multiplicity(self) -> int: + """ return multiplicity """ + return self._multiplicity + + @property + def charge(self) -> int: + """ return charge """ + return self._charge diff --git a/test/chemistry/test_driver_gaussian.py b/test/chemistry/test_driver_gaussian.py index 50d61796ce..77444c8cbc 100644 --- a/test/chemistry/test_driver_gaussian.py +++ b/test/chemistry/test_driver_gaussian.py @@ -16,8 +16,8 @@ from test.chemistry import QiskitChemistryTestCase from test.chemistry.test_driver import TestDriver -from qiskit.chemistry.drivers import GaussianDriver -from qiskit.chemistry import QiskitChemistryError +from qiskit.chemistry.drivers import GaussianDriver, HFMethodType +from qiskit.chemistry import QiskitChemistryError, Molecule class TestDriverGaussian(QiskitChemistryTestCase, TestDriver): @@ -41,5 +41,22 @@ def setUp(self): self.qmolecule = driver.run() +class TestDriverGaussianMolecule(QiskitChemistryTestCase, TestDriver): + """Gaussian Driver tests.""" + + def setUp(self): + super().setUp() + mol = Molecule(geometry=[('H', [.0, .0, .0]), ('H', [.0, .0, 0.735])], + multiplicity=1, + charge=0) + mol.basis_set = 'sto-3g' + mol.hf_method = HFMethodType.RHF.value + try: + driver = GaussianDriver(mol) + except QiskitChemistryError: + self.skipTest('GAUSSIAN driver does not appear to be installed') + self.qmolecule = driver.run() + + if __name__ == '__main__': unittest.main() diff --git a/test/chemistry/test_driver_psi4.py b/test/chemistry/test_driver_psi4.py index 288b3806a2..51f06a89ff 100644 --- a/test/chemistry/test_driver_psi4.py +++ b/test/chemistry/test_driver_psi4.py @@ -16,8 +16,8 @@ from test.chemistry import QiskitChemistryTestCase from test.chemistry.test_driver import TestDriver -from qiskit.chemistry.drivers import PSI4Driver -from qiskit.chemistry import QiskitChemistryError +from qiskit.chemistry.drivers import PSI4Driver, HFMethodType +from qiskit.chemistry import QiskitChemistryError, Molecule class TestDriverPSI4(QiskitChemistryTestCase, TestDriver): @@ -44,5 +44,22 @@ def setUp(self): self.qmolecule = driver.run() +class TestDriverPSI4Molecule(QiskitChemistryTestCase, TestDriver): + """PSI4 Driver molecule tests.""" + + def setUp(self): + super().setUp() + mol = Molecule(geometry=[('H', [.0, .0, .0]), ('H', [.0, .0, 0.735])], + multiplicity=1, + charge=0) + mol.basis_set = 'sto-3g' + mol.hf_method = HFMethodType.RHF.value + try: + driver = PSI4Driver(mol) + except QiskitChemistryError: + self.skipTest('PSI4 driver does not appear to be installed') + self.qmolecule = driver.run() + + if __name__ == '__main__': unittest.main() diff --git a/test/chemistry/test_driver_pyquante.py b/test/chemistry/test_driver_pyquante.py index 8e7b4354a7..41bcfe9204 100644 --- a/test/chemistry/test_driver_pyquante.py +++ b/test/chemistry/test_driver_pyquante.py @@ -15,8 +15,8 @@ import unittest from test.chemistry import QiskitChemistryTestCase from test.chemistry.test_driver import TestDriver -from qiskit.chemistry.drivers import PyQuanteDriver, UnitsType, BasisType -from qiskit.chemistry import QiskitChemistryError +from qiskit.chemistry.drivers import PyQuanteDriver, UnitsType, BasisType, HFMethodType +from qiskit.chemistry import QiskitChemistryError, Molecule class TestDriverPyQuante(QiskitChemistryTestCase, TestDriver): @@ -35,5 +35,22 @@ def setUp(self): self.qmolecule = driver.run() +class TestDriverPyQuanteMolecule(QiskitChemistryTestCase, TestDriver): + """PYQUANTE Driver molecule tests.""" + + def setUp(self): + super().setUp() + mol = Molecule(geometry=[('H', [.0, .0, .0]), ('H', [.0, .0, 0.735])], + multiplicity=1, + charge=0) + mol.basis_set = BasisType.BSTO3G.value + mol.hf_method = HFMethodType.RHF.value + try: + driver = PyQuanteDriver(mol) + except QiskitChemistryError: + self.skipTest('PYQUANTE driver does not appear to be installed') + self.qmolecule = driver.run() + + if __name__ == '__main__': unittest.main() diff --git a/test/chemistry/test_driver_pyscf.py b/test/chemistry/test_driver_pyscf.py index 0f5a0f7a5a..4a6d62fe2f 100644 --- a/test/chemistry/test_driver_pyscf.py +++ b/test/chemistry/test_driver_pyscf.py @@ -15,8 +15,8 @@ import unittest from test.chemistry import QiskitChemistryTestCase from test.chemistry.test_driver import TestDriver -from qiskit.chemistry.drivers import PySCFDriver, UnitsType -from qiskit.chemistry import QiskitChemistryError +from qiskit.chemistry.drivers import PySCFDriver, UnitsType, HFMethodType +from qiskit.chemistry import QiskitChemistryError, Molecule class TestDriverPySCF(QiskitChemistryTestCase, TestDriver): @@ -35,5 +35,22 @@ def setUp(self): self.qmolecule = driver.run() +class TestDriverPySCFMolecule(QiskitChemistryTestCase, TestDriver): + """PYSCF Driver Molecule tests.""" + + def setUp(self): + super().setUp() + mol = Molecule(geometry=[('H', [.0, .0, .0]), ('H', [.0, .0, 0.735])], + multiplicity=0, + charge=0) + mol.basis_set = 'sto3g' + mol.hf_method = HFMethodType.RHF.value + try: + driver = PySCFDriver(mol) + except QiskitChemistryError: + self.skipTest('PYSCF driver does not appear to be installed') + self.qmolecule = driver.run() + + if __name__ == '__main__': unittest.main()