def test_rot_decomposition(self, diff_method): """Test that the rotation gate is correctly decomposed""" dev = qml.device("default.qubit", wires=1) def circuit(weights): qml.Rot(weights[0], weights[1], weights[2], wires=0) return qml.expval(qml.PauliX(0)) circuit = qml.QNode(circuit, dev, diff_method=diff_method) params = np.array([1., 2., 3.]) tapes = qml.metric_tensor(circuit, only_construct=True)(params) assert len(tapes) == 3 # first parameter subcircuit assert len(tapes[0].operations) == 0 # Second parameter subcircuit assert len(tapes[1].operations) == 4 assert isinstance(tapes[1].operations[0], qml.RZ) assert tapes[1].operations[0].data == [1] # PauliY decomp assert isinstance(tapes[1].operations[1], qml.PauliZ) assert isinstance(tapes[1].operations[2], qml.S) assert isinstance(tapes[1].operations[3], qml.Hadamard) # Third parameter subcircuit assert len(tapes[2].operations) == 2 assert isinstance(tapes[2].operations[0], qml.RZ) assert isinstance(tapes[2].operations[1], qml.RY) assert tapes[2].operations[0].data == [1] assert tapes[2].operations[1].data == [2] result = qml.metric_tensor(circuit)(params) assert result.shape == (3, 3)
def test_multiple_devices(self, mocker): """Test that passing multiple devices to ExpvalCost works correctly""" dev = [ qml.device("default.qubit", wires=2), qml.device("default.mixed", wires=2) ] spy = mocker.spy(DefaultQubit, "apply") spy2 = mocker.spy(DefaultMixed, "apply") obs = [qml.PauliZ(0), qml.PauliZ(1)] h = qml.Hamiltonian([1, 1], obs) qnodes = qml.ExpvalCost(qml.templates.BasicEntanglerLayers, h, dev) np.random.seed(1967) w = np.random.random( qml.templates.BasicEntanglerLayers.shape(n_layers=3, n_wires=2)) res = qnodes(w) spy.assert_called_once() spy2.assert_called_once() mapped = qml.map(qml.templates.BasicEntanglerLayers, obs, dev) exp = sum(mapped(w)) assert np.allclose(res, exp) with pytest.warns( UserWarning, match="ExpvalCost was instantiated with multiple devices."): qml.metric_tensor(qnodes)(w)
def test_generator_no_expval(self, monkeypatch): """Test exception is raised if subcircuit contains an operation with generator object that is not an observable""" with monkeypatch.context() as m: m.setattr("pennylane.RX.generator", [qml.RX, 1]) with qml.tape.QuantumTape() as tape: qml.RX(np.array(0.5, requires_grad=True), wires=0) qml.expval(qml.PauliX(0)) with pytest.raises(qml.QuantumFunctionError, match="no corresponding observable"): qml.metric_tensor(tape, approx="block-diag")
def test_construct_subcircuit(self): """Test correct subcircuits constructed""" dev = qml.device("default.qubit", wires=2) with qml.tape.QuantumTape() as tape: qml.RX(np.array(1.0, requires_grad=True), wires=0) qml.RY(np.array(1.0, requires_grad=True), wires=0) qml.CNOT(wires=[0, 1]) qml.PhaseShift(np.array(1.0, requires_grad=True), wires=1) return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1)) tapes, _ = qml.metric_tensor(tape, approx="block-diag") assert len(tapes) == 3 # first parameter subcircuit assert len(tapes[0].operations) == 1 assert isinstance(tapes[0].operations[0], qml.Hadamard) # PauliX decomp # second parameter subcircuit assert len(tapes[1].operations) == 4 assert isinstance(tapes[1].operations[0], qml.RX) # PauliY decomp assert isinstance(tapes[1].operations[1], qml.PauliZ) assert isinstance(tapes[1].operations[2], qml.S) assert isinstance(tapes[1].operations[3], qml.Hadamard) # third parameter subcircuit assert len(tapes[2].operations) == 4 assert isinstance(tapes[2].operations[0], qml.RX) assert isinstance(tapes[2].operations[1], qml.RY) assert isinstance(tapes[2].operations[2], qml.CNOT) # Phase shift generator assert isinstance(tapes[2].operations[3], qml.QubitUnitary)
def test_multi_qubit_gates(self): """Test that a tape with Ising gates has the correct metric tensor tapes.""" dev = qml.device("default.qubit", wires=3) with qml.tape.JacobianTape() as tape: qml.Hadamard(0) qml.Hadamard(2) qml.IsingXX(0.2, wires=[0, 1]) qml.IsingXX(-0.6, wires=[1, 2]) qml.IsingZZ(1.02, wires=[0, 1]) qml.IsingZZ(-4.2, wires=[1, 2]) tapes, proc_fn = qml.metric_tensor(tape, approx="block-diag") assert len(tapes) == 4 assert [len(tape.operations) for tape in tapes] == [2, 4, 5, 6] assert [len(tape.measurements) for tape in tapes] == [1] * 4 expected_ops = [ [qml.Hadamard, qml.QubitUnitary], [qml.Hadamard, qml.Hadamard, qml.IsingXX, qml.QubitUnitary], [ qml.Hadamard, qml.Hadamard, qml.IsingXX, qml.IsingXX, qml.QubitUnitary ], [ qml.Hadamard, qml.Hadamard, qml.IsingXX, qml.IsingXX, qml.IsingZZ, qml.QubitUnitary ], ] assert [[type(op) for op in tape.operations] for tape in tapes] == expected_ops
def test_evaluate_diag_metric_tensor(self, tol): """Test that a diagonal metric tensor evaluates correctly""" dev = qml.device("default.qubit", wires=2) def circuit(a, b, c): qml.RX(a, wires=0) qml.RY(b, wires=0) qml.CNOT(wires=[0, 1]) qml.PhaseShift(c, wires=1) return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1)) circuit = qml.QNode(circuit, dev) a = 0.432 b = 0.12 c = -0.432 # evaluate metric tensor g = qml.metric_tensor(circuit, approx="block-diag")(a, b, c) # check that the metric tensor is correct expected = ( np.array( [1, np.cos(a) ** 2, (3 - 2 * np.cos(a) ** 2 * np.cos(2 * b) - np.cos(2 * a)) / 4] ) / 4 ) assert np.allclose(g, np.diag(expected), atol=tol, rtol=0)
def test_metric_tensor_tape_mode(self): """Test that the metric tensor can be calculated in tape mode, and that it is equal to a metric tensor calculated in non-tape mode.""" if not qml.tape_mode_active(): pytest.skip("This test is only intended for tape mode") dev = qml.device("default.qubit", wires=2) p = np.array([1., 1., 1.]) def ansatz(params, **kwargs): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) qml.CNOT(wires=[0, 1]) qml.PhaseShift(params[2], wires=1) h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)]) qnodes = qml.ExpvalCost(ansatz, h, dev) mt = qml.metric_tensor(qnodes)(p) assert qml.tape_mode_active() # Check that tape mode is still active qml.disable_tape() @qml.qnode(dev) def circuit(params): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) qml.CNOT(wires=[0, 1]) qml.PhaseShift(params[2], wires=1) return qml.expval(qml.PauliZ(0)) mt2 = circuit.metric_tensor([p]) assert np.allclose(mt, mt2)
def test_torch(self, diff_method, tol): """Test metric tensor differentiability in the torch interface""" if diff_method == "backprop": pytest.skip("Does not support backprop") torch = pytest.importorskip("torch") dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, interface="torch", diff_method=diff_method) def circuit(weights): qml.RX(weights[0], wires=0) qml.RY(weights[1], wires=0) qml.CNOT(wires=[0, 1]) qml.PhaseShift(weights[2], wires=1) return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1)) weights = np.array([0.432, 0.12, -0.432]) a, b, c = weights weights_t = torch.tensor(weights, requires_grad=True) loss = qml.metric_tensor(circuit)(weights_t)[2, 2] loss.backward() grad = weights_t.grad expected = np.array([ np.cos(a) * np.cos(b)**2 * np.sin(a) / 2, np.cos(a)**2 * np.sin(2 * b) / 4, 0 ]) assert np.allclose(grad, expected, atol=tol, rtol=0)
def test_rot_decomposition(self, diff_method): """Test that the rotation gate is correctly decomposed""" dev = qml.device("default.qubit", wires=1) params = np.array([1.0, 2.0, 3.0], requires_grad=True) with qml.tape.QuantumTape() as circuit: qml.Rot(params[0], params[1], params[2], wires=0) qml.expval(qml.PauliX(0)) tapes, _ = qml.metric_tensor(circuit, approx="block-diag") assert len(tapes) == 3 # first parameter subcircuit assert len(tapes[0].operations) == 0 # Second parameter subcircuit assert len(tapes[1].operations) == 4 assert isinstance(tapes[1].operations[0], qml.RZ) assert tapes[1].operations[0].data == [1] # PauliY decomp assert isinstance(tapes[1].operations[1], qml.PauliZ) assert isinstance(tapes[1].operations[2], qml.S) assert isinstance(tapes[1].operations[3], qml.Hadamard) # Third parameter subcircuit assert len(tapes[2].operations) == 2 assert isinstance(tapes[2].operations[0], qml.RZ) assert isinstance(tapes[2].operations[1], qml.RY) assert tapes[2].operations[0].data == [1] assert tapes[2].operations[1].data == [2]
def test_tf(self, diff_method, tol): """Test metric tensor differentiability in the TF interface""" tf = pytest.importorskip("tensorflow", minversion="2.0") dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, interface="tf", diff_method=diff_method) def circuit(weights): qml.RX(weights[0], wires=0) qml.RY(weights[1], wires=0) qml.CNOT(wires=[0, 1]) qml.PhaseShift(weights[2], wires=1) return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1)) weights = np.array([0.432, 0.12, -0.432]) weights_t = tf.Variable(weights) a, b, c = weights with tf.GradientTape() as tape: loss = qml.metric_tensor(circuit)(weights_t)[2, 2] grad = tape.gradient(loss, weights_t) expected = np.array([ np.cos(a) * np.cos(b)**2 * np.sin(a) / 2, np.cos(a)**2 * np.sin(2 * b) / 4, 0 ]) assert np.allclose(grad, expected, atol=tol, rtol=0)
def test_evaluate_diag_metric_tensor_classical_processing(self, tol): """Test that a diagonal metric tensor evaluates correctly when the QNode includes classical processing.""" dev = qml.device("default.qubit", wires=2) def circuit(a, b): # The classical processing function is # f: ([a0, a1], b) -> (a1, a0, b) # So the classical Jacobians will be a permutation matrix and an identity matrix: # classical_jacobian(circuit)(a, b) == ([[0, 1], [1, 0]], [[1]]) qml.RX(a[1], wires=0) qml.RY(a[0], wires=0) qml.CNOT(wires=[0, 1]) qml.PhaseShift(b, wires=1) return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1)) circuit = qml.QNode(circuit, dev) a = np.array([0.432, 0.1]) b = 0.12 # evaluate metric tensor g = qml.metric_tensor(circuit, approx="block-diag")(a, b) assert isinstance(g, tuple) assert len(g) == 2 assert g[0].shape == (len(a), len(a)) assert g[1].shape == tuple() # check that the metric tensor is correct expected = np.array([np.cos(a[1]) ** 2, 1]) / 4 assert np.allclose(g[0], np.diag(expected), atol=tol, rtol=0) expected = (3 - 2 * np.cos(a[1]) ** 2 * np.cos(2 * a[0]) - np.cos(2 * a[1])) / 16 assert np.allclose(g[1], expected, atol=tol, rtol=0)
def step_and_cost(self, qnode, *args, grad_fn=None, recompute_tensor=True, metric_tensor_fn=None, **kwargs): """Update the parameter array :math:`x` with one step of the optimizer and return the corresponding objective function value prior to the step. Args: qnode (QNode): the QNode for optimization *args : variable length argument list for qnode grad_fn (function): optional gradient function of the qnode with respect to the variables ``*args``. If ``None``, the gradient function is computed automatically. Must return a ``tuple[array]`` with the same number of elements as ``*args``. Each array of the tuple should have the same shape as the corresponding argument. recompute_tensor (bool): Whether or not the metric tensor should be recomputed. If not, the metric tensor from the previous optimization step is used. metric_tensor_fn (function): Optional metric tensor function with respect to the variables ``args``. If ``None``, the metric tensor function is computed automatically. **kwargs : variable length of keyword arguments for the qnode Returns: tuple: the new variable values :math:`x^{(t+1)}` and the objective function output prior to the step """ # pylint: disable=arguments-differ if not isinstance( qnode, (qml.QNode, qml.ExpvalCost)) and metric_tensor_fn is None: raise ValueError( "The objective function must either be encoded as a single QNode or " "an ExpvalCost object for the natural gradient to be automatically computed. " "Otherwise, metric_tensor_fn must be explicitly provided to the optimizer." ) if recompute_tensor or self.metric_tensor is None: if metric_tensor_fn is None: metric_tensor_fn = qml.metric_tensor( qnode, diag_approx=self.diag_approx) self.metric_tensor = metric_tensor_fn(*args, **kwargs) self.metric_tensor += self.lam * np.identity( self.metric_tensor.shape[0]) g, forward = self.compute_grad(qnode, args, kwargs, grad_fn=grad_fn) new_args = self.apply_grad(g, args) if forward is None: forward = qnode(*args, **kwargs) # unwrap from list if one argument, cleaner return if len(new_args) == 1: return new_args[0], forward return new_args, forward
def test_template_integration(self, strategy, tol): """Test that the metric tensor transform acts on QNodes correctly when the QNode contains a template""" dev = qml.device("default.qubit", wires=3) @qml.beta.qnode(dev, expansion_strategy=strategy) def circuit(weights): qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1, 2]) return qml.probs(wires=[0, 1]) weights = np.ones([2, 3, 3], dtype=np.float64, requires_grad=True) res = qml.metric_tensor(circuit, approx="block-diag")(weights) assert res.shape == (2, 3, 3, 2, 3, 3)
def test_multirz_decomposition(self, diff_method): """Test that the MultiRZ gate is correctly decomposed""" dev = qml.device("default.qubit", wires=3) def circuit(a, b): qml.RX(a, wires=0) qml.MultiRZ(b, wires=[0, 1, 2]) return qml.expval(qml.PauliX(0)) circuit = qml.QNode(circuit, dev, diff_method=diff_method) params = [0.1, 0.2] result = qml.metric_tensor(circuit)(*params) assert result.shape == (2, 2)
def test_full_tensor_not_implemented(self): """Test that the full metric tensor can not be computed yet.""" dev = qml.device("default.qubit", wires=3) def circuit(a, b): qml.RX(a, wires=0) qml.MultiRZ(b, wires=[0, 1, 2]) return qml.expval(qml.PauliX(0)) circuit = qml.QNode(circuit, dev) params = [0.1, 0.2] with pytest.raises(NotImplementedError): result = qml.metric_tensor(circuit, approx=None)(*params)
def test_single_qubit_vqe(self, tol, approx): """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=(2 if approx is None else 1)) def circuit(params, wires=None): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) 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=qml.metric_tensor( qnodes.qnodes[0], approx=approx)) # 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 metric_tensor(self, *args, diag_approx=False, only_construct=False, **kwargs): """Evaluate the value of the metric tensor. Args: args (tuple[Any]): positional arguments kwargs (dict[str, Any]): auxiliary arguments diag_approx (bool): iff True, use the diagonal approximation only_construct (bool): Iff True, construct the circuits used for computing the metric tensor but do not execute them, and return the tapes. Returns: array[float]: metric tensor """ return qml.metric_tensor(self, diag_approx=diag_approx, only_construct=only_construct)( *args, **kwargs )
def step_and_cost(self, qnode, x, recompute_tensor=True, metric_tensor_fn=None): """Update the parameter array :math:`x` with one step of the optimizer and return the corresponding objective function value prior to the step. Args: qnode (QNode): the QNode for optimization x (array): NumPy array containing the current values of the variables to be updated recompute_tensor (bool): Whether or not the metric tensor should be recomputed. If not, the metric tensor from the previous optimization step is used. metric_tensor_fn (function): Optional metric tensor function with respect to the variables ``x``. If ``None``, the metric tensor function is computed automatically. Returns: tuple: the new variable values :math:`x^{(t+1)}` and the objective function output prior to the step """ # pylint: disable=arguments-differ if not isinstance( qnode, (qml.QNode, qml.ExpvalCost)) and metric_tensor_fn is None: raise ValueError( "The objective function must either be encoded as a single QNode or " "an ExpvalCost object for the natural gradient to be automatically computed. " "Otherwise, metric_tensor_fn must be explicitly provided to the optimizer." ) if recompute_tensor or self.metric_tensor is None: if metric_tensor_fn is None: # pseudo-inverse metric tensor self.metric_tensor = qml.metric_tensor( qnode, diag_approx=self.diag_approx)(x) else: self.metric_tensor = metric_tensor_fn(x) self.metric_tensor += self.lam * np.identity( self.metric_tensor.shape[0]) # The QNGOptimizer.step does not permit passing an external gradient function. # Autograd will always calculate the gradient and `forward` will never be `None`. g, forward = self.compute_grad(qnode, (x, ), dict()) x_out = self.apply_grad(g, x) return x_out, forward
def test_parameter_fan_out(self, diff_method): """The metric tensor is always with respect to the quantum circuit. Any classical processing is not taken into account. As a result, if there is parameter fan-out, the returned metric tensor will be larger than expected. """ dev = qml.device("default.qubit", wires=2) def circuit(a): qml.RX(a, wires=0) qml.RX(a, wires=0) return qml.expval(qml.PauliX(0)) circuit = qml.QNode(circuit, dev, diff_method=diff_method) params = [0.1] result = qml.metric_tensor(circuit)(*params) assert result.shape == (2, 2)
def test_metric_tensor(self): """Test that the metric tensor can be calculated.""" dev = qml.device("default.qubit", wires=2) p = np.array([1.0, 1.0, 1.0]) def ansatz(params, **kwargs): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) qml.CNOT(wires=[0, 1]) qml.PhaseShift(params[2], wires=1) h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)]) qnodes = qml.ExpvalCost(ansatz, h, dev) mt = qml.metric_tensor(qnodes)(p) assert mt.shape == (3, 3) assert isinstance(mt, np.ndarray)
def test_evaluate_block_diag_metric_tensor(self, sample_circuit, tol): """Test that a block-diagonal metric tensor evaluates correctly, by comparing it to a known analytic result as well as numerical computation.""" dev, circuit, non_parametrized_layer, a, b, c = sample_circuit params = [-0.282203, 0.145554, 0.331624, -0.163907, 0.57662, 0.081272] x, y, z, h, g, f = params G = qml.metric_tensor(circuit, approx="block-diag")(*params) # ============================================ # Test block-diag metric tensor of first layer is correct. # We do this by comparing against the known analytic result. # First layer includes the non_parametrized_layer, # followed by observables corresponding to generators of: # qml.RX(x, wires=0) # qml.RY(y, wires=1) # qml.RZ(z, wires=2) G1 = np.zeros([3, 3]) # diag elements G1[0, 0] = np.sin(a) ** 2 / 4 G1[1, 1] = ( 16 * np.cos(a) ** 2 * np.sin(b) ** 3 * np.cos(b) * np.sin(2 * c) + np.cos(2 * b) * (2 - 8 * np.cos(a) ** 2 * np.sin(b) ** 2 * np.cos(2 * c)) + np.cos(2 * (a - b)) + np.cos(2 * (a + b)) - 2 * np.cos(2 * a) + 14 ) / 64 G1[2, 2] = (3 - np.cos(2 * a) - 2 * np.cos(a) ** 2 * np.cos(2 * (b + c))) / 16 # off diag elements G1[0, 1] = np.sin(a) ** 2 * np.sin(b) * np.cos(b + c) / 4 G1[0, 2] = np.sin(a) ** 2 * np.cos(b + c) / 4 G1[1, 2] = ( -np.sin(b) * ( np.cos(2 * (a - b - c)) + np.cos(2 * (a + b + c)) + 2 * np.cos(2 * a) + 2 * np.cos(2 * (b + c)) - 6 ) / 32 ) G1[1, 0] = G1[0, 1] G1[2, 0] = G1[0, 2] G1[2, 1] = G1[1, 2] assert np.allclose(G[:3, :3], G1, atol=tol, rtol=0) # ============================================= # Test block-diag metric tensor of second layer is correct. # We do this by computing the required expectation values # numerically using multiple circuits. # The second layer includes the non_parametrized_layer, # RX, RY, RZ gates (x, y, z params), and a 2nd non_parametrized_layer. # # Observables are the generators of: # qml.RY(f, wires=1) # qml.RZ(g, wires=2) G2 = np.zeros([2, 2]) def layer2_diag(x, y, z, h, g, f): non_parametrized_layer(a, b, c) qml.RX(x, wires=0) qml.RY(y, wires=1) qml.RZ(z, wires=2) non_parametrized_layer(a, b, c) return qml.var(qml.PauliZ(2)), qml.var(qml.PauliY(1)) layer2_diag = qml.QNode(layer2_diag, dev) def layer2_off_diag_first_order(x, y, z, h, g, f): non_parametrized_layer(a, b, c) qml.RX(x, wires=0) qml.RY(y, wires=1) qml.RZ(z, wires=2) non_parametrized_layer(a, b, c) return qml.expval(qml.PauliZ(2)), qml.expval(qml.PauliY(1)) layer2_off_diag_first_order = qml.QNode(layer2_off_diag_first_order, dev) def layer2_off_diag_second_order(x, y, z, h, g, f): non_parametrized_layer(a, b, c) qml.RX(x, wires=0) qml.RY(y, wires=1) qml.RZ(z, wires=2) non_parametrized_layer(a, b, c) return qml.expval(qml.Hermitian(np.kron(Z, Y), wires=[2, 1])) layer2_off_diag_second_order = qml.QNode(layer2_off_diag_second_order, dev) # calculate the diagonal terms varK0, varK1 = layer2_diag(x, y, z, h, g, f) G2[0, 0] = varK0 / 4 G2[1, 1] = varK1 / 4 # calculate the off-diagonal terms exK0, exK1 = layer2_off_diag_first_order(x, y, z, h, g, f) exK01 = layer2_off_diag_second_order(x, y, z, h, g, f) G2[0, 1] = (exK01 - exK0 * exK1) / 4 G2[1, 0] = (exK01 - exK0 * exK1) / 4 assert np.allclose(G[4:6, 4:6], G2, atol=tol, rtol=0) # ============================================= # Test block-diag metric tensor of third layer is correct. # We do this by computing the required expectation values # numerically. # The third layer includes the non_parametrized_layer, # RX, RY, RZ gates (x, y, z params), a 2nd non_parametrized_layer, # followed by the qml.RY(f, wires=2) operation. # # Observable is simply generator of: # qml.RY(f, wires=2) # # Note: since this layer only consists of a single parameter, # only need to compute a single diagonal element. def layer3_diag(x, y, z, h, g, f): non_parametrized_layer(a, b, c) qml.RX(x, wires=0) qml.RY(y, wires=1) qml.RZ(z, wires=2) non_parametrized_layer(a, b, c) qml.RY(f, wires=2) return qml.var(qml.PauliX(1)) layer3_diag = qml.QNode(layer3_diag, dev) G3 = layer3_diag(x, y, z, h, g, f) / 4 assert np.allclose(G[3:4, 3:4], G3, atol=tol, rtol=0) # ============================================ # Finally, double check that the entire metric # tensor is as computed. G_expected = block_diag(G1, G3, G2) assert np.allclose(G, G_expected, atol=tol, rtol=0)
def test_evaluate_diag_approx_metric_tensor(self, sample_circuit, tol): """Test that a metric tensor under the diagonal approximation evaluates correctly and that the old option ``diag_approx`` raises a Warning.""" dev, circuit, non_parametrized_layer, a, b, c = sample_circuit params = [-0.282203, 0.145554, 0.331624, -0.163907, 0.57662, 0.081272] x, y, z, h, g, f = params G = qml.metric_tensor(circuit, approx="diag")(*params) with pytest.warns(UserWarning): G_alias = qml.metric_tensor(circuit, diag_approx=True)(*params) # ============================================ # Test block-diag metric tensor of first layer is correct. # We do this by comparing against the known analytic result. # First layer includes the non_parametrized_layer, # followed by observables corresponding to generators of: # qml.RX(x, wires=0) # qml.RY(y, wires=1) # qml.RZ(z, wires=2) G1 = np.zeros([3, 3]) # diag elements G1[0, 0] = np.sin(a) ** 2 / 4 G1[1, 1] = ( 16 * np.cos(a) ** 2 * np.sin(b) ** 3 * np.cos(b) * np.sin(2 * c) + np.cos(2 * b) * (2 - 8 * np.cos(a) ** 2 * np.sin(b) ** 2 * np.cos(2 * c)) + np.cos(2 * (a - b)) + np.cos(2 * (a + b)) - 2 * np.cos(2 * a) + 14 ) / 64 G1[2, 2] = (3 - np.cos(2 * a) - 2 * np.cos(a) ** 2 * np.cos(2 * (b + c))) / 16 assert np.allclose(G[:3, :3], G1, atol=tol, rtol=0) assert np.allclose(G_alias[:3, :3], G1, atol=tol, rtol=0) # ============================================= # Test block-diag metric tensor of second layer is correct. # We do this by computing the required expectation values # numerically using multiple circuits. # The second layer includes the non_parametrized_layer, # RX, RY, RZ gates (x, y, z params), and a 2nd non_parametrized_layer. # # Observables are the generators of: # qml.RY(f, wires=1) # qml.RZ(g, wires=2) G2 = np.zeros([2, 2]) def layer2_diag(x, y, z, h, g, f): non_parametrized_layer(a, b, c) qml.RX(x, wires=0) qml.RY(y, wires=1) qml.RZ(z, wires=2) non_parametrized_layer(a, b, c) return qml.var(qml.PauliZ(2)), qml.var(qml.PauliY(1)) layer2_diag = qml.QNode(layer2_diag, dev) # calculate the diagonal terms varK0, varK1 = layer2_diag(x, y, z, h, g, f) G2[0, 0] = varK0 / 4 G2[1, 1] = varK1 / 4 assert np.allclose(G[4:6, 4:6], G2, atol=tol, rtol=0) assert np.allclose(G_alias[4:6, 4:6], G2, atol=tol, rtol=0) # ============================================= # Test metric tensor of third layer is correct. # We do this by computing the required expectation values # numerically. # The third layer includes the non_parametrized_layer, # RX, RY, RZ gates (x, y, z params), a 2nd non_parametrized_layer, # followed by the qml.RY(f, wires=2) operation. # # Observable is simply generator of: # qml.RY(f, wires=2) # # Note: since this layer only consists of a single parameter, # only need to compute a single diagonal element. def layer3_diag(x, y, z, h, g, f): non_parametrized_layer(a, b, c) qml.RX(x, wires=0) qml.RY(y, wires=1) qml.RZ(z, wires=2) non_parametrized_layer(a, b, c) qml.RY(f, wires=2) return qml.var(qml.PauliX(1)) layer3_diag = qml.QNode(layer3_diag, dev) G3 = layer3_diag(x, y, z, h, g, f) / 4 assert np.allclose(G[3:4, 3:4], G3, atol=tol, rtol=0) assert np.allclose(G_alias[3:4, 3:4], G3, atol=tol, rtol=0) # ============================================ # Finally, double check that the entire metric # tensor is as computed. G_expected = block_diag(G1, G3, G2) assert np.allclose(G, G_expected, atol=tol, rtol=0) assert np.allclose(G_alias, G_expected, atol=tol, rtol=0)
def cost(weights): return qml.metric_tensor(circuit)(weights)[2, 2]
def test_construct_subcircuit_layers(self): """Test correct subcircuits constructed when a layer structure exists""" dev = qml.device("default.qubit", wires=3) params = np.ones([8]) with qml.tape.QuantumTape() as tape: # section 1 qml.RX(params[0], wires=0) # section 2 qml.RY(params[1], wires=0) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) # section 3 qml.RX(params[2], wires=0) qml.RY(params[3], wires=1) qml.RZ(params[4], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) # section 4 qml.RX(params[5], wires=0) qml.RY(params[6], wires=1) qml.RZ(params[7], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1)), qml.expval(qml.PauliX(2)) tapes, _ = qml.metric_tensor(tape, approx="block-diag") # this circuit should split into 4 independent # sections or layers when constructing subcircuits assert len(tapes) == 4 # first layer subcircuit assert len(tapes[0].operations) == 1 assert isinstance(tapes[0].operations[0], qml.Hadamard) # PauliX decomp # second layer subcircuit assert len(tapes[1].operations) == 4 assert isinstance(tapes[1].operations[0], qml.RX) # PauliY decomp assert isinstance(tapes[1].operations[1], qml.PauliZ) assert isinstance(tapes[1].operations[2], qml.S) assert isinstance(tapes[1].operations[3], qml.Hadamard) # # third layer subcircuit assert len(tapes[2].operations) == 8 assert isinstance(tapes[2].operations[0], qml.RX) assert isinstance(tapes[2].operations[1], qml.RY) assert isinstance(tapes[2].operations[2], qml.CNOT) assert isinstance(tapes[2].operations[3], qml.CNOT) # PauliX decomp assert isinstance(tapes[2].operations[4], qml.Hadamard) # PauliY decomp assert isinstance(tapes[2].operations[5], qml.PauliZ) assert isinstance(tapes[2].operations[6], qml.S) assert isinstance(tapes[2].operations[7], qml.Hadamard) # # fourth layer subcircuit assert len(tapes[3].operations) == 13 assert isinstance(tapes[3].operations[0], qml.RX) assert isinstance(tapes[3].operations[1], qml.RY) assert isinstance(tapes[3].operations[2], qml.CNOT) assert isinstance(tapes[3].operations[3], qml.CNOT) assert isinstance(tapes[3].operations[4], qml.RX) assert isinstance(tapes[3].operations[5], qml.RY) assert isinstance(tapes[3].operations[6], qml.RZ) assert isinstance(tapes[3].operations[7], qml.CNOT) assert isinstance(tapes[3].operations[8], qml.CNOT) # PauliX decomp assert isinstance(tapes[3].operations[9], qml.Hadamard) # PauliY decomp assert isinstance(tapes[3].operations[10], qml.PauliZ) assert isinstance(tapes[3].operations[11], qml.S) assert isinstance(tapes[3].operations[12], qml.Hadamard)
def step_and_cost( self, qnode, *args, grad_fn=None, recompute_tensor=True, metric_tensor_fn=None, **kwargs ): """Update the parameter array :math:`x` with one step of the optimizer and return the corresponding objective function value prior to the step. Args: qnode (QNode): the QNode for optimization *args : variable length argument list for qnode grad_fn (function): optional gradient function of the qnode with respect to the variables ``*args``. If ``None``, the gradient function is computed automatically. Must return a ``tuple[array]`` with the same number of elements as ``*args``. Each array of the tuple should have the same shape as the corresponding argument. recompute_tensor (bool): Whether or not the metric tensor should be recomputed. If not, the metric tensor from the previous optimization step is used. metric_tensor_fn (function): Optional metric tensor function with respect to the variables ``args``. If ``None``, the metric tensor function is computed automatically. **kwargs : variable length of keyword arguments for the qnode Returns: tuple: the new variable values :math:`x^{(t+1)}` and the objective function output prior to the step """ # pylint: disable=arguments-differ if not isinstance(qnode, (qml.QNode, qml.ExpvalCost)) and metric_tensor_fn is None: raise ValueError( "The objective function must either be encoded as a single QNode or " "an ExpvalCost object for the natural gradient to be automatically computed. " "Otherwise, metric_tensor_fn must be explicitly provided to the optimizer." ) if recompute_tensor or self.metric_tensor is None: if metric_tensor_fn is None: metric_tensor_fn = qml.metric_tensor(qnode, approx=self.approx) _metric_tensor = metric_tensor_fn(*args, **kwargs) # Reshape metric tensor to be square shape = qml.math.shape(_metric_tensor) size = qml.math.prod(shape[: len(shape) // 2]) self.metric_tensor = qml.math.reshape(_metric_tensor, (size, size)) # Add regularization self.metric_tensor = self.metric_tensor + self.lam * qml.math.eye( size, like=_metric_tensor ) g, forward = self.compute_grad(qnode, args, kwargs, grad_fn=grad_fn) new_args = np.array(self.apply_grad(g, args), requires_grad=True) if forward is None: forward = qnode(*args, **kwargs) # Note: for now, we only have single element lists as the new # arguments, but this might change, see TODO below. # Once the other approach is implemented, we need to unwrap from list # if one argument for a cleaner return. # if len(new_args) == 1: return new_args[0], forward
g1[0, 1] = (exK0K1 - exK0 * exK1) / 4 g1[1, 0] = g1[0, 1] ############################################################################## # Putting this altogether, the block-diagonal approximation to the Fubini-Study # metric tensor for this variational quantum circuit is from scipy.linalg import block_diag g = block_diag(g0, g1) print(np.round(g, 8)) ############################################################################## # PennyLane contains a built-in function for computing the Fubini-Study metric # tensor, :func:`~.pennylane.metric_tensor`, which # we can use to verify this result: print(np.round(qml.metric_tensor(circuit)(params), 8)) ############################################################################## # As opposed to our manual computation, which required 6 different quantum # evaluations, the PennyLane Fubini-Study metric tensor implementation # requires only 2 quantum evaluations, one per layer. This is done by # automatically detecting the layer structure, and noting that every # observable that must be measured commutes, allowing for simultaneous measurement. # # Therefore, by combining the quantum natural gradient optimizer with the analytic # parameter-shift rule to optimize a variational circuit with :math:`d` parameters # and :math:`L` parametrized layers, a total of :math:`2d+L` quantum evaluations # are required per optimization step. # # Note that the :func:`~.pennylane.metric_tensor` function also supports computing the diagonal # approximation to the metric tensor:
def natural_gradient(params): """Calculate the natural gradient of the qnode() cost function. The code you write for this challenge should be completely contained within this function between the # QHACK # comment markers. You should evaluate the metric tensor and the gradient of the QNode, and then combine these together using the natural gradient definition. The natural gradient should be returned as a NumPy array. The metric tensor should be evaluated using the equation provided in the problem text. Hint: you will need to define a new QNode that returns the quantum state before measurement. Args: params (np.ndarray): Input parameters, of dimension 6 Returns: np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6 """ natural_grad = np.zeros(6) # QHACK # grad_func = qml.grad(qnode) gradient = grad_func(params)[0] block_diag_mt = qml.metric_tensor(qnode)(params) approx_nat_gradient = np.dot(np.linalg.pinv(block_diag_mt), gradient) # print(gradient) # print(np.round(block_diag_mt, 8)) # print(approx_nat_gradient) def second_oder_parameter_shift(params, i, j, shift_i, shift_j): shifted = params.copy() shifted[np.unravel_index(i, shifted.shape)] += shift_i shifted[np.unravel_index(j, shifted.shape)] += shift_j return shifted @qml.qnode(dev) def overlap(params, shifted_params): variational_circuit(shifted_params) qml.inv(qml.template(variational_circuit)(params)) #obs = qml.expval(qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliZ(2)) return qml.probs([0, 1, 2]) # print('\n', overlap(params, params)[0]) f_matrix = np.zeros([6, 6], dtype=np.float64) shift = 0.5 * np.pi # print('\n', overlap(params, second_oder_parameter_shift(params, 1, 1, shift, shift))[0]) for i in range(len(gradient)): for j in range(i, len(gradient)): pp = overlap( params, second_oder_parameter_shift(params, i, j, shift, shift))[0] mp = overlap( params, second_oder_parameter_shift(params, i, j, -shift, shift))[0] pm = overlap( params, second_oder_parameter_shift(params, i, j, shift, -shift))[0] mm = overlap( params, second_oder_parameter_shift(params, i, j, -shift, -shift))[0] fij = (-pp + mp + pm - mm) / 8.0 f_matrix[i, j] = fij f_matrix[j, i] = fij # for i in range(len(gradient)): # shifted = params.copy() # shifted[np.unravel_index(i, shifted.shape)] += shift # fii_prob = 1 - 0.5 * (overlap(params, shifted) + 1.0) # f_matrix[i, i] = fii_prob - 1 # print(np.round(f_matrix, 8)) natural_grad = np.dot(np.linalg.pinv(f_matrix), gradient) # QHACK # return natural_grad
def natural_gradient(params): """Calculate the natural gradient of the qnode() cost function. The code you write for this challenge should be completely contained within this function between the # QHACK # comment markers. You should evaluate the metric tensor and the gradient of the QNode, and then combine these together using the natural gradient definition. The natural gradient should be returned as a NumPy array. The metric tensor should be evaluated using the equation provided in the problem text. Hint: you will need to define a new QNode that returns the quantum state before measurement. Args: params (np.ndarray): Input parameters, of dimension 6 Returns: np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6 """ natural_grad = np.zeros(6) # QHACK # @qml.qnode(dev) def probabs(params, i): variational_circuit(params) return qml.probs(int(i)) def state(params): qstate = np.zeros([ 8, ]) prob0 = probabs(params, 0) prob1 = probabs(params, 1) prob2 = probabs(params, 2) index = 0 for i in range(2): for j in range(2): for k in range(2): qstate[index] = np.abs( np.sqrt((prob0[i] * prob1[j] * prob2[k]))) index += 1 normalise = np.vdot(qstate, qstate) normalise = 1 / np.abs((np.sqrt(normalise))) qstate = normalise * qstate return qstate def getTensorValue(params, i, j, qstate): params1 = params.copy() params1[i], params1[j] = params[i] + np.pi / 2, params[j] + np.pi / 2 params2 = params.copy() params2[i], params2[j] = params[i] + np.pi / 2, params[j] - np.pi / 2 params3 = params.copy() params3[i], params3[j] = params[i] - np.pi / 2, params[j] + np.pi / 2 params4 = params.copy() params4[i], params4[j] = params[i] - np.pi / 2, params[j] - np.pi / 2 answer = ((-(np.abs(np.vdot(qstate, state(params1))))**2 + (np.abs(np.vdot(qstate, state(params2))))**2 + (np.abs(np.vdot(qstate, state(params3))))**2 - (np.abs(np.vdot(qstate, state(params4))))**2) / 8) return answer def PST(w, i): shifted_g = w.copy() shifted_g[i] += np.pi / 2 pst_g_plus = qnode(shifted_g) shifted_g[i] -= np.pi pst_g_minus = qnode(shifted_g) return (pst_g_plus - pst_g_minus) / (2 * np.sin(np.pi / 2)) qstate = state(params) tensor = [[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]] for i in range(6): for j in range(6): tensor[i][j] = getTensorValue(params, i, j, qstate) gradient = np.zeros([ 6, ]) # for i in range(6): # gradient[i][j] = PST(params, i) gradient0 = qml.grad(qnode, argnum=0) gradient = gradient0(params) #print(gradient) # tensor_inv = np.linalg.inv(tensor) a = np.linalg.inv(qml.metric_tensor(qnode)(params)) natural_grad = np.matmul(a, gradient) # QHACK # return natural_grad
def natural_gradient(params): """Calculate the natural gradient of the qnode() cost function. The code you write for this challenge should be completely contained within this function between the # QHACK # comment markers. You should evaluate the metric tensor and the gradient of the QNode, and then combine these together using the natural gradient definition. The natural gradient should be returned as a NumPy array. The metric tensor should be evaluated using the equation provided in the problem text. Hint: you will need to define a new QNode that returns the quantum state before measurement. Args: params (np.ndarray): Input parameters, of dimension 6 Returns: np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6 """ natural_grad = np.zeros(6) # QHACK # #@qml.qnode(dev) N = len(params) gradient = np.zeros([N], dtype=np.float64) Fubini = np.zeros([N, N]) for i in range(N): for j in range(N): params2_1 = params.copy() params2_2 = params.copy() params2_3 = params.copy() params2_4 = params.copy() params2_1[i] += np.pi / 2 params2_2[i] += np.pi / 2 params2_3[i] -= np.pi / 2 params2_4[i] -= np.pi / 2 params2_1[j] += np.pi / 2 params2_2[j] -= np.pi / 2 params2_3[j] += np.pi / 2 params2_4[j] -= np.pi / 2 Fubini[i, j] = (1 / 8) * (-inner_prod(params, params2_1) + inner_prod( params, params2_2) + inner_prod(params, params2_3) - inner_prod(params, params2_4)) test = qml.metric_tensor(qnode) res_test = test(params) #Gradient for i in range(N): shifted = params.copy() shifted[i] = params[i] + np.pi / 2 forward = qnode(shifted) shifted[i] = params[i] - np.pi / 2 backward = qnode(shifted) gradient[i] = (forward - backward) / (2 * np.sin(np.pi / 2)) Fubini_inv = np.linalg.inv(Fubini) test3 = np.linalg.pinv(res_test) test3 = test3.dot(gradient) res_test_inv = np.linalg.inv(res_test) natural_grad = Fubini_inv.dot(gradient) # QHACK # return natural_grad
def cost(weights): return qml.metric_tensor(circuit, approx="block-diag")(weights)[2, 2]