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

Refactor molecular Hamiltonian code #91

Merged
merged 8 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions src/qibochem/driver/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
from qibochem.driver.hamiltonian import (
fermionic_hamiltonian,
parse_pauli_string,
qubit_hamiltonian,
symbolic_hamiltonian,
)
from qibochem.driver.molecule import Molecule
165 changes: 68 additions & 97 deletions src/qibochem/driver/hamiltonian.py
Original file line number Diff line number Diff line change
@@ -1,97 +1,68 @@
"""
Functions for obtaining and transforming the molecular Hamiltonian
"""

import openfermion
from qibo import hamiltonians
from qibo.symbols import X, Y, Z


def fermionic_hamiltonian(oei, tei, constant):
"""
Build molecular Hamiltonian as an InteractionOperator using the 1-/2- electron integrals

Args:
oei: 1-electron integrals in the MO basis
tei: 2-electron integrals in 2ndQ notation and MO basis
constant: Nuclear-nuclear repulsion, and inactive Fock energy if HF embedding used

Returns:
Molecular Hamiltonian as an InteractionOperator
"""
oei_so, tei_so = openfermion.ops.representations.get_tensors_from_integrals(oei, tei)
# tei_so already multiplied by 0.5, no need to include in InteractionOperator
return openfermion.InteractionOperator(constant, oei_so, tei_so)


def qubit_hamiltonian(fermion_hamiltonian, ferm_qubit_map):
"""
Converts the molecular Hamiltonian to a QubitOperator

Args:
fermion_hamiltonian: Molecular Hamiltonian as a InteractionOperator/FermionOperator
ferm_qubit_map: Which Fermion->Qubit mapping to use

Returns:
qubit_operator : Molecular Hamiltonian as a QubitOperator
"""
# Map the fermionic molecular Hamiltonian to a QubitHamiltonian
if ferm_qubit_map == "jw":
q_hamiltonian = openfermion.jordan_wigner(fermion_hamiltonian)
elif ferm_qubit_map == "bk":
q_hamiltonian = openfermion.bravyi_kitaev(fermion_hamiltonian)
else:
raise KeyError("Unknown fermion->qubit mapping!")
q_hamiltonian.compress() # Remove terms with v. small coefficients
return q_hamiltonian


def parse_pauli_string(pauli_string, coeff):
"""
Helper function: Converts a single Pauli string to a Qibo Symbol

Args:
pauli_string (tuple of tuples): Indicate what gates to apply onto which qubit
e.g. ((0, 'Z'), (2, 'Z'))
coeff (float): Coefficient of the Pauli string

Returns:
qibo.symbols.Symbol for a single Pauli string, e.g. -0.04*X0*X1*Y2*Y3
"""
# Dictionary for converting
xyz_to_symbol = {"X": X, "Y": Y, "Z": Z}
# Check that pauli_string is non-empty
if pauli_string:
# pauli_string format: ((0, 'Y'), (1, 'Y'), (3, 'X'))
qibo_pauli_string = 1.0
for p_letter in pauli_string:
qibo_pauli_string *= xyz_to_symbol[p_letter[1]](p_letter[0])
# Include coefficient after all symbols
qibo_pauli_string = coeff * qibo_pauli_string
else:
# Empty word, i.e. constant term in Hamiltonian
qibo_pauli_string = coeff
return qibo_pauli_string


def symbolic_hamiltonian(q_hamiltonian):
"""
Converts a OpenFermion QubitOperator to a Qibo SymbolicHamiltonian

Args:
q_hamiltonian: QubitOperator

Returns:
qibo.hamiltonians.SymbolicHamiltonian
"""
# Sums over each individual Pauli string in the QubitOperator
symbolic_ham = sum(
parse_pauli_string(pauli_string, coeff)
# Iterate over all operators
for operator in q_hamiltonian.get_operators()
# .terms gives one operator as a dictionary with one entry
for pauli_string, coeff in operator.terms.items()
)

# Map the QubitHamiltonian to a Qibo SymbolicHamiltonian and return it
return hamiltonians.SymbolicHamiltonian(symbolic_ham)
"""
Functions for obtaining and transforming the molecular Hamiltonian
"""

from functools import reduce

import openfermion
from qibo import symbols
from qibo.hamiltonians import SymbolicHamiltonian


def fermionic_hamiltonian(oei, tei, constant):
"""
Build molecular Hamiltonian as an InteractionOperator using the 1-/2- electron integrals

Args:
oei: 1-electron integrals in the MO basis
tei: 2-electron integrals in 2ndQ notation and MO basis
constant: Nuclear-nuclear repulsion, and inactive Fock energy if HF embedding used

Returns:
Molecular Hamiltonian as an InteractionOperator
"""
oei_so, tei_so = openfermion.ops.representations.get_tensors_from_integrals(oei, tei)
# tei_so already multiplied by 0.5, no need to include in InteractionOperator
return openfermion.InteractionOperator(constant, oei_so, tei_so)


