Skip to content

Latest commit

 

History

History
648 lines (448 loc) · 40.3 KB

0003-Aqua_0.7_operator_redesign.md

File metadata and controls

648 lines (448 loc) · 40.3 KB

Aqua 0.7 Operator Redesign

Status Deprecated
RFC # 0003
Authors Donny Greenberg (donny@ibm.com)
Deprecates NA
Submitted 2020-01-17
Updated 2023-05-10

Purpose

To improve the transparency, ease of understanding, and programming power of Aqua’s operator logic and usage. Specifically, to reconcile with the Terra operator hierarchy and make the Aqua algorithmic flow more visible, explicit, and extensible.

Throughout this doc, we rely on definitions of Operators roughly derived from the first chapter of John Watrous's "The Theory of Quantum Information," with a focus on Square Operators over binary alphabets.

Items which are intended for the March 17th 2020 Aqua release have been labeled [0.7].

Background: Motivation and Opportunities

The representation of matrices sparsely as linear combinations of Pauli operators is critical in many quantum algorithms. As such, the Operator classes are the workhorses of Aqua today (0.6.2), containing both the expectation value and evolution logic used by most of its algorithms.

However, there are several opportunities for improvement:

  • Basic Construction & Rapid Protoyping: Aqua's Operators were initially built as procedural infrastructure rather than first-class programming primitives. Improvements to syntax and interfaces can enable the succinctness and power typical of mathematical Operator language
  • Separation of Operator Math and Operator Algorithms
    • Ease of understanding: The "Operator algorithm" logic - the ExpectationValue, Evolution, grouping, and symmetry analysis - is mostly spread across the 3000-line operator hierarchy, and is very branchy for different modes of execution
    • Ease of extension: Modification to the expectation value, evolution, grouping, and symmetry logic is a core use case (e.g. the CVaR expectation, linear combination evolution, or the many recent papers on Pauli grouping), but not explicitly supported today
  • Smooth Borders with Broader Qiskit
    • Terra's quantum_info module also supports operator math, but is mostly matrix-based
    • Remote Operator Algorithms: Aer's fast ExpectationValue is not transparently or cleanly interchangeable with Aqua's local ExpectationValue today. The concept of an Algorithm not provided by Aqua is not yet defined to support this type of interchangeability cleanly

Present State of Operators in Qiskit

Both Aqua and Terra include suites of modules to support Operator math, but do so very differently.

  • Aqua
    • Operators are focused primarily on the procedural requirements of algorithmic execution
      • Modules are very large and include hundreds of lines of procedural algorithm code
      • Interfaces were not initial built for end-user usage as a programming primitive, and are therefore wordy and difficult for users to understand
      • Syntax is not built for rapid prototyping and lacks syntactic power of mathematical Operator language
    • Primarily focused on Pauli-basis Operators
      • WeightedPauli - $2^n\times 2^n$ Operators sparsely represented as complex combination of Paulis
      • MatrixOperator in the standard basis with $2^n\times 2^n$ elements was initially built for performance improvements which are no longer relevant
    • Only dependency on Terra is through Pauli module, but this is largely symbolic (not an inexorable component)
  • Terra
    • Operator math is mostly built around QCVV (Quantum Characterization Verification & Validation) and open Quantum systems modelling use cases
      • Support for Channel, Choi, Superoperator, Kraus, etc.
    • Operators are largely matrix-based and therefore do not support the Pauli-basis operations necessary to non-exponentially execute quantum algorithms
    • Used by:
      • Aqua, 29 dependencies - Only Pauli module
      • Aer, 10 dependencies
      • Ignis, 2 dependencies
  • Ignis includes a clifford.py module somewhat specific to characterization needs.

Aqua Present Usage (0.6.2)

Within Aqua, the primary uses of Operators are:

  • Qubit Observable (Hamiltonian, Cost Function, etc.) Construction
    • Used as sparse representations of large observables when constructing problems in Chemistry, Physics, Optimization, and Finance today
    • Also often a translation step between domain-specific problems and Quantum hardware-addressable equivalents
  • ExpectationValues
    • Primarily used in VQE (and derivatives QAOA, UCCSD, etc.) as a device-executable cost function of the ansatz state
    • Expectation values can only be taken of Operators in the Pauli basis on Quantum hardware
    • Also present in the "Evolution of Hamiltonian" algorithm, which is simply state evolution by one operator followed by an expectation value by another operator
  • State Evolution
    • Used in QPE (and derivatives HHL, iQPE, etc.) as a Quantum circuit-representable matrix exponentiation
    • Used in UCCSD and QAOA ansatze and EOH algorithm as representation of system dynamics to simulate time evolution of a system on quantum hardware
    • Evolution can only be taken by Operators in the Pauli basis on Quantum hardware

