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_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_param_no_observables(self, operable_mock_CV_device_2_wires): """Tests that a parameter has 0 gradient if it is not followed by any observables""" par = [0.4] def qf(x): qml.Displacement(x, 0, wires=[0]) qml.Squeezing(0.3, x, wires=[0]) qml.Rotation(1.3, wires=[1]) return qml.expval(qml.X(1)) q = CVQNode(qf, operable_mock_CV_device_2_wires) q._construct(par, {}) assert q.par_to_grad_method == {0: "0"}
def test_param_not_differentiable(self, operable_mock_CV_device_2_wires): """Tests that a parameter is not differentiable if used in an operation where grad_method=None""" par = [0.4] def qf(x): qml.FockState(x, wires=[0]) qml.Rotation(1.3, wires=[0]) return qml.expval(qml.X(0)) q = CVQNode(qf, operable_mock_CV_device_2_wires) q._construct(par, {}) assert q.par_to_grad_method == {0: None}
def test_correct_method_non_gaussian_preceeding_one_param( self, operable_mock_CV_device_2_wires): """Tests that a non-Gaussian preceeding one parameter fallsback to finite-diff""" par = [0.4, -2.3] def qf(x, y): qml.Kerr(y, wires=[1]) qml.Displacement(x, 0, wires=[0]) qml.Beamsplitter(0.2, 1.7, wires=[0, 1]) return qml.expval(qml.X(0)), qml.expval(qml.X(1)) q = CVQNode(qf, operable_mock_CV_device_2_wires) q._construct(par, {}) assert q.par_to_grad_method == {0: "A", 1: "F"}
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_displacement_gradient(self, mag, theta, tol): """Tests that the automatic gradient of a phase space displacement is correct.""" def circuit(r, phi): qml.Displacement(r, phi, wires=[0]) return qml.expval(qml.X(0)) dev = qml.device('default.gaussian', wires=1) circuit = to_autograd(CVQNode(circuit, dev)) grad_fn = autograd.grad(circuit) #alpha = mag * np.exp(1j * theta) autograd_val = grad_fn(mag, theta) # qfunc evalutes to hbar * Re(alpha) manualgrad_val = hbar * np.cos(theta) assert autograd_val == pytest.approx(manualgrad_val, abs=tol)
def test_beamsplitter_gradient(self, theta, tol): """Tests that the automatic gradient of a beamsplitter is correct.""" def circuit(y): qml.Displacement(alpha, 0., wires=[0]) qml.Beamsplitter(y, 0, wires=[0, 1]) return qml.expval(qml.X(0)) dev = qml.device('default.gaussian', wires=2) circuit = to_autograd(CVQNode(circuit, dev)) grad_fn = autograd.grad(circuit) autograd_val = grad_fn(theta) # qfunc evalutes to hbar * alpha * cos(theta) manualgrad_val = -hbar * alpha * np.sin(theta) assert autograd_val == pytest.approx(manualgrad_val, abs=tol)
def test_squeeze_gradient(self, r, tol): """Tests that the automatic gradient of a phase space squeezing is correct.""" def circuit(y): qml.Displacement(alpha, 0., wires=[0]) qml.Squeezing(y, 0., wires=[0]) return qml.expval(qml.X(0)) dev = qml.device('default.gaussian', wires=1) circuit = to_autograd(CVQNode(circuit, dev)) grad_fn = autograd.grad(circuit) autograd_val = grad_fn(r) # qfunc evaluates to -exp(-r) * hbar * Re(alpha) manualgrad_val = -np.exp(-r) * hbar * alpha assert autograd_val == pytest.approx(manualgrad_val, abs=tol)
def test_number_state_gradient(self, r, tol): """Tests that the automatic gradient of a squeezed state with number state expectation is correct.""" def circuit(y): qml.Squeezing(y, 0., wires=[0]) return qml.expval(qml.FockStateProjector(np.array([2, 0]), wires=[0, 1])) dev = qml.device('default.gaussian', wires=2) circuit = to_autograd(CVQNode(circuit, dev)) grad_fn = autograd.grad(circuit) # (d/dr) |<2|S(r)>|^2 = 0.5 tanh(r)^3 (2 csch(r)^2 - 1) sech(r) autograd_val = grad_fn(r) manualgrad_val = 0.5*np.tanh(r)**3 * (2/(np.sinh(r)**2)-1) / np.cosh(r) assert autograd_val == pytest.approx(manualgrad_val, abs=tol)
def test_correct_method_non_gaussian_successor_one_param( self, operable_mock_CV_device_2_wires): """Tests that a non-Gaussian succeeding a parameter fallsback to finite-diff""" par = [0.4, -2.3] def qf(x, y): qml.Displacement(x, 0, wires=[0]) qml.CubicPhase(0.2, wires=[0]) qml.Squeezing(0.3, y, wires=[1]) qml.Rotation(1.3, wires=[1]) # nongaussian succeeding x but not y return qml.expval(qml.X(0)), qml.expval(qml.X(1)) q = CVQNode(qf, operable_mock_CV_device_2_wires) q._construct(par, {}) assert q.par_to_grad_method == {0: "F", 1: "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_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_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_correct_method_non_gaussian_observable( self, operable_mock_CV_device_2_wires): """Tests that a non-Gaussian observable one parameter fallsback to finite-diff""" par = [0.4, -2.3] def qf(x, y): qml.Displacement(x, 0, wires=[0]) # followed by nongaussian observable qml.Beamsplitter(0.2, 1.7, wires=[0, 1]) qml.Displacement(y, 0, wires=[1]) # followed by order-2 observable return qml.expval(qml.FockStateProjector(np.array([2]), 0)), qml.expval( qml.NumberOperator(1)) q = CVQNode(qf, operable_mock_CV_device_2_wires) q._construct(par, {}) assert q.par_to_grad_method == {0: "F", 1: "A"}
def test_correct_method_non_gaussian_successor_all_params( self, operable_mock_CV_device_2_wires): """Tests that a non-Gaussian succeeding all parameters fallsback to finite-diff""" par = [0.4, -2.3] def qf(x, y): qml.Displacement(x, 0, wires=[0]) qml.Displacement(1.2, y, wires=[1]) qml.Beamsplitter(0.2, 1.7, wires=[0, 1]) qml.Rotation(1.9, wires=[0]) qml.Kerr(0.3, wires=[ 1 ]) # nongaussian succeeding both x and y due to the beamsplitter return qml.expval(qml.X(0)), qml.expval(qml.X(1)) q = CVQNode(qf, operable_mock_CV_device_2_wires) q._construct(par, {}) assert q.par_to_grad_method == {0: "F", 1: "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_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_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_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)
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)