Skip to content

Commit

Permalink
Add a solve_fermion function for compatibility with SQD (#27)
Browse files Browse the repository at this point in the history
* Add a solve_fermion function for compatibility with SQD

* Add SQD as a dependency

* Update qiskit_addon_dice_solver/dice_solver.py

Co-authored-by: Kevin J. Sung <[email protected]>

* Add solve_fermion to docs

* docstring

* Update qiskit_addon_dice_solver/dice_solver.py

Co-authored-by: Kevin J. Sung <[email protected]>

* Update qiskit_addon_dice_solver/dice_solver.py

Co-authored-by: Kevin J. Sung <[email protected]>

* working --> temp

* Update qiskit_addon_dice_solver/dice_solver.py

Co-authored-by: Kevin J. Sung <[email protected]>

* Update qiskit_addon_dice_solver/dice_solver.py

Co-authored-by: Kevin J. Sung <[email protected]>

* readability

* Add a release note

---------

Co-authored-by: Kevin J. Sung <[email protected]>
  • Loading branch information
caleb-johnson and kevinsung authored Sep 25, 2024
1 parent 120dc46 commit 2154f0c
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 20 deletions.
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

0 comments on commit 2154f0c

Please sign in to comment.