Other Important Aqua Operator Features

  • Grouping - Grouping is a technique to reduce the number of circuit evaluations required to compute an ExpectationValue based on mutually commuting Paulis in the Operator decomposition.
  • Tapering - Tapering is a technique to remove qubits from a Hamiltonian of interest by identifying Z2 symmetries in the Hamiltonian.
  • Gradients - Many variational algorithms are improved dramatically when exact gradients of gate parameters with respect to the cost function observable are computed analytically rather than numerically. Aqua can compute these gradients and provide them to the optimizer directly.

Aqua Present (0.6.2) Operator Object Model and Hierarchy

Aqua's Operators are organized as follows:

  • qiskit.aqua.operators
    • base_operator.py: BaseOperator(ABC)
    • matrix_operator.py: MatrixOperator(BaseOperator)
    • weighted_pauli_operator.py: WeightedPauliOperator(BaseOperator), and Z2Symmetries
    • tpb_grouped_weighted_pauli_operator.py: TPBGroupedWeightedPauliOperator(WeightedPauliOperator), essentially a wrapper around WeightedPauliOperator for backward compatibility.
    • pauli_graph: PauliGraph
    • op_converter.py: to_weighted_pauli_operator(operator), to_matrix_operator(operator), to_tpb_grouped_weighted_pauli_operator(operator, grouping_func, **kwargs)
    • common.py: Utility functions, inc. evolution_instruction, pauli_measurement(circuit, pauli, qr, cr, barrier=False), measure_pauli_z(data, pauli), covariance(data, pauli_1, pauli_2, avg_1, avg_2), etc.
  • qiskit.chemistry - OUT OF SCOPE OF THIS DOC
    • fermionic_operator.py: FermionicOperator, contains jordan_wigner, parity, bravyi_kitaev Fermion-to-qubit operator mappings.
    • bksf.py: Another mapping
    • .core
      • chemistry_operator.py: ChemistryOperator(ABC)
      • hamiltonian.py: Hamiltonian(ChemistryOperator)

Terra Present (0.11.0) Operator Object Model and Hierarchy

Terra's Operators are organized as follows:

  • qiskit.quantum_info
    • .operators
      • base_operator.py, pauli.py, operator.py (matrix operator), measures.py (process_fidelity), predicates.py (is_unitary_matrix, is_hermitian_matrix, matrix_equal, etc.), quaternion.py
      • .channel
        • quantum_channel.py (base), chi.py, choi.py, kraus.py, ptm.py, stinespring.py, superop.py, transformations.py
    • .states
      • quantum_state.py (base), densitymatrix.py, statevector.py, measures.py (state_fidelity), states.py (basis_state, projector, purity)
    • .analysis
      • average.py - ExpectationValue of diagonal operator
      • make_observable.py - Convert an observable in matrix form to dictionary form

WeightedPauliOperator Not Available in Terra

Terra does not contain any of the logic for working in the Pauli-basis implemented in Aqua today, and is not interoptable with Aqua's operator algorithms. As such, these utilities are only accessible to Aqua users.

Operator Construction and Manipulation Present State

The center of Qiskit's algorithmic Operator logic is the WeightedPauli, being the only non-exponential scaling operator basis available today (the only other being the standard basis).

Qiskit supports several methods of WeightedPauli operator construction, none of which are self explanatory to a new user:

# from qiskit.quantum_info.operators import WeightedPauliOperator
from qiskit.aqua.operators import WeightedPauliOperator, MatrixOperator, op_converter
from qiskit.quantum_info.operators import Pauli
pauli_op = WeightedPauliOperator([
    [.5, Pauli.from_label('IX')],
    [.2, Pauli.from_label('ZY')],
    [.1j, Pauli.from_label('ZZ')],
])
pauli_op = WeightedPauliOperator.from_list(
    paulis=[Pauli.from_label('IX'),
            Pauli.from_label('ZY'),
            Pauli.from_label('ZZ')],
    weights=[.5, .2, .1j])
mat = [[0. +0.1j, 0.5-0.2j, 0. +0.j , 0. +0.j ],
       [0.5+0.2j, 0. -0.1j, 0. +0.j , 0. +0.j ],
       [0. +0.j , 0. +0.j , 0. -0.1j, 0.5+0.2j],
       [0. +0.j , 0. +0.j , 0.5-0.2j, 0. +0.1j]]
