# QUBO formulation

#### Device: Dirac-1

## Introduction

Quadratic Unconstrained Binary Optimization (QUBO) problems are a class of optimization problems where the goal is to maximize or minimize a quadratic objective function without any constraints on the decision variables. Furthermore, QUBO problems serve as fundamental building blocks for more complex optimization models that can address many real-world problems. This tutorial will go through why QUBOs are valuable, what they can do, and how to run such problems on QCi's Dirac entropy computing systems.

## Importance

QUBO encodings tend to naturally arise in quantum settings, or any setting where variable interactions are directly encoded into physical interactions between bits. The reason for this is that physical interactions tend to naturally be two-body, while most constraints in computer science problems do not. For this reason, QUBOs are a natural underlying model to think about when using a wide variety of quantum optimization systems. Since QUBOs are formally NP-hard, it is technically possible to map any optimization problem to a QUBO. Efforts have been made to do so efficiently. For example, this work expresses many common problems as QUBOs with the aim of bridging to quantum technology. QUBOs can be used to construct the kinds of linear constraints often found in optimization problems. Since automatic handling of such constraints is often very useful, we have also developed a related model called Quadratic Linearly Constrained Binary Optimization (QLCBO), which is documented in this tutorial. This lesson however will focus on using the underlying unconstrained model.

## Applications

QUBOs are fundamental in various fields, serving as a basis for formulating discrete combinatorial optimization problems. As an NP-hard problem, QUBO finds numerous applications across diverse domains, including machine learning, operations research, finance, chemistry, medicine, machine learning and beyond. A number of our tutorials and use cases use the QUBO representation directly:

- QBoost: A QUBO Based Binary Classification Method
- Max-Cut Demonstration using Dirac-1
- Dimensionality reduction
- Set Partitioning (A QUBO is sent to the device but linear constraints are constructed directly)

## Mathematical Definition

A Quadratic Unconstrained Binary Optimization (QUBO) defined by second order interactions between binary variables with no constraints, is defined by linear and quadratic terms, but since $x^2 = x$ when $x \in \{0,1\}$, we can simplify the optimization expression as such $f(x) = \sum_{i} \sum_{j} J_{ij} x_i x_j$, where $f: \mathbb{B}^n \rightarrow \mathbb{R}$. Note that the coefficients naturally encode as a square symmetric matrix, so that $f(x) = x^T Q x$, where $Q$ has entries $Q_{ij}$. The goal of the optimization problem is to find the binary vector, $x^{*}$, that minimizes $f(x)$, $x^{*} = \mathop{\arg \min}_{x} x^T Q x$.

## Package Installation and Remote Connection

Begin by importing the necessary packages:

`numpy`

`qci_client`

`os`

In [2]:

- import numpy as np
- from qci_client import QciClient
- import os

Establish connection with `qci_client`

and QCi's server using your unique token ID and our default API URL.

In [3]:

- token = "token_here"
- api_url = "https://api.qci-prod.com"
- qclient = QciClient(api_token=token, url=api_url)

## Uploading a QUBO job

### Data format

To upload a **square symmetric matrix** or QUBO, we encode it in a sparse matrix format as shown below. We use Numpy array notation.

In [4]:

- Q = np.array([[0, -1.5, 0.5], [-1.5, 0, 0], [0.5, 0, 0]])

Enter your file credentials, including the QUBO file.

In [5]:

- qubo_data = {
- 'file_name': "smallest_objective.json",
- 'file_config': {'qubo':{"data": Q}}
- }

### Uploading

To upload the matrix encoded above in `qubo_data`

, we use the `qci_client`

imported previously. The following line

In [7]:

- response_json = qclient.upload_file(file=qubo_data)

