# Performance Management: A Qiskit Function by Q-CTRL Fire Opal

Qiskit Functions are an experimental feature available only to IBM Quantum™ Premium Plan users. They are in preview release status and subject to change.

## Overview

Fire Opal Performance Management makes it simple for anyone to achieve meaningful results from quantum computers at scale without needing to be quantum hardware experts. When running circuits with Fire Opal Performance Management, the function automatically applies AI-driven error suppression techniques for successfully scaling to larger problems, using more gates and qubits on the device. At the same time, reaching the correct answer takes fewer shots and requires no overhead, meaning that you save on compute time and cost.

Performance Management suppresses errors and increases the probability of getting the correct answer on noisy hardware. In other words, it increases the signal-to-noise ratio. The following image shows how increased accuracy enabled by Performance Management can reduce the need for additional shots in the case of a 10-qubit Quantum Fourier Transform algorithm. With only 30 shots, Q-CTRL reaches the 99% confidence threshold, whereas the default (`QiskitRuntime`

Sampler, `optimization_level`

=3 and `resilience_level`

=1, `ibm_sherbrooke`

) requires 170,000 shots. By getting the right answer faster, you save significant compute runtime.

The Performance Management function can be used in place of the standard Qiskit Runtime primitives. Behind the scenes, multiple error suppression techniques work together to prevent errors from happening at runtime. All Fire Opal pipeline methods are pre-configured and algorithm-agnostic, meaning you always get the best performance out of the box.

To get access to Performance Management, contact Q-CTRL.

## Function description

Fire Opal Performance Management has two options for execution that are similar to the Qiskit Runtime primitives, so you can easily swap in the Q-CTRL Sampler and Estimator. The general workflow for using the Performance Management function is:

- Define your circuit (and operators in the case of the Estimator).
- Run the circuit.
- Retrieve the results.

To reduce hardware noise, Fire Opal employs a range of AI-driven error suppression techniques depicted in the following image. Error suppression works at both the gate and pulse levels to address various sources of noise and to prevent the likelihood of an error occurring. With Fire Opal, the entire pipeline is completely automated with zero need for configuration. By preventing errors, the need for expensive post-processing is eliminated.

The following image depicts the error suppression methods automated by Fire Opal Performance Management.

## Inputs and outputs

The Sampler and Estimator inputs are meant to closely follow the implemented spec for Qiskit Runtime V2 primitives. The Q-CTRL primitives accept inputs in the form of Primitive Unified Blocs (PUBs), which are tuples containing circuits and their relevant execution data. For more information on the PUB data structure, refer to the IBM Quantum Documentation. The main difference is that the Q-CTRL primitives accept PUBs containing basic types - for example, QASM strings, dictionaries, and so on.

### Sampler inputs

Name | Type | Description | Required | Example |
---|---|---|---|---|

pubs | `SamplerPubLike` or `list[SamplerPubLike]` | One or more tuples containing the inputs listed under `SamplerPubLike` components. | Yes | `(circuit, parameter_values)` |

instance | str | The hub/group/project to use in that format. | Yes | `"hub1/group1/project1"` |

run_options | dict | Input options which includes the following: (Optional) `backend_name` : str, `shots` : int. Defaults to least busy backend and 4000 shots. | No | `{"backend_name": backend_name, "shots": 2048}` |

`SamplerPubLike`

components:

- A single circuit in the OpenQASM 2.0 or 3.0 string format.
- (Optional) A collection of parameter values to bind the circuit against.
- (Optional) A dictionary of run options, such as the shot count.

### Sampler outputs

Name | Type | Description | Example |
---|---|---|---|

result | `list[ArrayLike[dict[str, int]]]` | The resultant list of counts dictionaries corresponding to the list of input PUBs. | `[{'000001': 100, '000011': 2}]` |

### Estimator inputs

Name | Type | Description | Required | Example |
---|---|---|---|---|

pubs | `EstimatorPubLike` or `list[EstimatorPubLike]` | One or more tuples containing the inputs listed under `EstimatorPubLike` components. | Yes | `(circuit, observables, parameter_values)` |

instance | str | The hub/group/project to use in that format. | Yes | `"hub1/group1/project1"` |

run_options | dict | Input options which includes the following: (Optional) `backend_name` : str, `shots` : int. Defaults to least busy backend and 4000 shots. | No | `{"backend_name": backend_name, "shots": 2048}` |

`EstimatorPubLike`

components:

- A single circuit in the OpenQASM 2.0 or 3.0 string format.
- One or more observables that specify the expectation values to estimate, in any of the formats denoted in the list "Accepted observables formats".
- (Optional) A collection of parameter values to bind the circuit against.
- (Optional) A dictionary of run options, such as the shot count.

**Accepted observables formats:**

- A Pauli string:
`"XY"`