mat_op = MatrixOperator(mat)
pauli_op_from_mat = op_converter.to_weighted_pauli_operator(mat_op)
pauli_op == pauli_op_from_mat
True

Classical matrices can be exported for classical usage, again if the user already knows the Operator hierarchy somewhat well:

op_converter.to_matrix_operator(pauli_op).matrix.toarray()
array([[0. +0.1j, 0.5-0.2j, 0. +0.j , 0. +0.j ],
       [0.5+0.2j, 0. -0.1j, 0. +0.j , 0. +0.j ],
       [0. +0.j , 0. +0.j , 0. -0.1j, 0.5+0.2j],
       [0. +0.j , 0. +0.j , 0.5-0.2j, 0. +0.1j]])

Composition uses the * operator, while Terra's operators and Python use @.

3*pauli_op + .2j*pauli_op == (3+.2j)*pauli_op
True
print((pauli_op * pauli_op).print_details())
II	(0.28+0j)
ZZ	0j
ZY	0j
IX	0j

Aqua's ExpectationValue is Procedural and Inextensible

Aqua's ExpectationValue is not contained within a single function or module, but rather split into several functions without a clear interface or flow for user usage. This is due to structural constraints in Aqua which are no longer present, where the algorithm requiring the expectation value held the backend object and could run circuits, but the operator could not. We encourage the reader to scan lines 361-395 of Aqua 6.1 VQE’s ExpectationValue calculation to try to understand where and how the expectation is computed. We’ve been asked by numerous Aqua users to explain how this code works, and most do not attempt to use it on their own.

The following is the shortest possible way to write an expectation value in Aqua. Note that it fundamentally requires the user to understand a certain execution flow, the correct functions to use to do this, and how those functions work with their execution mode. This takes a few hours to understand at least, often days. Further, there are no hints that a change from the Z basis for each Pauli is being performed here, or matrix multiplication if the system chooses to do that instead.

from qiskit.aqua.operators import WeightedPauliOperator
from qiskit.aqua.components.variational_forms import RY
from qiskit.quantum_info import Pauli
from qiskit import BasicAer, execute, QuantumCircuit
from qiskit.circuit import Parameter
qasm_sim = BasicAer.get_backend('qasm_simulator')
op = WeightedPauliOperator([
    [.5, Pauli.from_label('IX')],
    [.2j, Pauli.from_label('ZY')],
])
circuit = QuantumCircuit(2)
circuit.h([0,1])

evaluation_circuits = op.construct_evaluation_circuit(wave_function=circuit, statevector_mode=False)
result = execute(evaluation_circuits, qasm_sim).result()
expect, std = op.evaluate_with_result(result=result, statevector_mode=False)
expect
(0.5+0.005078125j)

Alternative Expectation Values and the Aer Expectation Value

Because the ExpectationValue logic is embedded directly in the Operator, modifications to the ExpectationValue (e.g. CVaR) are impossible without editing the Operator directly with heavy branching or duplicating the entire Operator. This branching is already in effect within Aqua, automatically choosing between several execution modes mostly opaquely to the user. This is also the case for grouping, evolution, and symmetry logic.

The most dramatic example of this is the Aer-provided fast ExpectationValue simulation, which is so buried into the Operator it is effectively a super-superuser feature today. It was introduced quickly to achieve critical performance gains, but must be formalized to become a true first-class feature.

  • In Aqua, there is no simple way to specify which ExpectationValue algorithm the user wants, Aer or otherwise, and most users do not know that the Aer Expectation Exists
  • Aer's ExpectationValue is woven throughout the core operator code in a way that is branchy, inexorable, and difficult for users to understand and control
  • A new ExpectationValue, such as one provided by BasicAer or IBMQProvider, would simply introduce additional branches following the existing style

Aqua's State Evolution is Inextensible and Difficult to Navigate

Evolution is somewhat more succinct, but more difficult to navigate in code. The logic for evolution is distributed over several branchy static modules, and the evolution is pre-compiled as a CNOT-chain circuit, which is often not the ideal evaluation format (e.g. matrix multiplication if simulating, or Swap Networks).

from qiskit.circuit import Parameter

op = WeightedPauliOperator([
    [.5, Pauli.from_label('IX')],
    [.2, Pauli.from_label('ZY')],
])
circuit = QuantumCircuit(2)

θ = Parameter('θ')
instr = op.evolve_instruction(evo_time=θ)
circuit.append(instr, [0,1])
print(circuit.draw(fold=4000))
print('Decomposed:')
circuit.decompose().draw(fold=4000)
        ┌─────────────────┐
q_0: |0>┤0                ├
        │  Evolution^1(θ) │
q_1: |0>┤1                ├
        └─────────────────┘
Decomposed:
        ┌─────────────────────┐                       ┌──────────────────────┐┌──────────┐┌───────────┐┌──────────┐
q_0: |0>┤ U3(pi/2,-pi/2,pi/2) ├──■─────────────────■──┤ U3(-pi/2,-pi/2,pi/2) ├┤ U2(0,pi) ├┤ U1(1.0*θ) ├┤ U2(0,pi) ├
        └─────────────────────┘┌─┴─┐┌───────────┐┌─┴─┐└──────────────────────┘└──────────┘└───────────┘└──────────┘
q_1: |0>───────────────────────┤ X ├┤ U1(0.4*θ) ├┤ X ├─────────────────────────────────────────────────────────────
                               └───┘└───────────┘└───┘                                                             

Requirements and Design

  1. Location and Ownership
    1. Operators
    2. Provider-specific Algorithms
  2. Object Model
    1. Operator Definition - Primitives and Composites
    2. Algorithms Definition - Primitives and Composite Operations
    3. Parameterization and Eagerness
  3. Changes to Terra
  4. Changes to Aqua
    1. Algorithms as composite Operations
    2. Circuit Execution Algorithms
    3. Expectation Algorithms
    4. Evolution Algorithms
    5. Other Primitive Algorithms

Location and Ownership in Qiskit

Given the presence of Operator logic in both Aqua and Terra, there are several options for their placement within Qiskit. The primary considerations here relate to which master branch tests them, who owns what in the case of breakage, and who owns what in the case of design.

In addition, some remote Operator algorithms are being discussed, with one already in production - the Aer Expectation Value. The location of these algorithms is also an important question.

Operator Location Considerations

  • The Operator's centrality to Aqua means relying on an external library is a big overhead
    • Reliance on Terra has created frequent firedrills because behavior and interfaces change without integration testing
    • Firedrills are very difficult to troubleshoot because presently there is no integration testing between Terra and Aqua or design review to check whether a change will have downstream implications
    • Operator is so central to Aqua that it will require strong ownership by the Aqua team, constant maintenance and changes
  • Centralized Operator primitives can simplify interfaces across Qiskit
    • By accepting a common Operator format derived from Terra, methods in different areas of Qiskit can communicate in a consistent format without dependencies
    • For example, Aer's expectation value can take a circuit and an Operator, rather than depend on Aqua to define its interface, or rely on an informal interface (e.g. lists) which must be validated
  • Terra and Aqua's respective Operators can be delineated somewhat cleanly and used quite differently
    • Aqua and Terra's operators are seemingly used by completely different users for very different tasks - Aqua primarily uses Operators for lazy algorithmic manipulation while Terra primarily uses them for numerical computation.
    • Terra's Operators are primarily matrix-based, while Aqua's are primarily composites of sparse representations (e.g. sums of Paulis or Circuits)
    • Though some are definitely shared, such as Pauli
  • Operators and Gates may need to be reconciled at some point
    • The X, Y, and Z Paulis are not different from the X, Y, and Z Gates
    • Both the gate and operator models include functionality for converting unitary matrices to circuit operations

Operator Location Options

A. Move Aqua Operators into Terra, with:

  1. Joint ownership by Aqua team
  2. Aqua integration tests run on Terra's master branch (e.g. pulling in Aqua's master branch to execute tests). Unit tests alone are not sufficient, as they are usually modified along with breaking changes to pass.
  3. Aligned release cycles so Aqua does not need to scramble to release when Terra does

Big-A. Combine Aqua and Terra into a single repo and jointly own Operators

B. Move all operators and states into Aqua, jointly owned by Terra team

C. Leave Operators split between Aqua and Terra, with dependency on Terra for primitives (QuantumCircuit, MatrixOperator, Pauli), with joint ownership and Aqua integration testing

Decision: Following a discussion in Aqua Design Review, option C will be pursued.

Provider-Specific Algorithm Location Options (Decision)

A. Remote algorithms live in provider repo, and are tested and released at provider’s discretion

B. Remote algorithms live in Aqua, with Aqua integration testing of functionality in provider repo

C. Remote algorithms live in Aqua, with agreed upon interface to enforce consistency, and data interchange (e.g. an Operator format defined in Terra) tested in provider repo

Decision: Following a discussion in Aqua Design Review, option C will be pursued.

Object Model and Hierarchy

What is an Operator to a QA&A (Quantum Algorithms & Applications) programmer?

