From 08122e12fa25f49753a55c59ab4aefb2f2f9cd16 Mon Sep 17 00:00:00 2001 From: Vincent Michaud-Rioux Date: Tue, 13 Feb 2024 20:41:57 +0000 Subject: [PATCH 1/5] isclose => allclose --- pennylane/drawer/utils.py | 1 - .../ops/op_math/controlled_decompositions.py | 24 +++++++++---------- pennylane/ops/op_math/controlled_ops.py | 1 - 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pennylane/drawer/utils.py b/pennylane/drawer/utils.py index 90c05ec08ba..3d7cc4a1eb0 100644 --- a/pennylane/drawer/utils.py +++ b/pennylane/drawer/utils.py @@ -128,7 +128,6 @@ def unwrap_controls(op): next_ctrl = op while hasattr(next_ctrl, "base"): - if _is_controlled(next_ctrl.base): base_control_wires = getattr(next_ctrl.base, "control_wires", []) control_wires += base_control_wires diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index 26c4c5a1a50..a486cf6bf89 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -74,7 +74,7 @@ def _bisect_compute_a(u: np.ndarray): z = u[1, 1] zr = np.real(z) zi = np.imag(z) - if np.isclose(zr, -1): + if np.allclose(zr, -1): # special case [[-1, 0], [0, -1]] # would cause divide by 0 with the other formula, so we use hardcoded solution return np.array([[1, -1], [1, 1]]) * 2**-0.5 @@ -99,9 +99,9 @@ def _bisect_compute_b(u: np.ndarray): w = np.real(u[0, 0]) s = np.real(u[1, 0]) t = np.imag(u[1, 0]) - if np.isclose(s, 0): + if np.allclose(s, 0): b = 0 - if np.isclose(t, 0): + if np.allclose(t, 0): if w < 0: c = 0 d = sqrt(-w) @@ -111,7 +111,7 @@ def _bisect_compute_b(u: np.ndarray): else: c = sqrt(2 - 2 * w) * (-w / 2 - 1 / 2) / t d = sqrt(2 - 2 * w) / 2 - elif np.isclose(t, 0): + elif np.allclose(t, 0): b = (1 / 2 - w / 2) * sqrt(2 * w + 2) / s c = sqrt(2 * w + 2) / 2 d = 0 @@ -193,9 +193,9 @@ def decomp_circuit(op): decomp = [] - if not qml.math.isclose(0.0, phi, atol=1e-8, rtol=0): + if not qml.math.allclose(0.0, phi, atol=1e-8, rtol=0): decomp.append(qml.RZ(phi, wires=target_wire)) - if not qml.math.isclose(0.0, theta / 2, atol=1e-8, rtol=0): + if not qml.math.allclose(0.0, theta / 2, atol=1e-8, rtol=0): decomp.extend( [ qml.RY(theta / 2, wires=target_wire), @@ -205,10 +205,10 @@ def decomp_circuit(op): ) else: decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires)) - if not qml.math.isclose(0.0, -(phi + omega) / 2, atol=1e-6, rtol=0): + if not qml.math.allclose(0.0, -(phi + omega) / 2, atol=1e-6, rtol=0): decomp.append(qml.RZ(-(phi + omega) / 2, wires=target_wire)) decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires)) - if not qml.math.isclose(0.0, (omega - phi) / 2, atol=1e-8, rtol=0): + if not qml.math.allclose(0.0, (omega - phi) / 2, atol=1e-8, rtol=0): decomp.append(qml.RZ((omega - phi) / 2, wires=target_wire)) return decomp @@ -245,7 +245,7 @@ def _ctrl_decomp_bisect_od( """ ui = np.imag(u) - if not np.isclose(ui[1, 0], 0) or not np.isclose(ui[0, 1], 0): + if not np.allclose(ui[1, 0], 0) or not np.allclose(ui[0, 1], 0): raise ValueError(f"Target operation's matrix must have real off-diagonal, but it is {u}") a = _bisect_compute_a(u) @@ -296,7 +296,7 @@ def _ctrl_decomp_bisect_md( """ ui = np.imag(u) - if not np.isclose(ui[0, 0], 0) or not np.isclose(ui[1, 1], 0): + if not np.allclose(ui[0, 0], 0) or not np.allclose(ui[1, 1], 0): raise ValueError(f"Target operation's matrix must have real main-diagonal, but it is {u}") h_matrix = qml.Hadamard.compute_matrix() @@ -431,10 +431,10 @@ def ctrl_decomp_bisect( target_matrix = _convert_to_su2(target_matrix) target_matrix_imag = np.imag(target_matrix) - if np.isclose(target_matrix_imag[1, 0], 0) and np.isclose(target_matrix_imag[0, 1], 0): + if np.allclose(target_matrix_imag[1, 0], 0) and np.allclose(target_matrix_imag[0, 1], 0): # Real off-diagonal specialized algorithm - 16n+O(1) CNOTs return _ctrl_decomp_bisect_od(target_matrix, target_wire, control_wires) - if np.isclose(target_matrix_imag[0, 0], 0) and np.isclose(target_matrix_imag[1, 1], 0): + if np.allclose(target_matrix_imag[0, 0], 0) and np.allclose(target_matrix_imag[1, 1], 0): # Real main-diagonal specialized algorithm - 16n+O(1) CNOTs return _ctrl_decomp_bisect_md(target_matrix, target_wire, control_wires) # General algorithm - 20n+O(1) CNOTs diff --git a/pennylane/ops/op_math/controlled_ops.py b/pennylane/ops/op_math/controlled_ops.py index 5601351bb61..edcf33f8845 100644 --- a/pennylane/ops/op_math/controlled_ops.py +++ b/pennylane/ops/op_math/controlled_ops.py @@ -353,7 +353,6 @@ def decomposition(self): def _check_and_convert_control_values(control_values, control_wires): - if isinstance(control_values, str): # Make sure all values are either 0 or 1 if not set(control_values).issubset({"1", "0"}): From c1ffa37570022eb54d937a8c20e044a2c1fef094 Mon Sep 17 00:00:00 2001 From: Vincent Michaud-Rioux Date: Tue, 13 Feb 2024 21:01:03 +0000 Subject: [PATCH 2/5] Update changelog --- doc/releases/changelog-dev.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 3a0b7163a2b..df31e720379 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -426,8 +426,11 @@

Bug fixes 🐛

+* `ctrl_decomp_zyz` is now differentiable after changing `isclose` to `allclose`. + [(#5198)](https://github.com/PennyLaneAI/pennylane/pull/5198) + * `qml.ops.Pow.matrix()` is now differentiable with TensorFlow with integer exponents. -[(#5178)](https://github.com/PennyLaneAI/pennylane/pull/5178) + [(#5178)](https://github.com/PennyLaneAI/pennylane/pull/5178) * The `qml.MottonenStatePreparation` template is updated to include a global phase operation. [(#5166)](https://github.com/PennyLaneAI/pennylane/pull/5166) From 7f8875a1a9c4400a9880365d9f89bf8c71153d01 Mon Sep 17 00:00:00 2001 From: Vincent Michaud-Rioux Date: Tue, 13 Feb 2024 17:47:04 -0500 Subject: [PATCH 3/5] Limit changes to backprop friendly routine. --- .../ops/op_math/controlled_decompositions.py | 16 ++++----- .../op_math/test_controlled_decompositions.py | 35 +++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index a486cf6bf89..59f3da1f1c4 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -74,7 +74,7 @@ def _bisect_compute_a(u: np.ndarray): z = u[1, 1] zr = np.real(z) zi = np.imag(z) - if np.allclose(zr, -1): + if np.isclose(zr, -1): # special case [[-1, 0], [0, -1]] # would cause divide by 0 with the other formula, so we use hardcoded solution return np.array([[1, -1], [1, 1]]) * 2**-0.5 @@ -99,9 +99,9 @@ def _bisect_compute_b(u: np.ndarray): w = np.real(u[0, 0]) s = np.real(u[1, 0]) t = np.imag(u[1, 0]) - if np.allclose(s, 0): + if np.isclose(s, 0): b = 0 - if np.allclose(t, 0): + if np.isclose(t, 0): if w < 0: c = 0 d = sqrt(-w) @@ -111,7 +111,7 @@ def _bisect_compute_b(u: np.ndarray): else: c = sqrt(2 - 2 * w) * (-w / 2 - 1 / 2) / t d = sqrt(2 - 2 * w) / 2 - elif np.allclose(t, 0): + elif np.isclose(t, 0): b = (1 / 2 - w / 2) * sqrt(2 * w + 2) / s c = sqrt(2 * w + 2) / 2 d = 0 @@ -245,7 +245,7 @@ def _ctrl_decomp_bisect_od( """ ui = np.imag(u) - if not np.allclose(ui[1, 0], 0) or not np.allclose(ui[0, 1], 0): + if not np.isclose(ui[1, 0], 0) or not np.isclose(ui[0, 1], 0): raise ValueError(f"Target operation's matrix must have real off-diagonal, but it is {u}") a = _bisect_compute_a(u) @@ -296,7 +296,7 @@ def _ctrl_decomp_bisect_md( """ ui = np.imag(u) - if not np.allclose(ui[0, 0], 0) or not np.allclose(ui[1, 1], 0): + if not np.isclose(ui[0, 0], 0) or not np.isclose(ui[1, 1], 0): raise ValueError(f"Target operation's matrix must have real main-diagonal, but it is {u}") h_matrix = qml.Hadamard.compute_matrix() @@ -431,10 +431,10 @@ def ctrl_decomp_bisect( target_matrix = _convert_to_su2(target_matrix) target_matrix_imag = np.imag(target_matrix) - if np.allclose(target_matrix_imag[1, 0], 0) and np.allclose(target_matrix_imag[0, 1], 0): + if np.isclose(target_matrix_imag[1, 0], 0) and np.isclose(target_matrix_imag[0, 1], 0): # Real off-diagonal specialized algorithm - 16n+O(1) CNOTs return _ctrl_decomp_bisect_od(target_matrix, target_wire, control_wires) - if np.allclose(target_matrix_imag[0, 0], 0) and np.allclose(target_matrix_imag[1, 1], 0): + if np.isclose(target_matrix_imag[0, 0], 0) and np.isclose(target_matrix_imag[1, 1], 0): # Real main-diagonal specialized algorithm - 16n+O(1) CNOTs return _ctrl_decomp_bisect_md(target_matrix, target_wire, control_wires) # General algorithm - 20n+O(1) CNOTs diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index b51dd44adaf..1713107c0ee 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -133,6 +133,41 @@ def expected_circuit(): assert np.allclose(res, expected, atol=tol, rtol=0) + @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3])) + def test_decomposition_circuit_gradient(self, control_wires, tol): + """Tests that the controlled decomposition of a single-qubit operation + behaves as expected in a quantum circuit""" + n_qubits = 4 + np.random.seed(1337) + + dev = qml.device("default.qubit", wires=n_qubits) + init_state = np.random.rand(2**n_qubits) + 1.0j * np.random.rand(2**n_qubits) + init_state /= np.sqrt(np.dot(np.conj(init_state), init_state)) + init_state = np.array(init_state) + target_wires = [0] + control_values = [True] * len(control_wires) + + def circuit(p): + qml.StatePrep(init_state, wires=range(n_qubits)) + qml.ctrl( + qml.Rot(*p, wires=target_wires), + control_wires, + control_values=control_values, + ) + return qml.probs(wires=target_wires) + + circ_ad = qml.QNode(circuit, dev, diff_method="adjoint") + circ_ps = qml.QNode(circuit, dev, diff_method="finite-diff") + par = qml.numpy.array([0.1234, 0.235, 0.5678]) + jac_ad = qml.jacobian(circ_ad)(par) + jac_ps = qml.jacobian(circ_ps)(par) + + # different methods must agree + assert jac_ad.size == 2 * 3 + assert np.allclose(jac_ad.shape, [2, 3]) + assert np.allclose(jac_ad.shape, jac_ps.shape) + assert np.allclose(jac_ad, jac_ps, atol=tol, rtol=0) + @pytest.mark.parametrize("op", su2_ops) @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3])) def test_decomposition_matrix(self, op, control_wires, tol): From 31933b24300ad78c49f64910aafbc653c10e0226 Mon Sep 17 00:00:00 2001 From: Vincent Michaud-Rioux Date: Thu, 15 Feb 2024 08:00:30 -0500 Subject: [PATCH 4/5] Update doc/releases/changelog-dev.md Co-authored-by: Mudit Pandey --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index df31e720379..a06005a3027 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -426,7 +426,7 @@

Bug fixes 🐛

-* `ctrl_decomp_zyz` is now differentiable after changing `isclose` to `allclose`. +* `ctrl_decomp_zyz` is now differentiable. [(#5198)](https://github.com/PennyLaneAI/pennylane/pull/5198) * `qml.ops.Pow.matrix()` is now differentiable with TensorFlow with integer exponents. From 799bb458413aef23d6ce4aeceb1d4306235a9998 Mon Sep 17 00:00:00 2001 From: Vincent Michaud-Rioux Date: Thu, 15 Feb 2024 15:35:39 +0000 Subject: [PATCH 5/5] Change finite-diff for backprop in test_decomposition_circuit_gradient --- tests/ops/op_math/test_controlled_decompositions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index 1713107c0ee..46626d395f1 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -157,16 +157,16 @@ def circuit(p): return qml.probs(wires=target_wires) circ_ad = qml.QNode(circuit, dev, diff_method="adjoint") - circ_ps = qml.QNode(circuit, dev, diff_method="finite-diff") + circ_bp = qml.QNode(circuit, dev, diff_method="backprop") par = qml.numpy.array([0.1234, 0.235, 0.5678]) jac_ad = qml.jacobian(circ_ad)(par) - jac_ps = qml.jacobian(circ_ps)(par) + jac_bp = qml.jacobian(circ_bp)(par) # different methods must agree assert jac_ad.size == 2 * 3 assert np.allclose(jac_ad.shape, [2, 3]) - assert np.allclose(jac_ad.shape, jac_ps.shape) - assert np.allclose(jac_ad, jac_ps, atol=tol, rtol=0) + assert np.allclose(jac_ad.shape, jac_bp.shape) + assert np.allclose(jac_ad, jac_bp, atol=tol, rtol=0) @pytest.mark.parametrize("op", su2_ops) @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3]))