Skip to content

Commit

Permalink
VQE supports initialization by computer (#263)
Browse files Browse the repository at this point in the history
* VQE supports initialization by computer
  • Loading branch information
JonathonMisiewicz authored May 28, 2024
1 parent 2035e07 commit 243aa2d
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 39 deletions.
51 changes: 42 additions & 9 deletions src/qforte/abc/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,31 @@ def __init__(
self._refprep = build_refprep(self._ref)
self._Uprep = reference

elif self._state_prep_type == "computer":
if not isinstance(reference, qf.Computer):
raise ValueError("computer reference must be a Computer.")
if not fast:
raise ValueError(
"`self._fast = False` specifies not to skip steps, but `self._state_prep_type = computer` specifies to skip state initialization. That's inconsistent."
)
if reference.get_nqubit() != len(system.hf_reference):
raise ValueError(
f"Computer needs {len(system.hf_reference)} qubits, found {reference.get_nqubit()}."
)
if (
not hasattr(self, "computer_initializable")
or not self.computer_initializable
):
raise ValueError("Class cannot be initialized with a computer.")

self._ref = system.hf_reference
self._refprep = build_refprep(self._ref)
self._Uprep = qf.Circuit()
self.computer = reference

else:
raise ValueError(
"QForte only suppors references as occupation lists and Circuits."
"QForte only supports references as occupation lists, Circuits, or Computers."
)

self._nqb = len(self._ref)
Expand Down Expand Up @@ -284,21 +306,26 @@ def fill_pool(self):
len(operator.jw_transform().terms()) for _, operator in self._pool_obj
]

def measure_energy(self, Ucirc):
def measure_energy(self, Ucirc, computer=None):
"""
This function returns the energy expectation value of the state
Uprep|0>.
Ucirc|Ψ>.
Parameters
----------
Ucirc : Circuit
The state preparation circuit.
"""
if self._fast:
myQC = qforte.Computer(self._nqb)
myQC.apply_circuit(Ucirc)
val = np.real(myQC.direct_op_exp_val(self._qb_ham))
if computer is None:
computer = qf.Computer(self._nqb)
computer.apply_circuit(Ucirc)
val = np.real(computer.direct_op_exp_val(self._qb_ham))
else:
if compute is not None:
raise TypeError(
"measure_energy in slow mode does not support custom Computer."
)
Exp = qforte.Experiment(self._nqb, Ucirc, self._qb_ham, 2000)
val = Exp.perfect_experimental_avg()

Expand Down Expand Up @@ -431,8 +458,8 @@ def __init__(
def energy_feval(self, params):
"""
This function returns the energy expectation value of the state
Uprep(params)|0>, where params are parameters that can be optimized
for some purpouse such as energy minimizaiton.
Uprep(params)|Ψ>, where params are parameters that can be optimized
for some purpouse such as energy minimization.
Parameters
----------
Expand All @@ -441,7 +468,13 @@ def energy_feval(self, params):
the state preparation circuit.
"""
Ucirc = self.build_Uvqc(amplitudes=params)
Energy = self.measure_energy(Ucirc)
Energy = self.measure_energy(Ucirc, self.get_initial_computer())

self._curr_energy = Energy
return Energy

def get_initial_computer(self) -> qf.Computer:
if hasattr(self, "computer"):
return qf.Computer(self.computer)
else:
return qf.Computer(self._nqb)
49 changes: 19 additions & 30 deletions src/qforte/abc/uccvqeabc.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class UCCVQE(UCC, VQE):
.. math::
E(\\mathbf{t}) = \\langle \\Phi_0 | \\hat{U}^\\dagger(\\mathbf{\\mathbf{t}}) \\hat{H} \\hat{U}(\\mathbf{\\mathbf{t}}) | \\Phi_0 \\rangle
using a disentagled UCC type ansatz
using a disentangled UCC type ansatz
.. math::
\\hat{U}(\\mathbf{t}) = \\prod_\\mu e^{t_\\mu (\\hat{\\tau}_\\mu - \\hat{\\tau}_\\mu^\\dagger)},
Expand Down Expand Up @@ -71,6 +71,7 @@ class UCCVQE(UCC, VQE):
"""

def __init__(self, *args, **kwargs):
self.computer_initializable = True
super().__init__(*args, **kwargs)

@abstractmethod
Expand Down Expand Up @@ -104,7 +105,7 @@ def measure_operators(self, operators, Ucirc, idxs=[]):
"""

if self._fast:
myQC = qforte.Computer(self._nqb)
myQC = self.get_initial_computer()
myQC.apply_circuit(Ucirc)
if not idxs:
grads = myQC.direct_oppl_exp_val(operators)
Expand All @@ -120,7 +121,8 @@ def measure_operators(self, operators, Ucirc, idxs=[]):

def measure_gradient(self, params=None):
"""Returns the disentangled (factorized) UCC gradient, using a
recursive approach.
recursive approach, as described in Section D of the Appendix of
10.1038/s41467-019-10988-2
Parameters
----------
Expand All @@ -140,18 +142,11 @@ def measure_gradient(self, params=None):
else:
Utot = self.build_Uvqc(params)

qc_psi = qforte.Computer(
self._nqb
) # build | sig_N > according ADAPT-VQE analytical grad section
qc_psi = self.get_initial_computer()
qc_psi.apply_circuit(Utot)
qc_sig = qforte.Computer(
self._nqb
) # build | psi_N > according ADAPT-VQE analytical grad section
psi_i = copy.deepcopy(qc_psi.get_coeff_vec())
qc_sig.set_coeff_vec(
copy.deepcopy(psi_i)
) # not sure if copy is faster or reapplication of state
qc_sig = qf.Computer(qc_psi)
qc_sig.apply_operator(self._qb_ham)
qc_temp = qf.Computer(qc_psi)

mu = M - 1

Expand All @@ -161,15 +156,13 @@ def measure_gradient(self, params=None):
)
Kmu_prev.mult_coeffs(self._pool_obj[self._tops[mu]][0])

qc_psi.apply_operator(Kmu_prev)
qc_temp.apply_operator(Kmu_prev)
grads[mu] = 2.0 * np.real(
np.vdot(qc_sig.get_coeff_vec(), qc_psi.get_coeff_vec())
np.vdot(qc_sig.get_coeff_vec(), qc_temp.get_coeff_vec())
)

# reset Kmu_prev |psi_i> -> |psi_i>
qc_psi.set_coeff_vec(copy.deepcopy(psi_i))

for mu in reversed(range(M - 1)):
qc_temp = qf.Computer(qc_psi)
# mu => N-1 => M-2
# mu+1 => N => M-1
# Kmu => KN-1
Expand Down Expand Up @@ -243,15 +236,14 @@ def measure_gradient(self, params=None):

qc_sig.apply_circuit(Umu)
qc_psi.apply_circuit(Umu)
psi_i = copy.deepcopy(qc_psi.get_coeff_vec())
qc_temp = qf.Computer(qc_psi)

qc_psi.apply_operator(Kmu)
qc_temp.apply_operator(Kmu)
grads[mu] = 2.0 * np.real(
np.vdot(qc_sig.get_coeff_vec(), qc_psi.get_coeff_vec())
np.vdot(qc_sig.get_coeff_vec(), qc_temp.get_coeff_vec())
)

# reset Kmu |psi_i> -> |psi_i>
qc_psi.set_coeff_vec(copy.deepcopy(psi_i))
Kmu_prev = Kmu

np.testing.assert_allclose(np.imag(grads), np.zeros_like(grads), atol=1e-7)
Expand All @@ -269,25 +261,22 @@ def measure_gradient3(self):
raise ValueError("self._fast must be True for gradient measurement.")

Utot = self.build_Uvqc()
qc_psi = qforte.Computer(self._nqb)
qc_psi = self.get_initial_computer()
qc_psi.apply_circuit(Utot)
psi_i = copy.deepcopy(qc_psi.get_coeff_vec())

qc_sig = qforte.Computer(self._nqb)
# TODO: Check if it's faster to recompute psi_i or copy it.
qc_sig.set_coeff_vec(copy.deepcopy(psi_i))
qc_sig = qforte.Computer(qc_psi)
qc_sig.apply_operator(self._qb_ham)

grads = np.zeros(len(self._pool_obj))

for mu, (coeff, operator) in enumerate(self._pool_obj):
qc_temp = qf.Computer(qc_psi)
Kmu = operator.jw_transform(self._qubit_excitations)
Kmu.mult_coeffs(coeff)
qc_psi.apply_operator(Kmu)
qc_temp.apply_operator(Kmu)
grads[mu] = 2.0 * np.real(
np.vdot(qc_sig.get_coeff_vec(), qc_psi.get_coeff_vec())
np.vdot(qc_sig.get_coeff_vec(), qc_temp.get_coeff_vec())
)
qc_psi.set_coeff_vec(copy.deepcopy(psi_i))

np.testing.assert_allclose(np.imag(grads), np.zeros_like(grads), atol=1e-7)

Expand Down
46 changes: 46 additions & 0 deletions tests/test_computer_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import numpy as np
from pytest import approx
from qforte import ADAPTVQE, UCCNVQE
from qforte import Circuit, Computer, gate, system_factory

import os

THIS_DIR = os.path.dirname(os.path.abspath(__file__))
data_path = os.path.join(THIS_DIR, "H4-sto6g-075a.json")


class TestComputerInit:
# @mark.skip(reason="long")
def test_H4_VQE(self):
mol = system_factory(
system_type="molecule",
build_type="external",
basis="sto-6g",
filename=data_path,
)
nqubits = len(mol.hf_reference)
fci_energy = -2.162897881184882

computer = Computer(nqubits)
coeff_vec = np.zeros(2**nqubits)
coeff_vec[int("00001111", 2)] = 1
coeff_vec[int("00110011", 2)] = 0.2
coeff_vec[int("00111100", 2)] = 0.1
coeff_vec[int("11001100", 2)] = 0.04
coeff_vec /= np.linalg.norm(coeff_vec)
computer.set_coeff_vec(coeff_vec)

# Analytic and fin dif gradients agree
analytic = UCCNVQE(mol, reference=computer, state_prep_type="computer")
analytic.run(use_analytic_grad=False, pool_type="SD")
findif = UCCNVQE(mol, reference=computer, state_prep_type="computer")
findif.run(use_analytic_grad=True, pool_type="SD")
assert analytic.get_gs_energy() == approx(findif.get_gs_energy(), abs=1.0e-8)

# Computer-based and non-compute based agree
hf = ADAPTVQE(mol)
hf.run(use_analytic_grad=True, pool_type="GSD", avqe_thresh=1e-5)
comp = ADAPTVQE(mol, reference=computer, state_prep_type="computer")
comp.run(use_analytic_grad=True, pool_type="GSD", avqe_thresh=1e-5)
assert hf.get_gs_energy() == approx(comp.get_gs_energy(), abs=1.0e-8)
assert hf.get_gs_energy() == approx(fci_energy, abs=1.0e-8)

0 comments on commit 243aa2d

Please sign in to comment.