Skip to content

Fix Statevector.evolve modifying input state in-place (Fixes 15750) (backport #15905)#16227

Open
mergify[bot] wants to merge 1 commit into
stable/2.4from
mergify/bp/stable/2.4/pr-15905
Open

Fix Statevector.evolve modifying input state in-place (Fixes 15750) (backport #15905)#16227
mergify[bot] wants to merge 1 commit into
stable/2.4from
mergify/bp/stable/2.4/pr-15905

Conversation

@mergify
Copy link
Copy Markdown
Contributor

@mergify mergify Bot commented May 21, 2026

Summary

Statevector.evolve(gate) should leave the input state untouched. However, the input state is currently modified with DiagonalGate as an argument (see #15750). This is caused by a shallow copy and an in-place-array modification when gate has a global phase. This PR adds failing tests, a fix for the failing tests, and a reno release note for the bug fix.

Details and comments

Proof and cause of bug

Statevector.evolve calls Statevector._evolve_instruction where the following code is run (lines 1029-1030)

        if obj.definition.global_phase:
            statevec._data *= np.exp(1j * float(obj.definition.global_phase))

This is an in-place modification of statevec._data. However, when Statevector.evolve is called, a shallow copy is made (line 461). This means that a gate with a global phase modifies the original input state. This is verified with the following short script:

from qiskit.quantum_info import Statevector
from qiskit.circuit.library import DiagonalGate
import numpy as np

gate = DiagonalGate([1, -1])
print(f"Global Phase of instruction is {gate.definition.global_phase:0.3f} rad")

sv = Statevector.from_label("1")
sv_original = sv.copy()
_ = sv.evolve(gate)

np.testing.assert_allclose(sv_original.data, sv.data)

Possible fixes and implemented fix

  1. (Implemented fix) Reassign statevec._data when applying the global phase, instead of an in-place multiplication. This comes at the cost of copying the _data array but only when the instruction has a global phase.
  2. Deep copy instead of a shallow copy. Instead of _copy.copy (shallow copy) at the beginning of Statevector.evolve, we could call self.copy(). However, this causes test.python.primitives.test_statevector_estimator.TestStatevectorEstimator.test_reset to fail. I tracked this down to the internal rng being copied and the behaviour for resets changing. Instead of fixing the deep copy of the rng, the first fix was chosen.

Implemented Tests

I implemented four tests which verify that five different instructions do not modify the input state to Statevector.evolve. Only the one with DiagonalGate failed. With the fix it passes.

  1. XGate: Passes.
  2. CXGate: Passes.
  3. Operator(XGate()): Passes.
  4. QuantumCircuit (Bell-state preparation): Passes.
  5. DiagonalGate([1.0, -1.0, -1.0, 1.0]): Failed but now passes.
    This is an automatic backport of pull request Fix Statevector.evolve modifying input state in-place (Fixes 15750) #15905 done by Mergify.

…15905)

* [quantum_info.states] Add tests (1 failing) for Statevector.evolve modifying original state

Statevector.evolve() is meant to return the evolved state, with the
original state remaining unmodified. However, some instructions result
in the state being modified. The added tests validate
Statevector.evolve() with various gates, Operator, and QuantumCircuit.
The only instance where the original state is modified is with
DiagonalGate.

* [quantum_info.states] Use new _data array when applying global phase

The _data array of Statevector may be a shallow copy of another
Statevector. Applying a global phase in-place would modify the other
instances. A deep copy doesn't work as this breaks another test:

- test.python.primitives.test_statevector_estimator.TestStatevectorEstimator.test_reset

* [reno] Add bugfix releasenote

* Update releasenotes/notes/fix-statevector-evolve-inplace-modification-be2f31976d5a2e1d.yaml

Co-authored-by: Julien Gacon <gaconju@gmail.com>

* Update releasenotes/notes/fix-statevector-evolve-inplace-modification-be2f31976d5a2e1d.yaml

Co-authored-by: Julien Gacon <gaconju@gmail.com>

* [tests] Fix test names not being snake-case

* [tests] Fix lint black failing

---------

Co-authored-by: Julien Gacon <gaconju@gmail.com>
(cherry picked from commit a8d3aef)
@mergify mergify Bot requested a review from a team as a code owner May 21, 2026 14:08
@mergify mergify Bot requested a review from gadial May 21, 2026 14:08
@qiskit-bot
Copy link
Copy Markdown
Collaborator

Thank you for opening a new pull request.

Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient.

While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone.

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@github-actions github-actions Bot added Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog. Community PR PRs from contributors that are not 'members' of the Qiskit repo labels May 21, 2026
@github-actions github-actions Bot added this to the 2.5.0 milestone May 21, 2026
@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 26231217034

Coverage increased (+1.1%) to 88.822%

Details

  • Coverage increased (+1.1%) from the base build.
  • Patch coverage: 1 of 1 lines across 1 file are fully covered (100%).
  • 18 coverage regressions across 4 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

18 previously-covered lines in 4 files lost coverage.

File Lines Losing Coverage Coverage
crates/qasm2/src/lex.rs 7 91.26%
crates/qasm2/src/parse.rs 6 97.15%
crates/circuit/src/parameter/symbol_expr.rs 4 74.38%
crates/circuit/src/parameter/parameter_expression.rs 1 90.53%

Coverage Stats

Coverage Status
Relevant Lines: 87973
Covered Lines: 78139
Line Coverage: 88.82%
Coverage Strength: 1365084.92 hits per line

💛 - Coveralls

@Cryoris Cryoris modified the milestones: 2.5.0, 2.4.2 May 22, 2026
@Cryoris Cryoris removed this from Qiskit 2.5 May 22, 2026
@Cryoris Cryoris added this pull request to the merge queue May 22, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 22, 2026
@alexanderivrii alexanderivrii added this pull request to the merge queue May 22, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog. Community PR PRs from contributors that are not 'members' of the Qiskit repo

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants