Use this file to discover all available pages before exploring further.
View on GitHub
Open this notebook in GitHub to run it yourself
This notebook is based on Ref. [1].Given an efficient block-encoding for a Hamiltonian, this algorithm preforms an efficient Quantum Phase Estimation.The core quantum function of this model uses almost all Qmod built-in operations: control, power, within_apply, and invert.The algorithm assumes we have the block-encoding of a matrix HU(s,m)−H=(H/s∗∗∗),with m being the size of the block variable and s some scaling factor.Given this quantum function, we can define the following unitary (usually called the Szegedy quantum walk operator [2]):W≡Π∣0⟩mU(s,m)−H,where Π∣0⟩m is a reflection operator about the block state
The spectrum of the walk operator has a nice relation to the spectrum of the block-encoded Hamiltonian [3]:
{eigenvalues:}e{±iarccos(λ/s)},{witheigenvectors:}∣φ{±}{λ}⟩≡1{}{{2}}(∣v{λ}⟩∣0⟩m±i∣⊥{λ}⟩),(1)
where ∣vλ⟩ is an eigenstate of the Hamiltonian H with an eigenvalue λ.
Namely, the eigenphases of H are related by some nonlinear function (arccos) to the eigenvalues of H.The algorithm works under the assumption that the block-encoding unitary itself is also Hermitian, that is, U(s,m)−H is Unitary and Hermitian.
We start with defining some utility functions that are not implemented as part of Classiq, and might be included in the future.These functions are used for the specific block-encoding used in this notebook.
!pip install -qq "classiq[chemistry]"
import matplotlib.pyplot as pltimport numpy as npfrom classiq import *from classiq.applications.chemistry.op_utils import qubit_op_to_qmod
from sympy import fwhtfrom classiq.open_library.functions.state_preparation import apply_phase_tabledef get_graycode(size, i) -> int: if i == 2**size: return get_graycode(size, 0) return i ^ (i >> 1)def get_graycode_angles_wh(size, angles): transformed_angles = fwht(np.array(angles) / 2**size) return [transformed_angles[get_graycode(size, j)] for j in range(2**size)]def get_graycode_ctrls(size): return [ (get_graycode(size, i) ^ get_graycode(size, i + 1)).bit_length() - 1 for i in range(2**size) ]@qfuncdef multiplex_ra(a_y: float, a_z: float, angles: list[float], qba: QArray, ind: QBit): assert a_y**2 + a_z**2 == 1 # TODO support general (0,a_y,a_z) rotation assert ( a_z == 1.0 or a_y == 1.0 ), "currently only strict y or z rotations are supported" size = max(1, (len(angles) - 1).bit_length()) extended_angles = angles + [0] * (2**size - len(angles)) transformed_angles = get_graycode_angles_wh(size, extended_angles) controllers = get_graycode_ctrls(size) for k in range(2**size): if a_z == 0.0: RY(transformed_angles[k], ind) else: RZ(transformed_angles[k], ind) skip_control(lambda: CX(qba[controllers[k]], ind))@qfuncdef lcu_paulis_graycode(terms: list[SparsePauliTerm], data: QArray, block: QArray): n_qubits = data.len n_terms = len(terms) table_z = np.zeros([n_qubits, n_terms]) table_y = np.zeros([n_qubits, n_terms]) probs = [abs(term.coefficient) for term in terms] + [0.0] * (2**block.len - n_terms) hamiltonian_coeffs = np.angle([term.coefficient for term in terms]).tolist() + [ 0.0 ] * (2**block.len - n_terms) accumulated_phase = np.zeros(2**block.len).tolist() for k in range(n_terms): for pauli in terms[k].paulis: if pauli.pauli == Pauli.Z: table_z[pauli.index, k] = -np.pi accumulated_phase[k] += np.pi / 2 elif pauli.pauli == Pauli.Y: table_y[pauli.index, k] = -np.pi accumulated_phase[k] += np.pi / 2 elif pauli.pauli == Pauli.X: table_z[pauli.index, k] = -np.pi table_y[pauli.index, k] = np.pi accumulated_phase[k] += np.pi / 2 def select_graycode(block: QArray, data: QArray): for i in range(n_qubits): multiplex_ra(0, 1, table_z[i, :], block, data[i]) multiplex_ra(1, 0, table_y[i, :], block, data[i]) apply_phase_table( [p1 - p2 for p1, p2 in zip(hamiltonian_coeffs, accumulated_phase)], block ) within_apply( lambda: inplace_prepare_state(probs, 0.0, block), lambda: select_graycode(block, data), )
from openfermion.chem import MolecularDatafrom openfermionpyscf import run_pyscfbasis = "sto-3g" # Basis setmultiplicity = 1 # Singlet state S=0charge = 0 # Neutral moleculemolecule = MolecularData(molecule_H2_geometry, basis, multiplicity, charge)molecule = run_pyscf( molecule, run_fci=True, # relevant for small, classically solvable problems)
from classiq.applications.chemistry.mapping import FermionToQubitMapperfrom classiq.applications.chemistry.problems import FermionHamiltonianProblem# Define a Hamiltonian in an active spaceproblem = FermionHamiltonianProblem.from_molecule(molecule=molecule)mapper = FermionToQubitMapper()qubit_hamiltonian = mapper.map(problem.fermion_hamiltonian)print("Your Hamiltonian is", qubit_hamiltonian, sep="\n")
We use the reflect_around_zero function from Classiq’s open library to define a walk operator function with the declaration below.This function implements I−2∣0⟩⟨0∣, so we must insert a minus phase (This can be done by adding a minus sign using the phase function).
We define a classical function that takes the eigenphases of the Walk operator, and returns the (scaled) eigenvalues of H, according to equation (1) above.
from classiq.applications.chemistry.hartree_fock import get_hf_state# We take the Hartree Fock statehf_state = get_hf_state(problem, mapper)QPE_SIZE = 5
Classical solution:, -1.1373060357533995 Ha probability to measure the block variable at state zero: 0.50732421875 Energy with maximal probability: -1.102846988772674 Ha
We construct the model design according to Ref. [1], shown in the figure below, with RL being the reflection around zero operation, and χm is taken as the usual Hadamard transform for the phase initialization:
Classical solution:, -1.1373060357533995 Ha probability to measure the block variable at state zero: 0.47119140625 Energy with maximal probability: -1.102846988772674 Ha
print("For the naive QPE on the walk operator:")print(f"depth: {qprog_qpe_naive.transpiled_circuit.depth}")print("=" * 40)print("For the optimized QPE on the walk operator:")print(f"depth: {qprog_qpe_walk.transpiled_circuit.depth}")print("=" * 40)
Output:
For the naive QPE on the walk operator: depth: 19686 ======================================== For the optimized QPE on the walk operator: depth: 4724 ========================================