From c5c04e74877106ef2b85bdedddaa1dc78c0d38ca Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 30 Oct 2023 09:47:18 -0400 Subject: [PATCH 01/13] Updated Sampler Interface (V2) --- 0017-sampler-interface.md | 248 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 0017-sampler-interface.md diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md new file mode 100644 index 0000000..779174a --- /dev/null +++ b/0017-sampler-interface.md @@ -0,0 +1,248 @@ +# SamplerV2 + +| **Status** | **Proposed/Accepted/Deprecated** | +|:------------------|:---------------------------------------------| +| **RFC #** | 0017 | +| **Authors** | Lev Bishop (lsbishop@us.ibm.com) | +| | Ian Hincks (ian.hincks@ibm.com) | +| | Blake Johnson (blake.johnson@ibm.com) | +| | Chris Wood (cjwood@us.ibm.com) | +| **Submitted** | 2023-10-03 | +| **Updated** | YYYY-MM-DD | + + +## Summary + +This RFC proposes an interface for the next version of the `Sampler`. +The changes proposed here mirror those accepted in [(the paired Estimator inteface RFC)[https://github.com/Qiskit/RFCs/pull/51]](https://github.com/Qiskit/RFCs/pull/51), where the arguments of the `Estimator.run()` signature were transposed to accept, instead of three iterable arguments, an iterable of "tasks", with each task comprising a circuit and auxiliary execution information. +In the case of the `Sampler`, a task consists only of a circuit and an array of parameter value sets if it is a parametric circuit. +We aim to move the `Sampler` to be a viable replacement for `backend.run`, and eliminate the notion of sample weights (i.e. quasi-probabilities). + +## Motivation + +We will propose a `Sampler` that + + * _Obeys program outputs._ Every circuit defines a set of classical registers whose output names and types are known before execution. The prototypical type is a bit-array, but the OpenQASM 3 specification allows many other types, too, which Qiskit aims to follow. The current return structure of the sampler does not handle this gracefully, squishing bitstrings from separate registers together. It also does not attempt make a careful distinction between sampling from a program's output registers, and sampling from the output state that it prepares---it did not need to as these two views are equivalent for circuits that measure all qubits at the end of the circuit. However, this proposal takes the former stance, and separates returned data according to its output names. The motivation for this choice is that it would make the `Sampler` fully compliant with dynamic circuits. + * _Naturally allows multiple parameter value sets._ A common use case for many users is running the same parametric program with many parameter value sets in the same `run()` invocation. These changes will make it easier for implementations to do this efficiently with late-stage binding. Efficient invocation of the same parametric circuit circuit between multiple `run()` invocations is out of scope of this RFC. + * _Is a "`memory=True`" exection model._ We will break from the tradition of using a mapping structure as the default container type for circuit exection. Instead, results will always be returned in a format where one output axis iterates over shots, and maintains execution order. The motivation is that, in a program whose total output size is more than 100 qubits, due to gate and measurement noise, the chances that any two output values are equal to each other is too small to make special accomodation for via a hash map. When the output size is smaller than this loose threshhold, so that output value collisions are likely, the data will take up less space anyways. Container objects will contain methods returning count-like structures to ease migration. + * _Has no sample weights._ The current sampler attaches real weights summing to one to all samples. In effect, it returns an estimate of a (quasi)-distribution instead of samples from the distribution. This proposal eliminates this feature and returns only samples, and without weights. The rationale is to make the `Sampler` into more of a streamlined circuit executor, paring down all frills. + + +## User Benefit + +Users will be able to transition from `backend.run` to `sampler.run` without any compromises in execution efficiency. They will be able to run dynamic circuits without having to specify special flags, or invoke a special program. They will get their outputs in a format where they don't need to manually separate bitstrings from different registers. + +As with the [(the paired Estimator inteface RFC)[https://github.com/Qiskit/RFCs/pull/51]](https://github.com/Qiskit/RFCs/pull/51), implementations will more easily be able to enable features like late stage parameter value binding. + +## Design Proposal + +We will lead the design proposal with an example. + +```python +# Our first circuit has many Parameters, but elects to do all measurement at +# the end. Note that measure_all() chooses the register name "meas" for you. +circuit1 = QuantumCircuit(9) +... + +... +circuit1.measure_all() + +# Specify 128 different parameter value sets for circuit1, in a 32x4 shape. +parameter_values1 = np.random.uniform(size=(32, 4, 2039)) + +# Our second circuit happens to have no parameters, but three output registers +# named alpha, beta, and gamma of various sizes +alpha = ClassicalRegister(127, "alpha") +beta = ClassicalRegister(10, "beta") +gamma = ClassicalRegister(15, "gamma") +circuit2 = QuantumCircuit(QuantumRegister(127), alpha, beta, gamma) +... + +... + +# invoke a sampler +estimator = Sampler(..., shots=1024) +job = sampler.run([ + (circuit1, parameter_values1), + circuit2 +]) + +# wait for the result +job.result() + +# get a bundle of results for every task +>> PrimitiveResult( +>> SamplerTaskResult( +>> DataBundle(meas=BitArray()), +>> +>> ), +>> SamplerTaskResult( +>> DataBundle( +>> alpha=BitArray() +>> beta=BitArray() +>> gamma=BitArray() +>> ), +>> +>> ), +>> metadata= +>> ) +``` + +The above example shows various features of the design. +Notice the output results are placed in a `DataBundle`, a namespace container described [here](https://github.com/Qiskit/RFCs/pull/53), with one entry for each output of the corresponding task's circuits. +These circuits only have bit-array outcomes, as allowed by Qiskit, and therefore all results have type `BitArray`, described later. +For now, consider the following usage examples. + +```python + +# get result from the second task above +result = job.result()[1] + +# get the entire quasi-probability distribution +result.get_quasiprobabilities() +>> QuasiProbabilityDistribution({1351235663: 0.0009765625, 325: 0.0009765625, 45262346723465234523453: 0.0009765625, 35435345235234372457453: 0.0009765625, ...}) + +# marginalize a couple of indexes in the beta output +result.data.beta.get_counts(marginalize_idxs=[0, 1]) +>> Counts({"00": 189, "10": 11, "01": 801, "11": 123}) + +# flip all of the bits in the gamma according to some other compatibly-shaped bit-array +flips = BitArray(my_flips, num_bits=15) +flipped_gamma = result.data.gamma ^ flips +``` + +If a circuit had a non bit-array outcome, it would use a different type. +We suggest just using `numpy.ndarrays` with `dtype=numpy.float64` for all non-int types like `angle[20]` for simplicity, at least initially, but we can cross that bridge when we get there. +The shape of such an array would be `(*task_shape, num_samples, *output_register_shape)`. + +The detailed signature is as follows: + +```python +T = TypeVar("T", bound=Job) + +BindingsArrayLike = Union[ + BindingsArray, + NDArray, + Dict[Parameter | Tuple[Parameter], NDArray] +] + +SamplerTaskLike = Union[ + SamplerTask, + QuantumCircuit, + Tuple[QuantumCircuit, BindingsArrayLike] +] + +class SamplerBaseV2(PrimitiveBase[SamplerTask, SamplerTaskResult]): + ... + def run( + self, tasks: EstimatorTaskLike | Iterable[EstimatorTaskLike] + ) -> Job[PrimitiveResult[SamplerTask, SamplerTaskResult]]: + pass +``` + + +## Detailed Design + +Most new types and conventions have been described in either [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51) or the (`BasePrimitive.run()` RFC)[https://github.com/Qiskit/RFCs/pull/53]. + +### Tasks and Task Results + +A task consists of a `QuantumCircuit` and, if it has parameters, a `BindingArray` that specifies an array of parameter value sets each of which to bind it against during execution. + +A `SamplerTaskResult` is a subclass of `TaskResult` with the addition of methods for convenience, and to ease migration. For example, + +```python +class SamplerTaskResult(TaskResult): + def get_quasiprobabilities(self, loc: Tuple[int, ...] | None = None) -> QuasiProbabilityDistribution: + """Join all bit-array output registers together into a new quasi-probability distribution. + + Args: + loc: Which task coordinate to fetch. + + Raises: + ValueError: If no ``loc`` is given and this task result is more than 0-D. + """ + + def get_counts(self, loc: Tuple[int, ...] | None = None) -> Counts: + """Join all bit-array output registers together into a new counts structure. + + Args: + loc: Which task coordinate to fetch. + + Raises: + ValueError: If no ``loc`` is given and this task result is more than 0-D. + """ +``` + +### BitArray + +To store lots of bit-array outcomes, to enable fast iteration over bitstrings, and to enable bitwise operations, we propose a container that has a contiguous memory model over the entire shot-loop, where 8-bits are packed to the byte. +For this purpose, we choose to use a NumPy `uint8` array because, unlike `bytes` or `bytearray`, it has convenient methods for slicing and indexing multiple dimensions. +Its buffer can be directly accessed for those not wishing to use NumPy, and, as seen both in the previous section and also below, we can include convenience methods for converting to, for example, a `Counts` object. +We choose a convention where the second last axis is over samples (shots), and the last dimension has a big-endian byte order which describes a single value of the output bit-array. +For example, ``np.array([[1, 1], [2, 0]], dtype=np.uint8)`` would describe 2 shots without outcomes 257 and 512 in decimal, respectively. + +However, a `uint8` array alone is insufficient because, at the very least, it is incapabable of specifying whether it describes a bit-array register of size 14 or 16, for example, both of which require two bytes. +For this reason, and to handle convenience methods, we introduce the `BitArray`. +A bare bones sketch is below. +Methods like `__or__` or static constructors from other formats are in-scope of the vision, but discluded for brevity. + +```python +class BitArray(ShapedMixin): + """Stores bit-array outcomes.""" + + def __init__(self, array: NDArray, num_bits: int): + """ + Args: + array: The data, where the last axis is over packed bits whose byte order is + big-endian, the second last axis is over shots, and the preceding axes + correspond to the shape of the experiment. + num_bits: How many bit are in each outcome; the size of the bit-array register. + """ + self._array = np.array(array, copy=False, dtype=np.uint8) + if self._array.ndim < 2: + raise ValueError("The data arary needs to be at least 2-D") + self._num_bits = num_bits + if self._array.shape[-1] * 8 < self._num_Bits: + raise ValueError("The last axis of the data array is not big enough.") + # second last dimension is shots, last dimension is packed bits + self._shape = self._array.shape[:-2] + + def __repr__(self): + desc = f"" + return f"BitArray({desc})" + + @property + def array(self) -> NDArray[np.uint8]: + """The raw array of data.""" + return self._array + + @property + def num_bits(self) -> int: + """The number bits in each outcome.""" + return self._num_bits + + @property + def num_samples(self) -> int: + """The number of samples.""" + return self._array.shape[-2] + + def get_counts(self, loc: Tuple[int,...] | None = None, marginal_idxs: Tuple[int,...] | None = None) -> Mapping[str, int]: + def get_counts(self, idx: Tuple[int, ...] | None = None) -> Counts: + """Join all bit-array output registers together into a new counts structure. + + Args: + idx: Which task coordinate to fetch. + + Raises: + ValueError: If no ``idx`` is given and this task result is more than 0-D. + """ +``` + +## Migration Path + +An analagous path as [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51). Convenience methods can be placed on result containers to transform into familiar return types. + +## Questions + + * Should `num_samples` (i.e. `shots`) be an argument of the `Task`? Having ditched outcome weights, this seems natural. From debda266a5ca1bc8a35c4e07ddfeebaf03c91a76 Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 30 Oct 2023 10:07:35 -0400 Subject: [PATCH 02/13] Fix some estimator copy-pasta --- 0017-sampler-interface.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 779174a..2ac51e8 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -61,7 +61,7 @@ circuit2 = QuantumCircuit(QuantumRegister(127), alpha, beta, gamma) ... # invoke a sampler -estimator = Sampler(..., shots=1024) +sampler = Sampler(..., shots=1024) job = sampler.run([ (circuit1, parameter_values1), circuit2 @@ -135,7 +135,7 @@ SamplerTaskLike = Union[ class SamplerBaseV2(PrimitiveBase[SamplerTask, SamplerTaskResult]): ... def run( - self, tasks: EstimatorTaskLike | Iterable[EstimatorTaskLike] + self, tasks: SamplerTaskLike | Iterable[SamplerTaskLike] ) -> Job[PrimitiveResult[SamplerTask, SamplerTaskResult]]: pass ``` From 90d4cfee53ef186eba8ac1bcb70b7fcce1e3425a Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Mon, 30 Oct 2023 13:02:59 -0400 Subject: [PATCH 03/13] Update 0017-sampler-interface.md Co-authored-by: Blake Johnson --- 0017-sampler-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 2ac51e8..13299c1 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -23,7 +23,7 @@ We aim to move the `Sampler` to be a viable replacement for `backend.run`, and e We will propose a `Sampler` that * _Obeys program outputs._ Every circuit defines a set of classical registers whose output names and types are known before execution. The prototypical type is a bit-array, but the OpenQASM 3 specification allows many other types, too, which Qiskit aims to follow. The current return structure of the sampler does not handle this gracefully, squishing bitstrings from separate registers together. It also does not attempt make a careful distinction between sampling from a program's output registers, and sampling from the output state that it prepares---it did not need to as these two views are equivalent for circuits that measure all qubits at the end of the circuit. However, this proposal takes the former stance, and separates returned data according to its output names. The motivation for this choice is that it would make the `Sampler` fully compliant with dynamic circuits. - * _Naturally allows multiple parameter value sets._ A common use case for many users is running the same parametric program with many parameter value sets in the same `run()` invocation. These changes will make it easier for implementations to do this efficiently with late-stage binding. Efficient invocation of the same parametric circuit circuit between multiple `run()` invocations is out of scope of this RFC. + * _Naturally allows multiple parameter value sets._ A common use case for many users is running the same parametric program with many parameter value sets in the same `run()` invocation. These changes will make it easier for implementations to do this efficiently with late-stage binding. Efficient invocation of the same parametric circuit between multiple `run()` invocations is out of scope of this RFC. * _Is a "`memory=True`" exection model._ We will break from the tradition of using a mapping structure as the default container type for circuit exection. Instead, results will always be returned in a format where one output axis iterates over shots, and maintains execution order. The motivation is that, in a program whose total output size is more than 100 qubits, due to gate and measurement noise, the chances that any two output values are equal to each other is too small to make special accomodation for via a hash map. When the output size is smaller than this loose threshhold, so that output value collisions are likely, the data will take up less space anyways. Container objects will contain methods returning count-like structures to ease migration. * _Has no sample weights._ The current sampler attaches real weights summing to one to all samples. In effect, it returns an estimate of a (quasi)-distribution instead of samples from the distribution. This proposal eliminates this feature and returns only samples, and without weights. The rationale is to make the `Sampler` into more of a streamlined circuit executor, paring down all frills. From 00c342ca60df316cc0dc7bc70c4766c0f24f51d8 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Mon, 30 Oct 2023 13:03:09 -0400 Subject: [PATCH 04/13] Update 0017-sampler-interface.md Co-authored-by: Blake Johnson --- 0017-sampler-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 13299c1..3802c06 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -32,7 +32,7 @@ We will propose a `Sampler` that Users will be able to transition from `backend.run` to `sampler.run` without any compromises in execution efficiency. They will be able to run dynamic circuits without having to specify special flags, or invoke a special program. They will get their outputs in a format where they don't need to manually separate bitstrings from different registers. -As with the [(the paired Estimator inteface RFC)[https://github.com/Qiskit/RFCs/pull/51]](https://github.com/Qiskit/RFCs/pull/51), implementations will more easily be able to enable features like late stage parameter value binding. +As with the [(paired Estimator inteface RFC)[https://github.com/Qiskit/RFCs/pull/51]](https://github.com/Qiskit/RFCs/pull/51), implementations will more easily be able to enable features like late stage parameter value binding. ## Design Proposal From f9e316b2818d2a808f72d8cf508e7190ce34e8f9 Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 30 Oct 2023 13:34:45 -0400 Subject: [PATCH 05/13] Modify section on non-bit-array outputs. --- 0017-sampler-interface.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 3802c06..60ebb13 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -111,9 +111,11 @@ flips = BitArray(my_flips, num_bits=15) flipped_gamma = result.data.gamma ^ flips ``` -If a circuit had a non bit-array outcome, it would use a different type. -We suggest just using `numpy.ndarrays` with `dtype=numpy.float64` for all non-int types like `angle[20]` for simplicity, at least initially, but we can cross that bridge when we get there. +If a circuit had a non bit-array outcome, it would use a different type in place of `BitArray`. +We suggest just using `numpy.ndarrays` with the closest appropriate `dtype` for non-bit-array outputs. The shape of such an array would be `(*task_shape, num_samples, *output_register_shape)`. +For particular types, like `angle`, if it is for some reason decided that no NumPy type is satisfactory, +we can add more containers analagous to `BitArray`. The detailed signature is as follows: From bd03768adcaec923139bbe0447f0ab9ffa36f361 Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 30 Oct 2023 13:42:05 -0400 Subject: [PATCH 06/13] add inline bitshift calculations, fix typos --- 0017-sampler-interface.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 60ebb13..8dd0d5c 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -182,12 +182,12 @@ To store lots of bit-array outcomes, to enable fast iteration over bitstrings, a For this purpose, we choose to use a NumPy `uint8` array because, unlike `bytes` or `bytearray`, it has convenient methods for slicing and indexing multiple dimensions. Its buffer can be directly accessed for those not wishing to use NumPy, and, as seen both in the previous section and also below, we can include convenience methods for converting to, for example, a `Counts` object. We choose a convention where the second last axis is over samples (shots), and the last dimension has a big-endian byte order which describes a single value of the output bit-array. -For example, ``np.array([[1, 1], [2, 0]], dtype=np.uint8)`` would describe 2 shots without outcomes 257 and 512 in decimal, respectively. +For example, ``np.array([[1, 1], [2, 0]], dtype=np.uint8)`` would describe 2 shots with outcomes `257 = (1 << 8) + 1` and `512 = (2 << 8) + 0` in decimal, respectively. However, a `uint8` array alone is insufficient because, at the very least, it is incapabable of specifying whether it describes a bit-array register of size 14 or 16, for example, both of which require two bytes. For this reason, and to handle convenience methods, we introduce the `BitArray`. A bare bones sketch is below. -Methods like `__or__` or static constructors from other formats are in-scope of the vision, but discluded for brevity. +Methods like `__or__` or static constructors from other formats are in-scope of the vision, but excluded for brevity. ```python class BitArray(ShapedMixin): @@ -229,7 +229,7 @@ class BitArray(ShapedMixin): """The number of samples.""" return self._array.shape[-2] - def get_counts(self, loc: Tuple[int,...] | None = None, marginal_idxs: Tuple[int,...] | None = None) -> Mapping[str, int]: + def get_counts(self, loc: Tuple[int,...] | None = None, marginal_idxs: Tuple[int,...] | None = None) -> Counts: def get_counts(self, idx: Tuple[int, ...] | None = None) -> Counts: """Join all bit-array output registers together into a new counts structure. From fed002de00f7cfe06576c22daa4100911773e68e Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 30 Oct 2023 13:56:45 -0400 Subject: [PATCH 07/13] remove section --- 0017-sampler-interface.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 8dd0d5c..ae8e079 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -244,7 +244,3 @@ class BitArray(ShapedMixin): ## Migration Path An analagous path as [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51). Convenience methods can be placed on result containers to transform into familiar return types. - -## Questions - - * Should `num_samples` (i.e. `shots`) be an argument of the `Task`? Having ditched outcome weights, this seems natural. From 7c83c0bea4d59f14e1d23e214e275d7b9576493f Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Tue, 31 Oct 2023 08:37:59 -0400 Subject: [PATCH 08/13] Update 0017-sampler-interface.md Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- 0017-sampler-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index ae8e079..1d15d9f 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -145,7 +145,7 @@ class SamplerBaseV2(PrimitiveBase[SamplerTask, SamplerTaskResult]): ## Detailed Design -Most new types and conventions have been described in either [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51) or the (`BasePrimitive.run()` RFC)[https://github.com/Qiskit/RFCs/pull/53]. +Most new types and conventions have been described in either [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51) or the [`BasePrimitive.run()` RFC](https://github.com/Qiskit/RFCs/pull/53). ### Tasks and Task Results From 0b4c68f3a7e9c52556ae51195c5648ac18c15936 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 31 Oct 2023 10:39:04 -0400 Subject: [PATCH 09/13] Add section about measurement levels --- 0017-sampler-interface.md | 44 +++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 1d15d9f..ea99094 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -78,8 +78,8 @@ job.result() >> ), >> SamplerTaskResult( >> DataBundle( ->> alpha=BitArray() ->> beta=BitArray() +>> alpha=BitArray(), +>> beta=BitArray(), >> gamma=BitArray() >> ), >> @@ -145,7 +145,7 @@ class SamplerBaseV2(PrimitiveBase[SamplerTask, SamplerTaskResult]): ## Detailed Design -Most new types and conventions have been described in either [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51) or the [`BasePrimitive.run()` RFC](https://github.com/Qiskit/RFCs/pull/53). +Most new types and conventions have been described in either [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51) or the (`BasePrimitive.run()` RFC)[https://github.com/Qiskit/RFCs/pull/53]. ### Tasks and Task Results @@ -241,6 +241,42 @@ class BitArray(ShapedMixin): """ ``` +### Measurement Levels + +`backend.run` has the notion of measurement levels which specify whether measurement instructions cause traces (not generallly implemented), complex phases, or discriminated bits to be returned. +Eventually, measurement levels should be specified inside of a circuit itself by measuring into complex valued registers, as described in the OpenQASM 3 specification. +This would effectively enable a mix-and-match between different measurement levels, and also allow a circuit to determine its own output types. + +However, before this becomes available, in order to acheive feature parity with `backend.run`, we propose implementing this as a flag (similar to `backend.run`) whose value changes the type of all bit-array outputs from `BitArray`s to NumPy arrays with `dtype=np.complex128`. + +```python +class MeasLevel(enum.Enum): + # TRACES = 0 unsupported + PHASES = 1 + BITS = 2 +``` + +Phases for each shot: + +```python +sampler.run([(circuit1, parameter_values1), circuit2], meas_level=MeasLevel.PHASES).result() +>> PrimitiveResult( +>> SamplerTaskResult( +>> DataBundle(meas=np.ndarray()), +>> +>> ), +>> SamplerTaskResult( +>> DataBundle( +>> alpha=np.ndarray( +>> beta=np.ndarray( +>> gamma=np.ndarray( +>> ), +>> +>> ), +>> metadata= +>> ) +``` + ## Migration Path -An analagous path as [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51). Convenience methods can be placed on result containers to transform into familiar return types. +We propose the same path as [the paired Estimator RFC](https://github.com/Qiskit/RFCs/pull/51). Convenience methods can be placed on result containers to transform into familiar return types. From 2aad045d7a7102f8a65fedacbba2e20747bf5a92 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Tue, 31 Oct 2023 11:08:44 -0400 Subject: [PATCH 10/13] Update 0017-sampler-interface.md Co-authored-by: Jim Garrison --- 0017-sampler-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index ea99094..08714a3 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -14,7 +14,7 @@ ## Summary This RFC proposes an interface for the next version of the `Sampler`. -The changes proposed here mirror those accepted in [(the paired Estimator inteface RFC)[https://github.com/Qiskit/RFCs/pull/51]](https://github.com/Qiskit/RFCs/pull/51), where the arguments of the `Estimator.run()` signature were transposed to accept, instead of three iterable arguments, an iterable of "tasks", with each task comprising a circuit and auxiliary execution information. +The changes proposed here mirror those accepted in [the paired Estimator interface RFC](https://github.com/Qiskit/RFCs/pull/51), where the arguments of the `Estimator.run()` signature were transposed to accept, instead of three iterable arguments, an iterable of "tasks", with each task comprising a circuit and auxiliary execution information. In the case of the `Sampler`, a task consists only of a circuit and an array of parameter value sets if it is a parametric circuit. We aim to move the `Sampler` to be a viable replacement for `backend.run`, and eliminate the notion of sample weights (i.e. quasi-probabilities). From 1be1c9443bfa4213c3ca3f8d02df6b6b9c1e156c Mon Sep 17 00:00:00 2001 From: Blake Johnson Date: Mon, 6 Nov 2023 09:51:50 -0500 Subject: [PATCH 11/13] Mark measurement levels section as being IBM-specific. --- 0017-sampler-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 08714a3..358d062 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -243,7 +243,7 @@ class BitArray(ShapedMixin): ### Measurement Levels -`backend.run` has the notion of measurement levels which specify whether measurement instructions cause traces (not generallly implemented), complex phases, or discriminated bits to be returned. +[IBM's implementation](https://github.com/Qiskit/qiskit-ibm-provider/) of the `backend.run` interface has the notion of measurement levels which specify whether measurement instructions cause traces, complex phases, or discriminated bits to be returned. While this notion of measurement levels is not required by the existing `backend.run` interface, we wish to describe how to support this use case within this revised `Sampler`. Eventually, measurement levels should be specified inside of a circuit itself by measuring into complex valued registers, as described in the OpenQASM 3 specification. This would effectively enable a mix-and-match between different measurement levels, and also allow a circuit to determine its own output types. From de5a94dc69eebe56301afae02289114d188d5dbc Mon Sep 17 00:00:00 2001 From: Blake Johnson Date: Mon, 6 Nov 2023 09:53:35 -0500 Subject: [PATCH 12/13] Update 0017-sampler-interface.md Co-authored-by: John Lapeyre --- 0017-sampler-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index 358d062..be7441e 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -24,7 +24,7 @@ We will propose a `Sampler` that * _Obeys program outputs._ Every circuit defines a set of classical registers whose output names and types are known before execution. The prototypical type is a bit-array, but the OpenQASM 3 specification allows many other types, too, which Qiskit aims to follow. The current return structure of the sampler does not handle this gracefully, squishing bitstrings from separate registers together. It also does not attempt make a careful distinction between sampling from a program's output registers, and sampling from the output state that it prepares---it did not need to as these two views are equivalent for circuits that measure all qubits at the end of the circuit. However, this proposal takes the former stance, and separates returned data according to its output names. The motivation for this choice is that it would make the `Sampler` fully compliant with dynamic circuits. * _Naturally allows multiple parameter value sets._ A common use case for many users is running the same parametric program with many parameter value sets in the same `run()` invocation. These changes will make it easier for implementations to do this efficiently with late-stage binding. Efficient invocation of the same parametric circuit between multiple `run()` invocations is out of scope of this RFC. - * _Is a "`memory=True`" exection model._ We will break from the tradition of using a mapping structure as the default container type for circuit exection. Instead, results will always be returned in a format where one output axis iterates over shots, and maintains execution order. The motivation is that, in a program whose total output size is more than 100 qubits, due to gate and measurement noise, the chances that any two output values are equal to each other is too small to make special accomodation for via a hash map. When the output size is smaller than this loose threshhold, so that output value collisions are likely, the data will take up less space anyways. Container objects will contain methods returning count-like structures to ease migration. + * _Is a "`memory=True`" execution model._ We will break from the tradition of using a mapping structure as the default container type for circuit execution. Instead, results will always be returned in a format where one output axis iterates over shots, and maintains execution order. The motivation is that, in a program whose total output size is more than 100 qubits, due to gate and measurement noise, the chances that any two output values are equal to each other is too small to make special accomodation for via a hash map. When the output size is smaller than this loose threshhold, so that output value collisions are likely, the data will take up less space anyways. Container objects will contain methods returning count-like structures to ease migration. * _Has no sample weights._ The current sampler attaches real weights summing to one to all samples. In effect, it returns an estimate of a (quasi)-distribution instead of samples from the distribution. This proposal eliminates this feature and returns only samples, and without weights. The rationale is to make the `Sampler` into more of a streamlined circuit executor, paring down all frills. From a8a21824314795a1f836a85472a66277dc03a784 Mon Sep 17 00:00:00 2001 From: Blake Johnson Date: Mon, 6 Nov 2023 10:28:52 -0500 Subject: [PATCH 13/13] Update 0017-sampler-interface.md Co-authored-by: John Lapeyre --- 0017-sampler-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0017-sampler-interface.md b/0017-sampler-interface.md index be7441e..6316b70 100644 --- a/0017-sampler-interface.md +++ b/0017-sampler-interface.md @@ -181,7 +181,7 @@ class SamplerTaskResult(TaskResult): To store lots of bit-array outcomes, to enable fast iteration over bitstrings, and to enable bitwise operations, we propose a container that has a contiguous memory model over the entire shot-loop, where 8-bits are packed to the byte. For this purpose, we choose to use a NumPy `uint8` array because, unlike `bytes` or `bytearray`, it has convenient methods for slicing and indexing multiple dimensions. Its buffer can be directly accessed for those not wishing to use NumPy, and, as seen both in the previous section and also below, we can include convenience methods for converting to, for example, a `Counts` object. -We choose a convention where the second last axis is over samples (shots), and the last dimension has a big-endian byte order which describes a single value of the output bit-array. +We choose a convention where the second to last axis is over samples (shots), and the last dimension has a big-endian byte order which describes a single value of the output bit-array. For example, ``np.array([[1, 1], [2, 0]], dtype=np.uint8)`` would describe 2 shots with outcomes `257 = (1 << 8) + 1` and `512 = (2 << 8) + 0` in decimal, respectively. However, a `uint8` array alone is insufficient because, at the very least, it is incapabable of specifying whether it describes a bit-array register of size 14 or 16, for example, both of which require two bytes.