Skip to content

Commit 54f76a9

Browse files
lillian542Lillian FrederiksenantalszavaAlbertMitjans
authored
Add zero count outcomes (#2889)
* generate list of possible sampling outcomes * add all outcomes as dict keys * update _samples_to_counts docstring * move _samples_to_counts up a layer * pre-commit checks * pre-commit checks * minor doc string edit * update measurements.rst doc * update measurements.rst doc * fix bug in wire numbering in example code block * update tests accept additional dict keys in outcome * update changelog-dev * revert pre-commit config file * make 0 count int64 data type * update test_measurements.py for new dict format * add EigvalsUndefinedError exception to _samples_to_counts * add unit tests * add pytest xfail to test_scalar_counts_with_obs * Update doc/introduction/measurements.rst Co-authored-by: antalszava <[email protected]> * Update tests/test_new_return_types.py Co-authored-by: antalszava <[email protected]> * Update tests/test_measurements.py Co-authored-by: antalszava <[email protected]> * Update docstring _samples_to_counts Co-authored-by: antalszava <[email protected]> * Update tests/test_measurements.py Co-authored-by: antalszava <[email protected]> * remove ToDo * qml.eigvals(obs) instead of obs.compute_eigvals() Co-authored-by: antalszava <[email protected]> * add all_outcomes=False kwarg to counts fn * update docstrings to reflect new kwarg * update measurements.rst * update changelog * revert tests for standard (all_outcomes=False) counts fn to previous format * display default (all_outcomes=False) behaviour with warning for EigvalsUndefinedError * tests default (all_outcomes=False) counts behaviour, test passes * update test for counts(all_outcomes=True) * fix typo * add missing all_outcomes=True kwarg in test * modify ppre-commit file to circumvent error on pc * small formatting changes * revert change to pre-commit config * Update doc/introduction/measurements.rst Co-authored-by: antalszava <[email protected]> * Update doc/introduction/measurements.rst Co-authored-by: antalszava <[email protected]> * Update doc/introduction/measurements.rst Co-authored-by: antalszava <[email protected]> * Update doc/introduction/measurements.rst Co-authored-by: antalszava <[email protected]> * Update pennylane/measurements.py Co-authored-by: antalszava <[email protected]> * Update pennylane/measurements.py Co-authored-by: antalszava <[email protected]> * Update doc/introduction/measurements.rst Co-authored-by: antalszava <[email protected]> * Update doc/introduction/measurements.rst Co-authored-by: antalszava <[email protected]> * update counts docstring * Update doc/releases/changelog-dev.md Co-authored-by: Albert Mitjans <[email protected]> * temp modification to pre-commit yaml * Switch to AllCounts implementation * Update measurements.rst * Add test for multiple measurement types with counts(all_outcomes=True) * remove completed todo * Revert "temp modification to pre-commit yaml" This reverts commit 0ff4690. * indent codeblock in measurements.rst * Add AllCounts to updates from master * Remove irrelevant exception handling in _samples_to_counts * Fix too-many-boolean-expressions CodeFactor issue * indent code block * tidy up * Update tests/test_measurements.py Co-authored-by: Albert Mitjans <[email protected]> * indent docstring code-block * fix indentation * black reformatting Co-authored-by: Lillian Frederiksen <[email protected]> Co-authored-by: antalszava <[email protected]> Co-authored-by: Albert Mitjans <[email protected]>
1 parent a2afa07 commit 54f76a9

13 files changed

+289
-56
lines changed

doc/introduction/measurements.rst

+25-2
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,31 @@ And the result is:
171171
>>> circuit()
172172
{'00': 495, '11': 505}
173173

174+
Per default, only observed outcomes are included in the dictionary. The kwarg ``all_outcomes=True`` can
175+
be used to display all possible outcomes, including those that were observed ``0`` times in sampling.
176+
177+
For example, we could run the previous circuit with ``all_outcomes=True``:
178+
179+
.. code-block:: python
180+
181+
dev = qml.device("default.qubit", wires=2, shots=1000)
182+
183+
@qml.qnode(dev)
184+
def circuit():
185+
qml.Hadamard(wires=0)
186+
qml.CNOT(wires=[0, 1])
187+
return qml.counts(all_outcomes=True)
188+
189+
>>> result = circuit()
190+
>>> print(result)
191+
{'00': 518, '01': 0, '10': 0, '11': 482}
192+
193+
Note: For complicated Hamiltonians, this can add considerable overhead time (due to the cost of calculating
194+
eigenvalues to determine possible outcomes), and as the number of qubits increases, the length of the output
195+
dictionary showing possible computational basis states grows rapidly.
196+
174197
If counts are obtained along with a measurement function other than :func:`~.pennylane.sample`,
175-
a tensor of tensors is returned to provide differentiability for the outputs of QNodes.
198+
a tuple is returned to provide differentiability for the outputs of QNodes.
176199

177200
.. code-block:: python
178201
@@ -184,7 +207,7 @@ a tensor of tensors is returned to provide differentiability for the outputs of
184207
return qml.expval(qml.PauliZ(0)),qml.expval(qml.PauliZ(1)), qml.counts()
185208
186209
>>> circuit()
187-
(-0.036, 0.036, {'01': 482, '10': 518})
210+
(-0.036, 0.036, {'01': 482, '10': 518})
188211

189212
Probability
190213
-----------

doc/releases/changelog-dev.md

+17
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,22 @@
210210
False
211211
```
212212

213+
* Per default, counts returns only the outcomes observed in sampling. Optionally, specifying `qml.counts(all_outcomes=True)`
214+
will return a dictionary containing all possible outcomes. [(#2889)](https://github.com/PennyLaneAI/pennylane/pull/2889)
215+
216+
```pycon
217+
>>> dev = qml.device("default.qubit", wires=2, shots=1000)
218+
>>>
219+
>>> @qml.qnode(dev)
220+
>>> def circuit():
221+
... qml.Hadamard(wires=0)
222+
... qml.CNOT(wires=[0, 1])
223+
... return qml.counts(all_outcomes=True)
224+
>>> result = circuit()
225+
>>> print(result)
226+
{'00': 495, '01': 0, '10': 0, '11': 505}
227+
```
228+
213229
* Internal use of in-place inversion is eliminated in preparation for its deprecation.
214230
[(#2965)](https://github.com/PennyLaneAI/pennylane/pull/2965)
215231

@@ -332,6 +348,7 @@ Ankit Khandelwal,
332348
Korbinian Kottmann,
333349
Christina Lee,
334350
Meenu Kumari,
351+
Lillian Marie Austin Frederiksen,
335352
Albert Mitjans Coma,
336353
Rashid N H M,
337354
Zeyue Niu,

pennylane/_device.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -744,9 +744,15 @@ def batch_transform(self, circuit):
744744
elif (
745745
len(circuit._obs_sharing_wires) > 0
746746
and not hamiltonian_in_obs
747-
and qml.measurements.Sample not in return_types
748-
and qml.measurements.Probability not in return_types
749-
and qml.measurements.Counts not in return_types
747+
and all(
748+
t not in return_types
749+
for t in [
750+
qml.measurements.Sample,
751+
qml.measurements.Probability,
752+
qml.measurements.Counts,
753+
qml.measurements.AllCounts,
754+
]
755+
)
750756
):
751757
# Check for case of non-commuting terms and that there are no Hamiltonians
752758
# TODO: allow for Hamiltonians in list of observables as well.

pennylane/_qubit_device.py

+82-30
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
from pennylane.measurements import (
3434
Counts,
35+
AllCounts,
3536
Expectation,
3637
MeasurementProcess,
3738
MutualInfo,
@@ -319,7 +320,9 @@ def execute(self, circuit, **kwargs):
319320
self._samples = self.generate_samples()
320321

321322
ret_types = [m.return_type for m in circuit.measurements]
322-
counts_exist = any(ret is qml.measurements.Counts for ret in ret_types)
323+
counts_exist = any(
324+
ret in (qml.measurements.Counts, qml.measurements.AllCounts) for ret in ret_types
325+
)
323326

324327
# compute the required statistics
325328
if not self.analytic and self._shot_vector is not None:
@@ -448,7 +451,9 @@ def shot_vec_statistics(self, circuit):
448451
s1 = 0
449452

450453
ret_types = [m.return_type for m in circuit.measurements]
451-
counts_exist = any(ret is qml.measurements.Counts for ret in ret_types)
454+
counts_exist = any(
455+
ret in (qml.measurements.Counts, qml.measurements.AllCounts) for ret in ret_types
456+
)
452457
single_measurement = len(circuit.measurements) == 1
453458

454459
for shot_tuple in self._shot_vector:
@@ -545,7 +550,7 @@ def _multi_meas_with_counts_shot_vec(self, circuit, shot_tuple, r):
545550
else:
546551
result = r_[idx]
547552

548-
if not circuit.observables[idx2].return_type is Counts:
553+
if not circuit.observables[idx2].return_type in (Counts, AllCounts):
549554
result = self._asarray(result.T)
550555

551556
result_group.append(result)
@@ -745,7 +750,7 @@ def statistics(self, observables, shot_range=None, bin_size=None, circuit=None):
745750
self.sample(obs, shot_range=shot_range, bin_size=bin_size, counts=False)
746751
)
747752

748-
elif obs.return_type is Counts:
753+
elif obs.return_type in (Counts, AllCounts):
749754
results.append(
750755
self.sample(obs, shot_range=shot_range, bin_size=bin_size, counts=True)
751756
)
@@ -896,7 +901,7 @@ def statistics_new(self, observables, shot_range=None, bin_size=None):
896901
samples = self.sample(obs, shot_range=shot_range, bin_size=bin_size, counts=False)
897902
result = qml.math.squeeze(samples)
898903

899-
elif obs.return_type is Counts:
904+
elif obs.return_type in (Counts, AllCounts):
900905
result = self.sample(obs, shot_range=shot_range, bin_size=bin_size, counts=True)
901906

902907
elif obs.return_type is Probability:
@@ -1598,6 +1603,75 @@ def var(self, observable, shot_range=None, bin_size=None):
15981603
# TODO: do we need to squeeze here? Maybe remove with new return types
15991604
return np.squeeze(np.var(samples, axis=axis))
16001605

1606+
def _samples_to_counts(self, samples, obs, num_wires):
1607+
"""Groups the samples into a dictionary showing number of occurences for
1608+
each possible outcome.
1609+
1610+
The format of the dictionary depends on obs.return_type, which is set when
1611+
calling measurements.counts by setting the kwarg all_outcomes (bool). Per default,
1612+
the dictionary will only contain the observed outcomes. Optionally (all_outcomes=True)
1613+
the dictionary will instead contain all possible outcomes, with a count of 0
1614+
for those not observed. See example.
1615+
1616+
1617+
Args:
1618+
samples: samples in an array of dimension ``(shots,len(wires))``
1619+
obs (Observable): the observable sampled
1620+
num_wires (int): number of wires the sampled observable was performed on
1621+
1622+
Returns:
1623+
dict: dictionary with format ``{'outcome': num_occurences}``, including all
1624+
outcomes for the sampled observable
1625+
1626+
**Example**
1627+
1628+
>>> samples
1629+
tensor([[0, 0],
1630+
[0, 0],
1631+
[1, 0]], requires_grad=True)
1632+
1633+
Per default, this will return:
1634+
>>> self._samples_to_counts(samples, obs, num_wires)
1635+
{'00': 2, '10': 1}
1636+
1637+
However, if obs.return_type is AllCounts, this will return:
1638+
>>> self._samples_to_counts(samples, obs, num_wires)
1639+
{'00': 2, '01': 0, '10': 1, '11': 0}
1640+
1641+
The variable all_outcomes can be set when running measurements.counts, i.e.:
1642+
1643+
.. code-block:: python3
1644+
1645+
dev = qml.device("default.qubit", wires=2, shots=4)
1646+
1647+
@qml.qnode(dev)
1648+
def circuit(x):
1649+
qml.RX(x, wires=0)
1650+
return qml.counts(all_outcomes=True)
1651+
1652+
1653+
"""
1654+
1655+
outcomes = []
1656+
1657+
if isinstance(obs, MeasurementProcess):
1658+
# convert samples and outcomes (if using) from arrays to str for dict keys
1659+
samples = ["".join([str(s.item()) for s in sample]) for sample in samples]
1660+
1661+
if obs.return_type is AllCounts:
1662+
outcomes = self.generate_basis_states(num_wires)
1663+
outcomes = ["".join([str(o.item()) for o in outcome]) for outcome in outcomes]
1664+
elif obs.return_type is AllCounts:
1665+
outcomes = qml.eigvals(obs)
1666+
1667+
# generate empty outcome dict, populate values with state counts
1668+
outcome_dict = {k: np.int64(0) for k in outcomes}
1669+
states, counts = np.unique(samples, return_counts=True)
1670+
for s, c in zip(states, counts):
1671+
outcome_dict[s] = c
1672+
1673+
return outcome_dict
1674+
16011675
def sample(self, observable, shot_range=None, bin_size=None, counts=False):
16021676
"""Return samples of an observable.
16031677
@@ -1620,28 +1694,6 @@ def sample(self, observable, shot_range=None, bin_size=None, counts=False):
16201694
dimension ``(shots,)`` or counts
16211695
"""
16221696

1623-
def _samples_to_counts(samples, no_observable_provided):
1624-
"""Group the obtained samples into a dictionary.
1625-
1626-
**Example**
1627-
1628-
>>> samples
1629-
tensor([[0, 0, 1],
1630-
[0, 0, 1],
1631-
[1, 1, 1]], requires_grad=True)
1632-
>>> self._samples_to_counts(samples)
1633-
{'111':1, '001':2}
1634-
"""
1635-
if no_observable_provided:
1636-
# If we describe a state vector, we need to convert its list representation
1637-
# into string (it's hashable and good-looking).
1638-
# Before converting to str, we need to extract elements from arrays
1639-
# to satisfy the case of jax interface, as jax arrays do not support str.
1640-
samples = ["".join([str(s.item()) for s in sample]) for sample in samples]
1641-
1642-
states, counts = np.unique(samples, return_counts=True)
1643-
return dict(zip(states, counts))
1644-
16451697
# translate to wire labels used by device
16461698
device_wires = self.map_wires(observable.wires)
16471699
name = observable.name
@@ -1684,16 +1736,16 @@ def _samples_to_counts(samples, no_observable_provided):
16841736
f"Cannot compute samples of {observable.name}."
16851737
) from e
16861738

1739+
num_wires = len(device_wires) if len(device_wires) > 0 else self.num_wires
16871740
if bin_size is None:
16881741
if counts:
1689-
return _samples_to_counts(samples, no_observable_provided)
1742+
return self._samples_to_counts(samples, observable, num_wires)
16901743
return samples
16911744

1692-
num_wires = len(device_wires) if len(device_wires) > 0 else self.num_wires
16931745
if counts:
16941746
shape = (-1, bin_size, num_wires) if no_observable_provided else (-1, bin_size)
16951747
return [
1696-
_samples_to_counts(bin_sample, no_observable_provided)
1748+
self._samples_to_counts(bin_sample, observable, num_wires)
16971749
for bin_sample in samples.reshape(shape)
16981750
]
16991751

pennylane/devices/default_mixed.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
Snapshot,
3535
)
3636
from pennylane import numpy as np
37-
from pennylane.measurements import Counts, MutualInfo, Sample, State, VnEntropy
37+
from pennylane.measurements import Counts, AllCounts, MutualInfo, Sample, State, VnEntropy
3838
from pennylane.operation import Channel
3939
from pennylane.ops.qubit.attributes import diagonal_in_z_basis
4040
from pennylane.wires import Wires
@@ -579,7 +579,7 @@ def execute(self, circuit, **kwargs):
579579
# Assumed to only be allowed if it's the only measurement.
580580
self.measured_wires = []
581581
return super().execute(circuit, **kwargs)
582-
if obs.return_type in (Sample, Counts):
582+
if obs.return_type in (Sample, Counts, AllCounts):
583583
if obs.name == "Identity" and obs.wires in (qml.wires.Wires([]), self.wires):
584584
# Sample, Counts: Readout error applied to all device wires when wires
585585
# not specified or all wires specified.

pennylane/interfaces/autograd.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ def _execute(
111111

112112
for i, r in enumerate(res):
113113

114-
if any(m.return_type is qml.measurements.Counts for m in tapes[i].measurements):
114+
if any(
115+
m.return_type in (qml.measurements.Counts, qml.measurements.AllCounts)
116+
for m in tapes[i].measurements
117+
):
115118
continue
116119

117120
if isinstance(r, np.ndarray):

pennylane/interfaces/jax.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ def array_if_not_counts(tape, r):
175175
dictionaries. JAX NumPy arrays don't support dictionaries."""
176176
return (
177177
jnp.array(r)
178-
if not any(m.return_type is qml.measurements.Counts for m in tape.measurements)
178+
if not any(
179+
m.return_type in (qml.measurements.Counts, qml.measurements.AllCounts)
180+
for m in tape.measurements
181+
)
179182
else r
180183
)
181184

pennylane/interfaces/tensorflow.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ def execute(tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_d
9191
for i, tape in enumerate(tapes):
9292
# convert output to TensorFlow tensors
9393

94-
if any(m.return_type is qml.measurements.Counts for m in tape.measurements):
94+
if any(
95+
m.return_type in (qml.measurements.Counts, qml.measurements.AllCounts)
96+
for m in tape.measurements
97+
):
9598
continue
9699

97100
if isinstance(res[i], np.ndarray):

pennylane/interfaces/torch.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ def forward(ctx, kwargs, *parameters): # pylint: disable=arguments-differ
9797
# For backwards compatibility, we flatten ragged tape outputs
9898
r = np.hstack(r)
9999

100-
if any(m.return_type is qml.measurements.Counts for m in ctx.tapes[i].measurements):
100+
if any(
101+
m.return_type in (qml.measurements.Counts, qml.measurements.AllCounts)
102+
for m in ctx.tapes[i].measurements
103+
):
101104
continue
102105

103106
if isinstance(r, (list, tuple)):

0 commit comments

Comments
 (0)