Skip to content

Commit

Permalink
quantumchemistry: added encoding transformations (#345)
Browse files Browse the repository at this point in the history
* quantum chemistry: added encoding transformations
* Update CI
  • Loading branch information
benni-tec authored Jun 28, 2024
1 parent 477af27 commit 981c0a7
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 51 deletions.
188 changes: 141 additions & 47 deletions src/tequila/quantumchemistry/encodings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
Collections of Fermion-to-Qubit encodings known to tequila
Most are Interfaces to OpenFermion
"""
import abc

from tequila import TequilaException
from tequila.circuit.circuit import QCircuit
from tequila.circuit.gates import X
from tequila.circuit.gates import X, CNOT
from tequila.hamiltonian.qubit_hamiltonian import QubitHamiltonian
import openfermion
import numpy


def known_encodings():
# convenience for testing and I/O
encodings= {
"JordanWigner":JordanWigner,
"BravyiKitaev":BravyiKitaev,
encodings = {
"JordanWigner": JordanWigner,
"BravyiKitaev": BravyiKitaev,
"BravyiKitaevFast": BravyiKitaevFast,
"BravyiKitaevTree": BravyiKitaevTree,
"TaperedBravyiKitaev": TaperedBravyKitaev
Expand All @@ -22,43 +27,44 @@ def known_encodings():
"ReorderedBravyiKitaev": lambda **kwargs: BravyiKitaev(up_then_down=True, **kwargs),
"ReorderedBravyiKitaevTree": lambda **kwargs: BravyiKitaevTree(up_then_down=True, **kwargs),
}
return {k.replace("_","").replace("-","").upper():v for k,v in encodings.items()}
return {k.replace("_", "").replace("-", "").upper(): v for k, v in encodings.items()}

class EncodingBase:

class EncodingBase(metaclass=abc.ABCMeta):
# true if the encoding is fully integrated
# false: can only do special things (like building the Hamiltionian)
# but is not consistent with UCC gate generation
_ucc_support=False
_ucc_support = False

@property
def supports_ucc(self):
return self._ucc_support

@property
def name(self):
prefix=""
prefix = ""
if self.up_then_down:
prefix="Reordered"
prefix = "Reordered"
if hasattr(self, "_name"):
return prefix+self._name
return prefix + self._name
else:
return prefix+type(self).__name__
return prefix + type(self).__name__

def __init__(self, n_electrons, n_orbitals, up_then_down=False, *args, **kwargs):
self.n_electrons = n_electrons
self.n_orbitals = n_orbitals
self.up_then_down = up_then_down

def __call__(self, fermion_operator:openfermion.FermionOperator, *args, **kwargs) -> QubitHamiltonian:
def __call__(self, fermion_operator: openfermion.FermionOperator, *args, **kwargs) -> QubitHamiltonian:
"""
:param fermion_operator:
an openfermion FermionOperator
:return:
The openfermion QubitOperator of this class ecoding
"""
if self.up_then_down:
op = openfermion.reorder(operator=fermion_operator, order_function=openfermion.up_then_down, num_modes=2*self.n_orbitals)
op = openfermion.reorder(operator=fermion_operator, order_function=openfermion.up_then_down,
num_modes=2 * self.n_orbitals)
else:
op = fermion_operator

Expand All @@ -73,18 +79,18 @@ def up(self, i):
if self.up_then_down:
return i
else:
return 2*i
return 2 * i

def down(self, i):
if self.up_then_down:
return i+self.n_orbitals
return i + self.n_orbitals
else:
return 2*i+1
return 2 * i + 1

def do_transform(self, fermion_operator:openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
def do_transform(self, fermion_operator: openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
raise Exception("{}::do_transform: called base class".format(type(self).__name__))

def map_state(self, state:list, *args, **kwargs) -> list:
def map_state(self, state: list, *args, **kwargs) -> list:
"""
Expects a state in spin-orbital ordering
Returns the corresponding qubit state in the class encoding
Expand Down Expand Up @@ -112,7 +118,7 @@ def map_state(self, state:list, *args, **kwargs) -> list:
# default is a lazy workaround, but it workds
n_qubits = 2 * self.n_orbitals

spin_orbitals = sorted([i for i,x in enumerate(state) if int(x)==1])
spin_orbitals = sorted([i for i, x in enumerate(state) if int(x) == 1])

string = "1.0 ["
for i in spin_orbitals:
Expand All @@ -128,26 +134,82 @@ def map_state(self, state:list, *args, **kwargs) -> list:
key = list(wfn.keys())[0].array
return key

def hcb_to_me(self, *args, **kwargs):
return None
@abc.abstractmethod
def me_to_jw(self) -> QCircuit:
"""
This method needs to be implemented to enable default conversions via Jordan-Wigner
"""
pass

# independent conversion methods, these are used for default conversions
# arXiv:1808.10402 IV. B. 2, Eq. 57
# original: https://doi.org/10.1063/1.4768229
def _jw_to_bk(self) -> QCircuit:
U = QCircuit() # Constructs empty circuit

flipper = False
for i in range(self.n_orbitals * 2):
# even qubits only hold their own value
if i % 2 == 0:
continue

# sum always includes the last qubit
U += CNOT(control=i - 1, target=i)

# every second odd qubit ties together with the last odd qubit
if flipper:
U += CNOT(control=i - 2, target=i)

flipper = not flipper

# we have now created the 4x4 blocks on the diagonal of this operators matrix

# every power of 2 connects to the last power of 2
# this corresponds to the last row in the recursive definitions being all 1s
x = numpy.log2(i + 1)
if x.is_integer() and x >= 3:
x = int(x)
U += CNOT(control=2 ** (x - 1) - 1, target=i)

return U

def _hcb_to_jw(self):
U = QCircuit()
for i in range(self.n_orbitals):
U += X(target=self.down(i), control=self.up(i))
return U

# Convenience Methods
def jw_to_me(self) -> QCircuit:
return self.me_to_jw().dagger()

def me_to_bk(self) -> QCircuit:
return self.me_to_jw() + self._jw_to_bk()

def bk_to_me(self) -> QCircuit:
return self.me_to_bk().dagger()

def hcb_to_me(self) -> QCircuit:
return self._hcb_to_jw() + self.jw_to_me()

def __str__(self):
return type(self).__name__


class JordanWigner(EncodingBase):
"""
OpenFermion::jordan_wigner
"""
_ucc_support=True
_ucc_support = True

def do_transform(self, fermion_operator:openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
def do_transform(self, fermion_operator: openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
return openfermion.jordan_wigner(fermion_operator, *args, **kwargs)

def map_state(self, state:list, *args, **kwargs):
state = state + [0]*(self.n_orbitals-len(state))
result = [0]*len(state)
def map_state(self, state: list, *args, **kwargs):
state = state + [0] * (self.n_orbitals - len(state))
result = [0] * len(state)
if self.up_then_down:
return [state[2*i] for i in range(self.n_orbitals)] + [state[2*i+1] for i in range(self.n_orbitals)]
return [state[2 * i] for i in range(self.n_orbitals)] + [state[2 * i + 1] for i in range(self.n_orbitals)]
else:
return state

Expand All @@ -157,77 +219,109 @@ def hcb_to_me(self, *args, **kwargs):
U += X(target=self.down(i), control=self.up(i))
return U

def me_to_jw(self) -> QCircuit:
return QCircuit()

def jw_to_me(self) -> QCircuit:
return QCircuit()


class BravyiKitaev(EncodingBase):
"""
Uses OpenFermion::bravyi_kitaev
"""

_ucc_support=True
_ucc_support = True

def do_transform(self, fermion_operator: openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
return openfermion.bravyi_kitaev(fermion_operator, n_qubits=self.n_orbitals * 2)

def me_to_jw(self) -> QCircuit:
return self._jw_to_bk().dagger()

def do_transform(self, fermion_operator:openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
return openfermion.bravyi_kitaev(fermion_operator, n_qubits=self.n_orbitals*2)
def jw_to_me(self) -> QCircuit:
return self._jw_to_bk()

def bk_to_me(self) -> QCircuit:
return QCircuit()

def me_to_bk(self) -> QCircuit:
return QCircuit()


class BravyiKitaevTree(EncodingBase):
"""
Uses OpenFermion::bravyi_kitaev_tree
"""

_ucc_support=True
_ucc_support = True

def do_transform(self, fermion_operator: openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
return openfermion.bravyi_kitaev_tree(fermion_operator, n_qubits=self.n_orbitals * 2)

def me_to_jw(self) -> QCircuit:
raise TequilaException("{}::me_to_jw: unimplemented".format(type(self).__name__))

def do_transform(self, fermion_operator:openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
return openfermion.bravyi_kitaev_tree(fermion_operator, n_qubits=self.n_orbitals*2)

class BravyiKitaevFast(EncodingBase):
"""
Uses OpenFermion::bravyi_kitaev_tree
"""

_ucc_support=False
_ucc_support = False

def do_transform(self, fermion_operator:openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
def do_transform(self, fermion_operator: openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
n_qubits = openfermion.count_qubits(fermion_operator)
if n_qubits != self.n_orbitals*2:
raise Exception("BravyiKitaevFast transformation currently only possible for full Hamiltonians (no UCC generators).\nfermion_operator was {}".format(fermion_operator))
if n_qubits != self.n_orbitals * 2:
raise Exception(
"BravyiKitaevFast transformation currently only possible for full Hamiltonians (no UCC generators).\nfermion_operator was {}".format(
fermion_operator))
op = openfermion.get_interaction_operator(fermion_operator)
return openfermion.bravyi_kitaev_fast(op)

class TaperedBravyKitaev(EncodingBase):
def me_to_jw(self) -> QCircuit:
raise TequilaException("{}::me_to_jw: unimplemented".format(type(self).__name__))

_ucc_support=False

class TaperedBravyKitaev(EncodingBase):
_ucc_support = False

"""
Uses OpenFermion::symmetry_conserving_bravyi_kitaev (tapered bravyi_kitaev_tree arxiv:1701.07072)
Reduces Hamiltonian by 2 qubits
See OpenFermion Documentation for more
Does not work for UCC generators yet
"""

def __init__(self, n_electrons, n_orbitals, active_fermions=None, active_orbitals=None, *args, **kwargs):
if active_fermions is None:
self.active_fermions = n_electrons
else:
self.active_fermions = active_fermions

if active_orbitals is None:
self.active_orbitals = n_orbitals*2 # in openfermion those are spin-orbitals
self.active_orbitals = n_orbitals * 2 # in openfermion those are spin-orbitals
else:
self.active_orbitals = active_orbitals

if "up_then_down" in kwargs:
raise Exception("Don't pass up_then_down argument to {}, it can't be changed".format(type(self).__name__))
super().__init__(n_orbitals=n_orbitals, n_electrons=n_electrons, up_then_down=False, *args, **kwargs)

def do_transform(self, fermion_operator:openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
if openfermion.count_qubits(fermion_operator) != self.n_orbitals*2:
def do_transform(self, fermion_operator: openfermion.FermionOperator, *args, **kwargs) -> openfermion.QubitOperator:
if openfermion.count_qubits(fermion_operator) != self.n_orbitals * 2:
raise Exception("TaperedBravyiKitaev not ready for UCC generators yet")
return openfermion.symmetry_conserving_bravyi_kitaev(fermion_operator, active_orbitals=self.active_orbitals, active_fermions=self.active_fermions)
return openfermion.symmetry_conserving_bravyi_kitaev(fermion_operator, active_orbitals=self.active_orbitals,
active_fermions=self.active_fermions)

def map_state(self, state:list, *args, **kwargs):
non_tapered_trafo = BravyiKitaevTree(up_then_down=True, n_electrons=self.n_electrons, n_orbitals=self.n_orbitals)
def map_state(self, state: list, *args, **kwargs):
non_tapered_trafo = BravyiKitaevTree(up_then_down=True, n_electrons=self.n_electrons,
n_orbitals=self.n_orbitals)
key = non_tapered_trafo.map_state(state=state, *args, **kwargs)
n_qubits = self.n_orbitals*2
n_qubits = self.n_orbitals * 2
active_qubits = [i for i in range(n_qubits) if i not in [n_qubits - 1, n_qubits // 2 - 1]]
key = [key[i] for i in active_qubits]
return key


def me_to_jw(self) -> QCircuit:
raise TequilaException("{}::me_to_jw: unimplemented".format(type(self).__name__))
5 changes: 4 additions & 1 deletion src/tequila/quantumchemistry/madness_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,10 @@ def make_upccgsd_ansatz(self, name="UpCCGSD", label=None, direct_compiling=None,
"""
# check if the used qubit encoding has a hcb transformation
have_hcb_trafo = self.transformation.hcb_to_me() is not None
try:
have_hcb_trafo = self.transformation.hcb_to_me() is not None
except:
have_hcb_trafo = False
name = name.upper()

# Default Method
Expand Down
8 changes: 7 additions & 1 deletion src/tequila/quantumchemistry/qc_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1247,7 +1247,13 @@ def make_upccgsd_ansatz(self,
indices = self.make_upccgsd_indices(key=name)

# check if the used qubit encoding has a hcb transformation
have_hcb_trafo = self.transformation.hcb_to_me() is not None
have_hcb_trafo = True
try:
if self.transformation.hcb_to_me() is None:
have_hcb_trafo = False
except:
have_hcb_trafo = False


# consistency checks for optimization
if have_hcb_trafo and hcb_optimization is None and include_reference:
Expand Down
8 changes: 7 additions & 1 deletion tests/test_chemistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,8 +587,14 @@ def test_spa_consistency(geometry, name, optimize, transformation):
mol = tq.Molecule(geometry=geometry, basis_set="sto-3g",
transformation=transformation).use_native_orbitals()

if mol.transformation.hcb_to_me() is None:
# test compares HCB and non-HCB SPA implementations
# conversion needs to be implemented for comparisson
# if not, HCB is not used in circuit optimization anyways
try:
mol.transformation.hcb_to_me()
except:
return

# doesn't need to make physical sense for the test
edges = []
i = 0
Expand Down
2 changes: 1 addition & 1 deletion tests/test_chemistry_madness.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def test_madness_upccgsd(trafo):
E = tq.ExpectationValue(H=H, U=U)
assert (len(E.extract_variables()) == 2)
variables = result.variables
if "bravyi" in trafo.lower():
if "bravyikitaevtree" in trafo.lower():
# signs of angles change in BK compared to JW-like HCB
variables = {k: -v for k, v in variables.items()}
energy = tq.simulate(E, variables)
Expand Down

0 comments on commit 981c0a7

Please sign in to comment.