Programming with the Intel® Quantum SDK

In-lining & quantum_kernel functions

When the compiler prepares a quantum_kernel function, it separates all the quantum instructions (as Intermediate Representation (IR)) from the classical IR so that it can deliver a complete set of instructions to the quantum backend.

Local declarations and operations with traditional C++ data types are supported inside a quantum_kernel function, which aids readability and preserves programming concepts. At compile time, these “classical” instructions are pulled out of the quantum_kernel. This has a consequence on classical instructions, especially bool and cbit measurement results: any operations on classical variables written inside a quantum_kernel will be executed at the beginning of that quantum_kernel, unless they are written after the final quantum gate in the quantum_kernel.

qbit q0;
qbit q1;

quantum_kernel void myKernel() {
  bool b = false;
  std::cout << "b has value false (0) here after initialization: "
            << b << "\n";
  PrepZ(q0);
  X(q0);
  MeasZ(q0, b);
  std::cout << "b still has value 0 here since the quantum gates are not complete: "
            << (int)c << "\n";
  PrepZ(q1);
  std::cout << "After all gates in quantum_kernel have executed, b has value true (1): "
            << b << "\n";
}

A quantum_kernel may be called from within another quantum_kernel. Here, too, the compiler in-lines the quantum instructions from the innermost quantum_kernel and continues until it produces one sequence of instructions that corresponds to the “top-level” quantum_kernel call that begins the quantum algorithm.

In-lining combined with the earlier rule on rearranging operations on measurement results means that for quantum_kernel functions containing a measurement which are called in the middle of another quantum_kernel function, the operations on those cbit and bool measurement results will be moved to the beginning of the resulting set of instructions. This means that the following code:

If a user needs classical instructions to be executed strictly in the middle of a quantum algorithm, they should break up the algorithm into multiple top-level quantum kernel functions. Alternatively, they can use the bind operator on quantum kernel expressions (see FLEQ Guide and Reference (Barriers and binding)).

The restriction that the entire quantum_kernel be known at compile time together with the in-lining behavior means that the top-level kernel cannot accept an arbitrary variable of type qbit as a parameter. The variables of qbit type that will be operated on must be explicitly defined in the “top-level” kernel’s instructions; however, inner quantum_kernel functions may be written to accept qbit type variables as parameters.

Note

This restriction applies primarily to quantum_kernel functions, and not to FLEQ. See FLEQ Guide and Reference if you need this feature.

qbit qs[3];

// A nested quantum_kernel may take either classical or quantum arguments
quantum_kernel void bell(qbit &a, qbit &b) {
  PrepZ(a);
  PrepZ(b);
  H(a);
  CNOT(a,b);
}

// A top level quantum_kernel may take classical arguments, but not quantum
// arguments
quantum_kernel void topLevelBell() {
  bell(q[0],q[2]);
}

int main() {

  // may call top-level quantum_kernel
  topLevelBell();

  // may not call quantum_kernel with quantum arguments
  // invalid: bell(q[0], q[2]);
}

Measurements using Simulated Quantum Backends

A typical quantum program using the Intel® Quantum SDK will do effectively the following sequence: 1. Submit quantum_kernel functions to a quantum backend. 2. Execute quantum_kernel on the backend. 3. Retrieve results. 4. Repeat 1-3 as needed.

After the quantum_kernel has finished executing, users will need to retrieve results from the backend. This section describes the result retrieval and aggregation process, using the example of the FullStateSimulator backend.

The FullStateSimulator class provides three main approaches to obtain statistical measurements:

  1. getProbabilities() (and/or other simulation data)

  2. getSamples()

  3. Repeated execution of explicit measurement operations e.g. MeasZ (sampling).

These methods will be elaborated in the following sections.

Both Intel® Quantum Simulator (IQS) and Quantum Dot (QD) Simulator backends support collecting the simulation details, such as the quantum amplitudes, conditional probabilities, or single-qubit probabilities. The FullStateSimulator class provides these data regardless of which backend is selected to run the simulation.

Simulation Data

Table 2 Simulation Method Comparison

Method

Returned object

Efficiency (with IQS)

Recommended?

Other Notes

getProbabilities()

vector<double> or QssMap<double>

Best

Yes

getSingleQubitProbs()

vector<double>

Best

Yes

getSamples()

vector<vector<bool>>

Good

Yes

getAmplitudes()

vector<complex<double>> or QssMap<complex<double>>

Good

No

Accurate up to global phase

Repeated sampling calls

User-defined

Worst

No

Complexity scales with number of samples

Working with the simulation data returned by FullStateSimulator methods such as getProbabilities() is often the most computationally efficient route to simulating a quantum algorithm. This is because quantum algorithms often encode their results as probabilities of different states. If the entire algorithm needed to run many times to sample the probability, as required on a hardware quantum backend, the simulation time would increase significantly.