The response contains a file_id for the uploaded file. This id is provided when a job is run, along with a few other parameters (see #Running). Note: the same `file_id`

can be used multiple times to run a problem repeatedly. This enables an "upload once, run many times" scheme, which is especially useful for job types in which parameter searches may be involved.
Triggering a job requires two items: first a job body that contains essential and optional metadata for the job, and second, the type of job a user wants to run.

## Running a QUBO job

### Job body

This section defines the job body for a QUBO job. Be sure to state the Dirac device of your choice under `sampler_type`

and the number of samples `nsamples`

required for your job.

In [19]:

- job_body = qclient.build_job_body(job_type="sample-qubo",
- qubo_file_id=response_json['file_id'],
- job_params={"device_type": "dirac-1", "num_samples": 5})
- job_body

Out [19]:

{'job_submission': {'problem_config': {'quadratic_unconstrained_binary_optimization': {'qubo_file_id': '662faead98263204a36541fa'}}, 'device_config': {'dirac-1': {'num_samples': 5}}}}

Once the `job_body`

is complete, use `job_response`

to start running your job on the Dirac device.

In [10]:

- job_response = qclient.process_job(job_body=job_body)
- job_response

Out [ ]:

2024-04-29 15:29:26 - Dirac allocation balance = 0 s (unmetered) 2024-04-29 15:29:26 - Job submitted: job_id='662faec6e15a79bd9d02c485' 2024-04-29 15:29:26 - QUEUED 2024-04-29 15:29:29 - RUNNING 2024-04-29 15:31:05 - COMPLETED 2024-04-29 15:31:08 - Dirac allocation balance = 0 s (unmetered)

Out [10]:

{'job_info': {'job_id': '662faec6e15a79bd9d02c485', 'job_submission': {'problem_config': {'quadratic_unconstrained_binary_optimization': {'qubo_file_id': '662faead98263204a36541fa'}}, 'device_config': {'dirac-1': {'num_samples': 5}}}, 'job_status': {'submitted_at_rfc3339nano': '2024-04-29T14:29:26.546Z', 'queued_at_rfc3339nano': '2024-04-29T14:29:26.547Z', 'running_at_rfc3339nano': '2024-04-29T14:29:26.943Z', 'completed_at_rfc3339nano': '2024-04-29T14:31:04.134Z'}, 'job_result': {'file_id': '662faf2898263204a3654200', 'device_usage_s': 9}}, 'status': 'COMPLETED', 'results': {'counts': [5], 'energies': [-3], 'solutions': [[1, 1, 0]]}}

Below we show how to query the result object if an error occurs:

In [18]:

- def error_status(job_response):
- try:
- if job_response['status'] == "ERROR":
- return job_response['status'], job_response['job_info']['results']['error']
- else:
- return "No errors detected"
- except KeyError:
- return "Error: Unable to retrieve error status information from the job response"
- error_status(job_response)

Out [18]:

'No errors detected'

## Dissecting results

Under `file_config`

, the job title and your results should be present in `solutions`

.

`process_job`

status

Here, we have the `process_job`

presenting the timestamp of each step of the run process.

- Dirac allocation balance = 0
- Job submitted job_id='xxxxx'-: yyyy/mm/dd hh:mm:ss
- queued: yyyy/mm/dd hh:mm:ss
- running: yyyy/mm/dd hh:mm:ss
- completed: yyyy/mm/dd hh:mm:ss
- Dirac allocation balance = 0

`job_response`

status

Details pertaining to the configuration of your job are presented here, including information about the job submitted, `job_info`

, the device chosen, `device_config`

, the status of the job repeated from `process_job`

, `job_status`

, and details of whether the job was completed or uncompleted, `details`

.

- {
- "job_info": {
- "job_id": "6601958b832ac38bab8fe27f",
- "job_submission": {
- "problem_config": {
- "quadratic_unconstrained_binary_optimization": {
- "qubo_file_id": "6601953438d25ec78cae8a65"
- }
- },
- "device_config": {
- "dirac-1": {
- "num_samples": 1
- }
- }
- },
- "job_status": {
- "submitted_at_rfc3339nano": "yyyy-mm-ddThh:mm:ss",
- "queued_at_rfc3339nano": "yyyy-mm-ddThh:mm:ss",
- "running_at_rfc3339nano": "yyyy-mm-ddThh:mm:ss",
- "completed_at_rfc3339nano": "yyyy-mm-ddThh:mm:ss"
- },
- "job_result": {
- "file_id": "660198fc38d25ec78cae8a67",
- "device_usage_s": 1
- },
- "details": {
- "status": "completed"
- }
- },
- }

`job_response`

results

Within the `job_response`

we can identify the `solutions`

, `energies`

, and `counts`

resulting from the job.

`Solutions`

are the binary solutions of the polynomial function.
`Energies`

are values obtained by evaluating a polynomial function at each solution obtained from the optimization process.
`Counts`

are the number of times a solution was found among all the samples taken.

- {
- "results": {
- "file_id": "660198fc38d25ec78cae8a67",
- "num_parts": 1,
- "num_bytes": 235,
- "file_config": {
- "quadratic_unconstrained_binary_optimization_results": {
- "counts": [1],
- "energies": [-3],
- "solutions": [[1, 1, 0]]
- }
- }
- }
- }

## Conclusion

Quadratic Unconstrained Binary Optimization (QUBO) problems are the underlying model used by many quadratic solvers, based on physical interactions being naturally quadratic. Understanding QUBOs is therefore key to understanding how to solve problems on quantum hardware. Sometimes a slightly higher level of abstraction including constraints may also be useful, such as QLCBO as explained in this tutorial. If you feel like you understand QUBOs and want to see how they are applied without engineering constraints, then we encourage you to look at one of the lessons linked above. If however, your application includes constraints, you may wish to proceed to the QLCBO tutorial. Of course, once you are ready, you should try our device with one of your own problems.