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)