For applications that need a set of measurement outcomes, both backends of the FullStateSimulator offer a second route to obtain the simulation data, which avoids the need for repeated executions of a given quantum_kernel function. This route consists of calling getSamples() to get sequences of outcomes as if measurements were applied to the qubit register. This sampling of results doesn’t affect the state and can even be applied as many times as an application calls for.

Combining Simulation Data and Measurement Operations

IQS offers the ability to retrieve simulation results (i.e. from getProbabilities() or getSamples()) when quantum_kernel functions include measurement gates (e.g. MeasZ()).

Note

This feature is not available in QD Simulator because it doesn’t collapse the state (see the Quantum Dot (QD) Simulator). This means combining results of measurement operations and sampling results with the QD Simulator can yield unexpected results.

When using probability measurement and explicit measurement gates on a qubit in simulations, IQS will cause a ‘partial collapse’ of the state in the simulator to a sub-space. You can combine such operations with a sampling technique like getProbability or getSamples to compute data or collect statistics on the sub-space. To support combining measurement operations and simulation data, IQS will always collapse the quantum state of the simulator when it encounters a measurement operation in a quantum_kernel. Any subsequent querying of the FullStateSimulator after measurement will always give the same result on the qubits that had one of MeasX, MeasY, or MeasZ applied, and other qubits will have any correlated effects on their probabilities present.

Measuring a qubit leaves it in one of the two states into which the measurement was projected; e.g. measuring a qubit along the \(Z\)-axis (in a Bloch sphere representation) leaves it in either a \(\ket{0}\) or \(\ket{1}\) state. Another perspective on this is that the post-measurement state of the entire set of qubits now occupies a sub-space of the Hilbert space previously occupied by the pre-measurement qubits. This can be qualitatively understood by noting that there is no uncertainty in the state of the measured qubit. A measurement also has consequences on the correlations arising from entanglement between qubits. More simply, measuring one qubit can affect the probabilities of the outcomes of measuring a different qubit (provided the two qubits were entangled). In the extreme case, a large amount of correlation present in the system could mean that a single measurement applied on one qubit results in the state of the entire set of qubits being determined, such as for a Bell pair or GHZ state.

Using Only Measurement Operations

A third option is to collect your own statistical results by executing the entire quantum algorithm with all the required measurement operations many times in a loop (or other control-flow structure) to direct execution flow. Each iteration of the quantum algorithm produces and then stores, analyzes, or accumulates the result of the measurements. Under ideal conditions (no noise), the sampling & measurement approaches will each produce statistically-equivalent results, especially with large sample sizes. Because quantum algorithms running on quantum hardware must use the measurement approach, the simulation data and sampling approaches can be seen as a debugging mode for the measurement approach. IQS supports using measurements anywhere in the quantum algorithm; in contrast, QD Simulator only supports reading measurements at the end of the quantum_kernel.

Local qbit Variables

qbit variables can be declared globally or locally. When the compiler maps the program qubits to physical qubits, each qbit variable will be assigned to a physical qubit. Since the compiler cannot guarantee the state that a local qbit variable is in, local qbit variables must be initialized using PrepX, PrepY, or PrepZ before being used. At the end of the quantum_kernel, the local qbit variables must be released. This can be achieved through measurements or release_quantum_state().

Note that if using release_quantum_state(), the quantum states are unspecified after the function call (see Language Extensions). Without releasing the quantum states, the physical qubits assigned to the local qbit variables might be assigned to other local qbit variables in a new quantum_kernel function while still holding the quantum states of the out-of-scope variables. The out-of-scope variables’ physical qubits will not be assigned to unreleased global qbit variables, however.

In the following example, a local qbit variable is declared, initialized, and measured.

quantum_kernel void kernel() {
   qbit q;
   bool b;  // can also be of type cbit

   PrepZ(q);    // prepare the qbit variable before applying gates
   H(q);

   MeasZ(q, b);  // release the qbit variable at the end of the quantum_kernel
}

If local qbit variables are entangled with global qbit variables, the entanglement persists after the local qbit variables go out of scope. The user must insert gates needed to disentangle the local qbit variables from the global ones before releasing the local variables’ quantum states.

qbit global;

quantum_kernel void errorExampleEntangledQubits() {
   qbit local;

   PrepZ(local);    // Prep the qbit variable before applying gates
   H(local);
   CNOT(local, global);

   // After local goes out of scope, the physical qubit it was assigned to
   // is still entangled with global
}

The recommended best practice with regards to local qbit variables is therefore to prep them before they are used and insert gates to undo the entanglement between local and global qbit variables before releasing the quantum states at the end of quantum_kernel functions.

For information on how to use local qbit variables with quantum kernel expressions and FLEQ, refer to FLEQ Guide and Reference (Local qubits).