- A dictionary - Pauli strings with coefficients:
`{"XY": 0.5, "YZ": 0.3}`

- A list of Pauli strings:
`["XY", "YZ", "ZX"]`

- A list of Pauli strings with coefficients:
`[("XY", 0.5), ("YZ", 0.3)]`

- A nested list of Pauli strings:
`[["XY", "YZ"], ["ZX", "XX"]]`

- A nested list of Pauli strings with coefficients:
`[[("XY", 0.1), ("YZ", 0.2)], [("ZX", 0.3), ("XX", 0.4)]]`

### Estimator outputs

Name | Type | Description | Example |
---|---|---|---|

values | `valueslist[ArrayLike[float]]` | The resultant list of expectation values corresponding to the list of input PUBs. | `[0.43046065915270004]` |

### Parameter formats

The Sampler and Estimator both accept parameters, which follow the same array broadcasting rules as the `QiskitRuntime`

primitives. The following parameter formats are accepted:

Format | Example |
---|---|

0-d array (single binding) | `{"a": 4, ("b", "c"): [5, 6]}` |

Single array (last index for parameters) | `{"a[0]", "a[1]", "a[2]": [0.1, 0.2, 0.3]}` |

Multiple arrays (last index for parameters, flexible dimensions) | `{"c": np.ones((10, 10, 2)).tolist(), "b": np.zeros((10, 10)).tolist()}` |

## Benchmarks

Published algorithmic benchmarking results demonstrate significant performance improvement across various algorithms, including Bernstein-Vazirani, quantum Fourier transform, Grover’s search, quantum approximate optimization algorithm, and variational quantum eigensolver. The rest of this section provides more details about types of algorithms you can run, as well as the expected performance and runtimes.

The following independent studies demonstrate how Q-CTRL's Performance Management enables algorithmic research at record-breaking scale:

- Parametrized Energy-Efficient Quantum Kernels for Network Service Fault Diagnosis - up to 50-qubit quantum kernel learning
- Tensor-based quantum phase difference estimation for large-scale demonstration - up to 33-qubit quantum phase estimation
- Hierarchical Learning for Quantum ML: Novel Training Technique for Large-Scale Variational Quantum Circuits - up to 21-qubit quantum data loading

The following table provides a rough guide on accuracy and runtimes from prior benchmarking runs on `ibm_fez`

. Performance on other devices may vary. The usage time is based on an assumption of 10,000 shots per circuit. The "Number of qubits" indicated is not a hard limitation but represents rough thresholds where you can expect extremely consistent solution accuracy. Larger problem sizes have been successfully solved, and testing beyond these limits is encouraged.

Example | Number of qubits | Accuracy | Measure of accuracy | Total time (s) | Runtime usage (s) | Primitive (Mode) |
---|---|---|---|---|---|---|

Bernstein–Vazirani | 50Q | 100% | Success Rate (Percentage of runs where the correct answer is the highest count bitstring) | 10 | 8 | Sampler |

Quantum Fourier Transform | 30Q | 100% | Success Rate (Percentage of runs where the correct answer is the highest count bitstring) | 10 | 8 | Sampler |

Quantum Phase Estimation | 30Q | 99.9998% | Accuracy of the angle found: `1- abs(real_angle - angle_found)/pi` | 10 | 8 | Sampler |

Quantum simulation: Ising model (15 steps) | 20Q | 99.775% | $A$ (defined below) | 60 (per step) | 15 (per step) | Estimator |

Quantum simulation 2: molecular dynamics (20 time points) | 34Q | 96.78% | $A_{mean}$ (defined below) | 10 (per time point) | 6 (per time point) | Estimator |

Defining the accuracy of the measurement of an expectation value - the metric $A$ is defined as follows:

$A = 1 - \frac{|\epsilon^{ideal} - \epsilon^{meas}|}{\epsilon^{ideal}_{max} - \epsilon^{ideal}_{min}},$where $\epsilon^{ideal}$ = ideal expectation value, $\epsilon^{meas}$ = measured expectation value, $\epsilon^{ideal}_{max}$ = ideal maximum value, and $\epsilon^{ideal}_{min}$ = ideal minimum value. $A_{mean}$ is simply the average of the value of $A$ across multiple measurements.

This metric is used because it is invariant to global shifts and scaling in the range of attainable values. In other words, regardless of whether you shift the range of possible expectation values higher or lower or increase the spread, the value of $A$ should remain consistent.

## Get started

Authenticate using your IBM Quantum Platform API token, and select the Qiskit Function as follows:

[ ] :```
from qiskit_ibm_catalog import QiskitFunctionsCatalog
# Credentials
token = "<YOUR_IQP_API_TOKEN>"
hub = "<YOUR_IQP_HUB>"
group = "<YOUR_IQP_GROUP>"
project = "<YOUR_IQP_PROJECT>"
# Authentication
catalog = QiskitFunctionsCatalog(token=token)
# Access Function
perf_mgmt = catalog.load("q-ctrl/performance-management")
```

