Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a solve_fermion function for compatibility with SQD #27

Merged
merged 14 commits into from
Sep 25, 2024
Merged
4 changes: 3 additions & 1 deletion qiskit_addon_dice_solver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
:toctree: ../stubs/
:nosignatures:

solve_fermion
solve_dice
"""

from .dice_solver import solve_dice
from .dice_solver import solve_fermion, solve_dice

__all__ = [
"solve_fermion",
"solve_dice",
]
153 changes: 135 additions & 18 deletions qiskit_addon_dice_solver/dice_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import numpy as np
from pyscf import tools
from qiskit_addon_sqd.fermion import bitstring_matrix_to_ci_strs

# Ensure the runtime linker can find the local boost binaries at runtime
DICE_BIN = os.path.join(os.path.abspath(os.path.dirname(__file__)), "bin")
Expand All @@ -46,6 +47,124 @@ def __init__(self, command, returncode, log_path):
super().__init__(message)


def solve_fermion(
bitstring_matrix: np.ndarray,
/,
hcore: np.ndarray,
eri: np.ndarray,
*,
mpirun_options: Sequence[str] | str | None = None,
temp_dir: str | Path | None = None,
clean_temp_dir: bool = True,
) -> tuple[float, np.ndarray, tuple[np.ndarray, np.ndarray]]:
"""
Approximate the ground state of a molecular Hamiltonian given a bitstring matrix defining the Hilbert subspace.

This solver is designed for compatibility with `qiskit-addon-sqd <https://qiskit.github.io/qiskit-addon-sqd/>`_ workflows.

In order to leverage the multi-processing nature of this tool, the user must specify
the CPU resources to use via the `mpirun_options` argument.

For example, to use 8 CPU slots in parallel in quiet mode:

.. code-block:: python

# Run 8 parallel slots in quiet mode
mpirun_opts = "-quiet -n 8"
# OR
mpirun_opts = ["-quiet", "-n", "8"]

energy, sci_coeffs, avg_occs = solve_fermion(..., mpirun_options=mpirun_opts)

For more information on the ``mpirun`` command line options, refer to the `man page <https://www.open-mpi.org/doc/current/man1/mpirun.1.php>`_.

.. note::

Only closed-shell systems are supported. The particle number for both
spin-up and spin-down determinants is expected to be equal.

.. note::

Determinants are interpreted by the ``Dice`` command line application as 5-byte unsigned integers; therefore, only systems
of ``40`` or fewer orbitals are supported.

Args:
bitstring_matrix: A set of configurations defining the subspace onto which the Hamiltonian
will be projected and diagonalized. This is a 2D array of ``bool`` representations of bit
values such that each row represents a single bitstring. The spin-up configurations
should be specified by column indices in range ``(N, N/2]``, and the spin-down
configurations should be specified by column indices in range ``(N/2, 0]``, where ``N``
is the number of qubits.
hcore: Core Hamiltonian matrix representing single-electron integrals
eri: Electronic repulsion integrals representing two-electron integrals
mpirun_options: Options controlling the CPU resource allocation for the ``Dice`` command line application.
These command-line options will be passed directly to the ``mpirun`` command line application during
invocation of ``Dice``. These may be formatted as a ``Sequence`` of strings or a single string. If a ``Sequence``,
the elements will be combined into a single, space-delimited string and passed to
``mpirun``. If the input is a single string, it will be passed to ``mpirun`` as-is. If no
``mpirun_options`` are provided by the user, ``Dice`` will run on a single MPI slot. For more
information on the ``mpirun`` command line options, refer to the `man page <https://www.open-mpi.org/doc/current/man1/mpirun.1.php>`_.
temp_dir: An absolute path to a directory for storing temporary files. If not provided, the
system temporary files directory will be used.
clean_temp_dir: Whether to delete intermediate files generated by the ``Dice`` command line application.
These files will be stored in a directory created inside ``temp_dir``. If ``False``, then
this directory will be preserved.