Ignoring the Physical definition of an Operator for a moment, as a Quantum programming primitive, the Operator is:

  • Recursively defined - Operators can be one of several primitives - e.g. Matrix, Pauli, Clifford, QuantumCircuit, or an arbitrary combination of these primitives, e.g. Addition, Tensor, Composition.
    • It makes complete mathematical sense to add two primitives together, e.g. (my_matrix+my_circuit)@my_pauli. In classical programming, this would be like 5.7 + "pickle".
  • Both code and data - The Operator encodes both data (e.g. a matrix for eigensolution or a wavefunction being prepared) and computation (measure my wavefunction in this basis). There is little distinction between the two in Quantum programming.
  • Linear - The Operator is a recursively powerful construct, allowing algorithmic rearrangement not typically allowed in classical computation.
    • op1(op2(A,B)) == op1(op2(A)), op2(B)) in many cases, e.g. Expectation(A+B).
    • The idea that program(a*circuita + b*circuitb) gives a mathematically valid result is highly surprising.
  • Algorithmically ubiquitous - Every quantum algorithm uses Operators. Algorithms are nearly always defined in literature by Operator operations. This language is rigorous, accepted, and compact.
  • Eagerly Computable - In most cases, Operator computation can be partially compiled as parameters become available, allowing improved performance, functional modularity (e.g. passing a ready-to-run algorithm), and inspection transparency. For example:
    • A circuit can be compiled to a Qobj with parameters missing, to be filled in later
    • The full list of circuits necessary to execute an algorithm can be prepared pending some operator coefficients
    • A full algorithm can be prepared and passed to a user pending the insertion of some subcomponent (a choice of ExpectationValue algorithm) or parameters

Operator Definition: Primitives and Combinations

Operators can be primitives or combinations. Primitives are base-level Operator representations which have unique data structures and are not defined in terms of other primitives, but can be converted into one another with some computational work. Often a unique data structure is used to achieve better performance even when other representations are available, such as a Clifford tableau or Pauli representation as X and Z bits. Combinations are Operators which are constructed from functions of multiple primitives, such as sums and tensors. Combinations store the primitives from which they are constructed. Note that many Gates are present in other classes of primitives, and this must be reconciled as a follow-on to this redesign. The following should all be available in the Operator hierarchy:

  • Primitives
    • Matrix
    • Pauli
      • [0.7] Singletons for easy importing: X, Y, Z, I
    • QuantumCircuit, Gate
    • Clifford
    • WeightedPauli - A matrix of X And Z binary strings
    • Others: ZX Calculus, MPS, Dirac Matrix, Gell-Mann matrix
  • Combinations
    • [0.7] OpSum - Generalization of WeightedPauli. Stores a list of Operators of equal dimension and complex weights
    • [0.7] OpVec - Stores a list of Operators of any size
    • [0.7] OpEvo - Stores a single Operator and time parameter, acting as a placeholder for some Evolution algorithm to replace later
    • [0.7] OpCompose - Stores a list of Operators which are all of equal dimension
    • OpKron - Stores a list of Operators of any size
    • OpCombo - custom, user-defined recombination function
from qiskit.aqua.operators import X, Y, Z, I
op_new = .5*(I^X) + .2*(Z^Y) + .1j*(Z^Z)
op_new == pauli_op
True

The following overload operations may also be desirable:

  • Operator composition using @ overload
  • Power (**3), kronpower (^3)

Decision: Following a discussion in design review, the following was agreed:

  • Aqua should support op1.compose(op2) for circuit-direction matrix multiplication and op1.dot(op2) for linear algebra direction matrix multiplication.
  • Usage of this syntactic sugar should generally be avoided in the Aqua source code.
  • The composition, kron, power, and kronpower overload should be explored after the 0.7 release
(pauli_op^2)**2 == (pauli_op^pauli_op)@(pauli_op^pauli_op)
True

Algorithms Definition: Primitives and Composites

Operations on Operators also can be described as primitives or combinations of such. Primitives are computations which can be performed directly on some available computation engine, such as Numpy or Quantum Hardware, while composites are constructed from piping primitives together. Algorithms accept only specific primitives, so an algorithm taking a Pauli vs. one taking a matrix are fundamentally different, but are also defined over certain combinations of their input primitives. For example, a Change-of-Basis Expectation Value is defined to accept a Pauli and a Projector (or QuantumCircuit acting as one from Zero implicitly), but can also accept sums, tensors, and vectorizations of Paulis and Projectors. If an unsupported primitive, such as Matrix or OpCompose were passed in, an exception would be thrown.

  • Primitives
    • Classical sum, product, tensor, trace, etc.
    • Z-Basis QuantumCircuit measurement / Trace (traditional QASM backend)
    • Primitive Conversion - Pauli to matrix, matrix to Pauli, etc.
    • Evolution Conversion - Trotter, Suzuki, etc.
    • Pauli Sum, Composition, Tensor
    • Change of Basis - Pauli, Fourier
    • Optimizers
    • External functions, such as Drivers or imports
  • Composites
    • ExpectationValue
    • Existing Aqua Algorithms: VQE, QPE, HHL, etc.
    • Gradients

