You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mahout.apache.org by ak...@apache.org on 2026/10/20 17:57:06 UTC
(mahout) 01/04: [Qumat] Fix Qiskit state vector after SWAP and reuse its target (#749)
This is an automated email from the ASF dual-hosted git repository.
andrewmusselman pushed a commit to branch pr/749-param-optimization
in repository https://gitbox.apache.org/repos/asf/mahout.git
commit 2025f479e7edde564a79ded053e6352ca8e3d190
Author: Andrew Musselman <ak...@apache.org>
AuthorDate: Wed Oct 7 22:46:36 2026 -0700
[Qumat] Fix Qiskit state vector after SWAP and reuse its target (#749)
get_final_state_vector() on the Qiskit backend transpiled with the
default optimization level (2). That level runs ElidePermutations,
which replaces a SWAP with a virtual qubit relabelling. Aer's
save_statevector does not undo the relabelling, so the returned
amplitudes were those *before* the SWAP: X(0); SWAP(0, 1) returned
|q0=1> instead of |q1=1>. Cirq and Braket were already correct.
Transpile at optimization_level=1 instead. Level 1 does not elide
permutations and still produces bit-identical amplitudes to the old
path for the existing teleportation fixtures (level 0 differs in the
last bits of the floating-point result, which would break the
assert_array_equal comparisons in test_final_quantum_states.py).
Also build the statevector AerSimulator and its transpiler Target once
and reuse them. Rebuilding the Target dominated each call (~120 ms ->
~25 ms per call on a laptop CPU), which matters for the optimizers in
the following commits because they evaluate the state many times.
Add a regression test that checks the state after X; SWAP (with and
without a trailing H) on all three backends. Without this change the
two Qiskit cases fail.
Part of #749
Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
qumat/qiskit_backend.py | 24 +++++++++++++++++----
testing/qumat/test_final_quantum_states.py | 34 ++++++++++++++++++++++++++++++
2 files changed, 54 insertions(+), 4 deletions(-)
diff --git a/qumat/qiskit_backend.py b/qumat/qiskit_backend.py
index 6f723ede5..e8df0c694 100644
--- a/qumat/qiskit_backend.py
+++ b/qumat/qiskit_backend.py
@@ -17,6 +17,7 @@
from __future__ import annotations
from collections.abc import Mapping, MutableMapping
+from functools import lru_cache
from typing import Any
import qiskit
@@ -159,7 +160,17 @@ def execute_circuit(
return result.get_counts()
-# placeholder method for use in the testing suite
+@lru_cache(maxsize=1)
+def _statevector_simulator() -> tuple[AerSimulator, Any]:
+ """Return a shared statevector simulator and its transpiler target.
+
+ Building ``AerSimulator.target`` dominates the cost of a state-vector
+ call, so it is built once and reused.
+ """
+ simulator = AerSimulator(method="statevector")
+ return simulator, simulator.target
+
+
def get_final_state_vector(
circuit: qiskit.QuantumCircuit,
backend: AerSimulator,
@@ -167,7 +178,7 @@ def get_final_state_vector(
) -> Any:
working_circuit = circuit.copy()
- simulator = AerSimulator(method="statevector")
+ simulator, target = _statevector_simulator()
# Add save_statevector instruction
working_circuit.save_statevector()
@@ -180,8 +191,13 @@ def get_final_state_vector(
}
working_circuit = working_circuit.assign_parameters(parameter_bindings)
- # Simulate the circuit
- transpiled_circuit = qiskit.transpile(working_circuit, simulator)
+ # Simulate the circuit. Keep optimization_level at 1: levels 2 and 3
+ # (the default is 2) run ElidePermutations, which replaces a SWAP with
+ # a virtual qubit relabelling that save_statevector does not undo, so
+ # the reported amplitudes would be those *before* the SWAP.
+ transpiled_circuit = qiskit.transpile(
+ working_circuit, target=target, optimization_level=1
+ )
job = simulator.run(transpiled_circuit)
result = job.result()
diff --git a/testing/qumat/test_final_quantum_states.py b/testing/qumat/test_final_quantum_states.py
index 8c3a0ef93..75dc5ea9d 100644
--- a/testing/qumat/test_final_quantum_states.py
+++ b/testing/qumat/test_final_quantum_states.py
@@ -20,6 +20,8 @@ from importlib import import_module
import numpy as np
import pytest
+from qumat import QuMat
+
from ..utils import TESTING_BACKENDS
from ..utils.qumat_helpers import get_qumat_example_final_state_vector
@@ -75,3 +77,35 @@ class TestFinalQuantumStatesConsistency:
results[backend_name] = get_qumat_example_final_state_vector(
qumat_backend_config, initial_ket_str
)
+
+
+@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
+@pytest.mark.parametrize("trailing_h", [False, True])
+def test_final_state_vector_after_swap(backend_name, trailing_h):
+ """A SWAP must move amplitudes, not just relabel qubits.
+
+ Regression test: Qiskit's default transpiler can replace a SWAP with a
+ virtual qubit permutation, so ``save_statevector`` used to report the
+ state *before* the swap.
+ """
+ backend_module = import_module(f".{backend_name}_helpers", package="testing.utils")
+ qumat = QuMat(backend_module.get_qumat_backend_config("get_final_state_vector"))
+ qumat.create_empty_circuit(num_qubits=2)
+ qumat.apply_pauli_x_gate(0)
+ qumat.apply_swap_gate(0, 1)
+ if trailing_h:
+ qumat.apply_hadamard_gate(0)
+ state = np.asarray(qumat.get_final_state_vector())
+
+ # After X(0) and SWAP(0, 1) the register is |q0 q1> = |01>.
+ # Qiskit indexes amplitudes little-endian (qubit 0 = least significant
+ # bit), Cirq and Braket big-endian (qubit 0 = most significant bit).
+ one_on_qubit1 = 2 if backend_name == "qiskit" else 1
+ expected = np.zeros(4, dtype=complex)
+ if trailing_h:
+ # H on qubit 0 (which is |0>) gives (|01> + |11>)/sqrt(2).
+ one_on_both = 3
+ expected[one_on_qubit1] = expected[one_on_both] = 1 / np.sqrt(2)
+ else:
+ expected[one_on_qubit1] = 1.0
+ np.testing.assert_allclose(state, expected, atol=1e-6)