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_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_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_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_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_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_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_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)