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

VQE supports initialization by computer #263

Merged
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
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is _refprep still needed in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe - it depends on what "this case" is.

If you want to use an excitation-based pool, it's needed. If you want to do PQE, it's needed. If you want to do moment corrections, it's needed. Otherwise, it isn't needed. Giving QForte the infrastructure to skip this step if not needed seemed beyond the scope of the PR.

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)
Comment on lines +25 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an additional test, would it be possible to check that the initial guess energy agrees with the expectation value of the Hamiltonian with respect to the state generated by the computer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial guess energy isn't stored on the wavefunction. I could do it, but I'd need to hardcode that value of that initial guess energy.


# 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)
Loading