def test_independent_parameters(self): """Test the case where expectation values are independent of some parameters. For those parameters, the gradient should be evaluated to zero without executing the device.""" dev = qml.device("default.qubit", wires=2) with JacobianTape() as tape1: qml.RX(1, wires=[0]) qml.RX(1, wires=[1]) qml.expval(qml.PauliZ(0)) with JacobianTape() as tape2: qml.RX(1, wires=[0]) qml.RX(1, wires=[1]) qml.expval(qml.PauliZ(1)) j1 = tape1.jacobian(dev) # We should only be executing the device to differentiate 1 parameter (2 executions) assert dev.num_executions == 2 j2 = tape2.jacobian(dev) exp = -np.sin(1) assert np.allclose(j1, [exp, 0]) assert np.allclose(j2, [0, exp])
def test_differentiable_expand(self, tol): """Test that operation and nested tapes expansion is differentiable""" class U3(qml.U3): def expand(self): tape = JacobianTape() theta, phi, lam = self.data wires = self.wires tape._ops += [ qml.Rot(lam, theta, -lam, wires=wires), qml.PhaseShift(phi + lam, wires=wires), ] return tape tape = JacobianTape() dev = qml.device("default.qubit", wires=1) a = np.array(0.1) p_val = [0.1, 0.2, 0.3] p = torch.tensor(p_val, requires_grad=True) with tape: qml.RX(a, wires=0) U3(p[0], p[1], p[2], wires=0) qml.expval(qml.PauliX(0)) tape = TorchInterface.apply(tape.expand()) assert tape.trainable_params == [1, 2, 3, 4] assert [i.name for i in tape.operations] == ["RX", "Rot", "PhaseShift"] tape_params = [i.detach().numpy() for i in tape.get_parameters()] assert np.allclose( tape_params, [p_val[2], p_val[0], -p_val[2], p_val[1] + p_val[2]], atol=tol, rtol=0) res = tape.execute(device=dev) expected = np.cos(a) * np.cos(p_val[1]) * np.sin(p_val[0]) + np.sin( a) * (np.cos(p_val[2]) * np.sin(p_val[1]) + np.cos(p_val[0]) * np.cos(p_val[1]) * np.sin(p_val[2])) assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) res.backward() expected = np.array([ np.cos(p_val[1]) * (np.cos(a) * np.cos(p_val[0]) - np.sin(a) * np.sin(p_val[0]) * np.sin(p_val[2])), np.cos(p_val[1]) * np.cos(p_val[2]) * np.sin(a) - np.sin(p_val[1]) * (np.cos(a) * np.sin(p_val[0]) + np.cos(p_val[0]) * np.sin(a) * np.sin(p_val[2])), np.sin(a) * (np.cos(p_val[0]) * np.cos(p_val[1]) * np.cos(p_val[2]) - np.sin(p_val[1]) * np.sin(p_val[2])), ]) assert np.allclose(p.grad, expected, atol=tol, rtol=0)
def test_differentiable_expand(self, mocker, tol): """Test that operation and nested tapes expansion is differentiable""" spy = mocker.spy(JacobianTape, "jacobian") mock = mocker.patch.object(qml.operation.Operation, "do_check_domain", False) class U3(qml.U3): def expand(self): tape = JacobianTape() theta, phi, lam = self.data wires = self.wires tape._ops += [ qml.Rot(lam, theta, -lam, wires=wires), qml.PhaseShift(phi + lam, wires=wires), ] return tape qtape = JacobianTape() dev = qml.device("default.qubit.tf", wires=1) a = np.array(0.1) p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) with tf.GradientTape() as tape: with qtape: qml.RX(a, wires=0) U3(p[0], p[1], p[2], wires=0) qml.expval(qml.PauliX(0)) qtape = qtape.expand() assert [i.name for i in qtape.operations] == ["RX", "Rot", "PhaseShift"] assert np.all( qtape.get_parameters() == [a, p[2], p[0], -p[2], p[1] + p[2]]) res = qtape.execute(device=dev) expected = tf.cos(a) * tf.cos(p[1]) * tf.sin( p[0]) + tf.sin(a) * (tf.cos(p[2]) * tf.sin(p[1]) + tf.cos(p[0]) * tf.cos(p[1]) * tf.sin(p[2])) assert np.allclose(res, expected, atol=tol, rtol=0) res = tape.jacobian(res, p) expected = np.array([ tf.cos(p[1]) * (tf.cos(a) * tf.cos(p[0]) - tf.sin(a) * tf.sin(p[0]) * tf.sin(p[2])), tf.cos(p[1]) * tf.cos(p[2]) * tf.sin(a) - tf.sin(p[1]) * (tf.cos(a) * tf.sin(p[0]) + tf.cos(p[0]) * tf.sin(a) * tf.sin(p[2])), tf.sin(a) * (tf.cos(p[0]) * tf.cos(p[1]) * tf.cos(p[2]) - tf.sin(p[1]) * tf.sin(p[2])), ]) assert np.allclose(res, expected, atol=tol, rtol=0) spy.assert_not_called()
def test_choose_params_and_methods_raises(self, argnum): """Test that the _choose_params_and_methods helper method raises an error if incorrect trainable parameters are specified.""" tape = JacobianTape() tape.trainable_params = [0] diff_methods = ["F"] with pytest.raises( ValueError, match="Incorrect trainable parameters", ): res = tape._choose_params_with_methods(diff_methods, argnum)
def test_jacobian_options_copied(self): """Tests that the jacobian_options attribute is copied""" tape = JacobianTape() tape.jacobian_options = {"method": "device", "jacobian_method": "adjoint_jacobian"} tape_copy = tape.copy() assert tape_copy.jacobian_options == { "method": "device", "jacobian_method": "adjoint_jacobian", }
def test_differentiable_expand(self, tol): """Test that operation and nested tapes expansion is differentiable""" class U3(qml.U3): def expand(self): tape = JacobianTape() theta, phi, lam = self.data wires = self.wires tape._ops += [ qml.Rot(lam, theta, -lam, wires=wires), qml.PhaseShift(phi + lam, wires=wires), ] return tape qtape = JacobianTape() dev = qml.device("default.qubit", wires=1) a = np.array(0.1) p = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) with tf.GradientTape() as tape: with qtape: qml.RX(a, wires=0) U3(p[0], p[1], p[2], wires=0) qml.expval(qml.PauliX(0)) qtape = TFInterface.apply(qtape.expand()) assert qtape.trainable_params == [1, 2, 3, 4] assert [i.name for i in qtape.operations] == ["RX", "Rot", "PhaseShift"] assert np.all( qtape.get_parameters() == [p[2], p[0], -p[2], p[1] + p[2]]) res = qtape.execute(device=dev) expected = tf.cos(a) * tf.cos(p[1]) * tf.sin( p[0]) + tf.sin(a) * (tf.cos(p[2]) * tf.sin(p[1]) + tf.cos(p[0]) * tf.cos(p[1]) * tf.sin(p[2])) assert np.allclose(res, expected, atol=tol, rtol=0) res = tape.jacobian(res, p) expected = np.array([ tf.cos(p[1]) * (tf.cos(a) * tf.cos(p[0]) - tf.sin(a) * tf.sin(p[0]) * tf.sin(p[2])), tf.cos(p[1]) * tf.cos(p[2]) * tf.sin(a) - tf.sin(p[1]) * (tf.cos(a) * tf.sin(p[0]) + tf.cos(p[0]) * tf.sin(a) * tf.sin(p[2])), tf.sin(a) * (tf.cos(p[0]) * tf.cos(p[1]) * tf.cos(p[2]) - tf.sin(p[1]) * tf.sin(p[2])), ]) assert np.allclose(res, expected, atol=tol, rtol=0)
def test_choose_params_and_methods_warns_no_params(self): """Test that the _choose_params_and_methods helper method warns if an empty list was passed as argnum.""" tape = JacobianTape() tape.trainable_params = [0] diff_methods = ["F"] argnum = [] with pytest.warns( UserWarning, match="No trainable parameters", ): res = tape._choose_params_with_methods(diff_methods, argnum)
def cost_fn(a, p, device): tape = JacobianTape() with tape: qml.RX(a, wires=0) U3(*p, wires=0) qml.expval(qml.PauliX(0)) tape = tape.expand() assert [i.name for i in tape.operations] == ["RX", "Rot", "PhaseShift"] assert np.all(tape.get_parameters() == [a, p[2], p[0], -p[2], p[1] + p[2]]) return tape.execute(device=device)
def cost_fn(a, p, device): tape = JacobianTape() with tape: qml.RX(a, wires=0) U3(*p, wires=0) qml.expval(qml.PauliX(0)) tape = AutogradInterface.apply(tape.expand()) assert tape.trainable_params == [1, 2, 3, 4] assert [i.name for i in tape.operations] == ["RX", "Rot", "PhaseShift"] assert np.all(np.array(tape.get_parameters()) == [p[2], p[0], -p[2], p[1] + p[2]]) return tape.execute(device=device)
def cost(a, b, device): with AutogradInterface.apply(JacobianTape()) as tape: qml.RY(a, wires=0) qml.RX(b, wires=0) qml.expval(qml.PauliZ(0)) assert tape.trainable_params == {0} return tape.execute(device)
def cost(a, b, c, device): with JacobianTape() as tape: qml.RY(a * c, wires=0) qml.RZ(b, wires=0) qml.RX(c + c**2 + np.sin(a), wires=0) qml.expval(qml.PauliZ(0)) return tape.execute(device)
def cost(a, b, device): with JacobianTape() as tape: qml.RY(a, wires=0) qml.RX(b, wires=0) qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliY(1)) return tape.execute(device)
def test_jacobian(self, mocker, tol): """Test jacobian calculation""" spy = mocker.spy(JacobianTape, "jacobian") a = np.array(0.1, requires_grad=True) b = np.array(0.2, requires_grad=True) def cost(a, b, device): with JacobianTape() as tape: qml.RY(a, wires=0) qml.RX(b, wires=0) qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliY(1)) return tape.execute(device) dev = qml.device("default.qubit.autograd", wires=2) res = qml.jacobian(cost)(a, b, device=dev) spy.assert_not_called() assert res.shape == (2, 2) # compare to standard tape jacobian with JacobianTape() as tape: qml.RY(a, wires=0) qml.RX(b, wires=0) qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliY(1)) expected = tape.jacobian(dev) assert expected.shape == (2, 2) assert np.allclose(res, expected, atol=tol, rtol=0)
def test_ragged_differentiation(self, tol): """Tests correct output shape and evaluation for a tape with prob and expval outputs""" dev = qml.device("default.qubit", wires=2) x_val = 0.543 y_val = -0.654 x = torch.tensor(x_val, requires_grad=True) y = torch.tensor(y_val, requires_grad=True) with TorchInterface.apply(JacobianTape()) as tape: qml.RX(x, wires=[0]) qml.RY(y, wires=[1]) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliZ(0)) qml.probs(wires=[1]) res = tape.execute(dev) expected = np.array([ np.cos(x_val), (1 + np.cos(x_val) * np.cos(y_val)) / 2, (1 - np.cos(x_val) * np.cos(y_val)) / 2, ]) assert np.allclose(res.detach().numpy(), expected, atol=tol, rtol=0) loss = torch.sum(res) loss.backward() expected = np.array([ -np.sin(x_val) + -np.sin(x_val) * np.cos(y_val) / 2 + np.cos(y_val) * np.sin(x_val) / 2, -np.cos(x_val) * np.sin(y_val) / 2 + np.cos(x_val) * np.sin(y_val) / 2, ]) assert np.allclose(x.grad, expected[0], atol=tol, rtol=0) assert np.allclose(y.grad, expected[1], atol=tol, rtol=0)
def test_incorrect_inferred_output_dim(self): """Test that a quantum tape with an incorrect inferred output dimension corrects itself when computing the Jacobian.""" dev = qml.device("default.qubit", wires=3) params = [1.0, 1.0, 1.0] with JacobianTape() as tape: qml.RX(params[0], wires=[0]) qml.RY(params[1], wires=[1]) qml.RZ(params[2], wires=[2]) qml.CNOT(wires=[0, 1]) qml.probs(wires=0) qml.probs(wires=[1]) # inferred output dim should be correct assert tape.output_dim == sum([2, 2]) # modify the output dim tape._output_dim = 2 res = tape.jacobian(dev, order=2) # output dim should be correct assert tape.output_dim == sum([2, 2]) assert res.shape == (4, 3)
def test_classical_processing(self, tol): """Test classical processing within the quantum tape""" p_val = [0.1, 0.2] params = torch.tensor(p_val, requires_grad=True) dev = qml.device("default.qubit", wires=1) with TorchInterface.apply(JacobianTape()) as tape: qml.RY(params[0] * params[1], wires=0) qml.RZ(0.2, wires=0) qml.RX(params[1] + params[1]**2 + torch.sin(params[0]), wires=0) qml.expval(qml.PauliZ(0)) assert tape.trainable_params == [0, 2] tape_params = [i.detach().numpy() for i in tape.get_parameters()] assert np.allclose( tape_params, [p_val[0] * p_val[1], p_val[1] + p_val[1]**2 + np.sin(p_val[0])], atol=tol, rtol=0, ) res = tape.execute(dev) res.backward() assert isinstance(params.grad, torch.Tensor) assert params.shape == (2, )
def test_parameters(self, tol): """Test Jacobian computation works when parameters are both passed and not passed.""" dev = qml.device("default.qubit", wires=2) params = [0.1, 0.2] with JacobianTape() as tape: qml.RX(params[0], wires=[0]) qml.RY(params[1], wires=[1]) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) # test Jacobian with no parameters res1 = tape.jacobian(dev) assert tape.get_parameters() == params # test Jacobian with parameters res2 = tape.jacobian(dev, params=[0.5, 0.6]) assert tape.get_parameters() == params # test setting parameters tape.set_parameters(params=[0.5, 0.6]) res3 = tape.jacobian(dev) assert np.allclose(res2, res3, atol=tol, rtol=0) assert not np.allclose(res1, res2, atol=tol, rtol=0) assert tape.get_parameters() == [0.5, 0.6]
def test_jacobian_dtype(self, tol): """Test calculating the jacobian with a different datatype""" a_val = 0.1 b_val = 0.2 a = torch.tensor(a_val, requires_grad=True, dtype=torch.float32) b = torch.tensor(b_val, requires_grad=True, dtype=torch.float32) dev = qml.device("default.qubit", wires=2) with TorchInterface.apply(JacobianTape(), dtype=torch.float32) as tape: qml.RY(a, wires=0) qml.RX(b, wires=1) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliY(1)) assert tape.trainable_params == [0, 1] res = tape.execute(dev) assert isinstance(res, torch.Tensor) assert res.shape == (2, ) assert res.dtype is torch.float32 loss = torch.sum(res) loss.backward() assert a.grad.dtype is torch.float32 assert b.grad.dtype is torch.float32
def test_numeric_pd_no_y0(self, tol): """Test that, if y0 is not passed when calling the numeric_pd method, y0 is calculated.""" dev = qml.device("default.qubit", wires=2) params = [0.1, 0.2] with JacobianTape() as tape: qml.RX(params[0], wires=[0]) qml.RY(params[1], wires=[1]) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) # compute numeric gradient of parameter 0, without passing y0 tapes, fn = tape.numeric_pd(0) assert len(tapes) == 2 res1 = fn([tape.execute(dev) for tape in tapes]) # compute y0 in advance y0 = tape.execute(dev) tapes, fn = tape.numeric_pd(0, y0=y0) assert len(tapes) == 1 res2 = fn([tape.execute(dev) for tape in tapes]) assert np.allclose(res1, res2, atol=tol, rtol=0)
def test_jacobian(self, mocker, tol): """Test jacobian calculation""" spy = mocker.spy(JacobianTape, "jacobian") a = tf.Variable(0.1, dtype=tf.float64) b = tf.Variable(0.2, dtype=tf.float64) dev = qml.device("default.qubit", wires=2) with tf.GradientTape() as tape: with TFInterface.apply(JacobianTape()) as qtape: qml.RY(a, wires=0) qml.RX(b, wires=1) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliY(1)) assert qtape.trainable_params == [0, 1] res = qtape.execute(dev) assert isinstance(res, tf.Tensor) assert res.shape == (2, ) expected = [tf.cos(a), -tf.cos(a) * tf.sin(b)] assert np.allclose(res, expected, atol=tol, rtol=0) res = tape.jacobian(res, [a, b]) expected = [[-tf.sin(a), tf.sin(a) * tf.sin(b)], [0, -tf.cos(a) * tf.cos(b)]] assert np.allclose(res, expected, atol=tol, rtol=0) spy.assert_called()
def test_incorrect_ragged_output_dim(self, mocker): """Test that a quantum tape with an incorrect inferred *ragged* output dimension corrects itself after evaluation.""" dev = qml.device("default.qubit", wires=3) params = [1.0, 1.0, 1.0] with JacobianTape() as tape: qml.RX(params[0], wires=[0]) qml.RY(params[1], wires=[1]) qml.RZ(params[2], wires=[2]) qml.CNOT(wires=[0, 1]) qml.probs(wires=0) qml.probs(wires=[1, 2]) # inferred output dim should be correct assert tape.output_dim == sum([2, 4]) # modify the output dim tape._output_dim = 2 res = tape.jacobian(dev, order=2, method="numeric") # output dim should be correct assert tape.output_dim == sum([2, 4]) assert res.shape == (6, 3)
def test_ragged_differentiation(self, tol): """Tests correct output shape and evaluation for a tape with prob and expval outputs""" dev = qml.device("default.qubit", wires=2) x = tf.Variable(0.543, dtype=tf.float64) y = tf.Variable(-0.654, dtype=tf.float64) with tf.GradientTape() as tape: with TFInterface.apply(JacobianTape()) as qtape: qml.RX(x, wires=[0]) qml.RY(y, wires=[1]) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliZ(0)) qml.probs(wires=[1]) res = qtape.execute(dev) expected = np.array([ tf.cos(x), (1 + tf.cos(x) * tf.cos(y)) / 2, (1 - tf.cos(x) * tf.cos(y)) / 2 ]) assert np.allclose(res, expected, atol=tol, rtol=0) res = tape.jacobian(res, [x, y]) expected = np.array([ [ -tf.sin(x), -tf.sin(x) * tf.cos(y) / 2, tf.cos(y) * tf.sin(x) / 2 ], [0, -tf.cos(x) * tf.sin(y) / 2, tf.cos(x) * tf.sin(y) / 2], ]) assert np.allclose(res, expected, atol=tol, rtol=0)
def test_classical_processing(self, mocker, tol): """Test classical processing within the quantum tape""" spy = mocker.spy(JacobianTape, "jacobian") a = tf.Variable(0.1, dtype=tf.float64) b = tf.constant(0.2, dtype=tf.float64) c = tf.Variable(0.3, dtype=tf.float64) dev = qml.device("default.qubit.tf", wires=1) with tf.GradientTape() as tape: with JacobianTape() as qtape: qml.RY(a * c, wires=0) qml.RZ(b, wires=0) qml.RX(c + c**2 + tf.sin(a), wires=0) qml.expval(qml.PauliZ(0)) assert qtape.get_parameters() == [a * c, b, c + c**2 + tf.sin(a)] res = qtape.execute(dev) res = tape.jacobian(res, [a, b, c]) assert isinstance(res[0], tf.Tensor) assert res[1] is None assert isinstance(res[2], tf.Tensor) spy.assert_not_called()
def test_jacobian_dtype(self, tol): """Test calculating the jacobian with a different datatype. Here, we specify tf.float32, as opposed to the default value of tf.float64.""" a = tf.Variable(0.1, dtype=tf.float32) b = tf.Variable(0.2, dtype=tf.float32) dev = qml.device("default.qubit", wires=2) with tf.GradientTape() as tape: with TFInterface.apply(JacobianTape(), dtype=tf.float32) as qtape: qml.RY(a, wires=0) qml.RX(b, wires=1) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliZ(0)) qml.expval(qml.PauliY(1)) assert qtape.trainable_params == [0, 1] res = qtape.execute(dev) assert isinstance(res, tf.Tensor) assert res.shape == (2, ) assert res.dtype is tf.float32 res = tape.jacobian(res, [a, b]) assert [r.dtype is tf.float32 for r in res]
def test_single_expectation_value_with_argnum_one(self, tol): """Tests correct output shape and evaluation for a tape with a single expval output where only one parameter is chosen to estimate the jacobian. This test relies on the fact that exactly one term of the estimated jacobian will match the expected analytical value. """ dev = qml.device("default.qubit", wires=2) x = 0.543 y = -0.654 with JacobianTape() as tape: qml.RX(x, wires=[0]) qml.RY(y, wires=[1]) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) res = tape.jacobian( dev, argnum=1) # <--- we only choose one trainable parameter assert res.shape == (1, 2) expected = np.array([[0, np.cos(y) * np.cos(x)]]) res = res.flatten() expected = expected.flatten() assert np.allclose(res, expected, atol=tol, rtol=0)
def cost(a, U, device): with AutogradInterface.apply(JacobianTape()) as tape: qml.QubitUnitary(U, wires=0) qml.RY(a, wires=0) qml.expval(qml.PauliZ(0)) assert tape.trainable_params == [1] return tape.execute(device)
def cost(x, device): with AutogradInterface.apply(JacobianTape()) as tape: qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) qml.sample(qml.PauliZ(0)) qml.sample(qml.PauliX(1)) return tape.execute(device)
def cost(x, device): with JacobianTape() as tape: qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) qml.sample(qml.PauliZ(0)) qml.sample(qml.PauliX(1)) return tape.execute(device)
def test_complex_min_version(self, monkeypatch): """Test if an error is raised when a version of torch before 1.6.0 is used as the dtype in the apply() method""" with monkeypatch.context() as m: m.setattr(qml.interfaces.torch, "COMPLEX_SUPPORT", False) with pytest.raises(qml.QuantumFunctionError, match=r"Version 1\.6\.0 or above of PyTorch"): TorchInterface.apply(JacobianTape(), dtype=torch.complex128)
def test_interface_str(self): """Test that the interface string is correctly identified as JAX""" with JAXInterface.apply(JacobianTape()) as tape: qml.RX(0.5, wires=0) qml.expval(qml.PauliX(0)) assert tape.interface == "jax" assert isinstance(tape, JAXInterface)