Dirac-3 Developer Beginner Guide
Dirac-3 is the latest addition to QCi's Entropy Quantum Computing (EQC) product line, a unique quantum-hardware approach for tackling complex optimization problems. Dirac-3 uses qudits as its unit of quantum information, where each quantum state is represented by dimensions. This means that Dirac-3 can solve problems beyond binary (0,1), including integers and continuous numbers (all positive real rational numbers). For further information on qudits, please read through the Qudit Primer to better understand the benefits of high-dimensional programming. This allows Dirac-3 to solve a variety of important problems including higher order polynomials as well as integer optimization problems. This tutorial will provide an introduction to how to formulate problems for Dirac-3. We will begin by discussing what types of problems Dirac-3 can solve, how to submit problems, and how to analyze results.
To delve deeper into the underlying physics of EQC technology, refer to our paper: An Open Quantum System for Discrete Optimization
Prerequisites: In order to begin running problems with Dirac-3 you will need to:
Dirac-3 as a Continuous Solver
Dirac-3 solves problems of objective function minimization for optimization over discrete spaces by finding the ground state of a complex system with many inter-correlated variables. They correspond to minimizing the expected return of the following objective function:
Where is the value of each continuous variable with the expected resolution of , is the linear return of each variable which must be real numbers, , , , are joint returns of variables which must be real numbers, and is the constraint summation satisfying 1 ≤ . Dirac-3 allows direct submission for minimization only. It is assumed that users perform a simple transformation before submitting the problem to handle maximization (i.e. multiply coefficients by -1). For additional information please refer to our Dirac-3 User Guide.
Formulating and Running Problems on Dirac-3
The following code block uses the qci-client which automatically requests a session token from the API.
In [ ]:
- import numpy as np
- import matplotlib.pyplot as plt
- from qci_client import QciClient
- import os
- api_url = "https://api.qci-prod.com"
- api_token = "<your_secret_token>"
- client = QciClient(api_token=api_token, url=api_url)
Here we'll share a basic problem for submission to Dirac-3. In order to illustrate a standard problem submission a simple polynomial problem will be utilized:
The first step is to extract the polynomial coefficients and format polynomial variable indices for each term in the equation. The polynomial coefficients will be represented as a list as follows:
poly_coefs = [3, 2.1, 1.5, 7.9, 1, 1]
The polynomial indices for the coefficients will be represented as follows in the same order as represented in the original equation:
poly_indices = [[0,0,4], [0,1,1], [0,2,2], [0,2,3], [2,4,4], [3,3,3]]
We'll use the poly_coefficients and poly_indices to generate the data file. We will then use the qci-client (client) that we generated to upload the file.
In [ ]:
- # Let's consider a simple Hamiltonian problem
- poly_indices = [[0,0,4], [0,1,1], [0,2,2], [0,2,3], [2,4,4], [3,3,3]]
- poly_coefs = [3, 2.1, 1.5, 7.9, 1, 1]
- data = [{"idx": idx, "val": val} for idx, val in zip(poly_indices, poly_coefs)]
- file = {
- "file_name": "dirac_3_example",
- "file_config": {
- "polynomial": {
- "num_variables": 6,
- "min_degree": 1,
- "max_degree": 3,
- "data": data,
- }
- }
- }
- file_response = client.upload_file(file=file)
Job Body Parameters
Now we will build the job body with the following parameters:
-
job type: Specifies the type of job to be performed. In this case, ’sample-hamiltonian’ indicates that the job involves creating a Hamiltonian.
-
job name: An optional user-defined string that names the job. Here, it’s set to ’test hamiltonian job’.
-
job tags: An optional list of user-defined string identifiers to tag the job for easier reference and organization. In this example, the tags are [’tag1’, ’tag2’].
-
job params: A dictionary containing parameters for configuring the job and the device. The keys and values specify that the device type is ’dirac-3’, with a relaxation schedule of 1 and a sum constraint of 1.
- sum_constraint: a normalization constraint that is applied to the problem space meaning that solution variables from device must sum to provided value. Value must be between 1 and 10000.
- relaxation_schedule: four different schedules represented by an integer parameter. Higher values reduce the variation in the analog spin values and therefore lead to a better ground state for input problem. Accepts one of the values in the set {1, 2, 3, 4}
- solution_precision: optional parameter that specifies the level of precision to apply to the solutions. If specified a distillation method is applied to the continuous solutions to map them to the submitted solution_precision. R must be divisible by solution_precision. Also, solution_precision must meet following condition
- polynomial file id: The unique identifier for the uploaded polynomial file, retrieved from the file response ’file id’. This ID links the job to the specific problem data.
By preparing the job body in this manner, you set up all necessary configurations and metadata required by the QCi API to process the optimization task on the Dirac-3 device.
In [ ]:
- # Build the job body to be submitted to the QCi API.
- # This is where the job type and the Dirac-3 device and its configuration are specified.
- job_body = client.build_job_body(
- job_type='sample-hamiltonian',
- job_name='test_hamiltonian_job', # user-defined string, optional
- job_tags=['tag1', 'tag2'], # user-defined list of string identifiers, optional
- job_params={'device_type': 'dirac-3', 'relaxation_schedule': 1, 'sum_constraint': 1},
- polynomial_file_id=file_response['file_id'],
- )
Submitting Problems to the API
Now using the job body that we just created we'll submit the job to the QCi API. After submission your job will progress through the following states:
- QUEUED: waiting for the Dirac-3 to become available
- RUNNING: the job has been submitted to the Dirac-3 and is running
- COMPLETED: the job has completed and results are available for analysis
In [ ]:
- # Submit the job and await the result.
- job_response = client.process_job(job_body=job_body)
- assert job_response["status"] == client.JobStatus.COMPLETED.value
- print(
- f"solution: {job_response['results']['solutions'][0]} is "
- f"energy: {job_response['results']['energies'][0]}"
- )
- # This should output something similar to:
- # 2024-05-15 10:59:49 - Dirac allocation balance = 600 s
- # 2024-05-15 10:59:49 - Job submitted: job_id='6644ea05d448b017e54f9663'
- # 2024-05-15 10:59:49 - QUEUED
- # 2024-05-15 10:59:52 - RUNNING
- # 2024-05-15 11:00:46 - COMPLETED
- # 2024-05-15 11:00:48 - Dirac allocation balance = 598 s
- # energy: 12924.37675
- # solution: [73.25, 16.75, 3.45, 6.55]
In the python code above we select to print only the energy and the solution for the job. Where energy and solution are defined as:
- energy - objective value for best solution returned by the device
- solution - a vector representing the solution to the problem from a given run on the Dirac hardware
In [ ]:
- {'job_info': {'job_id': '#######', 'organization_id': '#######',
- 'user_id': '#######', 'job_submission': {'job_name': 'hamiltonian_job_0',
- 'problem_config': {'normalized_qudit_hamiltonian_optimization': {
- 'hamiltonian_file_id': '#######'}},
- 'device_config': {'dirac-3': {'num_samples': 1, 'relaxation_schedule': 2, 'sum_constraint': 1}}},
- 'job_status': {'submitted_at_rfc3339nano': '2024-10-04T19:58:35.921Z', 'queued_at_rfc3339nano':
- '2024-10-04T19:58:35.922Z', 'running_at_rfc3339nano': '2024-10-04T19:58:36.556Z',
- 'completed_at_rfc3339nano': '2024-10-04T19:58:45.084Z'}, 'job_result':
- {'file_id': '670048f55e0855263226d8ac', 'device_usage_s': 8}}, 'status': 'COMPLETED',
- 'results': {'counts': [1], 'energies': [12924.37675],
- 'solutions': [[73.25, 16.75, 3.45, 6.55]]}}
In addition to the energy and the solutions the job_response contains additional information that may be useful:
- job_id: The hash id of the job that was run
- organization_id: The orgagnization the user that ran the job belongs to
- user_id: The hash id of the user running the job
- problem_config: contains all the information about what device the job ran on, the parameters it ran with, and runtime information
- file_id: The file_id that was used to run the job
- results: The energies and solutions that were found for the user's job
As an additional example we'll run QPLIB18 a comprehensive benchmark library designed for evaluating the performance of optimization solvers on quadratic programming (QP) problems.
Let's start by importing the necessary python libraries. Next we'll initialize the client and we'll read in the poly_coefficients and poly_indicies for the problem. We'll use the poly_coefs and poly_indices to generate the data file. We will then use the qci-client (client) that we generated to upload the file.
In [ ]:
- from qci_client import QciClient, JobStatus
- import pkg_resources
- import os
- api_url = "https://api.qci-prod.com"
- api_token = "<your_secret_token>"
- client = QciClient(api_token=api_token, url=api_url)
- with open('qplib_0018_coefficients.csv', 'r') as f:
- poly_coefs = [float(line.strip()) for line in f]
- with open('qplib_0018_indices.csv', 'r') as f:
- poly_indicies = [tuple(map(int, line.strip().split(','))) for line in f]
- data = [{"idx": idx, "val": val} for idx, val in zip(poly_indices, poly_coefs)]
- file = {
- "file_name": "dirac_3_qplib18_example",
- "file_config": {
- "polynomial": {
- "num_variables": 50,
- "min_degree": 1,
- "max_degree": 2,
- "data": data,
- }
- }
- }
- file_response = client.upload_file(file=file)
Now that we've uploaded the file we can generate the job body. The parameters for the job body will be the same as those defined above in the Job Body Parameters section.
In [ ]:
- # Build the job body to be submitted to the QCi API.
- # This is where the job type and the Dirac-3 device and its configuration are specified.
- job_body = client.build_job_body(
- job_type='sample-hamiltonian',
- job_name='test_qplib0018_job', # user-defined string, optional
- job_tags=['tag1', 'tag2'], # user-defined list of string identifiers, optional
- job_params={'device_type': 'dirac-3', 'relaxation_schedule': 1, 'sum_constraint': 1},
- polynomial_file_id=file_response['file_id'],
- )
Now using the job body that we just created we'll submit the job to the QCi API. After submission your job will progress through the QUEUED, RUNNING, and COMPLETED states as defined above in the Submitting Problems to the API section.
In [ ]:
- # Submit the job and await the result.
- job_response = client.process_job(job_body=job_body)
- print(job_response)
The job response for the QPLIB0018 problem should look something like this:
In [ ]:
- {'job_info':
- {'job_id': '67057a80d18290207d13a48f', 'organization_id': '62e8109818e8fbebb2b8d953',
- 'user_id': '63efffebd9573deffdd4dd81', 'job_submission': {'job_name': 'test_qplib0018_job',
- 'job_tags': ['tag1', 'tag2'],
- 'problem_config': {
- 'normalized_qudit_hamiltonian_optimization': {
- 'polynomial_file_id': '67057a7f5e0855263226da8e'}
- },
- 'device_config': {
- 'dirac-3': {
- 'num_samples': 1,
- 'relaxation_schedule': 1,
- 'sum_constraint': 1}
- }
- },
- 'job_status': {
- 'submitted_at_rfc3339nano': '2024-10-08T18:31:28.057Z',
- 'queued_at_rfc3339nano': '2024-10-08T18:31:28.057Z',
- 'running_at_rfc3339nano': '2024-10-08T18:31:28.8Z',
- 'completed_at_rfc3339nano': '2024-10-08T18:31:46.397Z'
- }, 'job_result': {
- 'file_id': '67057a925e0855263226da90', 'device_usage_s': 2}
- },
- 'status': 'COMPLETED', 'results':
- {
- 'counts': [1],
- 'energies': [-5.8639507],
- 'solutions': [[0, 0, 0, 0.0466387, 0, 0, 0, 0.215652, 0, 0, 0, 0, 0,
- 0.2257261, 1e-07, 0, 2e-07, 5e-07, 0, 0, 0, 0, 0, 0, 0, 0, 2e-07,
- 0, 0, 0, 8.54e-05, 0.2333704, 1e-07, 0, 0, 0, 0, 0, 8.76e-05, 0,
- 0.195923, 0, 0, 0, 0, 0, 0, 0.0825149, 0, 0]]
- }
- }