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) par = [0.4, -0.3, -0.7, 0.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) 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_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_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_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_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_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_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_successor_unused_param(self, operable_mock_CV_device_2_wires): """Tests that a non-Gaussian succeeding a parameter fallsback to finite-diff alongside an unused parameter""" par = [0.4, -2.3] def qf(x, y): qml.Displacement(x, 0, wires=[0]) qml.CubicPhase(0.2, wires=[0]) # nongaussian succeeding x qml.Squeezing(0.3, x, wires=[1]) # x affects gates on both wires, y unused qml.Rotation(1.3, wires=[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: "F", 1: "0"}
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_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_non_gaussian_successors(self, gaussian_device, tol): """Tests that the analytic differentiation method is allowed and matches numerical differentiation if a non-Gaussian gate is not succeeded by 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(0)) node = CVQNode(circuit, gaussian_device) res = node.jacobian([0.321], wrt=[0], method="A") expected = node.jacobian([0.321], method="F") assert res == pytest.approx(expected, abs=tol) assert node.par_to_grad_method == {0: "A"}
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): """Test that a gate with two parameters yields correct gradients""" def qf(r0, phi0, r1, phi1): qml.Squeezing(r0, phi0, wires=[0]) qml.Squeezing(r1, phi1, wires=[0]) return qml.expval(qml.NumberOperator(0)) dev = qml.device("default.gaussian", wires=1) 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_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_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_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_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_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_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_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_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)