Skip to content

Commit

Permalink
Update qml.expval to correctly cast value type (#6939)
Browse files Browse the repository at this point in the history
**Context:**

Prior to this PR we had the expectation value being silently converted
to a real type,
```python
obs = (0.2 + 0.7j) * qml.PauliZ(0)

@qml.qnode(qml.device("default.qubit", 1))
def qnode():
    return qml.expval(obs)

>>> qnode()
0.2
```

**Description of the Change:**

This PR stops casting to a real number when calculating expval and
instead casts to whatever the eigenvalue type is.

**Benefits:**

Code is now correct with the following behaviour,
```python
obs = (0.2 + 0.7j) * qml.PauliZ(0)

@qml.qnode(qml.device("default.qubit", 1))
def qnode():
    return qml.expval(obs)

>>> qnode()
(0.2+0.7j)
```

**Possible Drawbacks:** The math is now correct, but this result is
non-physical. However, after some discussion we do not want to support
silently type casting and instead leave this to the researchers.

**Related GitHub Issues:** Fixes #6076 

[sc-70629]

---------

Co-authored-by: Yushao Chen (Jerry) <chenys13@outlook.com>
  • Loading branch information
andrijapau and JerryChen97 authored Feb 11, 2025
1 parent e0b2174 commit 67fbcba
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 2 deletions.
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@

<h3>Bug fixes 🐛</h3>

* `qml.expval` no longer silently casts to a real number when observable coefficients are imaginary.
[(#6939)](https://github.com/PennyLaneAI/pennylane/pull/6939)

* Fixed `qml.wires.Wires` initialization to disallow `Wires` objects as wires labels.
Now, `Wires` is idempotent, e.g. `Wires([Wires([0]), Wires([1])])==Wires([0, 1])`.
[(#6933)](https://github.com/PennyLaneAI/pennylane/pull/6933)
Expand Down
3 changes: 1 addition & 2 deletions pennylane/measurements/expval.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,4 @@ def _calculate_expectation(self, probabilities):
Args:
probabilities (array): the probabilities of collapsing to eigen states
"""
eigvals = qml.math.cast_like(self.eigvals(), 1.0)
return qml.math.dot(probabilities, eigvals)
return qml.math.dot(probabilities, self.eigvals())
55 changes: 55 additions & 0 deletions tests/measurements/test_expval.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,52 @@ def qnode():
_ = qnode()


# pylint: disable=too-many-public-methods
class TestExpval:
"""Tests for the expval function"""

@pytest.mark.parametrize("coeffs", [1, 1j, 1 + 1j])
def test_process_counts_dtype(self, coeffs):
"""Test that the return type of the process_counts function is correct"""
counts = {"000": 100, "100": 100}
wire_order = qml.wires.Wires((0, 1, 2))
res = qml.expval(coeffs * qml.Z(1)).process_counts(counts=counts, wire_order=wire_order)
assert np.allclose(res, coeffs)

@pytest.mark.parametrize("coeffs", [1, 1j, 1 + 1j])
def test_process_state_dtype(self, coeffs):
"""Test that the return type of the process_state function is correct"""
res = qml.measurements.ExpectationMP(obs=coeffs * qml.Z(0)).process_state(
state=[1, 0], wire_order=qml.wires.Wires(0)
)
assert np.allclose(res, coeffs)

@pytest.mark.parametrize("coeffs", [1, 1j, 1 + 1j])
@pytest.mark.parametrize(
"state,expected",
[
([[1.0, 0.0], [0.0, 0.0]], 1.0), # Pure |0⟩ state
([[0.0, 0.0], [0.0, 1.0]], -1.0), # Pure |1⟩ state
([[0.5, 0.5], [0.5, 0.5]], 0.0), # Mixed state
([[0.75, 0.0], [0.0, 0.25]], 0.5), # Another mixed state
],
)
def test_process_density_matrix_dtype(self, coeffs, state, expected):
mp = ExpectationMP(obs=coeffs * qml.PauliZ(0))
result = mp.process_density_matrix(state, wire_order=qml.wires.Wires([0]))
assert qml.math.allclose(result, expected * coeffs)

@pytest.mark.parametrize("coeffs", [1, 1j, 1 + 1j])
def test_process_samples_dtype(self, coeffs, seed):
"""Test that the return type of the process_samples function is correct"""
shots = 100
rng = np.random.default_rng(seed)
samples = rng.choice([0, 1], size=(shots, 2)).astype(np.int64)
obs = coeffs * qml.PauliZ(0)
expected = qml.expval(obs).process_samples(samples, [0, 1])
result = ExpectationMP(obs=obs).process_samples(samples, [0, 1])
assert qml.math.allclose(result, expected)

@pytest.mark.parametrize("shots", [None, 1111, [1111, 1111]])
def test_value(self, tol, shots, seed):
"""Test that the expval interface works"""
Expand Down Expand Up @@ -358,3 +401,15 @@ def test_estimate_expectation_with_counts(self, wire, expected):
res = qml.expval(qml.Z(wire)).process_counts(counts=counts, wire_order=wire_order)

assert np.allclose(res, expected)


@pytest.mark.parametrize("coeffs", [1, 1j, 1 + 1j])
def test_qnode_expval_dtype(coeffs):
"""System level test to ensure dtype is correctly preserved."""

@qml.qnode(qml.device("default.qubit"))
def circuit(coeffs):
return qml.expval(coeffs * qml.PauliZ(0))

res = circuit(coeffs)
assert np.allclose(res, coeffs)

0 comments on commit 67fbcba

Please sign in to comment.