Over time, we have found that it is easiest to describe the behavior of Algorithms in terms of the flow of Operators through various components and subroutines. This description is naturally recursive, and considerably easier to understand than the present presentation of algorithmic flow in Aqua.

To demonstrate this, consider the following VQE coded from scratch in this model:

ansatz = Ry(qubits=2, depth=3)
    # Ansatz state = Ry(θ)|00⟩
hamiltonian = 3*(I^Z) + .4j*(X^Z)
expectation = PauliExpectation(ansatz, hamiltonian, backend)
print(expectation.run({ansatz.params: np.zeroes(len(ansatz.params))})) 
    # Print starting expectation

gradient = ParamShiftGradient(expectation)
optimizer = AQGD(initial_point=np.zeroes(len(ansatz.params)))
my_vqe = AQGD(cost_fn=expectation.run, grad_fn=gradient.run)
min_eig = my_vqe.run()
Eager Partial Computation

Aqua should be eager in partial computation while some parameters necessary for execution are not yet available, to allow for inspection transparency and performance. For example, once backend information is available, circuits should be transpiled for the backend or otherwise prepared for execution. This can avoid many transpilations or preparations later if the circuits are duplicated for Operator composition, as in Change-of-Basis expectation values or gradients.

The choice of which partial computation to perform is left to the algorithm, so only worthwhile partial computations are performed. If parameters change, re-preparing the partial computation can be expensive, so a lazy parameter should be available in the callable function.

Desirable Changes in Terra

In parallel with the work described in this doc, the following changes in Terra would improve the Aqua's Operator interfaces and functionality:

  • Clifford, WeightedPauli (as a matrix of X and Z bitstrings) primitives
  • to_matrix() - Method to allow quick access to unscalable classical tools, e.g. numpy eigensolution, with adequate warnings for large matrices.
  • Kron, Kronpower, Trace, Partial Trace, Determinant, Norms, Adjoints - Where possible, linear algebra should be easily accessible for any applicable Operator primitives
  • Rename operator.py to matrix.py
  • Reconciliation Between Operators and Gates - Terra's Operators and Gates are currently fully distinct from one another. The X, Y, Z, Clifford Gates, Evolution by a matrix-specified Unitary (UnitaryGate), and more are direct overlaps between the two, but not interoperable. At some point, Terra should address this difference to allow Operators to be inserted onto a circuit, maintain only a single set of primitive unitaries, allow Gates to be composed with Operators, etc.

[0.7] Changes to Aqua

The changes needed for the 0.7 release are:

  • Introduction of OpSum, OpVec, OpEvo, OpCompose modules
  • Migrate Aqua usage of MatrixOperator to rely on Terra's Operator and WeightedPauliOperator to rely on OpSum
  • Implement the new Expectation and Evolution algorithms
  • Migrate Aqua algos to rely on the new Expectation and Evolution algorithms (including replicating grouping functionality from the TPBGroupedWeightedPauliOperator)
  • Deprecate the Aqua Operators
Expectation Algorithms

Aqua should support the following ExpectationValue algorithms.

  1. [0.7] A base Expectation factory module which automatically selects and returns an evolution algorithm based on the backend and operator given - e.g. if the user passes an Aer backend and Pauli OpSum, VQE will use the AerExpectation by default instead of QASM execution.
    1. The logic for Expectation's handling of OpSum, OpVec, OpCompose, and OpEvo are all contained in this base. When an Expectation encounters an Operator type which is not the primitive (or list thereof) it was built to handle, it calls super.run on the Operator, which will rearrange Operator combinations into an OpSum or OpVec of primitives, call the Operator's .run on the primitives, and then recombine the results by summation or vectorization. OpKron and OpCompose are handled by attempting to kron or compose each of the constituents. OpEvo will return an error, as the user must convert them with an Evolution algorithm before taking an expectation.
  2. [0.7] PauliExpectation (Change-of-Basis)
  3. [0.7] AerExpectation - Takes Pauli expectations using on Aer's fast expectation feature
  4. [0.7] MatrixExpectation
  5. [0.7] ProjectorOverlap - Takes the overlap with respect to the operator composed as-is with $|0\rangle\langle 0|$ (i.e. as a state-preparation), rather than changing into the basis of the operator.
  6. CVaRExpectation
  7. (tentative) BasicAerExpectation
  8. RichardsonExpectation - OUT OF SCOPE, BEING COVERED IN ANOTHER DOC.
