From ada8fbd860c027465a961abd62faee8985ac4cd0 Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 2 Oct 2023 17:35:58 -0400 Subject: [PATCH 01/51] Filled out the Summary, Motivation, and User Benefit sections --- 0015-estimator-interface.md | 99 +++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 0015-estimator-interface.md diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md new file mode 100644 index 0000000..8ecb9cb --- /dev/null +++ b/0015-estimator-interface.md @@ -0,0 +1,99 @@ +# Extended Estimator Interface + +| **Status** | **Proposed/Accepted/Deprecated** | +|:------------------|:---------------------------------------------| +| **RFC #** | 0015 | +| **Authors** | Ian Hincks (ian.hincks@ibm.com) | +| **Deprecates** | RFC that this RFC deprecates | +| **Submitted** | YYYY-MM-DD | +| **Updated** | YYYY-MM-DD | + + +## Summary +The current `Estimator.run()` method requires that a user provide one circuit for every observable and set of parameter values that they wish to run. +(This was not always the case, but the history will not be discussed here.) +It is common, if not typical, for a user to want to estimate many observables corresponding to a single circuit. Likewise, it is common, if not typical, for a user to want to supply multiple parameter value sets for the same circuit. +This RFC proposes to fix these issues by changing the `Estimator.run()` in the following ways: + + 1. Take the transpose of the current signature; rather than accepting `circuits`, `parameter_values`, and `observables` as three different (iterable) arguments which are to be zipped together, instead localize the distinct tasks to be run via an iterable of triples `(circuit, parameter_values, observables)`. + 2. In combination with 1, extend `parameter_values` and `observables` to be array-valued, that is, add the ability to explicity specify multiple parameter value sets and observables for a single circuit. + +## Motivation + +Here is a summary of pain-points with the current `Estimator.run()` interface: + +1. _Ergonomics._ It feels unnatural to invoke the estimator as `Estimator.run([circuit, circuit], observables=[obs1, obs2])` because of the redundancy of entering the circuit twice. + +1. _Trust._ The case `Estimator.run([circuit, circuit], observables=[obs1, obs2])` makes a user question whether the primitive implementation is going to do any optimizations to check if the same circuit appears twice, and if so, whether it will be done by memory location, circuit equality, etc.; the interface itself creates gap in trust. + +1. _Clarity._ Without reading the documentation in detail, it's not obvious that the invocation `Estimator.run([circuit], observables=[obs1, obs2])` wouldn't cause `circuit` to be run with both supplied observables. In other words, that zipping _is always what's done to the `run()` arguments_ is common source of user confusion. Conversely, it's not clear that the way to collect all combinations of two observables and two parameter sets is to invoke `Estimator.run([circuit] * 4, observables=[obs1, obs2] * 2), parameter_values=[params1] * 2 + [params2] * 2`. + +1. _Performance._ Given that `QuantumCircuit` hashing should be avoided and circuit equality checks can be expensive, we should move away from an interface which necessitates that implementations make associated tradeoffs in order to perform certain obvious optimizations. As one example, qubit-wise commuting observables like `"IZZ"` and `"XZI"` can both be estimated using the same simulation/execution, but only if the estimator understands that they share a base circuit. As a second example, when the circuits need to be seriazed before they are simulated/executed (e.g. runtime or pickling-for-multi-processing), it puts the onus on the primitive implementation to detect circuit duplications. + + +Here is why the detailed section of this proposal suggests "transposing" the signatureß and using array-based arguments with broadcasting: + + 1. Transposing the signature will make it obvious what the primitive intends to do with each circuit. + 2. Transposing the signature will let us introduce and reason about the notion "primitive unit of work", and carry it to other primitives. + 3. Array-based arguments will let users assign operational meaning to each axis (this axis is for twirling, that axis is for basis changes, etc.). + 4. Broadcasting rules will let users choose how to combine parameter value sets with observables in different ways, such as + 1. use one observable for all of N parameter value sets + 2. use one parameter value set for all of N observables + 3. zip N parameter value sets against N observables + 4. take all NxM combinations of N parameter value sets with M observables + 5. etc. + +## User Benefit + +All users of the primitives stand to benefit from this proposal. +Immediately, it will enable sophisticated and convenient workflows for power users though arrays and broadcasting. +However, standard broadcasting rules are such that the 0D and 1D cases will feel natural to existing users---0D is essentially what we already have, and 1D will be perceived as a simple-yet-welcome bonus. + +For all users, the interface changes in this proposal will enable specific primitive implementations to enhance performance through reduced bandwidth on the runtime, compatibility with fast parameter binding, and multiplexing qubit-wise commuting observables. + +## Design Proposal +This is the focus of the document. Explain the proposal from the perspective of +educating another user on the proposed features. + +This generally means: +- Introducing new concepts and nomenclature +- Using examples to introduce new features +- Implementation and Migration path with associated concerns +- Communication of features and changes to users + +Focus on giving an overview of impact of the proposed changes to the target +audience. + +Factors to consider: +- Performance +- Dependencies +- Maintenance +- Compatibility + +## Detailed Design +Technical reference level design. Elaborate on details such as: +- Implementation procedure + - If spans multiple projects cover these parts individually +- Interaction with other features +- Dissecting corner cases +- Reference definition, eg., formal definitions. + +## Alternative Approaches +Discuss other approaches to solving this problem and why these were not +selected. + +Single task per run call. + +## Questions +Open questions for discussion and an opening for feedback. + +## Future Extensions +Consider what extensions might spawn from this RFC. Discuss the roadmap of +related projects and how these might interact. This section is also an opening +for discussions and a great place to dump ideas. + +If you do not have any future extensions in mind, state that you cannot think +of anything. This section should not be left blank. + +Primitive.run +Sampler.run From 324771d7de08f86a6cdc191180264766c116366e Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 2 Oct 2023 20:34:32 -0400 Subject: [PATCH 02/51] Filled out Alternative Approaches section --- 0015-estimator-interface.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 8ecb9cb..22377fc 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -79,10 +79,16 @@ Technical reference level design. Elaborate on details such as: - Reference definition, eg., formal definitions. ## Alternative Approaches -Discuss other approaches to solving this problem and why these were not -selected. -Single task per run call. +An alternative is to consider letting the `run()` method accept, effectively, only a single `ObservablesTask`: + +`Estimator.run(cirucuit, parameter_values_array, observables_array)` + +This has the advantage of a simpler interface, where multiple tasks could be run +by invoking the estimator multiple times. The disadvantages, which we feel are significant enough to forego this simplification, are that: + + 1. For real backends, the user would lose the ability to cause multiple types of circuits to be loaded into the control hardware at one time. For example, if using an estimator to perform randomized benchmarking, each circuit depth would need to be a separate job. + 2. It would be more difficult for implementations that include mitigation to share resources between tasks. For example, if different tasks represent different trotter step counts, there would need to be a complicated mechanism to share learning resources---that are specific to the application circuit---between multiple jobs. ## Questions Open questions for discussion and an opening for feedback. From 5ba58976412f1f461fe7be27ba2bd719295af25b Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 13:03:28 -0400 Subject: [PATCH 03/51] Add Tasks section --- 0015-estimator-interface.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 22377fc..2216db7 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -55,20 +55,27 @@ For all users, the interface changes in this proposal will enable specific primi This is the focus of the document. Explain the proposal from the perspective of educating another user on the proposed features. -This generally means: -- Introducing new concepts and nomenclature -- Using examples to introduce new features -- Implementation and Migration path with associated concerns -- Communication of features and changes to users - -Focus on giving an overview of impact of the proposed changes to the target -audience. - -Factors to consider: -- Performance -- Dependencies -- Maintenance -- Compatibility +### Tasks + +In this proposal, we introduce the concept of a Task, which we define as a single circuit along with auxiliary data required to execute the circuit relative to the primitive in question. This concept is general enough that it can be used for all primitive types, current and future, where we stress that what the “auxiliary data” is can vary between primitive types. + +For example, a circuit with unbound parameters (or in OQ3 terms, a circuit with inputs) alone could never qualify as a Task for any primitive because there is not enough information to execute it, namely, numeric parameter binding values. On the other hand, conceptually, a circuit with no unbound parameters (i.e. an OQ3 circuit with no inputs) alone could form a Task for a hypothetical primitive that just runs circuits and returns counts. This suggests a natural base for all Tasks: + +```python +BaseTask = NamedTuple[circuit: QuantumCircuit] +``` + +For the `Estimator` primitive, in order to satisfy the definition as stated above, we propose the task structure + +```python +ObservablesTask = NamedTuple[ + circuit: QuantumCircuit, + parameter_values: BindingsArray, + observables: ObservablesArray +] +``` + +We expect the formal primitive API and primitive implementations to have a strong sense of Tasks, but we will not demand that users construct them manually in Python as they are little more than named tuples, and we do not wish to overburden them with types. This is discussed further in the “Type Coersion” section. ## Detailed Design Technical reference level design. Elaborate on details such as: From 81ea2c7b2c3eaece8e0e1a4799971bdd15d668b9 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 13:32:51 -0400 Subject: [PATCH 04/51] Add to Future Extensions section --- 0015-estimator-interface.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 2216db7..4fcc06e 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -16,7 +16,7 @@ It is common, if not typical, for a user to want to estimate many observables co This RFC proposes to fix these issues by changing the `Estimator.run()` in the following ways: 1. Take the transpose of the current signature; rather than accepting `circuits`, `parameter_values`, and `observables` as three different (iterable) arguments which are to be zipped together, instead localize the distinct tasks to be run via an iterable of triples `(circuit, parameter_values, observables)`. - 2. In combination with 1, extend `parameter_values` and `observables` to be array-valued, that is, add the ability to explicity specify multiple parameter value sets and observables for a single circuit. + 2. In combination with 1, extend `parameter_values` and `observables` to be array-valued, that is, add the ability to explicity and conveniently specify multiple parameter value sets and observables for a single circuit. ## Motivation @@ -28,7 +28,7 @@ Here is a summary of pain-points with the current `Estimator.run()` interface: 1. _Clarity._ Without reading the documentation in detail, it's not obvious that the invocation `Estimator.run([circuit], observables=[obs1, obs2])` wouldn't cause `circuit` to be run with both supplied observables. In other words, that zipping _is always what's done to the `run()` arguments_ is common source of user confusion. Conversely, it's not clear that the way to collect all combinations of two observables and two parameter sets is to invoke `Estimator.run([circuit] * 4, observables=[obs1, obs2] * 2), parameter_values=[params1] * 2 + [params2] * 2`. -1. _Performance._ Given that `QuantumCircuit` hashing should be avoided and circuit equality checks can be expensive, we should move away from an interface which necessitates that implementations make associated tradeoffs in order to perform certain obvious optimizations. As one example, qubit-wise commuting observables like `"IZZ"` and `"XZI"` can both be estimated using the same simulation/execution, but only if the estimator understands that they share a base circuit. As a second example, when the circuits need to be seriazed before they are simulated/executed (e.g. runtime or pickling-for-multi-processing), it puts the onus on the primitive implementation to detect circuit duplications. +1. _Performance._ Given that `QuantumCircuit` hashing should be avoided and circuit equality checks can be expensive, we should move away from an interface that necessitates performant primitive implementations to go down this path. As one example, qubit-wise commuting observables like `"IZZ"` and `"XZI"` can both be estimated using the same simulation/execution, but only if the estimator understands that they share a base circuit. As a second example, when the circuits need to be seriazed before they are simulated/executed (e.g. runtime or pickling-for-multi-processing), it puts the onus on the primitive implementation to detect circuit duplications. Here is why the detailed section of this proposal suggests "transposing" the signatureß and using array-based arguments with broadcasting: @@ -110,3 +110,5 @@ of anything. This section should not be left blank. Primitive.run Sampler.run + +In this proposal we have typed circuits as `QuantumCircuit`. It would be possible to extend this to a `CircuitLike` class which could be as simple as `Union[QuantumCircuit, str]` to explicitly allow OpenQASM3 circuits as first-class inputs. \ No newline at end of file From 9d87841c34e7829ba62adc1e829f7dcd051ed1eb Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 13:33:10 -0400 Subject: [PATCH 05/51] Add more minor edits --- 0015-estimator-interface.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 4fcc06e..2efb4d5 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -52,8 +52,6 @@ However, standard broadcasting rules are such that the 0D and 1D cases will feel For all users, the interface changes in this proposal will enable specific primitive implementations to enhance performance through reduced bandwidth on the runtime, compatibility with fast parameter binding, and multiplexing qubit-wise commuting observables. ## Design Proposal -This is the focus of the document. Explain the proposal from the perspective of -educating another user on the proposed features. ### Tasks @@ -89,26 +87,19 @@ Technical reference level design. Elaborate on details such as: An alternative is to consider letting the `run()` method accept, effectively, only a single `ObservablesTask`: -`Estimator.run(cirucuit, parameter_values_array, observables_array)` +```python +Estimator.run(cirucuit, parameter_values_array, observables_array) +``` This has the advantage of a simpler interface, where multiple tasks could be run by invoking the estimator multiple times. The disadvantages, which we feel are significant enough to forego this simplification, are that: 1. For real backends, the user would lose the ability to cause multiple types of circuits to be loaded into the control hardware at one time. For example, if using an estimator to perform randomized benchmarking, each circuit depth would need to be a separate job. - 2. It would be more difficult for implementations that include mitigation to share resources between tasks. For example, if different tasks represent different trotter step counts, there would need to be a complicated mechanism to share learning resources---that are specific to the application circuit---between multiple jobs. + 2. It would be difficult for implementations that include mitigation to share resources between tasks. For example, if different tasks represent different trotter step counts, there would need to be a complicated mechanism to share learning resources---that are specific to the application circuits---between multiple jobs. ## Questions Open questions for discussion and an opening for feedback. ## Future Extensions -Consider what extensions might spawn from this RFC. Discuss the roadmap of -related projects and how these might interact. This section is also an opening -for discussions and a great place to dump ideas. - -If you do not have any future extensions in mind, state that you cannot think -of anything. This section should not be left blank. - -Primitive.run -Sampler.run In this proposal we have typed circuits as `QuantumCircuit`. It would be possible to extend this to a `CircuitLike` class which could be as simple as `Union[QuantumCircuit, str]` to explicitly allow OpenQASM3 circuits as first-class inputs. \ No newline at end of file From 8c3620cac1754cd6723170b2a64369afe7082cfb Mon Sep 17 00:00:00 2001 From: Samantha Barron Date: Tue, 3 Oct 2023 14:56:08 -0400 Subject: [PATCH 06/51] Add Sections (#4) --- 0015-estimator-interface.md | 166 +++++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 2efb4d5..e925693 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -75,13 +75,177 @@ ObservablesTask = NamedTuple[ We expect the formal primitive API and primitive implementations to have a strong sense of Tasks, but we will not demand that users construct them manually in Python as they are little more than named tuples, and we do not wish to overburden them with types. This is discussed further in the “Type Coersion” section. +### BindingsArray + +It is common for a user to want to do a sweep over parameter values, that is, to execute the same parametric circuit with many different parameter binding sets. `BindingsArray` specifies multiple sets of parameters that can be bound to a circuit. For example, if a circuit has 200 parameters, and a user wishes to execute the circuit for 50 different sets of values, then a single instance of `BindingsArray` could represent 50 sets of 200 parameter values. Moreover, it is array-like (see the next section), so in this example the `BindingsArray` instance would be one-dimensional and have shape equal `(50,)`. + +We expect the formal primitive API and primitive implementations to have a strong sense of `BindingsArray`, but we will not demand that users construct them manually because we do not wish to overburden them with types, and we need to remain backwards compatible. This is discussed further in the "Type Coercion" and "Migration Path" sections. + +The "BindingsArray" object will support, at a minimum, the following constructor examples for a circuit with three input parameters `a`, `b`, and `c`, where we will use `<>` to denote some `array_like` of the specified shape: + +```python +# specify all 50 binding parameter sets in one big array +BindingsArray(<50, 3>) + +# specify bindings separately for each parameter, required if they have different types +BindingsArray([<50>, <50>, <50>]) + +# include parameter names with the arrays, where parameters can be grouped together in tuples, or supplied separately +BindingsArray(kwargs={(a, c): <50, 2>, b: <50>}) + +# “args” and “kwargs” can be mixed +BindingsArray(<50, 2>, {c: <50>}) +``` + +Note that `BindingsArray` is somewhat constrained by how `Parameters` currently work in Qiskit, namely, there is no support for array-valued inputs in the same way that there is in OpenQASM 3; `BindingsArray` assumes that every parameter represents a single number like a `float` or an `int`. + +### ObservablesArray + +With the `Estimator`, it is common for a user to want to estimate many observables of a single circuit. For example, all weight-1 and weight-2 Paulis that are adjacent on the connectivity graph. For a one-hundred qubit device, this corresponds to hundreds of unique estimates to be made for a single circuit, noting that for this particular example, on the heavy-hex graph, in the absence of mitigation, only 9 circuits need to be physically run. + +The `ObservablesArray` object will be an object array, where each element corresponds to a observable the user wants an estimated expectation value of. It is up to an `Estimator` implementation to solve a graph coloring problem to decide how to produce a sufficient set of physical circuits that are capable of producing data to make each of these estimates. + +### Arrays and Broadcasting + +An `nd-array` is an object whose elements are indexed by a tuple of integers, where each integer must be bounded by the dimension of that axis. The tuple of dimensions, one for each axis, is called the shape of the `nd-array`. For example, 1D list of 10 objects (typically numbers) is a vector and has shape `(10,)`; a matrix with 5 rows and 2 columns is 2D and has shape `(5, 2)`; a single number is 0D and has an empty shape tuple `()`; an `nd-array` with shape (20, 10, 5) can be interpreted as a length-20 list of 10×5 matrices. + +```python +shape1=(1, 5), shape2=(4, 1), broadcasted_shape=(4,5) + +shape1=(1, 5), shape2=(4, 1), broadcasted_shape=(4,5) + +shape1=(), shape2=(5, 10), broadcasted_shape=(5,10) +``` + +FAQ1: Why make `BindingArrays` `nd`, and not just `list`-like? + +* A1: The primitives are meant to be a convenient execution framework, and allowing multiple axes relieves certain book-keeping burdens from the caller. `nd-arrays` are a standard construct in data manipulation. Different axes can be assigned different operational meanings, for example axis 0 could be a sweep over basis transformations, and axis 1 could be a sweep over Pauli randomizations. + +* A2: There are different places in the runtime stack that binding can occur. in the runtime container, as a fast parametric. Having multiple axes gives us the freedom to offer this as an explicit feature in the future (?). + +* A3: It lets us specify certain common scenarios for `ObservablesTask` more efficiently. For example, suppose we want one axis to represent twirling variates, and the other axis to represent observable bases. Then, via broadcasting, described below, the information that needs to be transferred over the wire appears 1D for both the twirling variate phase information and the list of observables. Without `nd-array` support, broadcasting would have to be done client-side. + +We propose that any subtype of `ArrayTask` use broadcasting rules on auxillary data. + +### Primitive Interface + +We use the (non-standard) notation that `Type` denotes an instance of the given type with a constraint on attributes such as shape or format. + +```python +Estimator.run(Union[Iterable[ObservablesTask], ObservablesTask], **options) → List[ResultBundle<{evs: ndarray, stds: ndarray}>] +``` + +Example: + +```python +circuit = QuantumCircuit # with 2039 parameters + +parameter_values = np.random((9, 32, 2039)) +observables = [] +job = estimator.run((circuit, parameter_values, observables)) + +job.result() + +>> [ResultBundle<{evs: ndarray<9, 32>, stds: ndarray<9, 32>}>, metadata] + +Sampler.run(Union[Iterable[ArrayTask List[ResultBundle[{creg_name: CountsArray}]] +``` + +### Type Coercion Strategy + +To minimize the number of container types that an every-day user will need to interact with, and to make the transition more seamless, we propose that several container types be associated with a `TypeLike` pattern and a static method + +```python +def coerce(argument: TypeLike) -> Type +``` + +that is invoked inside of the `run()` method. + +For example, + +```python +BindingsArrayLike=Union[BindingsArray, ArrayLike, Iterable[float], Mapping[Parameter, float]] +``` + +and + +```python +@staticmethod +def coerce(bindings_array: BindingsArrayLike) -> BindingsArray: + if isinstance(bindings_array, (list, ArrayLike)): + bindings_array = BindingsArray(bindings_array) + elif isinstance(bindings_array, Mapping): + bindings_array = BindingsArray([], bindings_array) + + return bindings_array +``` + +In particular, we propose this kind of Coercion for the types: +* `ArrayTask` +* `ObservablesTask` +* `BindingsArray` +* `ObservablesArray` + +### ResultBundles + +The results from each `Task` will be array valued. However, we may require several arrays, possibly of different types. Consider an `ObservablesTask` with shape `<20, 30>`, where the shape has come from some combination of multiplexing an observables sweep with a parameter values sweep. This will result in a 20×30 array of real estimates. Moreover, we will want to return an array of standard deviations of the same shape. This would result in a bundle of broadcastable arrays: + +```python +ResultBundle({“evs”: <20, 30>, “stds”: <20, 30>}, metadata) +``` + +The reason we are proposing a generic container for the return type instead of, e.g., an `Estimator`-specific container, is because + +1. Provides unified experience across primitives for users. +1. code-reuse +1. Provides a certain certain amount of flexibility for what can be returned without modifying the container object. Here are some examples: + 1. Suppose that we want to give users the option of additionally returning the covariances between estimates that arise because of the circuit multiplexing, then we could update with the field `{"cov": <20,30,20,30>}`. + 1. Suppose we want to return some indication of which estimates came from the same physical circuit. + ## Detailed Design Technical reference level design. Elaborate on details such as: - Implementation procedure - If spans multiple projects cover these parts individually - Interaction with other features - Dissecting corner cases -- Reference definition, eg., formal definitions. +- Reference definition, e.g., formal definitions. + +## Migration Path + +We need to remain backwards compatible with the existing interface to adhere to the [Qiskit Deprecation Policy](https://qiskit.org/documentation/deprecation_policy.html). Notice that for both `Sampler` and `Estimator`, the first argument of the `run()` call will either contain `Tasks` (or `TaskLike`s) or it won’t. + +We propose a migration strategy based on this: If the user has provided no `TaskLike`s, proceed with the old API and old API output and emit a deprecation warning, or an error if something mandatory like `observables` has been omitted. Otherwise, proceed with the new API, raising if they have tried to use the old arguments in addition to providing tasks. + +```python +def run( + self, + circuits: Sequence[QuantumCircuit] | QuantumCircuit, + observables: Sequence[BaseOperator | PauliSumOp | str] | BaseOperator | PauliSumOp | str, + parameter_values: Sequence[Sequence[float]] | Sequence[float] | float | None = None, + **run_options, + ) +``` + +```python +def run(self, tasks, observables=None, parameter_values=None, **run_options): + has_tasks = isinstance(tasks, QuantumCircuit) or all(isinstance(task, QuantumCircuit) for task in tasks) + + if not has_tasks: + # Trigger old API and disallow new one + if not all_circuits: + raise ValueError("Cannot mix and match old API with new API") + + if observables is None: + raise ValueError("`observables` is required argument in the old API") + + circuits = [tasks] if isinstance(tasks, QuantumCircuit) else tasks + observables = [observables] is isinstance(observables, (BaseOperator, PauliSumOp, str)) else observables + tasks = zip(circuits, observables, parameter_values) + warnings.warn() + tasks = ObservableTask.coerce(task) for task in tasks + + return self._run_old_api(...) if no_tasks else return self._run(...) +``` ## Alternative Approaches From 02db7eb458d5ba2a30c7dabb5786f4c27a2076bd Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 15:58:50 -0400 Subject: [PATCH 07/51] add to Future Extensions (re Task in BasePrimitive) --- 0015-estimator-interface.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index e925693..02b5bfb 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -266,4 +266,13 @@ Open questions for discussion and an opening for feedback. ## Future Extensions -In this proposal we have typed circuits as `QuantumCircuit`. It would be possible to extend this to a `CircuitLike` class which could be as simple as `Union[QuantumCircuit, str]` to explicitly allow OpenQASM3 circuits as first-class inputs. \ No newline at end of file +In this proposal we have typed circuits as `QuantumCircuit`. It would be possible to extend this to a `CircuitLike` class which could be as simple as `Union[QuantumCircuit, str]` to explicitly allow OpenQASM3 circuits as first-class inputs. + +A consequence of switching to the concept of Tasks, alluded to in the section that introduced them, is that this will allow us to introduce the `.run()` method into `BasePrimitive` + +```python +class BasePrimitive(ABC, Generic[T]): + ... + def run(self, T | Iterable[T], **options) -> List[ResultBundle]: + ... +``` \ No newline at end of file From 8eec10b2e6c3ac73974f7960f1ea3d0fb33d921c Mon Sep 17 00:00:00 2001 From: Samantha Barron Date: Tue, 3 Oct 2023 16:05:33 -0400 Subject: [PATCH 08/51] Array broadcasting figure, example (#5) --- 0015-estimator-interface.md | 39 +- 0015-estimator-interface/broadcasting.svg | 1718 +++++++++++++++++++++ 2 files changed, 1753 insertions(+), 4 deletions(-) create mode 100644 0015-estimator-interface/broadcasting.svg diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 02b5bfb..b3597dd 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -109,14 +109,45 @@ The `ObservablesArray` object will be an object array, where each element corres An `nd-array` is an object whose elements are indexed by a tuple of integers, where each integer must be bounded by the dimension of that axis. The tuple of dimensions, one for each axis, is called the shape of the `nd-array`. For example, 1D list of 10 objects (typically numbers) is a vector and has shape `(10,)`; a matrix with 5 rows and 2 columns is 2D and has shape `(5, 2)`; a single number is 0D and has an empty shape tuple `()`; an `nd-array` with shape (20, 10, 5) can be interpreted as a length-20 list of 10×5 matrices. -```python -shape1=(1, 5), shape2=(4, 1), broadcasted_shape=(4,5) +Here are some examples of common patterns expressed in terms of array broadcasting, and their accompanying visual representation in the figure below: -shape1=(1, 5), shape2=(4, 1), broadcasted_shape=(4,5) +```python +# Broadcast single observable +parameter_values = np.array(1.0) +parameter_values.shape == () +observables = ObservablesArray([Pauli("III"), Pauli("XXX"), Pauli("YYY"), Pauli("ZZZ"), Pauli("XYZ")]) +observables.shape == (5,) +>> result_bundle.shape == (5,) + +# Inner/Zip +parameter_values = BindingsArray(np.random.uniform(size=(5,))) +parameter_values.shape == (5,) +observables = ObservablesArray([[Pauli("III")], [Pauli("XXX")], [Pauli("YYY")], [Pauli("ZZZ")], [Pauli("XYZ")]]) +observables.shape == (5,1) +>> result_bundle.shape == (5,) + +# Outer/Product +parameter_values = BindingsArray(np.random.uniform(size=(1,6))) +parameter_values.shape == (1,6) +observables = ObservablesArray([[Pauli("III"), Pauli("XXX"), Pauli("YYY"), Pauli("ZZZ"), Pauli("XYZ"), Pauli("IIZ")]]) +observables.shape == (4,1) +>> result_bundle.shape == (4,6) + +# Standard nd generalization +parameter_values = BindingsArray(np.random.uniform(size=(3,6))) +parameter_values.shape == (3,6) +observables = ObservablesArray([ + [[Pauli(...), Pauli(...)]], + [[Pauli(...), Pauli(...)]], + [[Pauli(...), Pauli(...)]] +]) +observables.shape == (3,1,2) +>> result_bundle.shape == (3,2,6) -shape1=(), shape2=(5, 10), broadcasted_shape=(5,10) ``` + + FAQ1: Why make `BindingArrays` `nd`, and not just `list`-like? * A1: The primitives are meant to be a convenient execution framework, and allowing multiple axes relieves certain book-keeping burdens from the caller. `nd-arrays` are a standard construct in data manipulation. Different axes can be assigned different operational meanings, for example axis 0 could be a sweep over basis transformations, and axis 1 could be a sweep over Pauli randomizations. diff --git a/0015-estimator-interface/broadcasting.svg b/0015-estimator-interface/broadcasting.svg new file mode 100644 index 0000000..247165a --- /dev/null +++ b/0015-estimator-interface/broadcasting.svg @@ -0,0 +1,1718 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (5,) + Parameter Value Sets + + + Observables Array + () + (5,) + (5,) + (5,) + (5,) + + (1,6) + (3,6) + (3,1,2) + (3,6,2) + + (4,1) + (4,6) + + + + + + + + + + Resulting EV Estimates + Broadcast single observable + Inner/Zip + Outer/Product + Standard nd generalization + + + + + + + + + + + + + + + + + + + + + + From d2e7ae92ce29459e739c2d18236c25eddc19f77c Mon Sep 17 00:00:00 2001 From: Samantha Barron Date: Tue, 3 Oct 2023 16:12:53 -0400 Subject: [PATCH 09/51] Add section references (#6) --- 0015-estimator-interface.md | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index b3597dd..c9dbd30 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -9,7 +9,7 @@ | **Updated** | YYYY-MM-DD | -## Summary +## Summary The current `Estimator.run()` method requires that a user provide one circuit for every observable and set of parameter values that they wish to run. (This was not always the case, but the history will not be discussed here.) It is common, if not typical, for a user to want to estimate many observables corresponding to a single circuit. Likewise, it is common, if not typical, for a user to want to supply multiple parameter value sets for the same circuit. @@ -18,7 +18,7 @@ This RFC proposes to fix these issues by changing the `Estimator.run()` in the f 1. Take the transpose of the current signature; rather than accepting `circuits`, `parameter_values`, and `observables` as three different (iterable) arguments which are to be zipped together, instead localize the distinct tasks to be run via an iterable of triples `(circuit, parameter_values, observables)`. 2. In combination with 1, extend `parameter_values` and `observables` to be array-valued, that is, add the ability to explicity and conveniently specify multiple parameter value sets and observables for a single circuit. -## Motivation +## Motivation Here is a summary of pain-points with the current `Estimator.run()` interface: @@ -31,7 +31,7 @@ Here is a summary of pain-points with the current `Estimator.run()` interface: 1. _Performance._ Given that `QuantumCircuit` hashing should be avoided and circuit equality checks can be expensive, we should move away from an interface that necessitates performant primitive implementations to go down this path. As one example, qubit-wise commuting observables like `"IZZ"` and `"XZI"` can both be estimated using the same simulation/execution, but only if the estimator understands that they share a base circuit. As a second example, when the circuits need to be seriazed before they are simulated/executed (e.g. runtime or pickling-for-multi-processing), it puts the onus on the primitive implementation to detect circuit duplications. -Here is why the detailed section of this proposal suggests "transposing" the signatureß and using array-based arguments with broadcasting: +Here is why the [Detailed Design](#detailed-design) section suggests "transposing" the signatureß and using array-based arguments with broadcasting: 1. Transposing the signature will make it obvious what the primitive intends to do with each circuit. 2. Transposing the signature will let us introduce and reason about the notion "primitive unit of work", and carry it to other primitives. @@ -43,7 +43,7 @@ Here is why the detailed section of this proposal suggests "transposing" the sig 4. take all NxM combinations of N parameter value sets with M observables 5. etc. -## User Benefit +## User Benefit All users of the primitives stand to benefit from this proposal. Immediately, it will enable sophisticated and convenient workflows for power users though arrays and broadcasting. @@ -51,9 +51,9 @@ However, standard broadcasting rules are such that the 0D and 1D cases will feel For all users, the interface changes in this proposal will enable specific primitive implementations to enhance performance through reduced bandwidth on the runtime, compatibility with fast parameter binding, and multiplexing qubit-wise commuting observables. -## Design Proposal +## Design Proposal -### Tasks +### Tasks In this proposal, we introduce the concept of a Task, which we define as a single circuit along with auxiliary data required to execute the circuit relative to the primitive in question. This concept is general enough that it can be used for all primitive types, current and future, where we stress that what the “auxiliary data” is can vary between primitive types. @@ -73,13 +73,13 @@ ObservablesTask = NamedTuple[ ] ``` -We expect the formal primitive API and primitive implementations to have a strong sense of Tasks, but we will not demand that users construct them manually in Python as they are little more than named tuples, and we do not wish to overburden them with types. This is discussed further in the “Type Coersion” section. +We expect the formal primitive API and primitive implementations to have a strong sense of Tasks, but we will not demand that users construct them manually in Python as they are little more than named tuples, and we do not wish to overburden them with types. This is discussed further in [Type Coersion Strategy](#type-coercion-strategy). -### BindingsArray +### BindingsArray -It is common for a user to want to do a sweep over parameter values, that is, to execute the same parametric circuit with many different parameter binding sets. `BindingsArray` specifies multiple sets of parameters that can be bound to a circuit. For example, if a circuit has 200 parameters, and a user wishes to execute the circuit for 50 different sets of values, then a single instance of `BindingsArray` could represent 50 sets of 200 parameter values. Moreover, it is array-like (see the next section), so in this example the `BindingsArray` instance would be one-dimensional and have shape equal `(50,)`. +It is common for a user to want to do a sweep over parameter values, that is, to execute the same parametric circuit with many different parameter binding sets. `BindingsArray` specifies multiple sets of parameters that can be bound to a circuit. For example, if a circuit has 200 parameters, and a user wishes to execute the circuit for 50 different sets of values, then a single instance of `BindingsArray` could represent 50 sets of 200 parameter values. Moreover, it is array-like (see [ObservablesArray](#observablesarray)), so in this example the `BindingsArray` instance would be one-dimensional and have shape equal `(50,)`. -We expect the formal primitive API and primitive implementations to have a strong sense of `BindingsArray`, but we will not demand that users construct them manually because we do not wish to overburden them with types, and we need to remain backwards compatible. This is discussed further in the "Type Coercion" and "Migration Path" sections. +We expect the formal primitive API and primitive implementations to have a strong sense of `BindingsArray`, but we will not demand that users construct them manually because we do not wish to overburden them with types, and we need to remain backwards compatible. This is discussed further in the [Type Coercion Strategy](#type-coercion-strategy) and [Migration Path](#migration-path) sections. The "BindingsArray" object will support, at a minimum, the following constructor examples for a circuit with three input parameters `a`, `b`, and `c`, where we will use `<>` to denote some `array_like` of the specified shape: @@ -99,13 +99,13 @@ BindingsArray(<50, 2>, {c: <50>}) Note that `BindingsArray` is somewhat constrained by how `Parameters` currently work in Qiskit, namely, there is no support for array-valued inputs in the same way that there is in OpenQASM 3; `BindingsArray` assumes that every parameter represents a single number like a `float` or an `int`. -### ObservablesArray +### ObservablesArray With the `Estimator`, it is common for a user to want to estimate many observables of a single circuit. For example, all weight-1 and weight-2 Paulis that are adjacent on the connectivity graph. For a one-hundred qubit device, this corresponds to hundreds of unique estimates to be made for a single circuit, noting that for this particular example, on the heavy-hex graph, in the absence of mitigation, only 9 circuits need to be physically run. The `ObservablesArray` object will be an object array, where each element corresponds to a observable the user wants an estimated expectation value of. It is up to an `Estimator` implementation to solve a graph coloring problem to decide how to produce a sufficient set of physical circuits that are capable of producing data to make each of these estimates. -### Arrays and Broadcasting +### Arrays and Broadcasting An `nd-array` is an object whose elements are indexed by a tuple of integers, where each integer must be bounded by the dimension of that axis. The tuple of dimensions, one for each axis, is called the shape of the `nd-array`. For example, 1D list of 10 objects (typically numbers) is a vector and has shape `(10,)`; a matrix with 5 rows and 2 columns is 2D and has shape `(5, 2)`; a single number is 0D and has an empty shape tuple `()`; an `nd-array` with shape (20, 10, 5) can be interpreted as a length-20 list of 10×5 matrices. @@ -158,7 +158,7 @@ FAQ1: Why make `BindingArrays` `nd`, and not just `list`-like? We propose that any subtype of `ArrayTask` use broadcasting rules on auxillary data. -### Primitive Interface +### Primitive Interface We use the (non-standard) notation that `Type` denotes an instance of the given type with a constraint on attributes such as shape or format. @@ -182,7 +182,7 @@ job.result() Sampler.run(Union[Iterable[ArrayTask List[ResultBundle[{creg_name: CountsArray}]] ``` -### Type Coercion Strategy +### Type Coercion Strategy To minimize the number of container types that an every-day user will need to interact with, and to make the transition more seamless, we propose that several container types be associated with a `TypeLike` pattern and a static method @@ -217,7 +217,7 @@ In particular, we propose this kind of Coercion for the types: * `BindingsArray` * `ObservablesArray` -### ResultBundles +### ResultBundles The results from each `Task` will be array valued. However, we may require several arrays, possibly of different types. Consider an `ObservablesTask` with shape `<20, 30>`, where the shape has come from some combination of multiplexing an observables sweep with a parameter values sweep. This will result in a 20×30 array of real estimates. Moreover, we will want to return an array of standard deviations of the same shape. This would result in a bundle of broadcastable arrays: @@ -233,7 +233,7 @@ The reason we are proposing a generic container for the return type instead of, 1. Suppose that we want to give users the option of additionally returning the covariances between estimates that arise because of the circuit multiplexing, then we could update with the field `{"cov": <20,30,20,30>}`. 1. Suppose we want to return some indication of which estimates came from the same physical circuit. -## Detailed Design +## Detailed Design Technical reference level design. Elaborate on details such as: - Implementation procedure - If spans multiple projects cover these parts individually @@ -241,7 +241,7 @@ Technical reference level design. Elaborate on details such as: - Dissecting corner cases - Reference definition, e.g., formal definitions. -## Migration Path +## Migration Path We need to remain backwards compatible with the existing interface to adhere to the [Qiskit Deprecation Policy](https://qiskit.org/documentation/deprecation_policy.html). Notice that for both `Sampler` and `Estimator`, the first argument of the `run()` call will either contain `Tasks` (or `TaskLike`s) or it won’t. @@ -278,7 +278,7 @@ def run(self, tasks, observables=None, parameter_values=None, **run_options): return self._run_old_api(...) if no_tasks else return self._run(...) ``` -## Alternative Approaches +## Alternative Approaches An alternative is to consider letting the `run()` method accept, effectively, only a single `ObservablesTask`: @@ -292,14 +292,14 @@ by invoking the estimator multiple times. The disadvantages, which we feel are s 1. For real backends, the user would lose the ability to cause multiple types of circuits to be loaded into the control hardware at one time. For example, if using an estimator to perform randomized benchmarking, each circuit depth would need to be a separate job. 2. It would be difficult for implementations that include mitigation to share resources between tasks. For example, if different tasks represent different trotter step counts, there would need to be a complicated mechanism to share learning resources---that are specific to the application circuits---between multiple jobs. -## Questions +## Questions Open questions for discussion and an opening for feedback. -## Future Extensions +## Future Extensions In this proposal we have typed circuits as `QuantumCircuit`. It would be possible to extend this to a `CircuitLike` class which could be as simple as `Union[QuantumCircuit, str]` to explicitly allow OpenQASM3 circuits as first-class inputs. -A consequence of switching to the concept of Tasks, alluded to in the section that introduced them, is that this will allow us to introduce the `.run()` method into `BasePrimitive` +A consequence of switching to the concept of Tasks (mentioned in [Tasks](#tasks)) is that this will allow us to introduce the `.run()` method into `BasePrimitive` ```python class BasePrimitive(ABC, Generic[T]): From c9f65101f15f7bb508f4d357225ea56cbd7c0b2e Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 16:33:03 -0400 Subject: [PATCH 10/51] Add "BindingsArray generalizations" subsection --- 0015-estimator-interface.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index c9dbd30..7a98455 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -306,4 +306,14 @@ class BasePrimitive(ABC, Generic[T]): ... def run(self, T | Iterable[T], **options) -> List[ResultBundle]: ... -``` \ No newline at end of file +``` + +### `BindingsArray` generalizations + +`BindingsArray` is somewhat constrained by how `Parameters` currently work in Qiskit, namely, there is no support for array-valued inputs in the same way that there is in OpenQASM 3; `BindingsArray` assumes that every parameter represents a single number like a `float` or an `int`. +One solution could be to extend the class to allow different sub-arrays to have extra dimensions. +For example, for an input angle-array `input angle[15] foo;`, and for a bindings array with shape `(30, 20)`, the corresponding array could have shape `(30, 20, 1, 15)`. + +Another generalization is to allow broadcastable shapes inside of a single `BindingsArray`. +For example, one could have one array with shape `(1, 4, 5)` for five parameters, and another with shape `(3, 1, 2)` for two parameters, resulting in a bindings array with shape `(3, 4)`. +The advantage of this is allowing sparser representations of bindings arrays, and also specifying "fast" and "slow" parameters. \ No newline at end of file From f1d94c552bdc5f2f76fd930ece82975609fff6e4 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 16:33:43 -0400 Subject: [PATCH 11/51] Add subsection titles --- 0015-estimator-interface.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 7a98455..87ed12a 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -297,8 +297,12 @@ Open questions for discussion and an opening for feedback. ## Future Extensions +### Circuit Formats + In this proposal we have typed circuits as `QuantumCircuit`. It would be possible to extend this to a `CircuitLike` class which could be as simple as `Union[QuantumCircuit, str]` to explicitly allow OpenQASM3 circuits as first-class inputs. +### Tasks in the base class (and other primitives) + A consequence of switching to the concept of Tasks (mentioned in [Tasks](#tasks)) is that this will allow us to introduce the `.run()` method into `BasePrimitive` ```python From 68e83178f5e15ecc86c3f33586da84531aea92d4 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 16:38:10 -0400 Subject: [PATCH 12/51] Add more BindingsArray examples --- 0015-estimator-interface.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 87ed12a..a8b65a3 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -84,6 +84,13 @@ We expect the formal primitive API and primitive implementations to have a stron The "BindingsArray" object will support, at a minimum, the following constructor examples for a circuit with three input parameters `a`, `b`, and `c`, where we will use `<>` to denote some `array_like` of the specified shape: ```python +# a single value for each a, b, and c; 0-dimensional, and compatible with current interface +BindingsArray([0.0, 1.0, 2.0]) + +# a single value for each a, b, and c; 0-dimensional, and compatible with current interface +# satisfies user wish to be able to specify values by name +BindingsArray(kwvals={Parameter("a"): 0.0, Parameter("b"): 1.0, Parameter("c"): 2.0}) + # specify all 50 binding parameter sets in one big array BindingsArray(<50, 3>) From 6be6fb368314b6ffc6dd03aea408fae02dde89c6 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 16:38:59 -0400 Subject: [PATCH 13/51] Wording change --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index a8b65a3..3a3a264 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -77,7 +77,7 @@ We expect the formal primitive API and primitive implementations to have a stron ### BindingsArray -It is common for a user to want to do a sweep over parameter values, that is, to execute the same parametric circuit with many different parameter binding sets. `BindingsArray` specifies multiple sets of parameters that can be bound to a circuit. For example, if a circuit has 200 parameters, and a user wishes to execute the circuit for 50 different sets of values, then a single instance of `BindingsArray` could represent 50 sets of 200 parameter values. Moreover, it is array-like (see [ObservablesArray](#observablesarray)), so in this example the `BindingsArray` instance would be one-dimensional and have shape equal `(50,)`. +It is common for a user to want to do a sweep over parameter values, that is, to execute the same parametric circuit with many different parameter binding sets. `BindingsArray` is an object that specifies multiple sets of parameters that can be bound to a circuit. For example, if a circuit has 200 parameters, and a user wishes to execute the circuit for 50 different sets of values, then a single instance of `BindingsArray` could represent 50 sets of 200 parameter values. Moreover, it is array-like (see [ObservablesArray](#observablesarray)), so in this example the `BindingsArray` instance would be one-dimensional and have shape equal `(50,)`. We expect the formal primitive API and primitive implementations to have a strong sense of `BindingsArray`, but we will not demand that users construct them manually because we do not wish to overburden them with types, and we need to remain backwards compatible. This is discussed further in the [Type Coercion Strategy](#type-coercion-strategy) and [Migration Path](#migration-path) sections. From 3b4ce7926c40a4ccbb08da2890eb09ad33eb1117 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 16:39:32 -0400 Subject: [PATCH 14/51] Move the example up (no significant changes) --- 0015-estimator-interface.md | 50 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 3a3a264..385e27f 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -53,11 +53,34 @@ For all users, the interface changes in this proposal will enable specific primi ## Design Proposal +We use the (non-standard) notation that `Type` denotes an instance of the given type with a constraint on attributes such as shape or format. + +```python +Estimator.run(Union[Iterable[ObservablesTask], ObservablesTask], **options) → List[ResultBundle<{evs: ndarray, stds: ndarray}>] +``` + +### Example 1 + +```python +circuit = QuantumCircuit() +... # populate with 2039 parameters + +parameter_values = np.random((4, 32, 2039)) +observables = ["ZZIIIIIII", "IZXIIIIII", "IIIIIIYZI", "IZXIIYIII"] + +estimator = Estimator() +job = estimator.run((circuit, parameter_values, observables)) + +job.result() + +>> [ResultBundle<{evs: ndarray<4, 32>, stds: ndarray<4, 32>}>, metadata] +``` + ### Tasks In this proposal, we introduce the concept of a Task, which we define as a single circuit along with auxiliary data required to execute the circuit relative to the primitive in question. This concept is general enough that it can be used for all primitive types, current and future, where we stress that what the “auxiliary data” is can vary between primitive types. -For example, a circuit with unbound parameters (or in OQ3 terms, a circuit with inputs) alone could never qualify as a Task for any primitive because there is not enough information to execute it, namely, numeric parameter binding values. On the other hand, conceptually, a circuit with no unbound parameters (i.e. an OQ3 circuit with no inputs) alone could form a Task for a hypothetical primitive that just runs circuits and returns counts. This suggests a natural base for all Tasks: +For example, a circuit with unbound parameters (or in OQ3 terms, a circuit with inputs) alone could never qualify as a Task for any primitive because there is not enough information to execute it, namely, numeric parameter binding values. On the other hand, conceptually, a circuit with no unbound parameters (i.e. an OQ3 circuit with no inputs) alone could form a Task for a hypothetical primitive that just runs circuits and returns counts. This suggests (using an ad-hoc annotation convention) a natural base for all Tasks: ```python BaseTask = NamedTuple[circuit: QuantumCircuit] @@ -104,7 +127,6 @@ BindingsArray(kwargs={(a, c): <50, 2>, b: <50>}) BindingsArray(<50, 2>, {c: <50>}) ``` -Note that `BindingsArray` is somewhat constrained by how `Parameters` currently work in Qiskit, namely, there is no support for array-valued inputs in the same way that there is in OpenQASM 3; `BindingsArray` assumes that every parameter represents a single number like a `float` or an `int`. ### ObservablesArray @@ -165,30 +187,6 @@ FAQ1: Why make `BindingArrays` `nd`, and not just `list`-like? We propose that any subtype of `ArrayTask` use broadcasting rules on auxillary data. -### Primitive Interface - -We use the (non-standard) notation that `Type` denotes an instance of the given type with a constraint on attributes such as shape or format. - -```python -Estimator.run(Union[Iterable[ObservablesTask], ObservablesTask], **options) → List[ResultBundle<{evs: ndarray, stds: ndarray}>] -``` - -Example: - -```python -circuit = QuantumCircuit # with 2039 parameters - -parameter_values = np.random((9, 32, 2039)) -observables = [] -job = estimator.run((circuit, parameter_values, observables)) - -job.result() - ->> [ResultBundle<{evs: ndarray<9, 32>, stds: ndarray<9, 32>}>, metadata] - -Sampler.run(Union[Iterable[ArrayTask List[ResultBundle[{creg_name: CountsArray}]] -``` - ### Type Coercion Strategy To minimize the number of container types that an every-day user will need to interact with, and to make the transition more seamless, we propose that several container types be associated with a `TypeLike` pattern and a static method From 464cc1d0cc534e05cb4e1a0e8fe75df0f8967609 Mon Sep 17 00:00:00 2001 From: Samantha Barron Date: Tue, 3 Oct 2023 21:01:05 -0400 Subject: [PATCH 15/51] Add detail to migration plan (#7) --- 0015-estimator-interface.md | 114 ++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 26 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 385e27f..0862d85 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -89,11 +89,10 @@ BaseTask = NamedTuple[circuit: QuantumCircuit] For the `Estimator` primitive, in order to satisfy the definition as stated above, we propose the task structure ```python -ObservablesTask = NamedTuple[ +class ObservablesTask(NamedTuple): circuit: QuantumCircuit, parameter_values: BindingsArray, observables: ObservablesArray -] ``` We expect the formal primitive API and primitive implementations to have a strong sense of Tasks, but we will not demand that users construct them manually in Python as they are little more than named tuples, and we do not wish to overburden them with types. This is discussed further in [Type Coersion Strategy](#type-coercion-strategy). @@ -248,39 +247,102 @@ Technical reference level design. Elaborate on details such as: ## Migration Path -We need to remain backwards compatible with the existing interface to adhere to the [Qiskit Deprecation Policy](https://qiskit.org/documentation/deprecation_policy.html). Notice that for both `Sampler` and `Estimator`, the first argument of the `run()` call will either contain `Tasks` (or `TaskLike`s) or it won’t. +We need to remain backwards compatible with the existing interface to adhere to the [Qiskit Deprecation Policy](https://qiskit.org/documentation/deprecation_policy.html). -We propose a migration strategy based on this: If the user has provided no `TaskLike`s, proceed with the old API and old API output and emit a deprecation warning, or an error if something mandatory like `observables` has been omitted. Otherwise, proceed with the new API, raising if they have tried to use the old arguments in addition to providing tasks. +In summary, we propose the following strategy: If the user has provided no `TaskLike`s, proceed with the old API, the old API output, and emit a deprecation warning, or an error if something mandatory like `observables` has been omitted. Otherwise, proceed with the new API, raising if they have tried to use the old arguments in addition to providing tasks. + +We now describe the strategy in detail, beginning with a restatement of the current interface, where `Estimator.run` has `circuits` as the only positional argument and accepts `observables` and `parameter_values` as keyword arguments (in addition to the var keyword `**run_options` arguments which isn't affected): ```python -def run( - self, - circuits: Sequence[QuantumCircuit] | QuantumCircuit, - observables: Sequence[BaseOperator | PauliSumOp | str] | BaseOperator | PauliSumOp | str, - parameter_values: Sequence[Sequence[float]] | Sequence[float] | float | None = None, - **run_options, - ) +class Estimator(BasePrimitive): + + # current signature + def run( + self, + circuits: Sequence[QuantumCircuit] | QuantumCircuit, + observables: Sequence[BaseOperator | PauliSumOp | str] | BaseOperator | PauliSumOp | str, + parameter_values: Sequence[Sequence[float]] | Sequence[float] | float | None = None, + **run_options, + ): + ... ``` -```python -def run(self, tasks, observables=None, parameter_values=None, **run_options): - has_tasks = isinstance(tasks, QuantumCircuit) or all(isinstance(task, QuantumCircuit) for task in tasks) +We propose the migration to using tasks in two phases. + +### Deprecation Phase 1 - if not has_tasks: - # Trigger old API and disallow new one - if not all_circuits: - raise ValueError("Cannot mix and match old API with new API") +In the first phase: +* Introduce `tasks: Sequence[ObservablesTask] | ObservablesTask` as the only positional argument, and make `circuits` a keyword argument. +* Coerce the arguments to account for the fact that the only existing positional argument is `circuit: Sequence[QuantumCircuit] | QuantumCircuit`. +* Check that the user does not attempt to mix the old/new APIs. +* If necessary, coerce the old-API arguments (now all keyword arguments) into `ObservableTask`s, **raise deprecation warning**. +* Always eventually run with `Estimator._run(tasks: Sequence[ObservablesTask], **run_options)`. - if observables is None: - raise ValueError("`observables` is required argument in the old API") +Here is a mock implementation: +```python +class Estimator(BasePrimitive): + + def run( + self, + tasks: Sequence[ObservablesTask] | ObservablesTask, + circuits = None, + observables = None, + parameter_values = None, + **run_options + ): + # Coerce into standard form to later allow for `tasks` to be the only non-kwarg. + if isinstance(tasks, QuantumCircuit) or all(isinstance(task, QuantumCircuit) for task in tasks): + circuits = tasks + tasks = None + + # Do not mix new/old API + if tasks and (circuits or observables or parameter_values): + raise ValueError("Cannot mix old and new APIs") + + # Deprecated path + if tasks is None: + # Verify values in old API + if not (circuits and observables): + raise ValueError(f"Need both circuits and observables for old API, got circuits={circuits}, observables={observables}") + + # Coerce into old form + if parameter_values is None: + parameter_values = itertools.repeat(None) + if isinstance(circuit, QuantumCircuit): + circuit = [circuit] + if isinstance(observables, (BaseOperator, PauliSumOp, str)): + observables = [observables] + + # Coerce old form into `ObservablesTask`s + tasks = [ObservablesTask.coerce(task) for task in zip(circuits, observables, parameter_values)] + warnings.warn("Deprecated API use") + + # Coerce into sequence form + if isinstance(tasks, ObservablesTasks): + tasks = [tasks] + + # Run using only tasks + return self._run(tasks, **run_options) +``` - circuits = [tasks] if isinstance(tasks, QuantumCircuit) else tasks - observables = [observables] is isinstance(observables, (BaseOperator, PauliSumOp, str)) else observables - tasks = zip(circuits, observables, parameter_values) - warnings.warn() - tasks = ObservableTask.coerce(task) for task in tasks +### Deprecation Phase 2 - return self._run_old_api(...) if no_tasks else return self._run(...) +After the deprecation period, everything except the `Estimator._run` call and some minor coercion (unrelated to this RFC) are necessary. + +```python +class Estimator(BasePrimitive): + + def run( + self, + tasks: Sequence[ObservablesTask] | ObservablesTask, + **run_options + ): + # Coerce into sequence form + if isinstance(tasks, ObservablesTasks): + tasks = [tasks] + + # Run using only tasks + return self._run(tasks, **run_options) ``` ## Alternative Approaches From 08e6641af3ca13109fa195f77a507d42bfe2fcdb Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 21:09:57 -0400 Subject: [PATCH 16/51] Light editing of first few sections. --- 0015-estimator-interface.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 0862d85..7318c01 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -10,10 +10,12 @@ ## Summary + The current `Estimator.run()` method requires that a user provide one circuit for every observable and set of parameter values that they wish to run. -(This was not always the case, but the history will not be discussed here.) -It is common, if not typical, for a user to want to estimate many observables corresponding to a single circuit. Likewise, it is common, if not typical, for a user to want to supply multiple parameter value sets for the same circuit. -This RFC proposes to fix these issues by changing the `Estimator.run()` in the following ways: +(This was not always the case via circuit indices, but the history will not be discussed here.) +It is common, if not typical, for a user to want to estimate many observables corresponding to a single circuit. +Likewise, it is common, if not typical, for a user to want to supply multiple parameter value sets for the same circuit. +This RFC proposes to fix these issues by changing the `Estimator.run()` signature in the following ways: 1. Take the transpose of the current signature; rather than accepting `circuits`, `parameter_values`, and `observables` as three different (iterable) arguments which are to be zipped together, instead localize the distinct tasks to be run via an iterable of triples `(circuit, parameter_values, observables)`. 2. In combination with 1, extend `parameter_values` and `observables` to be array-valued, that is, add the ability to explicity and conveniently specify multiple parameter value sets and observables for a single circuit. @@ -24,17 +26,17 @@ Here is a summary of pain-points with the current `Estimator.run()` interface: 1. _Ergonomics._ It feels unnatural to invoke the estimator as `Estimator.run([circuit, circuit], observables=[obs1, obs2])` because of the redundancy of entering the circuit twice. -1. _Trust._ The case `Estimator.run([circuit, circuit], observables=[obs1, obs2])` makes a user question whether the primitive implementation is going to do any optimizations to check if the same circuit appears twice, and if so, whether it will be done by memory location, circuit equality, etc.; the interface itself creates gap in trust. +1. _Trust._ Cases like `Estimator.run([circuit, circuit], observables=[obs1, obs2])` make a user question whether the primitive implementation is going to do any optimizations to check if the same circuit appears twice, and if so, whether it will be done by memory location, circuit equality, etc.; the interface itself creates gap in trust. 1. _Clarity._ Without reading the documentation in detail, it's not obvious that the invocation `Estimator.run([circuit], observables=[obs1, obs2])` wouldn't cause `circuit` to be run with both supplied observables. In other words, that zipping _is always what's done to the `run()` arguments_ is common source of user confusion. Conversely, it's not clear that the way to collect all combinations of two observables and two parameter sets is to invoke `Estimator.run([circuit] * 4, observables=[obs1, obs2] * 2), parameter_values=[params1] * 2 + [params2] * 2`. -1. _Performance._ Given that `QuantumCircuit` hashing should be avoided and circuit equality checks can be expensive, we should move away from an interface that necessitates performant primitive implementations to go down this path. As one example, qubit-wise commuting observables like `"IZZ"` and `"XZI"` can both be estimated using the same simulation/execution, but only if the estimator understands that they share a base circuit. As a second example, when the circuits need to be seriazed before they are simulated/executed (e.g. runtime or pickling-for-multi-processing), it puts the onus on the primitive implementation to detect circuit duplications. +1. _Performance._ Given that `QuantumCircuit` hashing should be avoided and circuit equality checks can be expensive, we should move away from an interface that necessitates performant primitive implementations to go down this path at all. As one example, qubit-wise commuting observables like `"IZZ"` and `"XZI"` can both be estimated using the same simulation/execution, but only if the estimator understands that they share a base circuit. As a second example, when the circuits need to be seriazed before they are simulated/executed (e.g. runtime or pickling-for-multi-processing), it puts the onus on the primitive implementation to detect circuit duplications. -Here is why the [Detailed Design](#detailed-design) section suggests "transposing" the signatureß and using array-based arguments with broadcasting: +Here is why the [Detailed Design](#detailed-design) section suggests "transposing" the signature and using array-based arguments with broadcasting: 1. Transposing the signature will make it obvious what the primitive intends to do with each circuit. - 2. Transposing the signature will let us introduce and reason about the notion "primitive unit of work", and carry it to other primitives. + 2. Transposing the signature will let us introduce and reason about the notion "primitive unit of work", for which this RFC proposes the name "task". 3. Array-based arguments will let users assign operational meaning to each axis (this axis is for twirling, that axis is for basis changes, etc.). 4. Broadcasting rules will let users choose how to combine parameter value sets with observables in different ways, such as 1. use one observable for all of N parameter value sets @@ -49,11 +51,11 @@ All users of the primitives stand to benefit from this proposal. Immediately, it will enable sophisticated and convenient workflows for power users though arrays and broadcasting. However, standard broadcasting rules are such that the 0D and 1D cases will feel natural to existing users---0D is essentially what we already have, and 1D will be perceived as a simple-yet-welcome bonus. -For all users, the interface changes in this proposal will enable specific primitive implementations to enhance performance through reduced bandwidth on the runtime, compatibility with fast parameter binding, and multiplexing qubit-wise commuting observables. +Moreover, the interface changes in this proposal will enable specific primitive implementations to enhance performance through reduced bandwidth on the runtime, compatibility with fast parameter binding, and multiplexing qubit-wise commuting observables. ## Design Proposal -We use the (non-standard) notation that `Type` denotes an instance of the given type with a constraint on attributes such as shape or format. +We use the (non-standard) notation `Type` to denote an instance of the given type with a constraint on attributes such as shape or format. ```python Estimator.run(Union[Iterable[ObservablesTask], ObservablesTask], **options) → List[ResultBundle<{evs: ndarray, stds: ndarray}>] From 6131db7d43f01d3d367abfc0255ebe20126737bb Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 21:11:34 -0400 Subject: [PATCH 17/51] Move all of the class ideas into Detailed Design --- 0015-estimator-interface.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 7318c01..3af06b1 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -78,6 +78,8 @@ job.result() >> [ResultBundle<{evs: ndarray<4, 32>, stds: ndarray<4, 32>}>, metadata] ``` +## Detailed Design + ### Tasks In this proposal, we introduce the concept of a Task, which we define as a single circuit along with auxiliary data required to execute the circuit relative to the primitive in question. This concept is general enough that it can be used for all primitive types, current and future, where we stress that what the “auxiliary data” is can vary between primitive types. @@ -239,14 +241,6 @@ The reason we are proposing a generic container for the return type instead of, 1. Suppose that we want to give users the option of additionally returning the covariances between estimates that arise because of the circuit multiplexing, then we could update with the field `{"cov": <20,30,20,30>}`. 1. Suppose we want to return some indication of which estimates came from the same physical circuit. -## Detailed Design -Technical reference level design. Elaborate on details such as: -- Implementation procedure - - If spans multiple projects cover these parts individually -- Interaction with other features -- Dissecting corner cases -- Reference definition, e.g., formal definitions. - ## Migration Path We need to remain backwards compatible with the existing interface to adhere to the [Qiskit Deprecation Policy](https://qiskit.org/documentation/deprecation_policy.html). From 8fee8fb7c562c7bc062b771a0c2c13b382562b07 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 21:35:52 -0400 Subject: [PATCH 18/51] Edits in the detailed design subsections --- 0015-estimator-interface.md | 68 ++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 3af06b1..bbb0c54 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -82,32 +82,34 @@ job.result() ### Tasks -In this proposal, we introduce the concept of a Task, which we define as a single circuit along with auxiliary data required to execute the circuit relative to the primitive in question. This concept is general enough that it can be used for all primitive types, current and future, where we stress that what the “auxiliary data” is can vary between primitive types. +We propose the concept of a _task_, which we define as _a single circuit along with auxiliary data required to execute the circuit relative to the primitive in question_. This concept is general enough that it can be used for all primitive types, current and future, where we stress that what the “auxiliary data” is can vary between primitive types. -For example, a circuit with unbound parameters (or in OQ3 terms, a circuit with inputs) alone could never qualify as a Task for any primitive because there is not enough information to execute it, namely, numeric parameter binding values. On the other hand, conceptually, a circuit with no unbound parameters (i.e. an OQ3 circuit with no inputs) alone could form a Task for a hypothetical primitive that just runs circuits and returns counts. This suggests (using an ad-hoc annotation convention) a natural base for all Tasks: +For example, a circuit with unbound parameters (or in OpenQASM3 terms, a circuit with `input`s) alone could never qualify as a Task for any primitive because there is not enough information to execute it, namely, numeric parameter binding values. On the other hand, conceptually, a circuit with no unbound parameters (i.e. an circuit with no `input`s) alone could form a task for a hypothetical primitive that just runs circuits and returns counts. This suggests (using an ad-hoc annotation convention) a natural base for all tasks: ```python -BaseTask = NamedTuple[circuit: QuantumCircuit] +@dataclass +class BaseTask: + circuit: QuantumCircuit ``` For the `Estimator` primitive, in order to satisfy the definition as stated above, we propose the task structure ```python -class ObservablesTask(NamedTuple): - circuit: QuantumCircuit, - parameter_values: BindingsArray, - observables: ObservablesArray +class ObservablesTask(BaseTask): + parameter_values: BindingsArray = None, + observables: ObservablesArray = None ``` -We expect the formal primitive API and primitive implementations to have a strong sense of Tasks, but we will not demand that users construct them manually in Python as they are little more than named tuples, and we do not wish to overburden them with types. This is discussed further in [Type Coersion Strategy](#type-coercion-strategy). +We expect the formal primitive API and primitive implementations to have a strong sense of tasks, but we will not demand that users construct them manually in Python as they are little more than simple data containers (incidentically implemented as dataclasses above), and we do not wish to overburden them with types. This is discussed further in [Type Coercion Strategy](#type-coercion-strategy). ### BindingsArray -It is common for a user to want to do a sweep over parameter values, that is, to execute the same parametric circuit with many different parameter binding sets. `BindingsArray` is an object that specifies multiple sets of parameters that can be bound to a circuit. For example, if a circuit has 200 parameters, and a user wishes to execute the circuit for 50 different sets of values, then a single instance of `BindingsArray` could represent 50 sets of 200 parameter values. Moreover, it is array-like (see [ObservablesArray](#observablesarray)), so in this example the `BindingsArray` instance would be one-dimensional and have shape equal `(50,)`. +It is common for a user to want to do a sweep over parameter values, that is, to execute the same parametric circuit with many different parameter binding sets. +We propose a new class `BindingsArray` that stores multiple sets of parameters, which can be bound to a circuit. +For example, if a circuit has 200 parameters, and a user wishes to execute the circuit for 50 different sets of values, then a single instance of `BindingsArray` could represent 50 sets of 200 parameter values. +Moreover, we propose that it be array-like, so that, in this example, the `BindingsArray` instance would be one-dimensional and have shape equal `(50,)`. -We expect the formal primitive API and primitive implementations to have a strong sense of `BindingsArray`, but we will not demand that users construct them manually because we do not wish to overburden them with types, and we need to remain backwards compatible. This is discussed further in the [Type Coercion Strategy](#type-coercion-strategy) and [Migration Path](#migration-path) sections. - -The "BindingsArray" object will support, at a minimum, the following constructor examples for a circuit with three input parameters `a`, `b`, and `c`, where we will use `<>` to denote some `array_like` of the specified shape: +The proposed `BindingsArray` object would support, at a minimum, the following constructor examples for a circuit with three input parameters `a`, `b`, and `c`, where we will use `<>` to denote some `array_like` of the specified shape: ```python # a single value for each a, b, and c; 0-dimensional, and compatible with current interface @@ -130,24 +132,46 @@ BindingsArray(kwargs={(a, c): <50, 2>, b: <50>}) BindingsArray(<50, 2>, {c: <50>}) ``` +We expect the formal primitive API and primitive implementations to have a strong sense of `BindingsArray`, but we will not demand that users construct them manually because we do not wish to overburden them with types, and we need to remain backwards compatible. This is discussed further in the [Type Coercion Strategy](#type-coercion-strategy) and [Migration Path](#migration-path) sections. ### ObservablesArray -With the `Estimator`, it is common for a user to want to estimate many observables of a single circuit. For example, all weight-1 and weight-2 Paulis that are adjacent on the connectivity graph. For a one-hundred qubit device, this corresponds to hundreds of unique estimates to be made for a single circuit, noting that for this particular example, on the heavy-hex graph, in the absence of mitigation, only 9 circuits need to be physically run. +With the `Estimator`, it is common for a user to want to estimate many observables of a single circuit. +For example, all weight-1 and weight-2 Paulis that are adjacent on the connectivity graph. +For a one-hundred qubit device, this corresponds to hundreds of unique estimates to be made for a single circuit, noting that for this particular example, on the heavy-hex graph, in the absence of mitigation, only 9 circuits need to be physically run. + +We propose a new `ObservablesArray` object, where each element corresponds to a observable the user wants an estimated expectation value of. +We propose that the internal data model of this object have an element format + +```python +Dict[Tuple[Tuple[int,...], str], float] +``` + +where, for example, the observable `0.5 * IIZY + 0.2 * XIII` would be stored as + +```python +{((2, 3), "ZY"): 0.5, ((0,), "X"): 0.2} +``` + +This is proposed instead of `{"IIZY": 0.5, "XIII": 0.2}` anticipating the overhead of storing and working with so many `"I"`'s for large devices. -The `ObservablesArray` object will be an object array, where each element corresponds to a observable the user wants an estimated expectation value of. It is up to an `Estimator` implementation to solve a graph coloring problem to decide how to produce a sufficient set of physical circuits that are capable of producing data to make each of these estimates. +It is up to each `Estimator` implementation to, if it sees fit, to solve a graph coloring problem that decides how to produce a sufficient set of physical circuits that are capable of producing data to make each of these estimates. ### Arrays and Broadcasting -An `nd-array` is an object whose elements are indexed by a tuple of integers, where each integer must be bounded by the dimension of that axis. The tuple of dimensions, one for each axis, is called the shape of the `nd-array`. For example, 1D list of 10 objects (typically numbers) is a vector and has shape `(10,)`; a matrix with 5 rows and 2 columns is 2D and has shape `(5, 2)`; a single number is 0D and has an empty shape tuple `()`; an `nd-array` with shape (20, 10, 5) can be interpreted as a length-20 list of 10×5 matrices. +An `nd-array` is an object whose elements are indexed by a tuple of integers, where each integer must be bounded by the dimension of that axis. +The tuple of dimensions, one for each axis, is called the shape of the `nd-array`. +For example, 1D list of 10 objects (typically numbers) is a vector and has shape `(10,)`; a matrix with 5 rows and 2 columns is 2D and has shape `(5, 2)`; a single number is 0D and has an empty shape tuple `()`; an `nd-array` with shape (20, 10, 5) can be interpreted as a length-20 list of 10×5 matrices. -Here are some examples of common patterns expressed in terms of array broadcasting, and their accompanying visual representation in the figure below: +We propose the constraint that a `BindingsArray` instance and an `ObservablesArray` instance that live in the same task must have shapes that are broadcastable, and in such case an `Estimator` promises to return one expectation value estimate for each element of the broadcasted shape. + +Here are some examples of common patterns expressed in terms of array broadcasting, and their accompanying visual representation in the figure that follows: ```python # Broadcast single observable parameter_values = np.array(1.0) parameter_values.shape == () -observables = ObservablesArray([Pauli("III"), Pauli("XXX"), Pauli("YYY"), Pauli("ZZZ"), Pauli("XYZ")]) +observables = ObservablesArray(["III", "XXX", Pauli("YYY"), "ZZZ", Pauli("XYZ")]) observables.shape == (5,) >> result_bundle.shape == (5,) @@ -180,16 +204,6 @@ observables.shape == (3,1,2) -FAQ1: Why make `BindingArrays` `nd`, and not just `list`-like? - -* A1: The primitives are meant to be a convenient execution framework, and allowing multiple axes relieves certain book-keeping burdens from the caller. `nd-arrays` are a standard construct in data manipulation. Different axes can be assigned different operational meanings, for example axis 0 could be a sweep over basis transformations, and axis 1 could be a sweep over Pauli randomizations. - -* A2: There are different places in the runtime stack that binding can occur. in the runtime container, as a fast parametric. Having multiple axes gives us the freedom to offer this as an explicit feature in the future (?). - -* A3: It lets us specify certain common scenarios for `ObservablesTask` more efficiently. For example, suppose we want one axis to represent twirling variates, and the other axis to represent observable bases. Then, via broadcasting, described below, the information that needs to be transferred over the wire appears 1D for both the twirling variate phase information and the list of observables. Without `nd-array` support, broadcasting would have to be done client-side. - -We propose that any subtype of `ArrayTask` use broadcasting rules on auxillary data. - ### Type Coercion Strategy To minimize the number of container types that an every-day user will need to interact with, and to make the transition more seamless, we propose that several container types be associated with a `TypeLike` pattern and a static method From 9fb795c4029c57539a6a175ba27b12d4be0591fa Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 21:48:32 -0400 Subject: [PATCH 19/51] Edits to migration section --- 0015-estimator-interface.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index bbb0c54..621b13b 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -288,7 +288,7 @@ In the first phase: * If necessary, coerce the old-API arguments (now all keyword arguments) into `ObservableTask`s, **raise deprecation warning**. * Always eventually run with `Estimator._run(tasks: Sequence[ObservablesTask], **run_options)`. -Here is a mock implementation: +Here is a mock implementation, with missing type annotations as above: ```python class Estimator(BasePrimitive): @@ -299,9 +299,11 @@ class Estimator(BasePrimitive): observables = None, parameter_values = None, **run_options - ): + ): # Coerce into standard form to later allow for `tasks` to be the only non-kwarg. if isinstance(tasks, QuantumCircuit) or all(isinstance(task, QuantumCircuit) for task in tasks): + if circuits is not None: + raise ValueError("Cannot mix old and new APIs") circuits = tasks tasks = None @@ -337,7 +339,7 @@ class Estimator(BasePrimitive): ### Deprecation Phase 2 -After the deprecation period, everything except the `Estimator._run` call and some minor coercion (unrelated to this RFC) are necessary. +After the deprecation period, remove extraneous arguments and checks: ```python class Estimator(BasePrimitive): @@ -346,7 +348,7 @@ class Estimator(BasePrimitive): self, tasks: Sequence[ObservablesTask] | ObservablesTask, **run_options - ): + ): # Coerce into sequence form if isinstance(tasks, ObservablesTasks): tasks = [tasks] From 0ac80256bbcca3861eab014074ff24eef5d065c5 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 22:53:47 -0400 Subject: [PATCH 20/51] Flesh out examples more --- 0015-estimator-interface.md | 82 +++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 621b13b..1f0823a 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -55,27 +55,69 @@ Moreover, the interface changes in this proposal will enable specific primitive ## Design Proposal -We use the (non-standard) notation `Type` to denote an instance of the given type with a constraint on attributes such as shape or format. +We will lead the design proposal with an example: ```python -Estimator.run(Union[Iterable[ObservablesTask], ObservablesTask], **options) → List[ResultBundle<{evs: ndarray, stds: ndarray}>] +# construct two circuits, the first with 2039 Parameters, the second with no Parameters +circuit1 = QuantumCircuit(9) +... +circuit2 = QuantumCircuit(2) +... + +# specify 128 different parameter value sets for circuit1, in a 4x32 shape +parameter_values1 = np.random.uniform((4, 32, 2039)) +# specify 4 observables to measure for circuit 1 +observables1 = ["ZZIIIIIII", "IZXIIIIII", "IIIIIIYZI", "IZXIIYIII"] + +# specify 18 observables to measure for circuit2 +observables2 = [Pauili("XYZIIII"), ..., SparsePauliOp({"YYIIIII": 0.2})] + +# invoke an estimator +estimator = Estimator() +job = estimator.run([ + (circuit1, observables1, parameter_values1), + (circuit2, observables2) +]) + +# wait for the result +job.result() + +# get a bundle of results for every task +>> EstimatorResult( +>> ResultBundle<{evs: ndarray<4, 32>, stds: ndarray<4, 32>}, local_metadata>, +>> ResultBundle<{evs: ndarray<18>, stds: ndarray<18>}, local_metadata>, +>> global_metadata +>> ) ``` -### Example 1 +The above example shows various features of the design, including combining large numbers of observables with large numbers of parameter value sets for a single circuit. + +The detailed targetted signature, following the [deprecation period](#migration-path), +is as follows: ```python -circuit = QuantumCircuit() -... # populate with 2039 parameters +T = TypeVar("T", bound=Job) -parameter_values = np.random((4, 32, 2039)) -observables = ["ZZIIIIIII", "IZXIIIIII", "IIIIIIYZI", "IZXIIYIII"] +BindingsArrayLike = Union[ + BindingsArray, + NDArray, + Dict[Parameter | Tuple[Parameter], NDArray] +] -estimator = Estimator() -job = estimator.run((circuit, parameter_values, observables)) +ObservablesArrayLike = Union[ + ObservablesArray, + Iterable[str | Pauli | SparsePauliOp] +] -job.result() +ObservableTaskLike = Union[ + ObservablesTask, + Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike] +] ->> [ResultBundle<{evs: ndarray<4, 32>, stds: ndarray<4, 32>}>, metadata] +class EstimatorBase(ABC, Generic[T]): + ... + def run(self, tasks: ObservableTaskLike | Iterable[ObservableTaskLike], **options) -> T: + pass ``` ## Detailed Design @@ -217,7 +259,7 @@ that is invoked inside of the `run()` method. For example, ```python -BindingsArrayLike=Union[BindingsArray, ArrayLike, Iterable[float], Mapping[Parameter, float]] +BindingsArrayLike = Union[BindingsArray, ArrayLike, Mapping[Parameter, float]] ``` and @@ -228,7 +270,7 @@ def coerce(bindings_array: BindingsArrayLike) -> BindingsArray: if isinstance(bindings_array, (list, ArrayLike)): bindings_array = BindingsArray(bindings_array) elif isinstance(bindings_array, Mapping): - bindings_array = BindingsArray([], bindings_array) + bindings_array = BindingsArray(kwargs=bindings_array) return bindings_array ``` @@ -241,19 +283,23 @@ In particular, we propose this kind of Coercion for the types: ### ResultBundles -The results from each `Task` will be array valued. However, we may require several arrays, possibly of different types. Consider an `ObservablesTask` with shape `<20, 30>`, where the shape has come from some combination of multiplexing an observables sweep with a parameter values sweep. This will result in a 20×30 array of real estimates. Moreover, we will want to return an array of standard deviations of the same shape. This would result in a bundle of broadcastable arrays: +The results from each `ObservablesTask` will be array valued, and each `ObservablesTask` in the same job may have a different shape. +Consider an `ObservablesTask` with shape `<20, 30>`, where the shape has come from the broadcasting rules discussed elsewhere. +This will result in a 20×30 array of real estimates. +Moreover, we will want to return an array of standard deviations of the same shape. +This would result in a bundle of broadcastable arrays: ```python ResultBundle({“evs”: <20, 30>, “stds”: <20, 30>}, metadata) ``` -The reason we are proposing a generic container for the return type instead of, e.g., an `Estimator`-specific container, is because +The reason we are proposing a generic container for the return type instead of, e.g., an `Estimator`-specific container, is because it 1. Provides unified experience across primitives for users. -1. code-reuse -1. Provides a certain certain amount of flexibility for what can be returned without modifying the container object. Here are some examples: +2. code-reuse +3. Provides a certain certain amount of flexibility for what can be returned without modifying the container object. Here are some examples: 1. Suppose that we want to give users the option of additionally returning the covariances between estimates that arise because of the circuit multiplexing, then we could update with the field `{"cov": <20,30,20,30>}`. - 1. Suppose we want to return some indication of which estimates came from the same physical circuit. + 2. Suppose we want to return some indication of which estimates came from the same physical circuit. ## Migration Path From e4bdb05ca1ebac6dcef166e5ed7fd3efa8c3d449 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 22:55:02 -0400 Subject: [PATCH 21/51] update date --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 1f0823a..ff703ec 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -5,7 +5,7 @@ | **RFC #** | 0015 | | **Authors** | Ian Hincks (ian.hincks@ibm.com) | | **Deprecates** | RFC that this RFC deprecates | -| **Submitted** | YYYY-MM-DD | +| **Submitted** | 2023-10-03 | | **Updated** | YYYY-MM-DD | From 1f81a4590df09941e85ac24f8bdc8886951ff3e7 Mon Sep 17 00:00:00 2001 From: --get-all Date: Tue, 3 Oct 2023 23:04:12 -0400 Subject: [PATCH 22/51] update authors --- 0015-estimator-interface.md | 1 + 1 file changed, 1 insertion(+) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index ff703ec..12da5d4 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -4,6 +4,7 @@ |:------------------|:---------------------------------------------| | **RFC #** | 0015 | | **Authors** | Ian Hincks (ian.hincks@ibm.com) | +| | Samantha Barron (samantha.v.barron@ibm.com) | | **Deprecates** | RFC that this RFC deprecates | | **Submitted** | 2023-10-03 | | **Updated** | YYYY-MM-DD | From a8f7ed271ce495f3ba0986022afc90c91f781650 Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 08:00:17 -0400 Subject: [PATCH 23/51] switch observable array format --- 0015-estimator-interface.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 12da5d4..1ce529b 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -187,13 +187,13 @@ We propose a new `ObservablesArray` object, where each element corresponds to a We propose that the internal data model of this object have an element format ```python -Dict[Tuple[Tuple[int,...], str], float] +List[Tuple[str, List[int], float]] ``` where, for example, the observable `0.5 * IIZY + 0.2 * XIII` would be stored as ```python -{((2, 3), "ZY"): 0.5, ((0,), "X"): 0.2} +[("ZY", [2, 3], 0.5), ("X", [0], 0.2)] ``` This is proposed instead of `{"IIZY": 0.5, "XIII": 0.2}` anticipating the overhead of storing and working with so many `"I"`'s for large devices. From d4fe1ef26352024354f737194afefe637f5161de Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 09:30:28 -0400 Subject: [PATCH 24/51] Small fixes here and there---nothing major. --- 0015-estimator-interface.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 1ce529b..b2f6de2 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -127,12 +127,12 @@ class EstimatorBase(ABC, Generic[T]): We propose the concept of a _task_, which we define as _a single circuit along with auxiliary data required to execute the circuit relative to the primitive in question_. This concept is general enough that it can be used for all primitive types, current and future, where we stress that what the “auxiliary data” is can vary between primitive types. -For example, a circuit with unbound parameters (or in OpenQASM3 terms, a circuit with `input`s) alone could never qualify as a Task for any primitive because there is not enough information to execute it, namely, numeric parameter binding values. On the other hand, conceptually, a circuit with no unbound parameters (i.e. an circuit with no `input`s) alone could form a task for a hypothetical primitive that just runs circuits and returns counts. This suggests (using an ad-hoc annotation convention) a natural base for all tasks: +For example, a circuit with unbound parameters (or in OpenQASM3 terms, a circuit with `input`s) alone could never qualify as a task for any primitive because there is not enough information to execute it, namely, numeric parameter binding values. On the other hand, conceptually, a circuit with no unbound parameters (i.e. an circuit with no `input`s) alone could form a task for a hypothetical primitive that just runs circuits and returns counts. This suggests a natural base for all tasks: ```python @dataclass class BaseTask: - circuit: QuantumCircuit + circuit: QuantumCircuit ``` For the `Estimator` primitive, in order to satisfy the definition as stated above, we propose the task structure @@ -206,7 +206,7 @@ An `nd-array` is an object whose elements are indexed by a tuple of integers, wh The tuple of dimensions, one for each axis, is called the shape of the `nd-array`. For example, 1D list of 10 objects (typically numbers) is a vector and has shape `(10,)`; a matrix with 5 rows and 2 columns is 2D and has shape `(5, 2)`; a single number is 0D and has an empty shape tuple `()`; an `nd-array` with shape (20, 10, 5) can be interpreted as a length-20 list of 10×5 matrices. -We propose the constraint that a `BindingsArray` instance and an `ObservablesArray` instance that live in the same task must have shapes that are broadcastable, and in such case an `Estimator` promises to return one expectation value estimate for each element of the broadcasted shape. +We propose the constraint that a `BindingsArray` instance and an `ObservablesArray` instance that live in the same task must have shapes that are broadcastable, in which case, the `Estimator` promises to return one expectation value estimate for each element of the broadcasted shape. Here are some examples of common patterns expressed in terms of array broadcasting, and their accompanying visual representation in the figure that follows: @@ -442,7 +442,7 @@ class BasePrimitive(ABC, Generic[T]): `BindingsArray` is somewhat constrained by how `Parameters` currently work in Qiskit, namely, there is no support for array-valued inputs in the same way that there is in OpenQASM 3; `BindingsArray` assumes that every parameter represents a single number like a `float` or an `int`. One solution could be to extend the class to allow different sub-arrays to have extra dimensions. -For example, for an input angle-array `input angle[15] foo;`, and for a bindings array with shape `(30, 20)`, the corresponding array could have shape `(30, 20, 1, 15)`. +For example, for an input angle-array `input array[angle, 15] foo;`, and for a bindings array with shape `(30, 20)`, the corresponding array could have shape `(30, 20, 1, 15)`, where the singleton dimension exists to express that it is targetting a single `input`. Another generalization is to allow broadcastable shapes inside of a single `BindingsArray`. For example, one could have one array with shape `(1, 4, 5)` for five parameters, and another with shape `(3, 1, 2)` for two parameters, resulting in a bindings array with shape `(3, 4)`. From 843ce55cf93149a90fbd169ebc680d0289c41fb6 Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 09:44:11 -0400 Subject: [PATCH 25/51] Update authors --- 0015-estimator-interface.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index b2f6de2..18dc4ff 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -4,7 +4,10 @@ |:------------------|:---------------------------------------------| | **RFC #** | 0015 | | **Authors** | Ian Hincks (ian.hincks@ibm.com) | +| | Chris Wood (cjwood@us.ibm.com) | +| | Ikko Hamamura (ikko.hamamura1@ibm.com) | | | Samantha Barron (samantha.v.barron@ibm.com) | +| | Takashi Imamichi (imamichi@jp.ibm.com) | | **Deprecates** | RFC that this RFC deprecates | | **Submitted** | 2023-10-03 | | **Updated** | YYYY-MM-DD | From c8874aca837bf219fd12504a26ea641f6279ab2d Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 10:39:00 -0400 Subject: [PATCH 26/51] Address Chris` "Task" comments --- 0015-estimator-interface.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 18dc4ff..e48c5f7 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -201,6 +201,9 @@ where, for example, the observable `0.5 * IIZY + 0.2 * XIII` would be stored as This is proposed instead of `{"IIZY": 0.5, "XIII": 0.2}` anticipating the overhead of storing and working with so many `"I"`'s for large devices. +Note that the above is simply the data model; the `ObservablesArray` object, either in the constructor or special static coercion method, will accept the `quantum_info` objects such as `Pauli` and `SparsePauliOp` that they are used to. +Further, through [type coersion](#type-coercion-strategy) in `Estimator.run()` itself, a user won't need to manually construct an `ObservablesArray`, as shown in the example in the [Design Proposal](#design-proposal) section. + It is up to each `Estimator` implementation to, if it sees fit, to solve a graph coloring problem that decides how to produce a sufficient set of physical circuits that are capable of producing data to make each of these estimates. ### Arrays and Broadcasting @@ -280,7 +283,6 @@ def coerce(bindings_array: BindingsArrayLike) -> BindingsArray: ``` In particular, we propose this kind of Coercion for the types: -* `ArrayTask` * `ObservablesTask` * `BindingsArray` * `ObservablesArray` From 9be1a4e1d4eb44ec9ba7b9f3ecca73d20eb5ca33 Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 11:02:17 -0400 Subject: [PATCH 27/51] Further response to Chris' "Task" comments --- 0015-estimator-interface.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index e48c5f7..d3f40b1 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -138,12 +138,13 @@ class BaseTask: circuit: QuantumCircuit ``` -For the `Estimator` primitive, in order to satisfy the definition as stated above, we propose the task structure +This base class can be added to qiskit if and when needed as a non-breaking change, and has been included here mainly to make the concepts clear, rather than to indicate that we need such a base class immediately. +Most relevant, for the `Estimator` primitive, in order to satisfy the definition as stated above, we propose the task structure ```python class ObservablesTask(BaseTask): - parameter_values: BindingsArray = None, observables: ObservablesArray = None + parameter_values: BindingsArray = None, ``` We expect the formal primitive API and primitive implementations to have a strong sense of tasks, but we will not demand that users construct them manually in Python as they are little more than simple data containers (incidentically implemented as dataclasses above), and we do not wish to overburden them with types. This is discussed further in [Type Coercion Strategy](#type-coercion-strategy). From 633187dac5ffad411ffe6c54a88c00cf87af33ba Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 11:31:22 -0400 Subject: [PATCH 28/51] alphebetize and edit authors --- 0015-estimator-interface.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index d3f40b1..262a3c3 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -3,11 +3,13 @@ | **Status** | **Proposed/Accepted/Deprecated** | |:------------------|:---------------------------------------------| | **RFC #** | 0015 | -| **Authors** | Ian Hincks (ian.hincks@ibm.com) | -| | Chris Wood (cjwood@us.ibm.com) | +| **Authors** | Samantha Barron (samantha.v.barron@ibm.com) | +| | Lev Bishop (lsbishop@us.ibm.com) | | | Ikko Hamamura (ikko.hamamura1@ibm.com) | -| | Samantha Barron (samantha.v.barron@ibm.com) | +| | Ian Hincks (ian.hincks@ibm.com) | | | Takashi Imamichi (imamichi@jp.ibm.com) | +| | Blake Johnson (blake.johnson@ibm.com) | +| | Chris Wood (cjwood@us.ibm.com) | | **Deprecates** | RFC that this RFC deprecates | | **Submitted** | 2023-10-03 | | **Updated** | YYYY-MM-DD | From ff2e2624104174abf027adefd4aa53be9f5f30c9 Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 13:19:15 -0400 Subject: [PATCH 29/51] rename ResultBundle -> TaskResult --- 0015-estimator-interface.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 262a3c3..53da23f 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -90,8 +90,8 @@ job.result() # get a bundle of results for every task >> EstimatorResult( ->> ResultBundle<{evs: ndarray<4, 32>, stds: ndarray<4, 32>}, local_metadata>, ->> ResultBundle<{evs: ndarray<18>, stds: ndarray<18>}, local_metadata>, +>> TaskResult<{evs: ndarray<4, 32>, stds: ndarray<4, 32>}, local_metadata>, +>> TaskResult<{evs: ndarray<18>, stds: ndarray<18>}, local_metadata>, >> global_metadata >> ) ``` @@ -290,7 +290,7 @@ In particular, we propose this kind of Coercion for the types: * `BindingsArray` * `ObservablesArray` -### ResultBundles +### TaskResults The results from each `ObservablesTask` will be array valued, and each `ObservablesTask` in the same job may have a different shape. Consider an `ObservablesTask` with shape `<20, 30>`, where the shape has come from the broadcasting rules discussed elsewhere. @@ -299,7 +299,7 @@ Moreover, we will want to return an array of standard deviations of the same sha This would result in a bundle of broadcastable arrays: ```python -ResultBundle({“evs”: <20, 30>, “stds”: <20, 30>}, metadata) +TaskResult({“evs”: <20, 30>, “stds”: <20, 30>}, metadata) ``` The reason we are proposing a generic container for the return type instead of, e.g., an `Estimator`-specific container, is because it @@ -442,7 +442,7 @@ A consequence of switching to the concept of Tasks (mentioned in [Tasks](#tasks) ```python class BasePrimitive(ABC, Generic[T]): ... - def run(self, T | Iterable[T], **options) -> List[ResultBundle]: + def run(self, T | Iterable[T], **options) -> List[TaskResult]: ... ``` From e400b2484de6a74095a6caa4a13e963577878ab3 Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 13:20:32 -0400 Subject: [PATCH 30/51] Address Jim's comment --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 53da23f..e6dc869 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -71,7 +71,7 @@ circuit2 = QuantumCircuit(2) ... # specify 128 different parameter value sets for circuit1, in a 4x32 shape -parameter_values1 = np.random.uniform((4, 32, 2039)) +parameter_values1 = np.random.uniform(size=(4, 32, 2039)) # specify 4 observables to measure for circuit 1 observables1 = ["ZZIIIIIII", "IZXIIIIII", "IIIIIIYZI", "IZXIIYIII"] From 2db2433f88d2706c1dc21329864dd9dea5162ddc Mon Sep 17 00:00:00 2001 From: --get-all Date: Wed, 4 Oct 2023 15:13:14 -0400 Subject: [PATCH 31/51] Rename ObservablesTask -> EstimatorTask --- 0015-estimator-interface.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index e6dc869..989c9ec 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -115,14 +115,14 @@ ObservablesArrayLike = Union[ Iterable[str | Pauli | SparsePauliOp] ] -ObservableTaskLike = Union[ - ObservablesTask, +EstimatorTaskLike = Union[ + EstimatorTask, Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike] ] class EstimatorBase(ABC, Generic[T]): ... - def run(self, tasks: ObservableTaskLike | Iterable[ObservableTaskLike], **options) -> T: + def run(self, tasks: EstimatorTaskLike | Iterable[EstimatorTaskLike], **options) -> T: pass ``` @@ -144,7 +144,7 @@ This base class can be added to qiskit if and when needed as a non-breaking chan Most relevant, for the `Estimator` primitive, in order to satisfy the definition as stated above, we propose the task structure ```python -class ObservablesTask(BaseTask): +class EstimatorTask(BaseTask): observables: ObservablesArray = None parameter_values: BindingsArray = None, ``` @@ -286,14 +286,14 @@ def coerce(bindings_array: BindingsArrayLike) -> BindingsArray: ``` In particular, we propose this kind of Coercion for the types: -* `ObservablesTask` +* `EstimatorTask` * `BindingsArray` * `ObservablesArray` ### TaskResults -The results from each `ObservablesTask` will be array valued, and each `ObservablesTask` in the same job may have a different shape. -Consider an `ObservablesTask` with shape `<20, 30>`, where the shape has come from the broadcasting rules discussed elsewhere. +The results from each `EstimatorTask` will be array valued, and each `EstimatorTask` in the same job may have a different shape. +Consider an `EstimatorTask` with shape `<20, 30>`, where the shape has come from the broadcasting rules discussed elsewhere. This will result in a 20×30 array of real estimates. Moreover, we will want to return an array of standard deviations of the same shape. This would result in a bundle of broadcastable arrays: @@ -337,11 +337,11 @@ We propose the migration to using tasks in two phases. ### Deprecation Phase 1 In the first phase: -* Introduce `tasks: Sequence[ObservablesTask] | ObservablesTask` as the only positional argument, and make `circuits` a keyword argument. +* Introduce `tasks: Sequence[EstimatorTask] | EstimatorTask` as the only positional argument, and make `circuits` a keyword argument. * Coerce the arguments to account for the fact that the only existing positional argument is `circuit: Sequence[QuantumCircuit] | QuantumCircuit`. * Check that the user does not attempt to mix the old/new APIs. -* If necessary, coerce the old-API arguments (now all keyword arguments) into `ObservableTask`s, **raise deprecation warning**. -* Always eventually run with `Estimator._run(tasks: Sequence[ObservablesTask], **run_options)`. +* If necessary, coerce the old-API arguments (now all keyword arguments) into `EstimatorTask`s, **raise deprecation warning**. +* Always eventually run with `Estimator._run(tasks: Sequence[EstimatorTask], **run_options)`. Here is a mock implementation, with missing type annotations as above: ```python @@ -349,7 +349,7 @@ class Estimator(BasePrimitive): def run( self, - tasks: Sequence[ObservablesTask] | ObservablesTask, + tasks: Sequence[EstimatorTask] | EstimatorTask, circuits = None, observables = None, parameter_values = None, @@ -380,12 +380,12 @@ class Estimator(BasePrimitive): if isinstance(observables, (BaseOperator, PauliSumOp, str)): observables = [observables] - # Coerce old form into `ObservablesTask`s - tasks = [ObservablesTask.coerce(task) for task in zip(circuits, observables, parameter_values)] + # Coerce old form into `EstimatorTask`s + tasks = [EstimatorTask.coerce(task) for task in zip(circuits, observables, parameter_values)] warnings.warn("Deprecated API use") # Coerce into sequence form - if isinstance(tasks, ObservablesTasks): + if isinstance(tasks, EstimatorTasks): tasks = [tasks] # Run using only tasks @@ -401,11 +401,11 @@ class Estimator(BasePrimitive): def run( self, - tasks: Sequence[ObservablesTask] | ObservablesTask, + tasks: Sequence[EstimatorTask] | EstimatorTask, **run_options ): # Coerce into sequence form - if isinstance(tasks, ObservablesTasks): + if isinstance(tasks, EstimatorTasks): tasks = [tasks] # Run using only tasks @@ -414,7 +414,7 @@ class Estimator(BasePrimitive): ## Alternative Approaches -An alternative is to consider letting the `run()` method accept, effectively, only a single `ObservablesTask`: +An alternative is to consider letting the `run()` method accept, effectively, only a single `EstimatorTask`: ```python Estimator.run(cirucuit, parameter_values_array, observables_array) From 098ff133b9a4681fcf5aa012f00591cb1c7a3d8a Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 5 Oct 2023 09:11:35 -0400 Subject: [PATCH 32/51] Update 0015-estimator-interface.md Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 989c9ec..8fdd60e 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -377,7 +377,7 @@ class Estimator(BasePrimitive): parameter_values = itertools.repeat(None) if isinstance(circuit, QuantumCircuit): circuit = [circuit] - if isinstance(observables, (BaseOperator, PauliSumOp, str)): + if isinstance(observables, (BaseOperator, str)): observables = [observables] # Coerce old form into `EstimatorTask`s From dd40413db7521b4a0ce6797747bc928895ac97ed Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 5 Oct 2023 09:12:02 -0400 Subject: [PATCH 33/51] Update 0015-estimator-interface.md Co-authored-by: Luciano Bello --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 8fdd60e..22f2908 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -140,7 +140,7 @@ class BaseTask: circuit: QuantumCircuit ``` -This base class can be added to qiskit if and when needed as a non-breaking change, and has been included here mainly to make the concepts clear, rather than to indicate that we need such a base class immediately. +This base class can be added to Qiskit if and when needed as a non-breaking change, and has been included here mainly to make the concepts clear, rather than to indicate that we need such a base class immediately. Most relevant, for the `Estimator` primitive, in order to satisfy the definition as stated above, we propose the task structure ```python From f8eb69f41b2fe7da47a5ca3e0d11c3c69a58d44e Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 5 Oct 2023 09:12:15 -0400 Subject: [PATCH 34/51] Update 0015-estimator-interface.md Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- 0015-estimator-interface.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 22f2908..db7bfb7 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -375,8 +375,8 @@ class Estimator(BasePrimitive): # Coerce into old form if parameter_values is None: parameter_values = itertools.repeat(None) - if isinstance(circuit, QuantumCircuit): - circuit = [circuit] + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] if isinstance(observables, (BaseOperator, str)): observables = [observables] From 08af67778806f9f420e18c45a1166e366063ef48 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 5 Oct 2023 09:13:31 -0400 Subject: [PATCH 35/51] Update 0015-estimator-interface.md Co-authored-by: Luciano Bello --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index db7bfb7..9db9d00 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -189,7 +189,7 @@ With the `Estimator`, it is common for a user to want to estimate many observabl For example, all weight-1 and weight-2 Paulis that are adjacent on the connectivity graph. For a one-hundred qubit device, this corresponds to hundreds of unique estimates to be made for a single circuit, noting that for this particular example, on the heavy-hex graph, in the absence of mitigation, only 9 circuits need to be physically run. -We propose a new `ObservablesArray` object, where each element corresponds to a observable the user wants an estimated expectation value of. +We propose a new `ObservablesArray` object, where each element corresponds to an observable the user wants an estimated expectation value of. We propose that the internal data model of this object have an element format ```python From 889a9e036e58edd0767fa10d0acce7291862efd6 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 5 Oct 2023 09:26:32 -0400 Subject: [PATCH 36/51] Update 0015-estimator-interface.md Co-authored-by: Blake Johnson --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 9db9d00..31d8954 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -36,7 +36,7 @@ Here is a summary of pain-points with the current `Estimator.run()` interface: 1. _Clarity._ Without reading the documentation in detail, it's not obvious that the invocation `Estimator.run([circuit], observables=[obs1, obs2])` wouldn't cause `circuit` to be run with both supplied observables. In other words, that zipping _is always what's done to the `run()` arguments_ is common source of user confusion. Conversely, it's not clear that the way to collect all combinations of two observables and two parameter sets is to invoke `Estimator.run([circuit] * 4, observables=[obs1, obs2] * 2), parameter_values=[params1] * 2 + [params2] * 2`. -1. _Performance._ Given that `QuantumCircuit` hashing should be avoided and circuit equality checks can be expensive, we should move away from an interface that necessitates performant primitive implementations to go down this path at all. As one example, qubit-wise commuting observables like `"IZZ"` and `"XZI"` can both be estimated using the same simulation/execution, but only if the estimator understands that they share a base circuit. As a second example, when the circuits need to be seriazed before they are simulated/executed (e.g. runtime or pickling-for-multi-processing), it puts the onus on the primitive implementation to detect circuit duplications. +1. _Performance._ Given that `QuantumCircuit` hashing should be avoided and circuit equality checks can be expensive, we should move away from an interface that necessitates performant primitive implementations to go down this path at all. As one example, qubit-wise commuting observables like `"IZZ"` and `"XZI"` can both be estimated using the same simulation/execution, but only if the estimator understands that they share a base circuit. As a second example, when the circuits need to be serialized before they are simulated/executed (e.g. runtime or pickling-for-multi-processing), it puts the onus on the primitive implementation to detect circuit duplications. Here is why the [Detailed Design](#detailed-design) section suggests "transposing" the signature and using array-based arguments with broadcasting: From 20859ef5c54298c0fa3474749c1f69b72beca685 Mon Sep 17 00:00:00 2001 From: --get-all Date: Fri, 6 Oct 2023 10:42:37 -0400 Subject: [PATCH 37/51] fix typos in broadcasting examples --- 0015-estimator-interface.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 31d8954..c14ba08 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -225,33 +225,32 @@ parameter_values = np.array(1.0) parameter_values.shape == () observables = ObservablesArray(["III", "XXX", Pauli("YYY"), "ZZZ", Pauli("XYZ")]) observables.shape == (5,) ->> result_bundle.shape == (5,) +>> task_result.shape == (5,) # Inner/Zip parameter_values = BindingsArray(np.random.uniform(size=(5,))) parameter_values.shape == (5,) -observables = ObservablesArray([[Pauli("III")], [Pauli("XXX")], [Pauli("YYY")], [Pauli("ZZZ")], [Pauli("XYZ")]]) -observables.shape == (5,1) ->> result_bundle.shape == (5,) +observables = ObservablesArray([Pauli("III"), Pauli("XXX"), Pauli("YYY"), Pauli("ZZZ"), Pauli("XYZ")]) +observables.shape == (5,) +>> task_result.shape == (5,) # Outer/Product -parameter_values = BindingsArray(np.random.uniform(size=(1,6))) -parameter_values.shape == (1,6) -observables = ObservablesArray([[Pauli("III"), Pauli("XXX"), Pauli("YYY"), Pauli("ZZZ"), Pauli("XYZ"), Pauli("IIZ")]]) -observables.shape == (4,1) ->> result_bundle.shape == (4,6) +parameter_values = BindingsArray(np.random.uniform(size=(1, 6))) +parameter_values.shape == (1, 6) +observables = ObservablesArray([[Pauli("III")], [Pauli("XXX")], [Pauli("YYY")], [Pauli("ZZZ")]]) +observables.shape == (4, 1) +>> task_result.shape == (4,6) # Standard nd generalization -parameter_values = BindingsArray(np.random.uniform(size=(3,6))) -parameter_values.shape == (3,6) +parameter_values = BindingsArray(np.random.uniform(size=(3, 6))) +parameter_values.shape == (3, 6) observables = ObservablesArray([ [[Pauli(...), Pauli(...)]], [[Pauli(...), Pauli(...)]], [[Pauli(...), Pauli(...)]] ]) -observables.shape == (3,1,2) ->> result_bundle.shape == (3,2,6) - +observables.shape == (3, 1, 2) +>> task_result.shape == (3, 2, 6) ``` From 0ed249763309c02ed0b9587a9072ea2eabffd888 Mon Sep 17 00:00:00 2001 From: --get-all Date: Fri, 6 Oct 2023 10:49:56 -0400 Subject: [PATCH 38/51] Address Blake's comment about exponential memory overhead for BaseOperator --- 0015-estimator-interface.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index c14ba08..03d018e 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -204,7 +204,8 @@ where, for example, the observable `0.5 * IIZY + 0.2 * XIII` would be stored as This is proposed instead of `{"IIZY": 0.5, "XIII": 0.2}` anticipating the overhead of storing and working with so many `"I"`'s for large devices. -Note that the above is simply the data model; the `ObservablesArray` object, either in the constructor or special static coercion method, will accept the `quantum_info` objects such as `Pauli` and `SparsePauliOp` that they are used to. +Note that the above is simply the data model; the `ObservablesArray` object, either in the constructor or special static coercion method, will accept the `quantum_info` objects that are convenient to users. +However, only types such as `Pauli` and `SparsePauliOp` that are known to have sparse Pauli representations will be allowed; we will drop general support for `BaseOperator`. Further, through [type coersion](#type-coercion-strategy) in `Estimator.run()` itself, a user won't need to manually construct an `ObservablesArray`, as shown in the example in the [Design Proposal](#design-proposal) section. It is up to each `Estimator` implementation to, if it sees fit, to solve a graph coloring problem that decides how to produce a sufficient set of physical circuits that are capable of producing data to make each of these estimates. From 66a90efe0c857292144c9a595195ba8ac1479d2f Mon Sep 17 00:00:00 2001 From: --get-all Date: Thu, 12 Oct 2023 10:22:15 -0400 Subject: [PATCH 39/51] Added Migration Examples section This is following a request to see some simpler examples of what the new interface would look like. --- 0015-estimator-interface.md | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 03d018e..dfc5f14 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -126,6 +126,51 @@ class EstimatorBase(ABC, Generic[T]): pass ``` +### Migration Examples + +Technical details of migration are discussed in the [Migration Path](#migration-path) section, but it is perhaps worthwhile to show some examples that demonstrate that, despite the new array and broadcasting abilities, the interface is not overly complicated for simple examples. + +One circuit, many observables: + +```python + # old API + estimator.run([circuit] * 4, observables=["ZIII", "IZII", "IIZI", "IIIZ"]).result() + >> EstimatorResult(np.array([0.1, 0.2, 0.3, 0.4]), metadata={...}) + + # new API (we can use a single task) + estimator.run((circuit, ["ZIII", "IZII", "IIZI", "IIIZ"])).result() + >> [ResultTask({"evs": np.array([0.1, 0.2, 0.3, 0.4]), "stds": <...>}, metadata={...})] +``` + +One circuit, one observable, many parameter value sets: + +```python + # old API + estimator.run([circuit] * 3, observables=["XYZI"] * 3, parameter_values=[[1,2,3], [3,4,5], [8,7,6]]).result() + >> EstimatorResult(np.array([0.1, 0.2, 0.3]), metadata={...}) + + # new API (we can use a single task) + estimator.run((circuit, ["XYZI"], [[1,2,3], [3,4,5], [8,7,6]])).result() + >> [ResultTask({"evs": np.array([0.1, 0.2, 0.3]), "stds": <...>}, metadata={...})] +``` + +Three different circuits, three different observables: + +```python + # old API + estimator.run([circuit1, circuit2, circuit3], observables=["ZIII", "IZII", "IIZI"]).result() + >> EstimatorResult(np.array([0.1, 0.2, 0.3]), metadata={...}) + + # new API (unique circuits always need their own tasks) + estimator.run([(circuit1, "ZIII"), (circuit2, "IZII"), (circuit3, "IIZI")]).result() + >> [ + >> ResultTask({"evs": np.array(0.1), "stds": <...>}, metadata={...}), + >> ResultTask({"evs": np.array(0.2), "stds": <...>}, metadata={...}), + >> ResultTask({"evs": np.array(0.3), "stds": <...>}, metadata={...}) + >> ] +``` + + ## Detailed Design ### Tasks From b366ed300a34594321d00967fe0cb1118a3db94d Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 12 Oct 2023 12:15:10 -0400 Subject: [PATCH 40/51] Update 0015-estimator-interface.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index dfc5f14..88c0ca7 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -76,7 +76,7 @@ parameter_values1 = np.random.uniform(size=(4, 32, 2039)) observables1 = ["ZZIIIIIII", "IZXIIIIII", "IIIIIIYZI", "IZXIIYIII"] # specify 18 observables to measure for circuit2 -observables2 = [Pauili("XYZIIII"), ..., SparsePauliOp({"YYIIIII": 0.2})] +observables2 = [Pauli("XYZIIII"), ..., SparsePauliOp({"YYIIIII": 0.2})] # invoke an estimator estimator = Estimator() From f12c5cafaa24431b8cf43f6aa588b2ff7b428100 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Fri, 13 Oct 2023 12:02:58 -0400 Subject: [PATCH 41/51] Update 0015-estimator-interface.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 88c0ca7..3b4b83a 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -462,7 +462,7 @@ class Estimator(BasePrimitive): An alternative is to consider letting the `run()` method accept, effectively, only a single `EstimatorTask`: ```python -Estimator.run(cirucuit, parameter_values_array, observables_array) +Estimator.run(circuit, parameter_values_array, observables_array) ``` This has the advantage of a simpler interface, where multiple tasks could be run From 88a8271f9533f897e7f97c4c1de7464c24c1e3d9 Mon Sep 17 00:00:00 2001 From: --get-all Date: Thu, 19 Oct 2023 09:58:28 -0400 Subject: [PATCH 42/51] Fix shapes --- 0015-estimator-interface.md | 16 +++++++--------- 0015-estimator-interface/broadcasting.svg | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 3b4b83a..90af33b 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -10,7 +10,6 @@ | | Takashi Imamichi (imamichi@jp.ibm.com) | | | Blake Johnson (blake.johnson@ibm.com) | | | Chris Wood (cjwood@us.ibm.com) | -| **Deprecates** | RFC that this RFC deprecates | | **Submitted** | 2023-10-03 | | **Updated** | YYYY-MM-DD | @@ -70,8 +69,8 @@ circuit1 = QuantumCircuit(9) circuit2 = QuantumCircuit(2) ... -# specify 128 different parameter value sets for circuit1, in a 4x32 shape -parameter_values1 = np.random.uniform(size=(4, 32, 2039)) +# specify 128 different parameter value sets for circuit1, in a 32x4 shape +parameter_values1 = np.random.uniform(size=(32, 4, 2039)) # specify 4 observables to measure for circuit 1 observables1 = ["ZZIIIIIII", "IZXIIIIII", "IIIIIIYZI", "IZXIIYIII"] @@ -90,7 +89,7 @@ job.result() # get a bundle of results for every task >> EstimatorResult( ->> TaskResult<{evs: ndarray<4, 32>, stds: ndarray<4, 32>}, local_metadata>, +>> TaskResult<{evs: ndarray<32, 4>, stds: ndarray<32, 4>}, local_metadata>, >> TaskResult<{evs: ndarray<18>, stds: ndarray<18>}, local_metadata>, >> global_metadata >> ) @@ -291,12 +290,11 @@ observables.shape == (4, 1) parameter_values = BindingsArray(np.random.uniform(size=(3, 6))) parameter_values.shape == (3, 6) observables = ObservablesArray([ - [[Pauli(...), Pauli(...)]], - [[Pauli(...), Pauli(...)]], - [[Pauli(...), Pauli(...)]] + [[Pauli(...)], [Pauli(...)], [Pauli(...)]]], + [[Pauli(...)], [Pauli(...)], [Pauli(...)]]], ]) -observables.shape == (3, 1, 2) ->> task_result.shape == (3, 2, 6) +observables.shape == (2, 3, 1) +>> task_result.shape == (3, 6, 2) ``` diff --git a/0015-estimator-interface/broadcasting.svg b/0015-estimator-interface/broadcasting.svg index 247165a..062be93 100644 --- a/0015-estimator-interface/broadcasting.svg +++ b/0015-estimator-interface/broadcasting.svg @@ -25,12 +25,12 @@ inkscape:document-units="mm" showgrid="false" inkscape:zoom="1.0431548" - inkscape:cx="304.84449" - inkscape:cy="294.29955" + inkscape:cx="304.8445" + inkscape:cy="294.29956" inkscape:window-width="2245" inkscape:window-height="1376" - inkscape:window-x="323" - inkscape:window-y="106" + inkscape:window-x="1850" + inkscape:window-y="25" inkscape:window-maximized="0" inkscape:current-layer="layer1" /> + y="90.212502" /> (3,6) (3,1,2) + x="82.194702" + y="149.74498">(2,3,1) (3,6,2) + y="149.74498">(2,3,6) Date: Thu, 19 Oct 2023 13:06:08 -0400 Subject: [PATCH 43/51] Update 0015-estimator-interface.md Co-authored-by: Luciano Bello --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 90af33b..66cdf65 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -272,7 +272,7 @@ observables = ObservablesArray(["III", "XXX", Pauli("YYY"), "ZZZ", Pauli("XYZ")] observables.shape == (5,) >> task_result.shape == (5,) -# Inner/Zip +# Zip parameter_values = BindingsArray(np.random.uniform(size=(5,))) parameter_values.shape == (5,) observables = ObservablesArray([Pauli("III"), Pauli("XXX"), Pauli("YYY"), Pauli("ZZZ"), Pauli("XYZ")]) From a070e27938a56f380f7fb55460c52aeb29470c64 Mon Sep 17 00:00:00 2001 From: --get-all Date: Thu, 19 Oct 2023 13:10:03 -0400 Subject: [PATCH 44/51] Removed 'Inner' from figure --- 0015-estimator-interface/broadcasting.svg | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/0015-estimator-interface/broadcasting.svg b/0015-estimator-interface/broadcasting.svg index 062be93..b150ddd 100644 --- a/0015-estimator-interface/broadcasting.svg +++ b/0015-estimator-interface/broadcasting.svg @@ -25,11 +25,11 @@ inkscape:document-units="mm" showgrid="false" inkscape:zoom="1.0431548" - inkscape:cx="304.8445" + inkscape:cx="307.24107" inkscape:cy="294.29956" - inkscape:window-width="2245" + inkscape:window-width="1872" inkscape:window-height="1376" - inkscape:window-x="1850" + inkscape:window-x="1816" inkscape:window-y="25" inkscape:window-maximized="0" inkscape:current-layer="layer1" /> @@ -1620,14 +1620,14 @@ Inner/Zip + x="-10.478675" + y="70.426208">Zip Date: Wed, 25 Oct 2023 08:48:27 -0400 Subject: [PATCH 45/51] Update 0015-estimator-interface.md Co-authored-by: Jim Garrison --- 0015-estimator-interface.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 66cdf65..edc2ad9 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -219,10 +219,10 @@ BindingsArray(<50, 3>) BindingsArray([<50>, <50>, <50>]) # include parameter names with the arrays, where parameters can be grouped together in tuples, or supplied separately -BindingsArray(kwargs={(a, c): <50, 2>, b: <50>}) +BindingsArray(kwargs={(Parameter("a"), Parameter("c")): <50, 2>, Parameter("b"): <50>}) # “args” and “kwargs” can be mixed -BindingsArray(<50, 2>, {c: <50>}) +BindingsArray(<50, 2>, {Parameter("c"): <50>}) ``` We expect the formal primitive API and primitive implementations to have a strong sense of `BindingsArray`, but we will not demand that users construct them manually because we do not wish to overburden them with types, and we need to remain backwards compatible. This is discussed further in the [Type Coercion Strategy](#type-coercion-strategy) and [Migration Path](#migration-path) sections. From 114cb47a52d693c92a412b97d7d05a8b7acf861f Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 26 Oct 2023 16:30:25 -0400 Subject: [PATCH 46/51] Update 0015-estimator-interface.md Co-authored-by: John Lapeyre --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index edc2ad9..46fe789 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -97,7 +97,7 @@ job.result() The above example shows various features of the design, including combining large numbers of observables with large numbers of parameter value sets for a single circuit. -The detailed targetted signature, following the [deprecation period](#migration-path), +The detailed targeted signature, following the [deprecation period](#migration-path), is as follows: ```python From 83aac05009a1a0c13edf1812fe6082e3d11cf275 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 26 Oct 2023 21:35:35 -0400 Subject: [PATCH 47/51] Update 0015-estimator-interface.md Co-authored-by: Toshinari Itoko <15028342+itoko@users.noreply.github.com> --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 46fe789..86db1c3 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -138,7 +138,7 @@ One circuit, many observables: # new API (we can use a single task) estimator.run((circuit, ["ZIII", "IZII", "IIZI", "IIIZ"])).result() - >> [ResultTask({"evs": np.array([0.1, 0.2, 0.3, 0.4]), "stds": <...>}, metadata={...})] + >> [TaskResult({"evs": np.array([0.1, 0.2, 0.3, 0.4]), "stds": <...>}, metadata={...})] ``` One circuit, one observable, many parameter value sets: From 17e18aeb2ea3eb5cc073aebfb1517d9e9844d2c7 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 26 Oct 2023 21:35:44 -0400 Subject: [PATCH 48/51] Update 0015-estimator-interface.md Co-authored-by: Toshinari Itoko <15028342+itoko@users.noreply.github.com> --- 0015-estimator-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 86db1c3..fe83b0d 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -150,7 +150,7 @@ One circuit, one observable, many parameter value sets: # new API (we can use a single task) estimator.run((circuit, ["XYZI"], [[1,2,3], [3,4,5], [8,7,6]])).result() - >> [ResultTask({"evs": np.array([0.1, 0.2, 0.3]), "stds": <...>}, metadata={...})] + >> [TaskResult({"evs": np.array([0.1, 0.2, 0.3]), "stds": <...>}, metadata={...})] ``` Three different circuits, three different observables: From 8119e872e2a978d8147aef1475a1b4ca05a5c69d Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 26 Oct 2023 21:35:50 -0400 Subject: [PATCH 49/51] Update 0015-estimator-interface.md Co-authored-by: Toshinari Itoko <15028342+itoko@users.noreply.github.com> --- 0015-estimator-interface.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index fe83b0d..78bad99 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -163,9 +163,9 @@ Three different circuits, three different observables: # new API (unique circuits always need their own tasks) estimator.run([(circuit1, "ZIII"), (circuit2, "IZII"), (circuit3, "IIZI")]).result() >> [ - >> ResultTask({"evs": np.array(0.1), "stds": <...>}, metadata={...}), - >> ResultTask({"evs": np.array(0.2), "stds": <...>}, metadata={...}), - >> ResultTask({"evs": np.array(0.3), "stds": <...>}, metadata={...}) + >> TaskResult({"evs": np.array(0.1), "stds": <...>}, metadata={...}), + >> TaskResult({"evs": np.array(0.2), "stds": <...>}, metadata={...}), + >> TaskResult({"evs": np.array(0.3), "stds": <...>}, metadata={...}) >> ] ``` From 22ec24bee56be054af14afa2545a8e47a849dc30 Mon Sep 17 00:00:00 2001 From: --get-all Date: Thu, 26 Oct 2023 21:59:16 -0400 Subject: [PATCH 50/51] change migration path to versioning --- 0015-estimator-interface.md | 103 ++---------------------------------- 1 file changed, 5 insertions(+), 98 deletions(-) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 78bad99..7283976 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -10,6 +10,7 @@ | | Takashi Imamichi (imamichi@jp.ibm.com) | | | Blake Johnson (blake.johnson@ibm.com) | | | Chris Wood (cjwood@us.ibm.com) | +| | Jessie Yu (jessieyu@us.ibm.com) | | **Submitted** | 2023-10-03 | | **Updated** | YYYY-MM-DD | @@ -97,8 +98,7 @@ job.result() The above example shows various features of the design, including combining large numbers of observables with large numbers of parameter value sets for a single circuit. -The detailed targeted signature, following the [deprecation period](#migration-path), -is as follows: +The detailed targeted signature is as follows: ```python T = TypeVar("T", bound=Job) @@ -357,103 +357,10 @@ The reason we are proposing a generic container for the return type instead of, We need to remain backwards compatible with the existing interface to adhere to the [Qiskit Deprecation Policy](https://qiskit.org/documentation/deprecation_policy.html). -In summary, we propose the following strategy: If the user has provided no `TaskLike`s, proceed with the old API, the old API output, and emit a deprecation warning, or an error if something mandatory like `observables` has been omitted. Otherwise, proceed with the new API, raising if they have tried to use the old arguments in addition to providing tasks. +In summary, we propose the following strategy: explicitly version the `Estimator` interface (i.e. add a `BaseEstimatorV2` as described in this proposal) and document both the differences and the end user migration path for consumers of estimators. The user can determine which version of the estimator to use, and existing implementations can be left intact. -We now describe the strategy in detail, beginning with a restatement of the current interface, where `Estimator.run` has `circuits` as the only positional argument and accepts `observables` and `parameter_values` as keyword arguments (in addition to the var keyword `**run_options` arguments which isn't affected): - -```python -class Estimator(BasePrimitive): - - # current signature - def run( - self, - circuits: Sequence[QuantumCircuit] | QuantumCircuit, - observables: Sequence[BaseOperator | PauliSumOp | str] | BaseOperator | PauliSumOp | str, - parameter_values: Sequence[Sequence[float]] | Sequence[float] | float | None = None, - **run_options, - ): - ... -``` - -We propose the migration to using tasks in two phases. - -### Deprecation Phase 1 - -In the first phase: -* Introduce `tasks: Sequence[EstimatorTask] | EstimatorTask` as the only positional argument, and make `circuits` a keyword argument. -* Coerce the arguments to account for the fact that the only existing positional argument is `circuit: Sequence[QuantumCircuit] | QuantumCircuit`. -* Check that the user does not attempt to mix the old/new APIs. -* If necessary, coerce the old-API arguments (now all keyword arguments) into `EstimatorTask`s, **raise deprecation warning**. -* Always eventually run with `Estimator._run(tasks: Sequence[EstimatorTask], **run_options)`. - -Here is a mock implementation, with missing type annotations as above: -```python -class Estimator(BasePrimitive): - - def run( - self, - tasks: Sequence[EstimatorTask] | EstimatorTask, - circuits = None, - observables = None, - parameter_values = None, - **run_options - ): - # Coerce into standard form to later allow for `tasks` to be the only non-kwarg. - if isinstance(tasks, QuantumCircuit) or all(isinstance(task, QuantumCircuit) for task in tasks): - if circuits is not None: - raise ValueError("Cannot mix old and new APIs") - circuits = tasks - tasks = None - - # Do not mix new/old API - if tasks and (circuits or observables or parameter_values): - raise ValueError("Cannot mix old and new APIs") - - # Deprecated path - if tasks is None: - # Verify values in old API - if not (circuits and observables): - raise ValueError(f"Need both circuits and observables for old API, got circuits={circuits}, observables={observables}") - - # Coerce into old form - if parameter_values is None: - parameter_values = itertools.repeat(None) - if isinstance(circuits, QuantumCircuit): - circuits = [circuits] - if isinstance(observables, (BaseOperator, str)): - observables = [observables] - - # Coerce old form into `EstimatorTask`s - tasks = [EstimatorTask.coerce(task) for task in zip(circuits, observables, parameter_values)] - warnings.warn("Deprecated API use") - - # Coerce into sequence form - if isinstance(tasks, EstimatorTasks): - tasks = [tasks] - - # Run using only tasks - return self._run(tasks, **run_options) -``` - -### Deprecation Phase 2 - -After the deprecation period, remove extraneous arguments and checks: - -```python -class Estimator(BasePrimitive): - - def run( - self, - tasks: Sequence[EstimatorTask] | EstimatorTask, - **run_options - ): - # Coerce into sequence form - if isinstance(tasks, EstimatorTasks): - tasks = [tasks] - - # Run using only tasks - return self._run(tasks, **run_options) -``` +Similar to `Backend`, we will have a `BaseEstimator`, `BaseEstimatorV1` equal to the existing `BaseEstimator`, and `BaseEstimatorV2`, as described in this proposal. +We will maintain backward compatibility by defaulting the import of `Estimator` to `EstimatorV1` (i.e. existing) for the duration of the deprecation period. ## Alternative Approaches From 4ad2e9006b167652ae52040e51af61a2809fe789 Mon Sep 17 00:00:00 2001 From: --get-all Date: Thu, 26 Oct 2023 22:14:30 -0400 Subject: [PATCH 51/51] clarify output type --- 0015-estimator-interface.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/0015-estimator-interface.md b/0015-estimator-interface.md index 7283976..bce4cff 100644 --- a/0015-estimator-interface.md +++ b/0015-estimator-interface.md @@ -98,6 +98,13 @@ job.result() The above example shows various features of the design, including combining large numbers of observables with large numbers of parameter value sets for a single circuit. +The result types in the example above are illustrative of what we propose, but do not necessarily correspond to-the-letter what will be introduced; we feel it is better to make certain detailed design choices through code review, and use this RFC to focus on concepts and structures. +However, we do want to make three points about the result type clear: + + 1. We propose that the errors appear in the data portion of the result, not the metadata, to be treated on equal footing to the estimates themselves. + 2. We propose that the error portion of the data should represent error bars on the expectation values, somehow. That is, standard error instead of standard deviation or variance, or possibly, a two-sided confidence interval. + 3. The previous bullet might make it seem like we are proposing to leave it up to an implemtation to choose which quantity to represent. We are not. We propose a homogenous choice for all implementations, documented in the base class. This can obviously not be enforced through typing, so it will have to be enforced through documentation, vigilence, and to some extent, testing. + The detailed targeted signature is as follows: ```python