def test_step_and_cost_with_grad_fn_grouped_input(self, tol): """Test that the correct cost and update is returned via the step_and_cost method for the QNG optimizer when providing an explicit grad_fn. Using a circuit with a single input containing all parameters.""" dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(params): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) return qml.expval(qml.PauliZ(0)) var = np.array([0.011, 0.012]) opt = qml.QNGOptimizer(stepsize=0.01) # With autograd gradient function grad_fn = qml.grad(circuit) step1, cost1 = opt.step_and_cost(circuit, var, grad_fn=grad_fn) step2 = opt.step(circuit, var, grad_fn=grad_fn) # With more custom gradient function, forward has to be computed explicitly. grad_fn = lambda param: np.array(qml.grad(circuit)(param)) step3, cost2 = opt.step_and_cost(circuit, var, grad_fn=grad_fn) step4 = opt.step(circuit, var, grad_fn=grad_fn) expected_step = var - opt.stepsize * 4 * grad_fn(var) expected_cost = circuit(var) for step in [step1, step2, step3, step3]: assert np.allclose(step, expected_step) assert np.isclose(cost1, expected_cost) assert np.isclose(cost2, expected_cost)
def test_deprecate_diag_approx(self, diag_approx, approx_expected): """Test that using the diag_approx argument raises a warning due to deprecation.""" with pytest.warns(UserWarning, match="keyword argument diag_approx is deprecated"): opt = qml.QNGOptimizer(0.1, diag_approx=True) assert opt.approx == "diag"
def test_single_qubit_vqe_using_expval_h_multiple_input_params( self, tol, recwarn): """Test single-qubit VQE by returning qml.expval(H) in the QNode and check for the correct QNG value every step, the correct parameter updates, and correct cost after 200 steps""" dev = qml.device("default.qubit", wires=1) coeffs = [1, 1] obs_list = [qml.PauliX(0), qml.PauliZ(0)] H = qml.Hamiltonian(coeffs=coeffs, observables=obs_list) @qml.qnode(dev) def circuit(x, y, wires=0): qml.RX(x, wires=wires) qml.RY(y, wires=wires) return qml.expval(H) eta = 0.01 x = np.array(0.011, requires_grad=True) y = np.array(0.022, requires_grad=True) def gradient(params): """Returns the gradient""" da = -np.sin(params[0]) * (np.cos(params[1]) + np.sin(params[1])) db = np.cos(params[0]) * (np.cos(params[1]) - np.sin(params[1])) return np.array([da, db]) eta = 0.01 num_steps = 200 opt = qml.QNGOptimizer(eta) # optimization for 200 steps total for t in range(num_steps): theta = np.array([x, y]) x, y = opt.step(circuit, x, y) # check metric tensor res = opt.metric_tensor exp = np.diag([0.25, (np.cos(x)**2) / 4]) assert np.allclose(res, exp, atol=0.00001, rtol=0) # check parameter update theta_new = np.array([x, y]) dtheta = eta * sp.linalg.pinvh(exp) @ gradient(theta) assert np.allclose(dtheta, theta - theta_new, atol=0.000001, rtol=0) # check final cost assert np.allclose(circuit(x, y), -1.41421356, atol=tol, rtol=0) assert len(recwarn) == 0
def run_vqe(H): """Runs the variational quantum eigensolver on the problem Hamiltonian using the variational ansatz specified above. Fill in the missing parts between the # QHACK # markers below to run the VQE. Args: H (qml.Hamiltonian): The input Hamiltonian Returns: The ground state energy of the Hamiltonian. """ # Initialize parameters num_qubits = len(H.wires) num_param_sets = (2**num_qubits) - 1 params = np.random.uniform(low=-np.pi / 2, high=np.pi / 2, size=(num_param_sets, 3)) energy = 0 # QHACK # # Create a quantum device, set up a cost funtion and optimizer, and run the VQE. # (We recommend ~500 iterations to ensure convergence for this problem, # or you can design your own convergence criteria) dev = qml.device("default.qubit", wires=num_qubits) cost_fn = qml.ExpvalCost(variational_ansatz, H, dev) max_iterations = 500 conv_tol = 1e-09 #opt = qml.GradientDescentOptimizer(0.08) opt = qml.QNGOptimizer(0.01, diag_approx=False, lam=0.001) energies = [] for n in range(max_iterations): params, prev_energy = opt.step_and_cost(cost_fn, params) energies.append(cost_fn(params)) conv = np.abs(energies[-1] - prev_energy) if conv <= conv_tol: break energy = energies[-1] # QHACK # # Return the ground state energy return energy
def run_vqe(H): """Runs the variational quantum eigensolver on the problem Hamiltonian using the variational ansatz specified above. Fill in the missing parts between the # QHACK # markers below to run the VQE. Args: H (qml.Hamiltonian): The input Hamiltonian Returns: The ground state energy of the Hamiltonian. """ energy = 0 # QHACK # num_qubits = len(H.wires) num_param_sets = (2 ** num_qubits) - 1 params = np.random.uniform(low=-np.pi / 2, high=np.pi / 2, size=(num_param_sets, 3)) # Initialize the quantum device dev = qml.device("default.qubit", wires=num_qubits) # Set up a cost function cost_fn = qml.ExpvalCost(variational_ansatz, H, dev) # Set up an optimizer #opt = qml.QNGOptimizer(0.01, diag_approx=False, lam=0.001) #opt = qml.GradientDescentOptimizer(0.8) opt = qml.QNGOptimizer(0.1, diag_approx=False, lam=0.001) # Run the VQE by iterating over many steps of the optimizer max_iterations = 30 conv_tol = 1e-09 energies = [] for n in range(max_iterations): params, prev_energy = opt.step_and_cost(cost_fn, params) energies.append(cost_fn(params)) conv = np.abs(energies[-1] - prev_energy) if conv <= conv_tol: break energy = energies[-1] # QHACK # # Return the ground state energy return energies, dev.state
def test_single_qubit_vqe(self, tol): """Test single-qubit VQE has the correct QNG value every step, the correct parameter updates, and correct cost after 200 steps""" dev = qml.device("default.qubit", wires=1) def circuit(params, wires=0): qml.RX(params[0], wires=wires) qml.RY(params[1], wires=wires) coeffs = [1, 1] obs_list = [ qml.PauliX(0), qml.PauliZ(0) ] qnodes = qml.map(circuit, obs_list, dev, measure='expval') cost_fn = qml.dot(coeffs, qnodes) def gradient(params): """Returns the gradient""" da = -np.sin(params[0]) * (np.cos(params[1]) + np.sin(params[1])) db = np.cos(params[0]) * (np.cos(params[1]) - np.sin(params[1])) return np.array([da, db]) eta = 0.01 init_params = np.array([0.011, 0.012]) num_steps = 200 opt = qml.QNGOptimizer(eta) theta = init_params # optimization for 200 steps total for t in range(num_steps): theta_new = opt.step(cost_fn, theta, metric_tensor_fn=qnodes.qnodes[0].metric_tensor) # check metric tensor res = opt.metric_tensor exp = np.diag([0.25, (np.cos(theta[0]) ** 2)/4]) assert np.allclose(res, exp, atol=tol, rtol=0) # check parameter update dtheta = eta * sp.linalg.pinvh(exp) @ gradient(theta) assert np.allclose(dtheta, theta - theta_new, atol=tol, rtol=0) theta = theta_new # check final cost assert np.allclose(cost_fn(theta), -1.41421356, atol=tol, rtol=0)
def test_single_qubit_vqe_using_vqecost(self, tol, recwarn): """Test single-qubit VQE using ExpvalCost has the correct QNG value every step, the correct parameter updates, and correct cost after 200 steps""" dev = qml.device("default.qubit", wires=1) def circuit(params, wires=0): qml.RX(params[0], wires=wires) qml.RY(params[1], wires=wires) coeffs = [1, 1] obs_list = [qml.PauliX(0), qml.PauliZ(0)] h = qml.Hamiltonian(coeffs=coeffs, observables=obs_list) cost_fn = qml.ExpvalCost(ansatz=circuit, hamiltonian=h, device=dev) def gradient(params): """Returns the gradient""" da = -np.sin(params[0]) * (np.cos(params[1]) + np.sin(params[1])) db = np.cos(params[0]) * (np.cos(params[1]) - np.sin(params[1])) return np.array([da, db]) eta = 0.01 init_params = np.array([0.011, 0.012], requires_grad=True) num_steps = 200 opt = qml.QNGOptimizer(eta) theta = init_params # optimization for 200 steps total for t in range(num_steps): theta_new = opt.step(cost_fn, theta) # check metric tensor res = opt.metric_tensor exp = np.diag([0.25, (np.cos(theta[0])**2) / 4]) assert np.allclose(res, exp, atol=tol, rtol=0) # check parameter update dtheta = eta * sp.linalg.pinvh(exp) @ gradient(theta) assert np.allclose(dtheta, theta - theta_new, atol=tol, rtol=0) theta = theta_new # check final cost assert np.allclose(cost_fn(theta), -1.41421356, atol=tol, rtol=0) assert len(recwarn) == 0
def test_step_and_cost_autograd(self, tol): """Test that the correct cost is returned via the step_and_cost method for the QNG optimizer""" dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(params): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) return qml.expval(qml.PauliZ(0)) var = np.array([0.011, 0.012]) opt = qml.QNGOptimizer(stepsize=0.01) _, res = opt.step_and_cost(circuit, var) expected = circuit(var) assert np.all(res == expected)
def QNGOPT(): for x_idx, shotnum in enumerate(SHOTRANGE): #%% Quantum natural gradient circuit = initialize(shotnum) print("Doing the quantum natural gradient") thetaqng = init_params opt = qml.QNGOptimizer(0.01) for y_idx in range(STEPS): working = False while not working: try: thetaqng = opt.step(circuit, thetaqng) QNG_COST[x_idx, y_idx] = circuit(thetaqng) working = True except: pass working = False np.save("./datafiles/qngshotcosttoy.npy", QNG_COST)
def test_qubit_rotation(self, tol): """Test qubit rotation has the correct QNG value every step, the correct parameter updates, and correct cost after 200 steps""" dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(params): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) return qml.expval(qml.PauliZ(0)) def gradient(params): """Returns the gradient of the above circuit""" da = -np.sin(params[0]) * np.cos(params[1]) db = -np.cos(params[0]) * np.sin(params[1]) return np.array([da, db]) eta = 0.01 init_params = np.array([0.011, 0.012]) num_steps = 200 opt = qml.QNGOptimizer(eta) theta = init_params # optimization for 200 steps total for t in range(num_steps): theta_new = opt.step(circuit, theta) # check metric tensor res = opt.metric_tensor_inv exp = np.diag([4, 4 / (np.cos(theta[0])**2)]) assert np.allclose(res, exp, atol=tol, rtol=0) # check parameter update dtheta = eta * exp @ gradient(theta) assert np.allclose(dtheta, theta - theta_new, atol=tol, rtol=0) theta = theta_new # check final cost assert np.allclose(circuit(theta), -0.9963791, atol=tol, rtol=0)
def test_obj_func_not_a_qnode(self): """Test that if the objective function is not a QNode, an error is raised.""" dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(a): qml.RX(a, wires=0) return qml.expval(qml.PauliZ(0)) def cost(a): return circuit(a) opt = qml.QNGOptimizer() params = 0.5 with pytest.raises( ValueError, match="Objective function must be encoded as a single QNode"): opt.step(cost, params)
print( "Iteration = {:}, Energy = {:.8f} Ha, Convergence parameter = {" ":.8f} Ha".format(n, energy, conv) ) if conv <= conv_tol: break print() print("Final value of the energy = {:.8f} Ha".format(energy)) print("Number of iterations = ", n) ############################################################################## # We then repeat the process for the optimizer employing quantum natural gradients: opt = qml.QNGOptimizer(stepsize=step_size, diag_approx=False) params = init_params qngd_param_history = [params] qngd_cost_history = [] for n in range(max_iterations): # Take step params, prev_energy = opt.step_and_cost(cost_fn, params) qngd_param_history.append(params) qngd_cost_history.append(prev_energy) # Compute energy energy = cost_fn(params)
############################################################################## # Performing vanilla gradient descent: gd_cost = [] opt = qml.GradientDescentOptimizer(0.01) theta = init_params for _ in range(steps): theta = opt.step(circuit, theta) gd_cost.append(circuit(theta)) ############################################################################## # Performing quantum natural gradient descent: qng_cost = [] opt = qml.QNGOptimizer(0.01) theta = init_params for _ in range(steps): theta = opt.step(circuit, theta) qng_cost.append(circuit(theta)) ############################################################################## # Plotting the cost vs optimization step for both optimization strategies: from matplotlib import pyplot as plt plt.style.use("seaborn") plt.plot(gd_cost, "b", label="Vanilla gradient descent") plt.plot(qng_cost, "g", label="Quantum natural gradient descent") plt.ylabel("Cost function value")
def run_vqe(cost_fn, max_iter, initial_params, opt_name, step_size, conv_tol=1e-6, diag_approx=False, lam=0, print_freq=20): """Launches a VQE calculation. Args: ===== cost_fn : VQECost VQE cost function we are trying to optimize max_iter : int Maximum number of optimization iterations initial_params : numpy.ndarray Vector of initial parameter values opt_name : str Name of optimizer. Valid options are: QNGOptimizer or GradientDescentOptimizer. step_size : float Stepsize or learning rate of the optimizer conv_tol : float Convergence tolerance for optimizer (relative improvement in energy) diag_approx : bool If using QNGOptimizer, diag_approx is an option for using the block-diagonal approximation to the Fubini-Study metric. If false, the diagonal approximation is used. lam : float Regularizer term for QNGOptimizer print_freq : int Optimizer progress printing frequency Returns: ======== energy_history : list/numpy.ndarray History of energies n : int Number of steps taken to optimize (could be less than max_iter if converged) """ energy_history = [] if opt_name =='GradientDescentOptimizer': opt = qml.GradientDescentOptimizer(stepsize=step_size) elif opt_name =='QNGOptimizer': opt = qml.QNGOptimizer(stepsize=step_size, diag_approx=diag_approx, lam=lam) else: raise ValueError('Use either QNGOptimizer of GradientDescentOptimizer.') params = initial_params prev_energy = cost_fn(params) energy_history = [prev_energy] for n in range(max_iter): params = opt.step(cost_fn, params) energy = cost_fn(params) conv = np.abs(energy - prev_energy) if n % print_freq == 0: print('Iteration = {:}, Energy = {:.8f} Ha, Convergence parameter = {' ':.8f} Ha'.format(n, energy, conv)) if conv <= conv_tol: break energy_history.append(energy) # Update energy prev_energy = energy print() print("Final value of the energy = {:.8f}".format(energy)) print("Number of iterations = ", n) return energy_history, n
qml.Hadamard(q) base_ansatz(params, wires) ansatz = ansatz_f else: raise NotImplementedError("Not found initial state!") # The circuit for computing the expectation value of H. cost_fn = qml.ExpvalCost(ansatz, H, dev, optimize=True) print(f"Total # of Parameters={num_params}") # Perform VQE. step_size = args.step if args.opt == "qng": opt = qml.QNGOptimizer(stepsize=step_size, diag_approx=True, lam=0.1) elif args.opt == "adagrad": opt = qml.AdagradOptimizer(stepsize=step_size) elif args.opt == "adam": opt = qml.AdamOptimizer(stepsize=step_size) else: raise NotImplementedError("Optimizer not found") # Initialize the parameters. np.random.seed(1) if args.randomize == 1: params = np.pi * (np.random.rand(num_params) - 1.0) elif args.randomize == 2: params = np.loadtxt( f"params_{args.ansatz}_{args.two_qubit}_{num_qubits}_{h}_{args.opt}_{step_size}_{initState}{1}.txt" ) print(len(params), num_params)
def run_single_qubit_vqe(cost_fn, dev, max_iter, initial_params, opt_name, step_size, conv_tol=1e-6, diag_approx=False): """Launches a VQE calculation for single-qubit systems, where we may be interested in plotting the optimization path on the Bloch sphere, and thus, need to save the statevector and circuit parameter history. Args: ===== cost_fn : VQECost VQE cost function we are trying to optimize dev : qml.Device Quantum simulator/device max_iter : int Maximum number of optimization iterations initial_params : numpy.ndarray Vector of initial parameter values opt_name : str Name of optimizer. Valid options are: QNGOptimizer and GradientDescentOptimizer. step_size : float Stepsize or learning rate of the optimizer conv_tol : float Convergence tolerance for optimizer (relative improvement in energy) diag_approx : bool If using QNGOptimizer, diag_approx is an option for using the block-diagonal approximation to the Fubini-Study metric. If false, the diagonal approximation is used. Returns: ======== energy_history : list/numpy.ndarray History of energies n : int Number of steps taken to optimize (could be less than max_iter if converged) state_history : list History of state vectors/wavefunctions param_history : list History of parameters """ energy_history = [] if opt_name =='GradientDescentOptimizer': opt = qml.GradientDescentOptimizer(stepsize=step_size) elif opt_name =='QNGOptimizer': opt = qml.QNGOptimizer(stepsize=step_size, diag_approx=diag_approx) else: raise ValueError('Use either QNGOptimizer of GradientDescentOptimizer.') params = initial_params prev_energy = cost_fn(params) energy_history = [prev_energy] state_history = [dev.state] param_history = [params] for n in range(max_iter): params = opt.step(cost_fn, params) energy = cost_fn(params) conv = np.abs(energy - prev_energy) if n % 20 == 0: print('Iteration = {:}, Energy = {:.8f} Ha, Convergence parameter = {' ':.8f} Ha'.format(n, energy, conv)) if conv <= conv_tol: break energy_history.append(energy) state_history.append(dev.state) param_history.append(params) # Update energy prev_energy = energy print() print("Final value of the energy = {:.8f}".format(energy)) print("Number of iterations = ", n) return energy_history, n, state_history, param_history
# Define the QNodeCollection qnodes = qml.map(circuit, operators, dev, measure="expval") # Evaluate the QNodeCollection def Hamiltonian(params): return np.dot(coeffs, qnodes(params, size=SIZE, layers=LAYERS)) ################### # TODO Add a simple option for optimizers # TODO Randomize the params -> Save later # TODO Manage experimental results using comet.ml or tensorboard ################### if args.optim == "qng": optim = qml.QNGOptimizer() elif args.optim == "adam": optim = qml.AdamOptimizer() else: optim = qml.GradientDescentOptimizer() for _ in range(100): print(f"Hamiltonian VEV: {Hamiltonian(params)}") params = optim.step(Hamiltonian, params) # model.quantum_circuits(torch.zeros(6), [1], 3) # node = qml.map(model.quantum_circuits, [qml.PauliZ(0) @ qml.PauliZ(1)], dev, # measure="expval", interface="torch") # qml.map()
def qaoa_maxcut(opt, graph, n_layers, verbose=False, shots=None, MeshGrid=False, NoiseModel=None): start = time.time() if opt == "adam": opt = qml.AdamOptimizer(0.1) elif opt == "gd": opt = qml.GradientDescentOptimizer(0.1) elif opt == "qng": opt = qml.QNGOptimizer(0.1) elif opt == "roto": opt = qml.RotosolveOptimizer() # SETUP PARAMETERS n_wires = len(graph.nodes) edges = graph.edges def U_B(beta): for wire in range(n_wires): qml.RX(2 * beta, wires=wire) def U_C(gamma): for edge in edges: wire1 = edge[0] wire2 = edge[1] qml.CNOT(wires=[wire1, wire2]) qml.RZ(gamma, wires=wire2) qml.CNOT(wires=[wire1, wire2]) if NoiseModel: if shots: print("Starting shots", shots) dev = qml.device("qiskit.aer", wires=n_wires, shots=shots, noise_model=NoiseModel) else: dev = qml.device("qiskit.aer", wires=n_wires, noise_model=NoiseModel) else: if shots: print("Starting shots", shots) dev = qml.device("default.qubit", wires=n_wires, analytic=False, shots=shots) else: dev = qml.device("default.qubit", wires=n_wires, analytic=True, shots=1) @qml.qnode(dev) def circuit(gammas, betas, edge=None, n_layers=1, n_wires=1): for wire in range(n_wires): qml.Hadamard(wires=wire) for i,j in zip(range(n_wires),range(n_layers)): U_C(gammas[i,j]) U_B(betas[i,j]) if edges is None: # measurement phase return qml.sample(comp_basis_measurement(range(n_wires))) return qml.expval(qml.Hermitian(pauli_z_2, wires=edge)) np.random.seed(42) init_params = 0.01 * np.random.rand(2, n_wires, n_layers) def obj_wrapper(params): objstart = partial(objective, params, True, False) objend = partial(objective, params, False, True) return np.vectorize(objstart), np.vectorize(objend) def objective(params, start=False, end=False, X=None, Y=None): gammas = params[0] betas = params[1] if start: gammas[0,0] = X betas[0,0] = Y elif end: gammas[-1,0] = X betas[-1,0] = Y neg_obj = 0 for edge in edges: neg_obj -= 0.5 * (1 - circuit(gammas, betas, edge=edge, n_layers=n_layers, n_wires=n_wires)) return neg_obj paramsrecord = [init_params.tolist()] print(f"Start objective fn {objective(init_params)}") params = init_params losses = [objective(params)] print(f"{str(opt).split('.')[-1]} with {len(graph.nodes)} nodes initial loss {losses[0]}") steps = NUM_STEPS for i in range(steps): params = opt.step(objective, params) if i == 0: print(f"{str(opt).split('.')[-1]} with {len(graph.nodes)} nodes took {time.time()-start:.5f}s for 1 iteration") paramsrecord.append(params.tolist()) losses.append(objective(params)) if verbose: if i % 5 == 0: print(f"Objective at step {i} is {losses[-1]}") if i % 10 == 0 and shots: print("Shots", shots, "is up to", i) if MeshGrid: grid_size = 100 X, Y = np.meshgrid(np.linspace(-np.pi,np.pi,grid_size),np.linspace(-np.pi,np.pi,grid_size)) objstart, objend = obj_wrapper(init_params) meshgridfirststartparams = objstart(X, Y) meshgridfirstlastparams = objend(X,Y) objstart, objend = obj_wrapper(params) meshgridendfirstparams = objstart(X, Y) meshgridendlastparams = objend(X,Y) return {"losses":losses, "params":paramsrecord,\ "MeshGridStartFirstParams":meshgridfirststartparams, "MeshGridStartLastParams":meshgridfirstlastparams, \ "MeshGridEndFirstParams":meshgridendfirstparams, "MeshGridEndLastParams":meshgridendlastparams} else: return shots, losses