Grouping

Grouping is an important feature within the PauliExpectation in Aqua today, but has an interface which is not obvious. Grouping should be moved into the PauliExpectation, with a simple interface for the user to specify whether to group the Paulis, or how aggresively to do so. By default, the PauliExpectation should group Paulis as aggressively as is performant on the given execution backend.

Circuit Evolution Algorithms

And similarly for Evolution, a variety of algorithms should be available for converting a OpEvo composite operator into a sum, composition, etc. More specifically, circuit evolution algorithms take an OpEvo placeholder and return operators which approximate the value of the exponentiation. For example, the PauliEvolution accepts a Pauli and returns a QuantumCircuit representing the unitary evolution of that Pauli.

  1. [0.7] A base Evolution factory module which automatically selects and returns an evolution algorithm based on the backend and operator given - e.g. if a MatrixOperator is passed, the MatrixEvolution module will be instantiated. A similar flow to the Expectation base will encapsulate the handling of combinations on behalf of the Evolution submodules below, but with evolution-specific rules. For example, OpCompose and OpVec can be handled trivially, but OpSum must be trotterized, and OpKron's components must be kroned together.
  2. [0.7] PauliEvolution (Change-of-Basis)
  3. [0.7] SumEvolution
    1. Trotter
    2. Suzuki
  4. [0.7] MatrixEvolution
  5. (tentative) LinCombEvolution
  6. (tentative) AerEvolution
  7. (tentative) BasicAerEvolution
Other algorithms to build out into first-class Algorithm groups
  1. [0.7] Converters - convert lazily between Operator types
  2. [0.7] Gradients
  3. Optimizers

⚰️⚰️⚰️⚰️⚰️⚰️ Graveyard ⚰️⚰️⚰️⚰️⚰️⚰️

The following were deemed out of scope or no longer necessary for inclusion in this doc, but preserved here for posterity.

Parameterization and Eagerness

Operators and algorithms can be parameterized, or missing some key information in order to execute. For Operators these may be sum coefficients, evolution times, QuantumCircuit parameters, and more. For Algorithms these may be input operators, execution parameters, or instances of algorithms used in computation which cannot be inferred by default (e.g. backend on which to execute, optimizer, etc.).

Eager Parameterization+Execution Interface Options:

An algorithm should execute as soon as it has filled the parameters necessary to do so. This is called Eager Execution. In a similar vein, OpSum can be seen as eagerly waiting for the contained operators to be summable, e.g. replaced with scalars by an expectation value. (Decision) Some interface options for eagerness:

Option A: Algorithms should be callable with a parameter dictionary, triggering a breadth-first search to parameterize any sub-objects with the parameter dictionary. This may be too much hocus pocus and difficult for implementers of algorithms to understand. A user may want to parameterize without executing, so an execute parameter should be available in the parameterization function.

my_op = Parameter('t1')*(Z^Z) + .6*(X^I)
my_vqe = VQE(backend=Parameter('backend'), 
             operator=my_op, 
             ansatz=Ry(qubits=2, reps=3), 
             optimizer=SLSQP(initial_point=Parameter('initial_rotations')))
my_vqe({'t1': .2j, 'backend': Aer.get_backend('qasm_simulator')}) 
    # Didn't return anything yet
rots = np.zeros(len(my_vqe.ansatz.params))
min_eig = my_vqe({'initial_rotations': rots})
    # Now a value is returned, and other execution information can be found inside the object
    # Alternatively
my_vqe({'initial_rotations': rots}, execute=False)
min_eig = my_vqe()

Option B: Algorithms should have a .run(param_dict) method which accepts parameters and performs the breadth-first parameterization. The form factor of this would be similar to the above, but with run() instead of direct function calls. This has the benefit of some backward compatibility.

Option C: Algorithms should support separate parameterization and execution functions. This is the most explicit, but is clunky in an eager execution regime, where execution is automatic if the algorithm is sufficiently parameterized.

All of an Algorithm or Operator's pending Parameters should be recursively returned by a .params function. (Tentative) A deepcopy option should be available to return a deep copy of the algorithm with the desired parameterization, rather than parameterize the algorithm in-place (this is evaluated with execute=False by default).

Change Algorithms to rely on Terra operators and new Operator algorithms