def qubit_hamiltonian(fermion_hamiltonian, ferm_qubit_map):
"""
Converts the molecular Hamiltonian to a QubitOperator

Args:
fermion_hamiltonian: Molecular Hamiltonian as a InteractionOperator/FermionOperator
ferm_qubit_map: Which Fermion->Qubit mapping to use

Returns:
qubit_operator : Molecular Hamiltonian as a QubitOperator
"""
# Map the fermionic molecular Hamiltonian to a QubitHamiltonian
if ferm_qubit_map == "jw":
q_hamiltonian = openfermion.jordan_wigner(fermion_hamiltonian)
elif ferm_qubit_map == "bk":
q_hamiltonian = openfermion.bravyi_kitaev(fermion_hamiltonian)
else:
raise KeyError("Unknown fermion->qubit mapping!")
q_hamiltonian.compress() # Remove terms with v. small coefficients
return q_hamiltonian


def qubit_to_symbolic_hamiltonian(q_hamiltonian):
"""
Converts a OpenFermion QubitOperator to a Qibo SymbolicHamiltonian

Args:
q_hamiltonian: QubitOperator

Returns:
qibo.hamiltonians.SymbolicHamiltonian
"""
symbolic_ham = sum(
reduce(lambda x, y: x * y, (getattr(symbols, pauli_op)(qubit) for qubit, pauli_op in pauli_string), coeff)
# Sums over each individual Pauli string in the QubitOperator
for operator in q_hamiltonian.get_operators()
# .terms gives one operator as a single-item dictionary, e.g. {((1: "X"), (2: "Y")): 0.33}
for pauli_string, coeff in operator.terms.items()
)
return SymbolicHamiltonian(symbolic_ham)
11 changes: 5 additions & 6 deletions src/qibochem/driver/molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

import numpy as np
import openfermion
import qibo
from qibo.hamiltonians import SymbolicHamiltonian

from qibochem.driver.hamiltonian import (
fermionic_hamiltonian,
qubit_hamiltonian,
symbolic_hamiltonian,
qubit_to_symbolic_hamiltonian,
)


Expand Down Expand Up @@ -123,7 +122,7 @@ def run_pyscf(self, max_scf_cycles=50):
Args:
max_scf_cycles: Maximum number of SCF cycles in PySCF
"""
import pyscf
import pyscf # pylint: disable=C0415

# Set up and run PySCF calculation
geom_string = "".join("{} {:.6f} {:.6f} {:.6f} ; ".format(_atom[0], *_atom[1]) for _atom in self.geometry)
Expand Down Expand Up @@ -179,7 +178,7 @@ def run_pyscf(self, max_scf_cycles=50):
# output: Name of PSI4 output file. ``None`` suppresses the output on non-Windows systems,
# and uses ``psi4_output.dat`` otherwise
# """
# import psi4 # pylint: disable=import-error
# import psi4 # pylint: disable=C0415

# # PSI4 input string
# chgmul_string = f"{self.charge} {self.multiplicity} \n"
Expand Down Expand Up @@ -413,7 +412,7 @@ def hamiltonian(
return ham
if ham_type in ("s", "sym"):
# Qibo SymbolicHamiltonian
return symbolic_hamiltonian(ham)
return qubit_to_symbolic_hamiltonian(ham)
raise NameError(f"Unknown {ham_type}!") # Shouldn't ever reach here

@staticmethod
Expand All @@ -427,7 +426,7 @@ def eigenvalues(hamiltonian):
``SymbolicHamiltonian`` (not recommended)
"""
if isinstance(hamiltonian, (openfermion.FermionOperator, openfermion.QubitOperator)):
from scipy.sparse import linalg
from scipy.sparse import linalg # pylint: disable=C0415

hamiltonian_matrix = openfermion.get_sparse_operator(hamiltonian)
# k argument in eigsh will depend on the size of the Hamiltonian
Expand Down
13 changes: 6 additions & 7 deletions tests/test_expectation_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,20 @@


@pytest.mark.parametrize(
"terms,gates_to_add,expected,threshold",
"terms,gates_to_add,expected",
[
(Z(0), [gates.X(0)], -1.0, None),
(Z(0) * Z(1), [gates.X(0)], -1.0, None),
(X(0), [gates.H(0)], 1.0, None),
(X(0), [gates.X(0), gates.X(0)], 0.0, 0.05),
(Z(0), [gates.X(0)], -1.0),
(Z(0) * Z(1), [gates.X(0)], -1.0),
(X(0), [gates.H(0)], 1.0),
],
)
def test_expectation_samples(terms, gates_to_add, expected, threshold):
def test_expectation_samples(terms, gates_to_add, expected):
"""Test from_samples functionality of expectation function with various Hamiltonians"""
hamiltonian = SymbolicHamiltonian(terms)
circuit = Circuit(2)
circuit.add(gates_to_add)
result = expectation(circuit, hamiltonian, from_samples=True)
assert pytest.approx(result, abs=threshold) == expected
assert result == expected


def test_measurement_basis_rotations_error():
Expand Down
Loading
Loading