Dirac-3 Quick Start
Device: Dirac-3
Entropy quantum computing (EQC) is a unique tool for optimization leveraging exotic effects in open quantum systems, including the quantum Zeno effect. Most quantum and quantum-inspired programming models, including quantum annealing models, are restricted to qubits. Dirac-3 hybrid quantum optimization machine, 3rd generation of EQC, allows the use of qudits, which are units of quantum information similar to qubits, but each taking more than two possible values. Be sure to read the Qudit Basics to further understand the benefit of high dimensional encoding using qudits.
This tutorial serves as a quick starter guide, offering practical examples to help users learn the solver characteristics, submit problem, and interpret the results from Dirac-3 effectively.
Device - Dirac-3
Dirac-3 solves quadratic Hamiltonians of up to 949 variables. It also solves highly complex problems with three and four-body interaction. For more details, see the user guide.
Tutorial Structure
The goal of this tutorial is to explain how to work with Dirac-3.
The first step is to turn an objective function into a Hamiltonian. It is important that the Hamiltonian is important in Hermitian form. That means the interaction matrices or tensors must be symmetric upon conjugation or transposition operations.
For example, if the objective function is
Then the Hamiltonian in the matrix form shall be (assuming is real)
for state vector . Note that cross-term in the second part of the Hamiltonian is . To code the Hamiltonian into Dirac-3, the linear and quadratic parts are defined separately as
and
These two matrices are input into the sample_hamiltonian
method in the examples below.
In the following, we will use three different examples to illustrate how to prepare and submit problems.
- The first is a simple example to help with understanding the use of the qudits.
- The second example is a polynomial which will be used for demonstrating how different values of the summation constraint can be used to change the solution of the problem.
- The third example is a simple problem which, like the first, is minimized by setting one value to the whole quantity of . The difference is that there are three solutions at optimality. Repeated runs will reveal different solutions.
First, we'll get our Python environment set up. We need numpy
, two methods to sample and evaluate quadratic Hamiltonians, respectively, and the connection to a device.
Environment Configuration
At this point in the tutorials, we should make it as easy as possible to setup the connection to Qatalyst. Depending on the OS you are using, there are different ways of making this setting permanent and visible to all applications.
There are two environment variables that must be present for a connection to Qatalyst to be successful through qci-client
. In the previous tutorials, there were two parameters supplied to QciClient
, but those could be supplied through the environment configuration. This has the advantage of not running the risk of accidentally saving access tokens in a code file.
Add these environment variables to your login environment. See Environment Variables in Windows or the appropriate shell configuration file for your login. Add these variables
QCI_API_URL
- a URL pointing to the Qatalyst environment where you have access, such ashttps://api.qci-prod.com
QCI_TOKEN
- an alphanumeric secret string which identifies you to the Qatalyst API. This should have been made available to you through an email.
In [1]:
- import numpy as np
- import matplotlib.pyplot as plt
- from qci_client import QciClient
- import os
- q1 = QciClient()
The sample_hamiltonian
method takes six required arguments. C
and J
are the linear and quadratic terms of the polynomial. These matrices are dense and the method converts into a sparse format for the process_job
call. The sum_constraint
parameter is a value that indicates the total returned given solution vector must add up to. The schedule
parameter takes four different values, 1, 2, 3 or 4.
- This is the quickest execution. It has the lowest probability of obtaining high-quality solutions.
- This execution takes more time, likely a few seconds, but has a higher probability of finding good solutions.
- This option may take tens of seconds. The quality of these solutions is expected to be higher.
- This is the longest-running relaxation option. It takes up to multiple minutes to run and has the highest probability of returning quality solutions.
The solution_precision
parameter is a value that indicates how granular a solution should be. Use a value of 1 for integer solutions. Use decimals such as 0.1 or 0.05 for higher precision with the highest precision accepted being 0.00001. Specify a connected EqcClient
in the client
parameter. Suppress print statements in the call using a value of True
for suppress_output
.
In [2]:
- def sample_hamiltonian(C : np.ndarray, J : np.ndarray, sum_constraint : float, schedule : int, solution_precision : float, client : QciClient):
- n = C.shape[0]
- H = np.hstack([C.reshape([n, 1]), J])
- ham_file = {"file_name": "qudit-tutorial-hame", "file_config": {"hamiltonian": {"data": H}}}
- file_id = client.upload_file(file=ham_file)["file_id"]
- job_tags = ["qudit-tutorial"]
- job_body = client.build_job_body(job_type="sample-hamiltonian", hamiltonian_file_id=file_id,
- job_params={"device_type": "dirac-3", "num_samples": 1, "solution_precision": solution_precision,
- "sum_constraint": sum_constraint, "relaxation_schedule": schedule}, job_tags=job_tags)
- response = client.process_job(job_body=job_body, wait=True)
- return response
- def get_results(response):
- if "results" in response and response["results"] is not None:
- results = response["results"]
- else:
- if "job_info" in response and "job_result" in response["job_info"]:
- details = response["job_info"]["job_result"]
- else:
- details = None
- raise RuntimeError(f"Execution failed. See details: {details}")
- return results
Qudit Domain
Dirac-3 uses qudits to solve discrete optimization problems.
The first example is a three-variable problem with uniform two-body interaction, whose solution corresponds to one variable taking on the total value of the summation constraint and the rest taking the value 0.
In the coding please note the dtype
of float32
. This is the digital precision for floating point numbers used in the gRPC protocol. A warning will be raised if higher precision decimal values are used.
In [3]:
- h = np.array([
- [-1],
- [0],
- [0]
- ], dtype=np.float32
- )
- J= np.array([
- [0, 1, 1],
- [1, 0, 1],
- [1, 1, 0]
- ], dtype=np.float32
- )
- h, J
Out [3]:
(array([[-1.], [ 0.], [ 0.]], dtype=float32), array([[0., 1., 1.], [1., 0., 1.], [1., 1., 0.]], dtype=float32))
In [4]:
- response = sample_hamiltonian(h, J, 400, 1, 0.1, q1)
- if "job_info" in response:
- print("Status:", response["job_info"]["job_status"])
- results = get_results(response)
- print("Energy:", results["energies"][0])
- print("Solution")
- print(results["solutions"][0])
- solution = np.array(results["solutions"][0])
- print("Solution Value (should match energy)", h.T@solution + solution.T@J@solution)
- x = np.array([400, 0, 0])
- print("Known ground state", h.T@x + x.T@J@x)
- print("With solution", x)
- else:
- print(response)
Out [ ]:
2024-06-18 15:49:33 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:49:34 - Job submitted: job_id='667200eea3e6a645a5c4e7b0' 2024-06-18 15:49:34 - QUEUED 2024-06-18 15:55:02 - RUNNING 2024-06-18 15:55:04 - COMPLETED 2024-06-18 15:55:07 - Dirac allocation balance = 0 s (unmetered) Status: {'submitted_at_rfc3339nano': '2024-06-18T21:49:34.087Z', 'queued_at_rfc3339nano': '2024-06-18T21:49:34.088Z', 'running_at_rfc3339nano': '2024-06-18T21:55:01.293Z', 'completed_at_rfc3339nano': '2024-06-18T21:55:03.08Z'} Energy: 0 Solution [0, 0, 400] Solution Value (should match energy) [0.] Known ground state [-400.] With solution [400 0 0]
Dynamic Range
Solution precision is dictated by the machine sensitivity and precision, including those of the photon-counting modules and optoelectronic devices, and shot-noise limited quantum measurements. Dirac-3 has a dynamic range of at least 23 dB. This means that Hamiltonian coefficients having an absolute value smaller than ~1/200 of the peak coefficient value may not be recognized and will be effectively treated as 0 when encoding the Hamiltonian. In some cases, the dynamic range can reach as high as 40 dB. However, it is not guaranteed.
During computing cycles, each qudit takes discrete numbers of levels, which is at least 200, but can reach 10,000, as limited by the 23 dB or 40 dB dynamic range. At the output, however, the optimal solutions may consist of variables that have fractional components, as a result of normalization to the summation constraint. Thus when the number of levels is 200, each qudit value will be a multiple of R/200.
Device sensitivity is not a controllable parameter and so the number of levels is not a part of problem formulation. More details on this are available in the Dirac-3 User Guide.
Summation Constraint
The summation constraint is required to be some value between 1 and 10,000. This restriction is a property of Dirac-3.1 and not EQC in general. This constraint is helpful in some models and a formulation hurdle in others. In all formulations of TSP, for instance, the sum of a final solution is known beforehand. A Hamiltonian cycle (another one of those confusing terminologies that shows up when mathematicians and physicists work together) has exactly edges, where is the number of nodes in a graph. This means that solving a TSP with Dirac-3 requires setting the sum constraint equal to the number of nodes in the graph.
Other cases where the sparsity or sum of the solution is not known before solving can be solved with a couple of different approaches.
Machine Slack Qudits
When formulating a model, it can be advantageous to add slack variables. To use this method when setting a summation constraint, formulate the Hamiltonian like normal, then determine a lower bound and an upper bound on the sum of the qudit values. Introduce an additional qudit into the formulated model which can sum to the upper bound minus the lower bound. This additional variable in the model will be ignored in the solution. See some of the more indepth examples, like Max Cut, to understand slack qudits in action.
Minimizing a simple polynomial
Let . This polynomial itself has no lower bound, but as we put a summation constraint, it does. If , then has a minimum value that changes negatively with and changes the cardinality of . Below are the results for different values.
In [5]:
- h = np.array([[-10.],
- [ 0.],
- [-10.]], dtype=np.float32)
- J = np.array([[0., 0., -2.5],
- [0., -4., 0. ],
- [-2.5, 0., 0. ]], dtype=np.float32)
- for S in [2, 3, 4, 5, 6, 7]:
- print("**********************************")
- print(f"S={S}")
- response = sample_hamiltonian(h, J, S, 3, 0.1, q1)
- results = get_results(response)
- print("Status:", response["job_info"]["job_status"])
- results = get_results(response)
- print("Energy:", results["energies"][0])
- print("Solution")
- print(results["solutions"][0])
- print()
Out [ ]:
********************************** S=2 2024-06-18 15:55:07 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:55:07 - Job submitted: job_id='6672023ba3e6a645a5c4e7b1' 2024-06-18 15:55:07 - QUEUED 2024-06-18 15:55:10 - RUNNING 2024-06-18 15:55:56 - COMPLETED 2024-06-18 15:55:59 - Dirac allocation balance = 0 s (unmetered) Status: {'submitted_at_rfc3339nano': '2024-06-18T21:55:07.919Z', 'queued_at_rfc3339nano': '2024-06-18T21:55:07.92Z', 'running_at_rfc3339nano': '2024-06-18T21:55:08.236Z', 'completed_at_rfc3339nano': '2024-06-18T21:55:55.211Z'} Energy: -25.0000019 Solution [0.9994924, 0, 1.0005077] ********************************** S=3 2024-06-18 15:55:59 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:55:59 - Job submitted: job_id='6672026fa3e6a645a5c4e7b2' 2024-06-18 15:55:59 - QUEUED 2024-06-18 15:56:02 - RUNNING 2024-06-18 15:56:33 - COMPLETED 2024-06-18 15:56:35 - Dirac allocation balance = 0 s (unmetered) Status: {'submitted_at_rfc3339nano': '2024-06-18T21:55:59.831Z', 'queued_at_rfc3339nano': '2024-06-18T21:55:59.832Z', 'running_at_rfc3339nano': '2024-06-18T21:56:00.46Z', 'completed_at_rfc3339nano': '2024-06-18T21:56:32.401Z'} Energy: -41.25 Solution [1.5001745, 0, 1.4998254] ********************************** S=4 2024-06-18 15:56:36 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:56:36 - Job submitted: job_id='66720294a3e6a645a5c4e7b3' 2024-06-18 15:56:36 - QUEUED 2024-06-18 15:56:39 - RUNNING 2024-06-18 15:57:09 - COMPLETED 2024-06-18 15:57:12 - Dirac allocation balance = 0 s (unmetered) Status: {'submitted_at_rfc3339nano': '2024-06-18T21:56:36.488Z', 'queued_at_rfc3339nano': '2024-06-18T21:56:36.488Z', 'running_at_rfc3339nano': '2024-06-18T21:56:36.561Z', 'completed_at_rfc3339nano': '2024-06-18T21:57:08.527Z'} Energy: -60 Solution [1.9999999, 0, 2] ********************************** S=5 2024-06-18 15:57:12 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:57:12 - Job submitted: job_id='667202b9a3e6a645a5c4e7b4' 2024-06-18 15:57:13 - QUEUED 2024-06-18 15:57:15 - RUNNING 2024-06-18 15:57:49 - COMPLETED 2024-06-18 15:57:51 - Dirac allocation balance = 0 s (unmetered) Status: {'submitted_at_rfc3339nano': '2024-06-18T21:57:13.028Z', 'queued_at_rfc3339nano': '2024-06-18T21:57:13.028Z', 'running_at_rfc3339nano': '2024-06-18T21:57:13.777Z', 'completed_at_rfc3339nano': '2024-06-18T21:57:46.875Z'} Energy: -100 Solution [2e-07, 5, 1e-07] ********************************** S=6 2024-06-18 15:57:52 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:57:52 - Job submitted: job_id='667202e0a3e6a645a5c4e7b5' 2024-06-18 15:57:52 - QUEUED 2024-06-18 15:57:54 - RUNNING 2024-06-18 15:58:28 - COMPLETED 2024-06-18 15:58:30 - Dirac allocation balance = 0 s (unmetered) Status: {'submitted_at_rfc3339nano': '2024-06-18T21:57:52.31Z', 'queued_at_rfc3339nano': '2024-06-18T21:57:52.311Z', 'running_at_rfc3339nano': '2024-06-18T21:57:53.123Z', 'completed_at_rfc3339nano': '2024-06-18T21:58:26.264Z'} Energy: -105 Solution [3.0002966, 0, 2.9997036] ********************************** S=7 2024-06-18 15:58:31 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:58:31 - Job submitted: job_id='66720307a3e6a645a5c4e7b6' 2024-06-18 15:58:31 - QUEUED 2024-06-18 15:58:34 - RUNNING 2024-06-18 15:59:07 - COMPLETED 2024-06-18 15:59:10 - Dirac allocation balance = 0 s (unmetered) Status: {'submitted_at_rfc3339nano': '2024-06-18T21:58:31.551Z', 'queued_at_rfc3339nano': '2024-06-18T21:58:31.551Z', 'running_at_rfc3339nano': '2024-06-18T21:58:32.511Z', 'completed_at_rfc3339nano': '2024-06-18T21:59:05.556Z'} Energy: -131.25 Solution [3.5007405, 0, 3.4992595]
For reference, here is a job response that is returned in JSON from the REST API
In [6]:
- response
Out [6]:
{'job_info': {'job_id': '66720307a3e6a645a5c4e7b6', 'job_submission': {'job_tags': ['qudit-tutorial'], 'problem_config': {'normalized_qudit_hamiltonian_optimization': {'hamiltonian_file_id': '6672030798263204a365dd81'}}, 'device_config': {'dirac-3': {'num_samples': 1, 'relaxation_schedule': 3, 'solution_precision': 0.1, 'sum_constraint': 7}}}, 'job_status': {'submitted_at_rfc3339nano': '2024-06-18T21:58:31.551Z', 'queued_at_rfc3339nano': '2024-06-18T21:58:31.551Z', 'running_at_rfc3339nano': '2024-06-18T21:58:32.511Z', 'completed_at_rfc3339nano': '2024-06-18T21:59:05.556Z'}, 'job_result': {'file_id': '6672032998263204a365dd83', 'device_usage_s': 33}}, 'status': 'COMPLETED', 'results': {'counts': [1], 'energies': [-131.25], 'solutions': [[3.5007405, 0, 3.4992595]], 'distilled_energies': [-131.25], 'distilled_solutions': [[3.5, 0, 3.5]]}}
Degeneracy Demonstration
Dirac-3 will return various solutions to a problem with multiple solutions near the optimal value.
Let's see what happens with a clearly degenerate problem. Take the first example modified to give the same weight to all the linear terms. Each solution: [S, 0, 0]
, [0, S, 0]
and [0, 0, S]
give the same value for the Hamiltonian.
In [7]:
- h = np.array([[-1],
- [-1],
- [-1]], dtype=np.float32)
- J = np.array([[0, 1, 1],
- [1, 0, 1],
- [1, 1, 0]], dtype=np.float32)
In [8]:
- super_test = {}
- energies_list = []
- for i in range(10):
- response = sample_hamiltonian(h, J, 400, 2, 0.1, q1)
- results = get_results(response)
- energies_list.append(results["energies"][0])
- solution = [round(v, 1) for v in results["solutions"][0]]
- solution = tuple(solution)
- if solution in super_test:
- super_test[solution] += 1
- else:
- super_test[solution] = 1
- print("Solution | Frequency")
- for solution in super_test:
- print(solution, " ", "*"*super_test[solution])
- labels = list(super_test.keys())
- sizes = [super_test[key] for key in labels]
- fig, ax = plt.subplots()
- patches, labels = ax.pie(sizes, labels=labels)
Out [ ]:
2024-06-18 15:59:10 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:59:10 - Job submitted: job_id='6672032ea3e6a645a5c4e7b7' 2024-06-18 15:59:10 - QUEUED 2024-06-18 15:59:13 - RUNNING 2024-06-18 15:59:21 - COMPLETED 2024-06-18 15:59:23 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:59:24 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:59:24 - Job submitted: job_id='6672033ca3e6a645a5c4e7b8' 2024-06-18 15:59:24 - QUEUED 2024-06-18 15:59:26 - RUNNING 2024-06-18 15:59:34 - COMPLETED 2024-06-18 15:59:37 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:59:37 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:59:37 - Job submitted: job_id='66720349a3e6a645a5c4e7b9' 2024-06-18 15:59:37 - QUEUED 2024-06-18 15:59:40 - RUNNING 2024-06-18 15:59:48 - COMPLETED 2024-06-18 15:59:50 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:59:51 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 15:59:51 - Job submitted: job_id='66720357a3e6a645a5c4e7ba' 2024-06-18 15:59:51 - QUEUED 2024-06-18 15:59:54 - RUNNING 2024-06-18 16:00:01 - COMPLETED 2024-06-18 16:00:04 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:00:05 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:00:05 - Job submitted: job_id='66720365a3e6a645a5c4e7bb' 2024-06-18 16:00:05 - QUEUED 2024-06-18 16:00:07 - RUNNING 2024-06-18 16:00:15 - COMPLETED 2024-06-18 16:00:17 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:00:18 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:00:18 - Job submitted: job_id='66720372a3e6a645a5c4e7bc' 2024-06-18 16:00:18 - QUEUED 2024-06-18 16:00:21 - RUNNING 2024-06-18 16:00:28 - COMPLETED 2024-06-18 16:00:31 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:00:32 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:00:32 - Job submitted: job_id='66720380a3e6a645a5c4e7bd' 2024-06-18 16:00:32 - QUEUED 2024-06-18 16:00:34 - RUNNING 2024-06-18 16:00:57 - COMPLETED 2024-06-18 16:01:00 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:01:01 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:01:01 - Job submitted: job_id='6672039da3e6a645a5c4e7be' 2024-06-18 16:01:01 - QUEUED 2024-06-18 16:01:03 - RUNNING 2024-06-18 16:01:11 - COMPLETED 2024-06-18 16:01:13 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:01:14 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:01:14 - Job submitted: job_id='667203aaa3e6a645a5c4e7bf' 2024-06-18 16:01:14 - QUEUED 2024-06-18 16:01:17 - RUNNING 2024-06-18 16:01:24 - COMPLETED 2024-06-18 16:01:27 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:01:28 - Dirac allocation balance = 0 s (unmetered) 2024-06-18 16:01:28 - Job submitted: job_id='667203b8a3e6a645a5c4e7c0' 2024-06-18 16:01:28 - QUEUED 2024-06-18 16:01:30 - RUNNING 2024-06-18 16:01:38 - COMPLETED 2024-06-18 16:01:41 - Dirac allocation balance = 0 s (unmetered) Solution | Frequency (0, 0, 400) ***** (400, 0, 0) **** (0, 400, 0) *
Out [ ]:
<Figure size 640x480 with 1 Axes>