Skip to content

Commit

Permalink
Merge pull request #91 from qiboteam/refactor-ham
Browse files Browse the repository at this point in the history
Refactor molecular Hamiltonian code
  • Loading branch information
damarkian authored Apr 1, 2024
2 parents 2dc47f6 + 1b5227c commit 2e60ecc
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 242 deletions.
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

0 comments on commit 2e60ecc

Please sign in to comment.