In particular, algorithms should be accessible with only Terra-defined inputs (meaning constructed using Terra alone) to provide a seamless experience between Terra and Aqua usage, and extensible interfaces. For example, a VQE should be runnable by passing only a parameterized QuantumCircuit and Terra-defined Operator, allowing a provider or collaborator to share a custom VQE without an unnecessary dependency on Aqua. In particular, this allows the Aer Expectation Value to be defined with the same interface as Aqua's Pauli Expectation, without a dependency on Aqua.

Circuit Execution Algorithms - Decision: Name - CircuitExecution? QCExecute? QuantumMeasureZ? RunCircuit?

Circuit execution is a utility in Aqua today, mediated by the QuantumInstance, which most users do not understand, and growing increasingly branchy to accommodate more and more execution variants. Measurement error mitigation, noisy simulation setup, hardware API fault handling, and more all fall into the same execution flow in various branches.

Circuit execution is an algorithm for sampling a circuit's expectation in exponentially many ${Z, I}^{\otimes n}$ bases, but is not reflected an an algorithm today. It should be promoted to be a first-class algorithm to be more transparent and compartmental, wherein for example, code for simulation and code for execution on hardware can be kept distinct. A CircuitExecution Algorithm accepts a backend and interacts with it in some well-defined way - in way breaking up and organizing of the functionality of the QuantumInstance. Some examples of CircuitExecution algorithms are:

  1. QuantumHardware - An Execution algorithm tailored for execution on remote hardware, including fault handling, slicing to limit job sizes, etc. Can stack up a queue of circuits for batch execution, or accept a list of jobids to use as the first n results objects, allowing the user to reuse results from a terminated execution.
  2. IdealSimulator - Algorithm tailored for execution in ideal simulation.
  3. NoisySimulator - Utility for querying a Hardware backend's properties and providing a noisy simulator using Aer's "noise config from device" functionality.
  4. ErrorMitigatedExecutor - OUT OF SCOPE, BEING COVERED IN ANOTHER DOC.

If none is explicitly specified, Aqua should aggressively guess the preferred execution algorithm for the user given the backend and other execution parameters.

Timeline and Gameplan

Stage 1: Implement new Operators in Terra with thorough unit and integration tests.

Stage 2: Implement Operator algorithms in Aqua, relying on Terra Operators

Stage 3: Migrate Aqua algorithms to rely on new Operator algorithms and new Terra Operators

Stage 4: Deprecate Present Aqua Operators (0.7 release)

Stage 5: Delete Present Aqua Operators (0.8 release)

Other Benefits of Operator Combinations

  • Obedient Eager Evaluation - Best of Eager and Lazy evaluation:
    • Partially evaluate whatever you can with the parameters you have
      • Allows transparency, inspection, rapid prototyping (e.g. Users couldn't find circuits or operator when working through JSON dictionaries)
      • Performance - partially compiled algorithms save massive amounts of compilation and deepcopy time
    • But not too early, not compiling preemptively for a possible parameter value
      • Objects can be returned without being totally incorrectly constructed for the next step or engine (e.g. building massive CNOT chains for UCCSD simulations)
      • Intractable but possible computations (e.g. convert to matrix and solve) are avoided
  • Natural, Powerful, and Self-defining Programming Interfaces
    • An algorithm's behavior is simply defined by the operator primitives it accepts and returns
    • Nesting of algorithms is identical to user algorithm execution
  • Ubiquitous parameters, and obvious interface for Optimization
    • OpCombo coefficients, primitive parameters, and algorithm parameters can all be parameterized
    • Algorithms of any level of completeness can be returned
    • Optimization is universal - simply pass a nearly-complete algorithm to an optimizer and the callable interface executes when the optimizer provides the parameters

Grouping

Aqua's grouping functionality is only relevant to ExpectationValues today.

qaoa_cost_op = WeightedPauliOperator([
    [.5, Pauli.from_label('ZIZ')],
    [.2, Pauli.from_label('ZZI')],
    [.1j, Pauli.from_label('IZZ')],
])

grouped_cost_op = TPBGroupedWeightedPauliOperator.sorted_grouping(qaoa_cost_op)
grouped_cost_op._basis
[(Pauli(z=[True, True, True], x=[False, False, False]), [0, 1, 2])]
class VQE(QuantumAlgorithm):
    def __init__(self, operator, var_form, optimizer,
                 initial_point=None, backend=backend, callback=None, ...):
        ...
        self._expectation_value = ExpectationValue(self._operator, self._backend)
    
    def _energy_evaluation(self, params):
        circuits = self._var_form.construct_circuit(params)
        energy, stdev = self._expectation_value.run(circuits)
        return energy