def test_cv_gradients_gaussian_circuit(self, G, O, tol): """Tests that the gradients of circuits of gaussian gates match between the finite difference and analytic methods.""" gaussian_dev = qml.device("default.gaussian", wires=2) tol = 1e-5 par = [0.4] def circuit(x): args = [0.3] * G.num_params args[0] = x qml.Displacement(0.5, 0, wires=0) G(*args, wires=range(G.num_wires)) qml.Beamsplitter(1.3, -2.3, wires=[0, 1]) qml.Displacement(-0.5, 0.1, wires=0) qml.Squeezing(0.5, -1.5, wires=0) qml.Rotation(-1.1, wires=0) return qml.expval(O(wires=0)) q = CVQNode(circuit, gaussian_dev) val = q.evaluate(par, {}) grad_F = q.jacobian(par, method="F") grad_A2 = q.jacobian(par, method="A", options={'force_order2': True}) if O.ev_order == 1: grad_A = q.jacobian(par, method="A") # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) # analytic method works for every parameter assert q.par_to_grad_method == {0: "A"} # the different methods agree assert grad_A2 == pytest.approx(grad_F, abs=tol)
def test_cv_gradients_parameters_inside_array(self, tol): """Tests that free parameters inside an array passed to an Operation yield correct gradients.""" gaussian_dev = qml.device("default.gaussian", wires=2) par = [0.4, 1.3] def qf(x, y): qml.Displacement(0.5, 0, wires=[0]) qml.Squeezing(x, 0, wires=[0]) M = np.zeros((5, 5), dtype=object) M[1, 1] = y M[1, 2] = 1.0 M[2, 1] = 1.0 return qml.expval(qml.PolyXP(M, [0, 1])) q = CVQNode(qf, gaussian_dev) grad_best = q.jacobian(par) grad_best2 = q.jacobian(par, options={"force_order2": True}) grad_F = q.jacobian(par, method="F") # par[0] can use the "A" method, par[1] cannot assert q.par_to_grad_method == {0: "A", 1: "F"} # the different methods agree assert grad_best == pytest.approx(grad_F, abs=tol) assert grad_best2 == pytest.approx(grad_F, abs=tol)
def test_cv_gradients_multiple_gate_parameters(self, tol): """Tests that gates with multiple free parameters yield correct gradients.""" gaussian_dev = qml.device("default.gaussian", wires=2) def qf(r0, phi0, r1, phi1): qml.Squeezing(r0, phi0, wires=[0]) qml.Squeezing(r1, phi1, wires=[0]) return qml.expval(qml.NumberOperator(0)) q = CVQNode(qf, gaussian_dev) par = [0.4, -0.3, -0.7, 0.2] grad_F = q.jacobian(par, method="F") grad_A = q.jacobian(par, method="A") grad_A2 = q.jacobian(par, method="A", options={'force_order2': True}) # analytic method works for every parameter assert q.par_to_grad_method == {i: "A" for i in range(4)} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A2 == pytest.approx(grad_F, abs=tol) # check against the known analytic formula r0, phi0, r1, phi1 = par dn = np.zeros([4]) dn[0] = np.cosh(2 * r1) * np.sinh( 2 * r0) + np.cos(phi0 - phi1) * np.cosh(2 * r0) * np.sinh(2 * r1) dn[1] = -0.5 * np.sin(phi0 - phi1) * np.sinh(2 * r0) * np.sinh(2 * r1) dn[2] = np.cos(phi0 - phi1) * np.cosh(2 * r1) * np.sinh( 2 * r0) + np.cosh(2 * r0) * np.sinh(2 * r1) dn[3] = 0.5 * np.sin(phi0 - phi1) * np.sinh(2 * r0) * np.sinh(2 * r1) assert dn[np.newaxis, :] == pytest.approx(grad_F, abs=tol)
def test_transform_observable_incorrect_heisenberg_size(): """The number of dimensions of a CV observable Heisenberg representation does not match the ev_order attribute.""" class P(CVObservable): """Dummy CV observable with incorrect ev_order""" num_wires = 1 num_params = 0 par_domain = None ev_order = 2 @staticmethod def _heisenberg_rep(p): return np.array([0, 1, 0]) dev = qml.device("default.gaussian", wires=1) def circuit(x): qml.Displacement(x, 0.1, wires=0) return qml.expval(P(0)) node = CVQNode(circuit, dev) with pytest.raises(QuantumFunctionError, match="Mismatch between the polynomial order"): node.jacobian([0.5])
def test_error_analytic_second_order_cv(self): """Test exception raised if attempting to use a second order observable to compute the variance derivative analytically""" dev = qml.device("default.gaussian", wires=1) def circuit(a): qml.Displacement(a, 0, wires=0) return qml.var(qml.NumberOperator(0)) circuit = CVQNode(circuit, dev) with pytest.raises(ValueError, match=r"cannot be used with the parameters \{0\}"): circuit.jacobian([1.0], method="A")
def test_non_gaussian_obs_predecessor(self, gaussian_device, tol): """Parshift differentiation method is allowed and matches finite diff if a non-Gaussian gate precedes an observable but is not preceded by the parametrized gate.""" def circuit(x): qml.Squeezing(x, 0, wires=[0]) qml.Kerr(0.54, wires=[1]) # nongaussian qml.Beamsplitter(1.1, 0, wires=[0, 1]) return qml.expval(qml.NumberOperator(0)) node = CVQNode(circuit, gaussian_device) par = [0.321] grad_A = node.jacobian(par, wrt=[0], method="A") grad_F = node.jacobian(par, method="F") assert grad_A == pytest.approx(grad_F, abs=tol) assert node.par_to_grad_method == {0: "A"}
def test_second_order_obs_not_following_gate(self, tol): """Parshift differentiation method matches finite diff and analytical result when we have order-2 observables that do not follow the parametrized gate. """ num_wires = 2 dev = qml.device("default.gaussian", wires=2) def circuit(params): for i in range(num_wires): qml.Squeezing(params[i], 0, wires=i) return [ qml.expval(qml.NumberOperator(wires=i)) for i in range(num_wires) ] node = CVQNode(circuit, dev) par = [0.321, -0.184] res = node(par) res_true = np.sinh(np.abs(par))**2 # analytical result assert res == pytest.approx(res_true, abs=tol) grad_A = node.jacobian([par], method="A") grad_F = node.jacobian([par], method="F") grad_true = np.diag(np.sinh(2 * np.abs(par)) * np.sign(par)) # analytical gradient assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A == pytest.approx(grad_true, abs=tol)
def test_first_order_cv(self, tol): """Test variance of a first order CV expectation value""" dev = qml.device("default.gaussian", wires=1) def circuit(r, phi): qml.Squeezing(r, 0, wires=0) qml.Rotation(phi, wires=0) return qml.var(qml.X(0)) circuit = CVQNode(circuit, dev) r = 0.543 phi = -0.654 var = circuit(r, phi) expected = np.exp(2 * r) * np.sin(phi)**2 + np.exp( -2 * r) * np.cos(phi)**2 assert var == pytest.approx(expected, abs=tol) # circuit jacobians gradA = circuit.jacobian([r, phi], method="A") gradF = circuit.jacobian([r, phi], method="F") expected = np.array([[ 2 * np.exp(2 * r) * np.sin(phi)**2 - 2 * np.exp(-2 * r) * np.cos(phi)**2, 2 * np.sinh(2 * r) * np.sin(2 * phi), ]]) assert gradA == pytest.approx(expected, abs=tol) assert gradF == pytest.approx(expected, abs=tol)
def test_error_unsupported_grad_recipe(self, monkeypatch): """Test exception raised if attempting to use the second order rule for computing the gradient analytically of an expectation value that contains an operation with an more than two terms in the gradient recipe""" class DummyOp(qml.operation.CVOperation): num_wires = 1 num_params = 1 par_domain = "R" grad_method = "A" grad_recipe = ([[1, 1, 1], [1, 1, 1], [1, 1, 1]], ) dev = qml.device("default.gaussian", wires=1) dev._operation_map["DummyOp"] = None def circuit(a): DummyOp(a, wires=[0]) return qml.expval(qml.NumberOperator(0)) with monkeypatch.context() as m: circuit = CVQNode(circuit, dev, force_order2=True) m.setattr(circuit, "_best_method", lambda arg: "A") with pytest.raises( NotImplementedError, match= r"analytic gradient for order-2 operators is unsupported"): grad_A = circuit.jacobian(0, method="A", options={'force_order2': True})
def test_gaussian_successors_fails(self, operable_mock_CV_device_2_wires): """Tests that the parameter-shift differentiation method is not allowed if a non-gaussian gate is between a differentiable gaussian gate and an observable.""" def circuit(x): qml.Squeezing(x, 0, wires=[0]) qml.Beamsplitter(np.pi / 4, 0, wires=[0, 1]) qml.Kerr(0.54, wires=[1]) return qml.expval(qml.NumberOperator(1)) node = CVQNode(circuit, operable_mock_CV_device_2_wires) with pytest.raises( ValueError, match="analytic gradient method cannot be used with"): node.jacobian([0.321], method="A") assert node.par_to_grad_method == {0: "F"}
def test_CVOperation_with_heisenberg_and_no_parshift(self, name, tol): """An integration test for Gaussian CV gates that have a Heisenberg representation but cannot be differentiated using the parameter-shift method themselves (for example, they may accept no parameters, or have no gradient recipe). Tests that the parameter-shift method can still be used with other gates in the circuit. """ gaussian_dev = qml.device("default.gaussian", wires=2) cls = getattr(qml.ops, name) if cls.supports_heisenberg and (not cls.supports_parameter_shift): U = np.array( [[0.51310276 + 0.81702166j, 0.13649626 + 0.22487759j], [0.26300233 + 0.00556194j, -0.96414101 - 0.03508489j]]) if cls.num_wires <= 0: w = list(range(2)) else: w = list(range(cls.num_wires)) def circuit(x): qml.Displacement(x, 0, wires=0) if cls.par_domain == "A": cls(U, wires=w) else: cls(wires=w) return qml.expval(qml.X(0)) qnode = CVQNode(circuit, gaussian_dev) grad_F = qnode.jacobian(0.5, method="F") grad_A = qnode.jacobian(0.5, method="A") grad_A2 = qnode.jacobian(0.5, method="A", options={'force_order2': True}) # par[0] can use the "A" method assert qnode.par_to_grad_method == {0: "A"} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A2 == pytest.approx(grad_F, abs=tol)
def test_cv_gradients_repeated_gate_parameters(self, tol): """Tests that repeated use of a free parameter in a multi-parameter gate yield correct gradients.""" gaussian_dev = qml.device("default.gaussian", wires=2) par = [0.2, 0.3] def qf(x, y): qml.Displacement(x, 0, wires=[0]) qml.Squeezing(y, -1.3 * y, wires=[0]) return qml.expval(qml.X(0)) q = CVQNode(qf, gaussian_dev) grad_F = q.jacobian(par, method="F") grad_A = q.jacobian(par, method="A") grad_A2 = q.jacobian(par, method="A", options={'force_order2': True}) # analytic method works for every parameter assert q.par_to_grad_method == {0: "A", 1: "A"} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A2 == pytest.approx(grad_F, abs=tol)
def test_quadoperator(self, tol): """Test the differentiation of CV observables that depend on positional qfunc parameters.""" def circuit(a): qml.Displacement(1.0, 0, wires=0) return qml.expval(qml.QuadOperator(a, 0)) gaussian_dev = qml.device("default.gaussian", wires=1) qnode = CVQNode(circuit, gaussian_dev) par = [0.6] grad_F = qnode.jacobian(par, method='F') grad_A = qnode.jacobian(par, method='A') grad_A2 = qnode.jacobian(par, method='A', options={'force_order2': True}) # par 0 can use the 'A' method assert qnode.par_to_grad_method == {0: 'A'} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A2 == pytest.approx(grad_F, abs=tol)
def test_cv_gradient_fanout(self, tol): """Tests that CV qnodes can compute the correct gradient when the same parameter is used in multiple gates.""" gaussian_dev = qml.device("default.gaussian", wires=2) par = [0.5, 1.3] def circuit(x, y): qml.Displacement(x, 0, wires=[0]) qml.Rotation(y, wires=[0]) qml.Displacement(0, x, wires=[0]) return qml.expval(qml.X(0)) q = CVQNode(circuit, gaussian_dev) grad_F = q.jacobian(par, method="F") grad_A = q.jacobian(par, method="A") grad_A2 = q.jacobian(par, method="A", options={'force_order2': True}) # analytic method works for every parameter assert q.par_to_grad_method == {0: "A", 1: "A"} # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A2 == pytest.approx(grad_F, abs=tol)
def test_keywordarg_with_positional_arg_immutable_second_order_cv( self, tol): """Non-differentiable keyword arguments appear in the same op with differentiable arguments, qfunc is immutable so kwargs are passed as Variables.""" dev = qml.device("default.gaussian", wires=1) def circuit(x, *, k=0.0): qml.Displacement(0.5, 0, wires=0) qml.Squeezing(x, k, wires=0) return qml.expval(qml.X(0)) node = CVQNode(circuit, dev, mutable=False) par = [0.39] aux = {'k': -0.7} # circuit jacobians grad_A = node.jacobian(par, aux, method="A", options={'force_order2': True}) grad_F = node.jacobian(par, aux, method="F") assert grad_A == pytest.approx(grad_F, abs=tol)
def test_gradient_gate_with_two_parameters(self, tol): """Gates with two parameters yield the correct parshift gradient.""" dev = qml.device("default.gaussian", wires=1) def qf(r0, phi0, r1, phi1): qml.Squeezing(r0, phi0, wires=[0]) qml.Squeezing(r1, phi1, wires=[0]) return qml.expval(qml.NumberOperator(0)) q = CVQNode(qf, dev) par = [0.543, 0.123, 0.654, -0.629] grad_A = q.jacobian(par, method="A") grad_F = q.jacobian(par, method="F") # the different methods agree assert grad_A == pytest.approx(grad_F, abs=tol)
def test_keywordarg_second_order_cv(self, tol): """Non-differentiable keyword arguments with a second order CV expectation value.""" dev = qml.device("default.gaussian", wires=3) def circuit(x, *, k=0.0): qml.Displacement(x, 0, wires=0) qml.Rotation(k, wires=0) return qml.expval(qml.PolyXP(np.diag([0, 1, 0]), wires=0)) # X^2 node = CVQNode(circuit, dev) par = [0.62] aux = {'k': 0.4} # circuit jacobians grad_A = node.jacobian(par, aux, method="A") grad_F = node.jacobian(par, aux, method="F") expected = np.array([[8 * par[0] * np.cos(aux['k'])**2]]) assert grad_A == pytest.approx(grad_F, abs=tol) assert grad_A == pytest.approx(expected, abs=tol)
def test_expval_and_variance_cv(self, tol): """Test that the qnode works for a combination of CV expectation values and variances""" dev = qml.device("default.gaussian", wires=3) def circuit(a, b): qml.Displacement(0.5, 0, wires=0) qml.Squeezing(a, 0, wires=0) qml.Squeezing(b, 0, wires=1) qml.Beamsplitter(0.6, -0.3, wires=[0, 1]) qml.Squeezing(-0.3, 0, wires=2) qml.Beamsplitter(1.4, 0.5, wires=[1, 2]) return qml.var(qml.X(0)), qml.expval(qml.X(1)), qml.var(qml.X(2)) node = CVQNode(circuit, dev) par = [0.54, -0.423] # jacobians must match gradA = node.jacobian(par, method="A") gradF = node.jacobian(par, method="F") assert gradA == pytest.approx(gradF, abs=tol)
def test_second_order_cv(self, tol): """Test variance of a second order CV expectation value""" dev = qml.device("default.gaussian", wires=1) def circuit(n, a): qml.ThermalState(n, wires=0) qml.Displacement(a, 0, wires=0) return qml.var(qml.NumberOperator(0)) circuit = CVQNode(circuit, dev) n = 0.12 a = 0.765 var = circuit(n, a) expected = n**2 + n + np.abs(a)**2 * (1 + 2 * n) assert var == pytest.approx(expected, abs=tol) # circuit jacobians gradF = circuit.jacobian([n, a], method="F") expected = np.array([[2 * a**2 + 2 * n + 1, 2 * a * (2 * n + 1)]]) assert gradF == pytest.approx(expected, abs=tol)