Returns:
- Minimum energy from SCI calculation
- SCI coefficients
- Average orbital occupancy
"""
# Hard-code target S^2 until supported
spin_sq = 0.0

# Convert bitstring matrix to integer determinants for spin-up/down
ci_strs = bitstring_matrix_to_ci_strs(bitstring_matrix)
num_configurations = len(ci_strs[0])
num_up = format(ci_strs[0][0], "b").count("1")
num_dn = format(ci_strs[1][0], "b").count("1")

# Set up the temp directory
temp_dir = temp_dir or tempfile.gettempdir()
dice_dir = Path(tempfile.mkdtemp(prefix="dice_cli_files_", dir=temp_dir))

# Write the integrals out as an FCI dump for Dice command line app
active_space_path = dice_dir / "fcidump.txt"
num_orbitals = hcore.shape[0]
tools.fcidump.from_integrals(
active_space_path, hcore, eri, num_orbitals, (num_up + num_dn)
)

_write_input_files(
addresses=ci_strs,
active_space_path=active_space_path,
num_up=num_up,
num_dn=num_dn,
num_configurations=num_configurations,
dice_dir=dice_dir,
spin_sq=spin_sq,
max_iter=1,
)

# Navigate to dice dir and call Dice
_call_dice(dice_dir, mpirun_options)

# Read and convert outputs
e_dice, sci_coefficients, avg_occupancies = _read_dice_outputs(
dice_dir, num_orbitals
)

# Clean up the temp directory of intermediate files, if desired
if clean_temp_dir:
shutil.rmtree(dice_dir)

return (
e_dice,
sci_coefficients,
(avg_occupancies[:num_orbitals], avg_occupancies[num_orbitals:]),
)


def solve_dice(
addresses: tuple[Sequence[int], Sequence[int]],
active_space_path: str | Path,
Expand Down Expand Up @@ -149,13 +268,11 @@ def solve_dice(


def _read_dice_outputs(
working_dir: str | Path, num_orbitals: int
dice_dir: str | Path, num_orbitals: int
) -> tuple[float, np.ndarray, np.ndarray]:
"""Calculate the estimated ground state energy and average orbitals occupancies from Dice outputs."""
# Read in the avg orbital occupancies
spin1_rdm_dice = np.loadtxt(
os.path.join(working_dir, "spin1RDM.0.0.txt"), skiprows=1
)
spin1_rdm_dice = np.loadtxt(os.path.join(dice_dir, "spin1RDM.0.0.txt"), skiprows=1)
avg_occupancies = np.zeros(2 * num_orbitals)
for i in range(spin1_rdm_dice.shape[0]):
if spin1_rdm_dice[i, 0] == spin1_rdm_dice[i, 1]:
Expand All @@ -166,23 +283,23 @@ def _read_dice_outputs(
)

# Read in the estimated ground state energy
file_energy = open(os.path.join(working_dir, "shci.e"), "rb")
file_energy = open(os.path.join(dice_dir, "shci.e"), "rb")
bytestring_energy = file_energy.read(8)
energy_dice = struct.unpack("d", bytestring_energy)[0]

# Construct the SCI wavefunction coefficients from Dice output dets.bin
occs, amps = _read_wave_function_magnitudes(os.path.join(working_dir, "dets.bin"))
occs, amps = _read_wave_function_magnitudes(os.path.join(dice_dir, "dets.bin"))
addresses = _addresses_from_occupancies(occs)
sci_coefficients = _construct_ci_vec_from_addresses_amplitudes(amps, addresses)

return energy_dice, sci_coefficients, avg_occupancies


def _call_dice(working_dir: Path, mpirun_options: Sequence[str] | str | None) -> None:
"""Navigate to the working dir, invoke Dice, and navigate back."""
def _call_dice(dice_dir: Path, mpirun_options: Sequence[str] | str | None) -> None:
"""Navigate to the dice dir, invoke Dice, and navigate back."""
script_dir = os.path.dirname(os.path.abspath(__file__))
dice_path = os.path.join(script_dir, "bin", "Dice")
dice_log_path = os.path.join(working_dir, "dice_solver_logfile.log")
dice_log_path = os.path.join(dice_dir, "dice_solver_logfile.log")
if mpirun_options:
if isinstance(mpirun_options, str):
mpirun_options = [mpirun_options]
Expand All @@ -193,7 +310,7 @@ def _call_dice(working_dir: Path, mpirun_options: Sequence[str] | str | None) ->
with open(dice_log_path, "w") as logfile:
try:
subprocess.run(
dice_call, cwd=working_dir, stdout=logfile, stderr=logfile, check=True
dice_call, cwd=dice_dir, stdout=logfile, stderr=logfile, check=True
)
except subprocess.CalledProcessError as e:
raise DiceExecutionError(
Expand All @@ -209,13 +326,13 @@ def _write_input_files(
num_up: int,
num_dn: int,
num_configurations: int,
working_dir: str | Path,
dice_dir: str | Path,
spin_sq: float,
max_iter: int,
) -> None:
"""Prepare the Dice inputs in the working directory."""
### Move the FCI Dump to working dir ###
shutil.copy(active_space_path, os.path.join(working_dir, "fcidump.txt"))
"""Prepare the Dice inputs in the specified directory."""
### Move the FCI Dump to dice dir ###
shutil.copy(active_space_path, os.path.join(dice_dir, "fcidump.txt"))

### Write the input.dat ###
num_elec = num_up + num_dn
Expand Down Expand Up @@ -263,19 +380,19 @@ def _write_input_files(
nocc,
dummy_det,
]
file1 = open(os.path.join(working_dir, "input.dat"), "w")
file1 = open(os.path.join(dice_dir, "input.dat"), "w")
file1.writelines(input_list)
file1.close()

### Write the determinants to working dir ###
### Write the determinants to dice dir ###
up_addr, dn_addr = addresses
bytes_up = _address_list_to_bytes(up_addr)
bytes_dn = _address_list_to_bytes(dn_addr)
file1 = open(os.path.join(working_dir, "AlphaDets.bin"), "wb") # type: ignore
file1 = open(os.path.join(dice_dir, "AlphaDets.bin"), "wb") # type: ignore
for bytestring in bytes_up:
file1.write(bytestring) # type: ignore
file1.close()
file1 = open(os.path.join(working_dir, "BetaDets.bin"), "wb") # type: ignore
file1 = open(os.path.join(dice_dir, "BetaDets.bin"), "wb") # type: ignore
for bytestring in bytes_dn:
file1.write(bytestring) # type: ignore
file1.close()
Expand Down
4 changes: 4 additions & 0 deletions releasenotes/notes/solve-fermion-b55bf481db6a2a51.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
features:
- |
Introduced a new function, :func:`qiskit_addon_dice_solver.solve_fermion`, which is intended to be compatible with `qiskit-addon-sqd <https://qiskit.github.io/qiskit-addon-sqd/>` workflows.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
packages=find_packages(),
package_data={"dice_solver": ["bin/Dice", "bin/*.so*"]},
include_package_data=True,
install_requires=["numpy", "pyscf"],
install_requires=["numpy", "pyscf", "qiskit-addon-sqd>=0.6"],
extras_require={
"dev": ["tox>=4.0", "pytest>=8.0"],
"docs": [
Expand Down