Skip to content

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

Open
conradhaupt wants to merge 9 commits intoQiskit:mainfrom
conradhaupt:fix/statevector_inplace_evolve
Open

Fix Statevector.evolve modifying input state in-place (Fixes 15750)#15905
conradhaupt wants to merge 9 commits intoQiskit:mainfrom
conradhaupt:fix/statevector_inplace_evolve

Conversation

@conradhaupt
Copy link
Copy Markdown

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.

…difying 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.
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
@conradhaupt conradhaupt requested a review from a team as a code owner March 30, 2026 15:54
@qiskit-bot qiskit-bot added the Community PR PRs from contributors that are not 'members' of the Qiskit repo label Mar 30, 2026
@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

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 30, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Collaborator

@Cryoris Cryoris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix -- I would've expected a deepcopy to be the right solution, but it seems that explicit steps were taken to not deepcopy here, so I think your solution works for fixing the bug now.

@Cryoris Cryoris added stable backport potential Make Mergify open a backport PR to the most recent stable branch on merge. Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog. labels Mar 31, 2026
@Cryoris Cryoris added this to the 2.5.0 milestone Mar 31, 2026
conradhaupt and others added 3 commits March 31, 2026 16:55
…-be2f31976d5a2e1d.yaml

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

Co-authored-by: Julien Gacon <gaconju@gmail.com>
@conradhaupt conradhaupt requested a review from Cryoris March 31, 2026 15:00
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 stable backport potential Make Mergify open a backport PR to the most recent stable branch on merge.

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants