From afcf5aebd98ca507e2346765e4ad602b01ac6fd7 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 8 Jan 2025 15:36:56 +0100 Subject: [PATCH 1/4] fix PauliEvo for all identities --- .../src/circuit_library/pauli_evolution.rs | 19 ++++++------ crates/circuit/src/operations.rs | 8 ++++- qiskit/synthesis/evolution/suzuki_trotter.py | 2 +- ...uli-evo-all-identity-b129acd854d8c391.yaml | 8 +++++ .../circuit/library/test_evolution_gate.py | 30 +++++++++++++++++++ 5 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml diff --git a/crates/accelerate/src/circuit_library/pauli_evolution.rs b/crates/accelerate/src/circuit_library/pauli_evolution.rs index 3c5164314c08..c3bd898e5b77 100644 --- a/crates/accelerate/src/circuit_library/pauli_evolution.rs +++ b/crates/accelerate/src/circuit_library/pauli_evolution.rs @@ -192,6 +192,7 @@ fn multi_qubit_evolution( /// followed by a CX-chain and then a single Pauli-Z rotation on the last qubit. Then the CX-chain /// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the /// Pauli string XIYZ we have the circuit +/// /// ┌───┐┌───────┐┌───┐ /// 0: ─────────────┤ X ├┤ Rz(2) ├┤ X ├─────────── /// ┌──────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌────┐ @@ -205,9 +206,9 @@ fn multi_qubit_evolution( /// Args: /// num_qubits: The number of qubits in the Hamiltonian. /// sparse_paulis: The Paulis to implement. Given in a sparse-list format with elements -/// ``(pauli_string, qubit_indices, coefficient)``. An element of the form -/// ``("IXYZ", [0,1,2,3], 0.2)``, for example, is interpreted in terms of qubit indices as -/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 0.4. +/// ``(pauli_string, qubit_indices, rz_rotation_angle)``. An element of the form +/// ``("XIYZ", [0,1,2,3], 2)``, for example, is interpreted in terms of qubit indices as +/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 2. /// insert_barriers: If ``true``, insert a barrier in between the evolution of individual /// Pauli terms. /// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each @@ -244,7 +245,7 @@ pub fn py_pauli_evolution( } paulis.push(pauli); - times.push(time); // note we do not multiply by 2 here, this is done Python side! + times.push(time); // note we do not multiply by 2 here, this is already done Python side! indices.push(tuple.get_item(1)?.extract::>()?) } @@ -266,12 +267,12 @@ pub fn py_pauli_evolution( }, ); - // When handling all-identity Paulis above, we added the time as global phase. - // However, the all-identity Paulis should add a negative phase, as they implement - // exp(-i t I). We apply the negative sign here, to only do a single (-1) multiplication, - // instead of doing it every time we find an all-identity Pauli. + // When handling all-identity Paulis above, we added the RZ rotation angle as global phase, + // meaning that we have implemented of exp(i 2t I). However, what we want it to implement + // exp(-i t I). To only use a single multiplication, we apply a factor of -0.5 here. + // This is faster, in particular as long as the parameter expressions are in Python. if modified_phase { - global_phase = multiply_param(&global_phase, -1.0, py); + global_phase = multiply_param(&global_phase, -0.5, py); } CircuitData::from_packed_operations(py, num_qubits as u32, 0, evos, global_phase) diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 59adfd9e0e8c..31f685b32434 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -2340,8 +2340,14 @@ pub fn add_param(param: &Param, summand: f64, py: Python) -> Param { } pub fn radd_param(param1: Param, param2: Param, py: Python) -> Param { - match [param1, param2] { + match [¶m1, ¶m2] { [Param::Float(theta), Param::Float(lambda)] => Param::Float(theta + lambda), + [Param::Float(theta), Param::ParameterExpression(_lambda)] => { + add_param(¶m2, *theta, py) + } + [Param::ParameterExpression(_theta), Param::Float(lambda)] => { + add_param(¶m1, *lambda, py) + } [Param::ParameterExpression(theta), Param::ParameterExpression(lambda)] => { Param::ParameterExpression( theta diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index 209f377351a7..0031063e75e0 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -138,7 +138,7 @@ def expand( .. code-block:: text - ("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], 2) + ("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], t) Note that the rotation angle contains a factor of 2, such that that evolution of a Pauli :math:`P` over time :math:`t`, which is :math:`e^{itP}`, is represented diff --git a/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml b/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml new file mode 100644 index 000000000000..03cbe8654cb9 --- /dev/null +++ b/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + The :class:`.PauliEvolutionGate`, if used with a product formula synthesis (this is the default), + did not correctly handle all-identity terms in the operator. The all-identity term + should introduce a global phase equal to ``-evolution_time``, but was off by a factor of 2 + and could break for parameterized times. This behavior is now fixed. + Fixed `#13625 `__. diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 1b55ea920289..468f61443fc6 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -479,6 +479,36 @@ def atomic_evolution(pauli, time): decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["cx"], reps * 3 * 4) + def test_all_identity(self): + """Test circuit with all identity Paulis works correctly.""" + evo = PauliEvolutionGate(I ^ I, time=1).definition + expected = QuantumCircuit(2, global_phase=-1) + self.assertEqual(expected, evo) + + def test_global_phase(self): + """Test a circuit with parameterized global phase terms. + + Regression test of #13625. + """ + pauli = (X ^ X) + (I ^ I) + (I ^ X) + time = Parameter("t") + evo = PauliEvolutionGate(pauli, time=time) + + expected = QuantumCircuit(2, global_phase=-time) + expected.rxx(2 * time, 0, 1) + expected.rx(2 * time, 0) + + with self.subTest(msg="check circuit"): + self.assertEqual(expected, evo.definition) + + # since all terms in the Pauli operator commute, we can compare to an + # exact matrix exponential + time_value = 1.76123 + bound = evo.definition.assign_parameters([time_value]) + exact = scipy.linalg.expm(-1j * time_value * pauli.to_matrix()) + with self.subTest(msg="check correctness"): + self.assertEqual(Operator(exact), Operator(bound)) + def exact_atomic_evolution(circuit, pauli, time): """An exact atomic evolution for Suzuki-Trotter. From 91dd9c2c4aadd164c635214ec8aae6382c754c15 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 8 Jan 2025 17:26:15 +0100 Subject: [PATCH 2/4] fix rustiq Co-authored-by: Alexander Ivrii --- crates/accelerate/src/synthesis/evolution/pauli_network.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/synthesis/evolution/pauli_network.rs b/crates/accelerate/src/synthesis/evolution/pauli_network.rs index b5a73262aae8..a3a03e2fddbe 100644 --- a/crates/accelerate/src/synthesis/evolution/pauli_network.rs +++ b/crates/accelerate/src/synthesis/evolution/pauli_network.rs @@ -211,7 +211,7 @@ fn inject_rotations( if pauli_support_size == 0 { // in case of an all-identity rotation, update global phase by subtracting // the angle - global_phase = radd_param(global_phase, multiply_param(&angles[i], -1.0, py), py); + global_phase = radd_param(global_phase, multiply_param(&angles[i], -0.5, py), py); hit_paulis[i] = true; dag.remove_node(i); } else if pauli_support_size == 1 && dag.is_front_node(i) { From 55a53576cbdd982933d61e3fbf987c56d3d45bdd Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 10 Jan 2025 13:30:12 +0100 Subject: [PATCH 3/4] fix docs --- .../src/circuit_library/pauli_evolution.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/accelerate/src/circuit_library/pauli_evolution.rs b/crates/accelerate/src/circuit_library/pauli_evolution.rs index c3bd898e5b77..b5aa71716341 100644 --- a/crates/accelerate/src/circuit_library/pauli_evolution.rs +++ b/crates/accelerate/src/circuit_library/pauli_evolution.rs @@ -193,22 +193,21 @@ fn multi_qubit_evolution( /// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the /// Pauli string XIYZ we have the circuit /// -/// ┌───┐┌───────┐┌───┐ -/// 0: ─────────────┤ X ├┤ Rz(2) ├┤ X ├─────────── -/// ┌──────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌────┐ -/// 1: ┤ √Xdg ├┤ X ├──■─────────────■──┤ X ├┤ √X ├ -/// └──────┘└─┬─┘ └─┬─┘└────┘ -/// 2: ──────────┼───────────────────────┼──────── -/// ┌───┐ │ │ ┌───┐ -/// 3: ─┤ H ├────■───────────────────────■──┤ H ├─ -/// └───┘ └───┘ +/// ┌───┐ ┌───┐┌───────┐┌───┐┌───┐ +/// 0: ┤ H ├──────┤ X ├┤ Rz(2) ├┤ X ├┤ H ├──────── +/// └───┘ └─┬─┘└───────┘└─┬─┘└───┘ +/// 1: ─────────────┼─────────────┼─────────────── +/// ┌────┐┌───┐ │ │ ┌───┐┌──────┐ +/// 2: ┤ √X ├┤ X ├──■─────────────■──┤ X ├┤ √Xdg ├ +/// └────┘└─┬─┘ └─┬─┘└──────┘ +/// 3: ────────■───────────────────────■────────── /// /// Args: /// num_qubits: The number of qubits in the Hamiltonian. /// sparse_paulis: The Paulis to implement. Given in a sparse-list format with elements /// ``(pauli_string, qubit_indices, rz_rotation_angle)``. An element of the form /// ``("XIYZ", [0,1,2,3], 2)``, for example, is interpreted in terms of qubit indices as -/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 2. +/// X_q0 I_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 2. /// insert_barriers: If ``true``, insert a barrier in between the evolution of individual /// Pauli terms. /// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each From 9254a32822aa8998b906ae11c1cb0d30a7657f30 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 13 Jan 2025 08:41:11 +0100 Subject: [PATCH 4/4] regression test 13644 --- .../circuit/library/test_evolved_op_ansatz.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/python/circuit/library/test_evolved_op_ansatz.py b/test/python/circuit/library/test_evolved_op_ansatz.py index 9c923764f408..2e7865a856c4 100644 --- a/test/python/circuit/library/test_evolved_op_ansatz.py +++ b/test/python/circuit/library/test_evolved_op_ansatz.py @@ -199,6 +199,21 @@ def test_detect_commutation(self): # this Hamiltonian should be split into 2 commuting groups, hence we get 2 parameters self.assertEqual(2, circuit.num_parameters) + def test_evolution_with_identity(self): + """Test a Hamiltonian containing an identity term. + + Regression test of #13644. + """ + hamiltonian = SparsePauliOp(["III", "IZZ", "IXI"]) + ansatz = hamiltonian_variational_ansatz(hamiltonian, reps=1) + bound = ansatz.assign_parameters([1, 1]) # we have two non-commuting groups, hence 2 params + + expected = QuantumCircuit(3, global_phase=-1) + expected.rzz(2, 0, 1) + expected.rx(2, 1) + + self.assertEqual(expected, bound) + def evolve(pauli_string, time): """Get the reference evolution circuit for a single Pauli string."""