## Example: Sampler

Use the Sampler mode of Fire Opal Performance Management to run a Bernstein–Vazirani circuit. This algorithm, used to find a hidden string from the outputs of a black box function, is a common benchmarking algorithm because there is a single correct answer.

### 1. Create the circuit

Define the correct answer to the algorithm, the hidden bitstring, and the Bernstein–Vazirani circuit. You can adjust the width of the circuit by simply changing the `circuit_width`

.

```
import qiskit
circuit_width = 35
hidden_bitstring = "1" * circuit_width
# Create circuit, reserving one qubit for BV oracle
bv_circuit = qiskit.QuantumCircuit(circuit_width + 1, circuit_width)
bv_circuit.x(circuit_width)
bv_circuit.h(range(circuit_width + 1))
for input_qubit, bit in enumerate(reversed(hidden_bitstring)):
if bit == "1":
bv_circuit.cx(input_qubit, circuit_width)
bv_circuit.barrier()
bv_circuit.h(range(circuit_width + 1))
bv_circuit.barrier()
for input_qubit in range(circuit_width):
bv_circuit.measure(input_qubit, input_qubit)
# Create PUB tuple
pubs = [(bv_circuit,)]
```

### 2. Run the circuit

Run the circuit and optionally define the backend and number of shots.

[ ] :```
# Choose a backend or remove this option to default to the least busy device
backend_name = "<CHOOSE_A_BACKEND>"
# Run the circuit using the sampler
qctrl_sampler_job = perf_mgmt.run(
runner_function ="sampler",
pubs = pubs,
instance = hub + "/" + group + "/" + project,
run_options = {"backend_name": backend_name},
)
```

You can use the familiar Qiskit Serverless APIs to check your Qiskit Function workload's status:

[ ] :`print(qctrl_sampler_job.status())`

### 3. Retrieve the result

[ ] :```
# Retrieve the counts from the result list
counts = qctrl_sampler_job.result()[0]
```

In case you need to retrieve results later, you can run the following cell to save your job ID and use it to retrieve your job.

[ ] :```
qctrl_sampler_job_id = qctrl_sampler_job.job_id
print(qctrl_sampler_job_id)
```

You can use the job ID to retrieve your job object, which can be used to get the status or results.

[ ] :`qctrl_sampler_job = catalog.get_job_by_id(qctrl_sampler_job_id)`

## Example: Estimator

Use the Sampler mode of Fire Opal Performance Management to run a Bernstein–Vazirani circuit. This algorithm, used to find a hidden string from the outputs of a black box function, is a common benchmarking algorithm because there is a single correct answer.

In addition to the `qiskit-ibm-catalog`

and `qiskit`

package, you will also use the `NumPy`

package to run this example. You can install this package by uncommenting the following cell if you are running this example in a notebook using the IPython kernel.

`# %pip install numpy`

### 1. Create the circuit

As an example, generate a random Hermitian operator and an observable to input to the Performance Management function.

[4] :```
import numpy as np
from qiskit.circuit.library import IQP
from qiskit.quantum_info import random_hermitian
n_qubits = 50
# Generate a random circuit
mat = np.real(random_hermitian(n_qubits, seed=1234))
circuit = IQP(mat)
circuit.measure_all()
# Define observables as a string
observable = "Z" * n_qubits
```

```
# Create PUB tuple
pubs = [(circuit, observable)]
```

### 2. Run the circuit

Run the circuit and optionally define the backend and number of shots.

[8] :```
# Choose a backend or remove this option to default to the least busy device
backend_name = "<CHOOSE_A_BACKEND>"
qctrl_estimator_job = perf_mgmt.run(
runner_function = "estimator",
pubs = pubs,
instance = hub + "/" + group + "/" + project,
run_options = {"backend_name": backend_name},
)
```

You can use the familiar Qiskit Serverless APIs to check your Qiskit Function workload's status:

[ ] :`print(qctrl_estimator_job.status())`

### 3. Retrieve the result

[ ] :```
# Retrieve the counts from the result list
expectation_value = qctrl_estimator_job.result()[0]
print(f"Expectation value: {expectation_value}")
```

In case you need to retrieve results later, you can run the following cell to save your job ID.

[ ] :```
qctrl_estimator_job_id = qctrl_estimator_job.job_id
print(qctrl_estimator_job_id)
```

You can use the job ID to retrieve your job object, which can be used to get the status or results.

[ ] :`qctrl_estimator_job = catalog.get_job_by_id(qctrl_estimator_job_id)`

## Get support

For any questions or issues, contact Q-CTRL.

## Next steps

- Request access to Q-CTRL Performance Management