Source code for qiskit_nature.second_q.drivers.psi4d.psi4driver

# This code is part of a Qiskit project.
# (C) Copyright IBM 2018, 2023.
# 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
# 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.

""" Psi4 Driver """

from __future__ import annotations

import logging
import os
import subprocess
import tempfile
from pathlib import Path
from typing import Any

from qiskit_nature import QiskitNatureError
from qiskit_nature.exceptions import UnsupportMethodError
from qiskit_nature.units import DistanceUnit
from qiskit_nature.second_q.problems import ElectronicBasis, ElectronicStructureProblem
from qiskit_nature.second_q.formats.molecule_info import MoleculeInfo
from qiskit_nature.second_q.formats.qcschema import QCSchema
from qiskit_nature.second_q.formats.qcschema_translator import qcschema_to_problem
import qiskit_nature.optionals as _optionals

from ..electronic_structure_driver import ElectronicStructureDriver, MethodType, _QCSchemaData

logger = logging.getLogger(__name__)

[docs]@_optionals.HAS_PSI4.require_in_instance class Psi4Driver(ElectronicStructureDriver): """ Qiskit Nature driver using the Psi4 program. See """ def __init__( self, config: str | list[ str ] = "molecule h2 {\n 0 1\n H 0.0 0.0 0.0\n H 0.0 0.0 0.735\n no_com\n no_reorient\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. Raises: QiskitNatureError: Psi4 Driver configuration should be a string or list of strings. """ super().__init__() if not isinstance(config, str) and not isinstance(config, list): raise QiskitNatureError( f"Psi4 Driver configuration should be a string or list of strings:'{config}'." ) if isinstance(config, list): config = "\n".join(config) self._config = config self._qcschemadata = _QCSchemaData()
[docs] @staticmethod @_optionals.HAS_PSI4.require_in_call def from_molecule( molecule: MoleculeInfo, basis: str = "sto3g", method: MethodType = MethodType.RHF, driver_kwargs: dict[str, Any] | None = None, ) -> "Psi4Driver": """Creates a driver from a molecule. Args: molecule: the molecular information. basis: the basis set. method: the SCF method type. driver_kwargs: keyword arguments to be passed to driver. Returns: The constructed driver instance. Raises: QiskitNatureError: when an unknown unit is encountered. """ # Ignore kwargs parameter for this driver del driver_kwargs Psi4Driver.check_method_supported(method) basis = Psi4Driver.to_driver_basis(basis) if molecule.units == DistanceUnit.ANGSTROM: units = "ang" elif molecule.units == DistanceUnit.BOHR: units = "bohr" else: raise QiskitNatureError(f"Unknown unit '{molecule.units.value}'") name = "".join(molecule.symbols) geom = "\n".join( [ name + " " + " ".join(map(str, coord)) for name, coord in zip(molecule.symbols, molecule.coords) ] ) cfg1 = f"molecule {name} {{\nunits {units}\n" cfg2 = f"{molecule.charge} {molecule.multiplicity}\n" cfg3 = f"{geom}\nno_com\nno_reorient\n}}\n\n" cfg4 = f"set {{\n basis {basis}\n scf_type pk\n reference {method.value}\n}}" return Psi4Driver(cfg1 + cfg2 + cfg3 + cfg4)
[docs] @staticmethod def to_driver_basis(basis: str) -> str: """Converts basis to a driver acceptable basis. Args: basis: The basis set to be used. Returns: A driver acceptable basis. """ if basis == "sto3g": return "sto-3g" return basis
[docs] @staticmethod def check_method_supported(method: MethodType) -> None: """Checks that Psi4 supports this method. Args: method: the SCF method type. Raises: UnsupportMethodError: If the method is not supported. """ if method not in [MethodType.RHF, MethodType.ROHF, MethodType.UHF]: raise UnsupportMethodError(f"Invalid Psi4 method {method.value}.")
[docs] def run(self) -> ElectronicStructureProblem: cfg = self._config psi4d_directory = Path(__file__).resolve().parent template_file = psi4d_directory.joinpath("_template.txt") input_text = [cfg] file_fd, hdf5_file = tempfile.mkstemp(suffix=".hdf5") os.close(file_fd) input_text += [f'_FILE_PATH = "{Path(hdf5_file).as_posix()}"'] with open(template_file, "r", encoding="utf8") as file: input_text += [line.strip("\n") for line in file.readlines()] file_fd, input_file = tempfile.mkstemp(suffix=".inp") os.close(file_fd) with open(input_file, "w", encoding="utf8") as stream: stream.write("\n".join(input_text)) file_fd, output_file = tempfile.mkstemp(suffix=".out") os.close(file_fd) try: Psi4Driver._run_psi4(input_file, output_file) if logger.isEnabledFor(logging.DEBUG): with open(output_file, "r", encoding="utf8") as file: logger.debug("Psi4 output file:\n%s", finally: run_directory = os.getcwd() for local_file in os.listdir(run_directory): if local_file.endswith(".clean"): os.remove(run_directory + "/" + local_file) try: os.remove("timer.dat") except Exception: # pylint: disable=broad-except pass try: os.remove(input_file) except Exception: # pylint: disable=broad-except pass try: os.remove(output_file) except Exception: # pylint: disable=broad-except pass self._qcschemadata = _QCSchemaData.from_hdf5(hdf5_file) try: os.remove(hdf5_file) except Exception: # pylint: disable=broad-except pass return self.to_problem()
[docs] def to_qcschema(self, *, include_dipole: bool = True) -> QCSchema: """Extracts all available information after the driver was run into a :class:`.QCSchema` object. Args: include_dipole: whether to include the custom dipole integrals in the QCSchema. Returns: A :class:`.QCSchema` storing all extracted system data computed by the driver. """ return Psi4Driver._to_qcschema(self._qcschemadata, include_dipole=include_dipole)
[docs] def to_problem( self, *, basis: ElectronicBasis = ElectronicBasis.MO, include_dipole: bool = True, ) -> ElectronicStructureProblem: return qcschema_to_problem( self.to_qcschema(include_dipole=include_dipole), basis=basis, include_dipole=include_dipole, )
@staticmethod def _run_psi4(input_file, output_file): process = None try: with subprocess.Popen( [_optionals.PSI4, input_file, output_file], stdout=subprocess.PIPE, universal_newlines=True, ) as process: stdout, _ = process.communicate() process.wait() except Exception as ex: if process is not None: process.kill() raise QiskitNatureError(f"{_optionals.PSI4} run has failed") from ex if process.returncode != 0: errmsg = "" if stdout is not None: lines = stdout.splitlines() for line in lines: logger.error(line) errmsg += line + "\n" raise QiskitNatureError( f"{_optionals.PSI4} process return code {process.returncode}\n{errmsg}" )