Tutorial 1: Preparation and Execution of Readout Noise Characterization Circuits on IBM Quantum Systems and Amazon Braket Systems
This tutorial focuses on preparing and executing characterization experiments on real quantum devices.
We start by preparing a collection of random quantum circuits. Each circuit consists of a single layer of one-qubit gates, randomly selected from a predefined set. In this context, we neglect the errors introduced by these gates.
In an ideal scenario, we would perform single-copy measurements of states from these random circuits and use the statistical outcomes to characterize the quantum device. However, due to the limitations of current noisy intermediate-scale quantum (NISQ) devices, a more practical approach involves reusing a set of unique quantum circuits (~hundreds) multiple times. This modified protocol is detailed in Appendix A of Tuziemski, Jan, et al..
This tutorial will guide you through the preparation, execution, and analysis of experiments on two leading quantum machine platforms: IBM Quantum Systems and Amazon Braket. The tutorial is structured as follows:
Execution of the Experiment on IBM Quantum System Eagle r2
2.1 Initial Steps
Execution of the Experiment on Amazon Braket Rigetti ASPEN M-3 System
2.1 Initial Steps
1. Preparation of Experiments Characterizing Readout Noise
1.1. The Configuration File
The specifics of the experiment are defined within a QREM ‘.ini’ configuration file. You can find examples of such files in our repository:
The configuration file is organized into sections: ‘general’, ‘data’, ‘experiment’, ‘characterization’ and ‘mitigation’. Here we will cover only the ‘general’, ‘data’ and ‘experiment’ parts: -general: Contains general settings for the experiment, such as the experiment’s name, author, and logging level.
data Manages settings related to data handling, including backups of circuits, job IDs, and circuit metadata.
experiment Specifies various parameters and settings directly related to the quantum experiment, including device information, provider details, experiment type, and quantum circuit configuration.
You can find the full specification of the configuration file in our documentation, here we will denote most important parameters from the ponit of view of the experiment design:
experiment_type : a string specifying a type of readout characterization experiment to be performed. Currently QREM supports two types of characterization experiments Quantum Detector Overlapping Tomography (‘QDOT’) or Diagonal Detector Overlapping Tomography (‘DDOT’). For this tutorial we will choose DDoT.
k_locality : The locality of the readout noise to consider within the experiment, with values ranging from 2 to 5. We choose 2 fot this tutorial.
gate_threshold : Gate error threshold, if crossed - qubits will be excluded from calculations. ranging from 0 to 1. A value of 0 or Null includes all qubits, which is the setting we choose here.
limited_circuit_randomness : For now should be True. Indicates if limitations should be imposed on a number of random circuits configurations (e.g., number of random circuits). For now number of random circuits we can process is limited by technical constrains on available machines (limited execution time, duration of execution)
-random_circuits_count : Total count of random circuits to be sent. We set it to 1500 for this tutorial
-random_circuits_count : Number of shots (repetitions) per unique circuit, we set it to 10000.
experiment_path : The file path for storing experiment-related files.
You can read all parameters we will run in this tutorial in the default configuration file here
1.2. Importing the configuration file
Let’s import the default configuration file. If you prepared your version - you can use the commented line to change the source of config file to the file specified by you.
from pathlib import Path
import qrem
from qrem.common.config import example_config_ibm_path
CONFIG_PATH = example_config_ibm_path
#CONFIG_PATH = "path_to_your_ini_file.ini"]
config = qrem.load_config(path = CONFIG_PATH, verbose_log = True)
# Let's read in basic configuration parameters:
EXPERIMENT_NAME = config.experiment_name
EXPERIMENT_FOLDER_PATH = Path(config.experiment_path)
if not EXPERIMENT_FOLDER_PATH.is_dir():
EXPERIMENT_FOLDER_PATH.mkdir( parents=True, exist_ok=True )
BACKUP_CIRCUITS = config.backup_circuits
BACKUP_JOB_IDs = config.backup_job_ids
BACKUP_CIRCUITS_METADATA = config.backup_circuits_metadata
JOB_TAGS = list(config.job_tags)
2. Execution of the experiments on IBM Quantum System Eagle r2
Setting up .env file
To connect with IBM machine you need to define environmental variables as per provider instructions. With qrem, you can set them up by setting up correctly a .env file in your project path. You can see an example .env file here. Keep in mind, that if you modify our template, you need to rename it to .env and place in your working directory.
In .env file:
QISKIT_IBM_TOKEN = xxxxx # put_your_personal_token_here
QISKIT_IBM_CHANNEL = ibm-q/open/main # put correct channel here for your organisation, or use open channel
2.1 Initial steps
To define how many qubits our circuits will contain, and get the indicies of the valid qubits (not all available systems label the availbale qubits sequentially), we need to comunicate with the IBM Backend. For that puirpose we will use qrem.providers.ibm submodule. Let’s connect to the backend and find get valid qubits.
from qrem.providers import ibm
# ----------------------------------------------------------------
# [1] Get info from provider about valid qubits
# ----------------------------------------------------------------
CONNECTION_METHOD = config.ibm_connection_method
backend, service, provider = ibm.connect(name = config.device_name, method = CONNECTION_METHOD, verbose_log = config.verbose_log)
valid_qubit_properties = ibm.get_valid_qubits_properties(backend, config.gate_threshold)
number_of_qubits = valid_qubit_properties["number_of_good_qubits"]
good_qubits_indices = valid_qubit_properties["good_qubits_indices"]
Let’s add some infomration to the metadata object, which will be saved in the experiment folder with circuits collection
from qrem.common import io
METADATA = {}
METADATA["date"] = io.date_time_formatted()
if BACKUP_CIRCUITS_METADATA:
METADATA["valid_qubit_properties"] = valid_qubit_properties
2.2 Creating characterisation circuits
Now we have all necessary info to generate our characterisation circuit collection. It is held by qrem specific data structure: qrem.qtypes.CircuitCollection. This implementation is QREM - specific and does not rely on any external packages/implementations.
from qrem.qtypes import CircuitCollection
from qrem.common.experiment import tomography
from qrem.common.printer import warprint
# ----------------------------------------------------------------
#[1] Generate circuits colelction object and fill in parameters
# ----------------------------------------------------------------
qrem_circuit_collection = CircuitCollection()
qrem_circuit_collection.experiment_name = EXPERIMENT_NAME
qrem_circuit_collection.load_config(config=config)
qrem_circuit_collection.device = config.device_name
qrem_circuit_collection.qubit_indices = good_qubits_indices
qrem_circuit_collection.metadata = METADATA
# ----------------------------------------------------------------
#[1] Generate circuits
# ----------------------------------------------------------------
qrem_circuit_collection.circuits, _, theoretical_total_circuit_count, theoretical_number_of_shots = tomography.generate_circuits( number_of_qubits = number_of_qubits,
experiment_type = config.experiment_type,
k_locality = config.k_locality,
limited_circuit_randomness = config.limited_circuit_randomness,
imposed_max_random_circuit_count = config.random_circuits_count,
imposed_max_number_of_shots = config.shots_per_circuit)
if BACKUP_CIRCUITS:
qrem_circuit_collection.export_json(str(EXPERIMENT_FOLDER_PATH.joinpath("input_circuit_collection.json")), overwrite = True)
else:
warprint("WARNING: Circuits were not saved to file, as BACKUP_CIRCUITS = False. It is recommended to save circuits to file for future reference.")
Using QREM package, you can add special circuits to the experiment, like circuits for benchmarking, circuits containing ground state approximation for benchmarked hamiltonians or coherenve witness circuits. Tutorial how to prepare them and add them to the mix will be prepared in short future.
2.3 Experiment preparation
As qrem.qtypes.CircuitCollection is a QREM internal object, we will need to translate it to qiskit format, required by the IBM backened for circuit execution.
ibm_circuits = ibm.translate_circuits_to_qiskit_format(qrem_circuit_collection)
2.4 Experiment execution
Now we can run circuits on our backend machine. Make always sure that you are using correct IBM instance, and chose correct backend when connecting at the beggining of the process. In this step we will also back up again our CircuitsCollection to json file, together with job ids, so after experiment is ready we will know which job IDs correspond to the sent experiment. In short, every unique quantum circuit will correspond to a separate job and job ID.
import orjson
#[6] Now we need to run circuits
job_ids = ibm.execute_circuits( qiskit_circuits= ibm_circuits,
job_tags = JOB_TAGS,
number_of_repetitions = config.shots_per_circuit,
instance = config.provider_instance,
service = service,
backend = backend,
method = CONNECTION_METHOD,
log_level='INFO',
verbose_log=True)
#[6.1] Backup jobs to circuit collection file
if BACKUP_CIRCUITS:
qrem_circuit_collection.job_IDs = job_ids
qrem_circuit_collection.export_json(str(EXPERIMENT_FOLDER_PATH.joinpath("input_circuit_collection.json")), overwrite = True)
#[6.2] Save job ids to a file
if BACKUP_JOB_IDs:
json_job_ids=orjson.dumps(job_ids)
with open(str(EXPERIMENT_FOLDER_PATH.joinpath("job_ids.json")), 'wb') as outfile:
outfile.write(json_job_ids)
2.5 Retrieving results
You can check on IBM Quantum Platform https://quantum.ibm.com/, when your jobs will complete. After they are complete, you can retrieve your results, based on the backed up json file with Circuit Collection.
backend, service, provider = ibm.connect(name = config.device_name, method = CONNECTION_METHOD, verbose_log = config.verbose_log)
valid_qubit_properties = ibm.get_valid_qubits_properties(backend, config.gate_threshold)
circuit_collection = CircuitCollection()
circuit_collection.load_json(str(EXPERIMENT_FOLDER_PATH.joinpath("input_circuit_collection.json")))
experiment_results = ibm.retrieve_results( device_name =config.device_name,
provider_instance = config.provider_instance,
job_IDs = circuit_collection.job_IDs,
original_circuits = circuit_collection,
save_experiment_results = str(EXPERIMENT_FOLDER_PATH.joinpath("ibm_experiment_results.json")),
overwrite = False,
verbose_log = True)
You can now use the experiment_results object and circuit_collection object in futher part of this tutorial series.
Below you will find how to connect and run characterisation experiment on Amazon machine (available soon)
3. Execution of the Experiments on IBM Quantum System Eagle r2
Setting up Amazon Braket Local Development Environment
To connect with Amazon Braket tou need to set up your local environment for connection with Amazon Braket. To do that please follow steps 1-4 of Setting up your local development environment in Amazon Braket tutorial. You don’t need to set up conda environment, the virtual environment prepared in installation tutorial will be sufficient and supports Amazon Braket SDK.
Also, remember to set up relevant parameter in your qrem config file, such as provider, device_name, provider_instance. For the next part of the tutorial, we will use a default ini file for Amazon Braket provided by qrem package. You can get this template from here and modify yourself. Let’s read in the confi file:
from pathlib import Path
import qrem
from qrem.common.config import example_config_ibm_path
CONFIG_PATH = example_config_aws_path
#CONFIG_PATH = "path_to_your_ini_file.ini"]
config = qrem.load_config(path = CONFIG_PATH, verbose_log = True)
# Let's read in basic configuration parameters:
EXPERIMENT_NAME = config.experiment_name
EXPERIMENT_FOLDER_PATH = Path(config.experiment_path)
if not EXPERIMENT_FOLDER_PATH.is_dir():
EXPERIMENT_FOLDER_PATH.mkdir( parents=True, exist_ok=True )
BACKUP_CIRCUITS = config.backup_circuits
BACKUP_CIRCUITS_METADATA = config.backup_circuits_metadata
JOB_TAGS = list(config.job_tags)
3.1 Initial steps
To define how many qubits our circuits will contain, and get the indicies of the valid qubits (not all available systems label the availbale qubits sequentially), we need to comunicate with the Amazon Braket Backend. For that puirpose we will use qrem.providers.aws submodule. Let’s connect to the backend and find get valid qubits.
from qrem.providers import aws_braket
aws_device, metadata = aws_braket.get_device(device_full_name = config.device_name, verbose_log = config.verbose_log);
valid_qubit_properties = aws_braket.get_valid_qubits_properties(device=aws_device, threshold=None, verbose_log = config.verbose_log)#config.gate_threshold, verbose_log = config.verbose_log)
number_of_qubits = valid_qubit_properties["number_of_good_qubits"]
good_qubits_indices = valid_qubit_properties["good_qubits_indices"]
Let’s add some infomration to the metadata object, which will be saved in the experiment folder with circuits collection
METADATA = metadata
METADATA["date"] = date_time_formatted()
METADATA["JOB_TAGS"] = JOB_TAGS
if BACKUP_CIRCUITS_METADATA:
METADATA["valid_qubit_properties"] = valid_qubit_properties
3.2 Creating characterisation circuits
Now we have all necessary info to generate our characterisation circuit collection. It is held by qrem specific data structure: qrem.qtypes.CircuitCollection. This implementation is QREM - specific and does not rely on any external packages/implementations.
from qrem.qtypes import CircuitCollection
from qrem.common.experiment import tomography
from qrem.common.printer import warprint
# ----------------------------------------------------------------
#[1] Generate circuits colelction object and fill in parameters
# ----------------------------------------------------------------
qrem_circuit_collection = CircuitCollection()
qrem_circuit_collection.experiment_name = EXPERIMENT_NAME
qrem_circuit_collection.load_config(config=config)
qrem_circuit_collection.qubit_indices = good_qubits_indices
qrem_circuit_collection.metadata = METADATA
# ----------------------------------------------------------------
#[1] Generate circuits colelction
# ----------------------------------------------------------------
qrem_circuit_collection.circuits, _, theoretical_total_circuit_count, theoretical_number_of_shots = tomography.generate_circuits( number_of_qubits = number_of_qubits,
experiment_type = config.experiment_type,
k_locality = config.k_locality,
limited_circuit_randomness = config.limited_circuit_randomness,
imposed_max_random_circuit_count = config.random_circuits_count,
imposed_max_number_of_shots = config.shots_per_circuit)
if BACKUP_CIRCUITS:
qrem_circuit_collection.export_json(str(EXPERIMENT_FOLDER_PATH.joinpath("input_circuit_collection.json")), overwrite = True)
else:
warprint("WARNING: Circuits were not saved to file, as BACKUP_CIRCUITS = False. It is recommended to save circuits to file for future reference.")
Using QREM package, you can add special circuits to the experiment, like circuits for benchmarking, circuits containing ground state approximation for benchmarked hamiltonians or coherenve witness circuits. Tutorial how to prepare them and add them to the mix will be prepared in short future.
3.3 Experiment preparation
As qrem.qtypes.CircuitCollection is a QREM internal object, we will need to translate it to qiskit format, required by the Amazon Braket backened for circuit execution.
For connecting with Amazon Braket, we will send additional files to the Amazon Service, thus it is expecially important to prepare a “Job Submission” folder in which we will output the files to be saved
braket_circuits = aws_braket.translate_circuits_to_braket_format(qrem_circuit_collection,valid_qubit_indices=good_qubits_indices)
SUBMISSION_FOLDER_PATH = Path(config.experiment_path).joinpath("job_submission")
if not SUBMISSION_FOLDER_PATH.is_dir():
EXPERIMENT_FOLDER_PATH.mkdir( parents=True, exist_ok=True )
total_number_of_circuits = len(qrem_circuit_collection.circuits)
print(f"Total number of circuits: {total_number_of_circuits} ")
3.4 Experiment execution
Now we can run circuits on our backend machine. Make always sure that you are using correct AWS Braket instance and available device, and chose correct backend region when connecting at the beggining of the process when setting up local Amazon Environment.
#Now we need to prepare and run circuits
circuits_ready = aws_braket.prepare_cricuits( braket_circuits = braket_circuits,
circuit_collection = qrem_circuit_collection,
good_qubits_indices = good_qubits_indices,
number_of_repetitions = config.shots_per_circuit,
number_of_task_retries = config.aws_braket_task_retries,
experiment_name = EXPERIMENT_NAME,
job_tags = JOB_TAGS,
pickle_submission = config.aws_pickle_results,
metadata = METADATA,
verbose_log = config.verbose_log,
job_dir = SUBMISSION_FOLDER_PATH,
overwrite_output = False)
if not circuits_ready:
print("ERROR during circuit creation, aborting.")
else:
aws_braket.execute_circuits( device_name=config.device_name,
pickle_submission = config.aws_pickle_results,
job_dir=SUBMISSION_FOLDER_PATH)
3.5 Retrieving results
You can check on AWS Braket Platform https://aws.amazon.com/console/, when your jobs and tasks will complete. After they are complete, you can retrieve your results, based on the backed up json file with Circuit Collection.
import orjson
backend, service, provider = ibm.connect(name = config.device_name, method = CONNECTION_METHOD, verbose_log = config.verbose_log)
valid_qubit_properties = ibm.get_valid_qubits_properties(backend, config.gate_threshold)
circuit_collection = CircuitCollection()
circuit_collection.load_json(str(EXPERIMENT_FOLDER_PATH.joinpath("input_circuit_collection.json")))
#task_arns.txt file needs to be downloaded from AWS Braket console after job and tasks are completed! It will be in the results folder at the respective S3 bucket.
circuit_collection.job_IDs = orjson.loads(open(str(EXPERIMENT_FOLDER_PATH.joinpath("task_arns.txt")), 'rb').read())
experiment_results = aws_braket.retrieve_results( task_ARNs = circuit_collection.job_IDs,
original_circuits = circuit_collection,
save_experiment_results = str(EXPERIMENT_FOLDER_PATH.joinpath("ibm_experiment_results.json")),
overwrite = False,
verbose_log = True)