Entropy Computing via QCi¶
David E. Bernal NeiraDavidson School of Chemical Engineering, Purdue University
Albert Lee
Davidson School of Chemical Engineering, Purdue University
Graduate Research Assistant
Entropy Computing via QCi¶
This notebook provides an introduction to solving constrained optimization problems using Quantum Computing Inc.'s (QCI) Entropy Quantum Computer. We will define a Constrained Quadratic Model by setting up an objective function and a series of linear constraints. The notebook demonstrates how to:
Structure the problem using QCI’s
eqc_modelslibrary.Submit the model to the
Dirac3IntegerCloudSolverandDirac3ContinuousSolverfor processing.Analyze the high-quality candidate solutions returned by the solver to find the optimal result.
# If using this on Google colab, we need to install the packages
try:
import google.colab
IN_COLAB = True
except:
IN_COLAB = False
if IN_COLAB:
!pip install eqc_models pyomo# Import the QCI models and solvers
import eqc_models
from eqc_models.base import ConstrainedPolynomialModel, PolynomialModel, QuadraticModel
from eqc_models.solvers import Dirac3IntegerCloudSolver, Dirac3ContinuousCloudSolver
# Import Matplotlib to generate plots
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
# Import numpy and scipy for certain numerical calculations below
import numpy as np
from scipy.special import gamma
import pyomo.environ as pyo
import osif IN_COLAB:
!pip install idaes-pse --pre
!idaes get-extensions --to ./bin
os.environ['PATH'] += ':bin'How Dirac-3 Works¶
The Dirac-3 solver is a cloud-based quantum solver that uses the Entropy Quantum Computer (EQC) to solve optimization problems. It encodes a problem into the physical properties of coherent light pulses, allowing it to represent and explore many potential solutions at once within its optical circuits. A controlled feedback loop, managed by electronics, interacts with the light to steer the system away from poor solutions. This process rapidly converges the system onto the single, lowest-energy state, which represents the optimal answer.
# Define the API URL and token for QCI
# Note: Replace the api_url and api_token with your actual QCI API URL and token
api_url = "https://api.qci-prod.com"
api_token = "" # Replace with your actual API tokenSolving Problems using Dirac-3¶
Dirac-3 can solve combinatorial optimization problems using the follwing objective function, E, that has the following high-order polynomial structure:
refers to the value of nonnegative variable, and , , , , and are the coefficients of the objective function. The solver defaults to minimization. To solve a maximization problem, you should multiply the entire objective function by -1.
Continuous Solver¶
The Dirac-3 continuous solver is particularly well-suited for this objective function, treating it as a quasi-continuous optimization problem. In this approach, the solver operates under the linear constraint
Note that is a fixed value, which for this problem must be in the range of [1, 10000].
Let’s try to solve the following simple quadratic problem with linear constraints using the Dirac-3 continuous solver:
# 1. Create a concrete Pyomo model
m = pyo.ConcreteModel(name="Simple_Quadratic_Program")
# 2. Define the decision variables with their bounds
# The variables x_1, x_2, and x_3 are defined with bounds [0, 10].
m.x = pyo.Var([1, 2, 3], domain=pyo.NonNegativeReals, bounds=(0, 10))
# 3. Define the objective function
# The objective is to minimize the quadratic expression.
m.obj = pyo.Objective(
expr=3*m.x[1]**2 + 2*m.x[2]**2 + m.x[3]**2,
sense=pyo.minimize
)
# 4. Define the linear constraint
# The sum of the variables must equal 10.
m.c1 = pyo.Constraint(expr=m.x[1] + m.x[2] + m.x[3] == 10)
# 5. Create a solver instance and solve the model
# We specify 'ipopt' as the solver. 'tee=True' displays the solver's log.
solver = pyo.SolverFactory('ipopt')
results = solver.solve(m, tee=False)
# 6. Display the optimization results
print("\n" + "="*30)
print("-- 📊 Optimization Results --")
print(f"Solver Status: {results.solver.status}")
print(f"Termination Condition: {results.solver.termination_condition}")
print(f"Objective Value (E): {pyo.value(m.obj):.4f}")
print("\n-- 🏗️ Variable Values --")
for i in m.x:
print(f"x[{i}] = {pyo.value(m.x[i]):.4f}")
print("="*30)
==============================
-- 📊 Optimization Results --
Solver Status: ok
Termination Condition: optimal
Objective Value (E): 54.5455
-- 🏗️ Variable Values --
x[1] = 1.8183
x[2] = 2.7272
x[3] = 5.4545
==============================
Now let’s solve this problem using the Dirac-3 continuous solver.¶
1. Defining the Objective Function¶
The polynomial objective function, E, is defined by combining two lists: coefficients and indices.
coefficients: This list contains the numerical multiplier for each term in your objective function.coefficients = [3, 2, 1]indices: This list of tuples specifies which variables are multiplied together for each term. The numbers in the tuples correspond to the variable indices (e.g.,1for ,2for ).# (1,1) -> x_1*x_1 | (2,2) -> x_2*x_2 | (3,3) -> x_3*x_3 indices = [(1,1), (2,2), (3,3)]
The PolynomialModel maps the coefficients to the indices in order, creating the full objective function:
2. Setting Constraints¶
Constraints are handled in two different places:
Variable Bounds
upper_bound: The individual upper bounds for each variable are set as an attribute on themodelobject after it has been created.# Sets the upper bound for all 3 variables to 10 model.upper_bound = 10 * np.ones((3,))
R = 10
coefficients = [3, 2, 1]
indices = [(1,1), (2,2), (3,3)] # Create a polynomial model
model = PolynomialModel(coefficients, indices)
model.upper_bound = R*np.ones((3,)) # Bounds for the variables3. Submitting the Model to the Solver¶
When submitting the model to the solver, you need to provide several key parameters:
sum_constraint: The constraint on the sum of all variables () is a required input for thesolver.solve()method.relaxation_schedule: An integer from the set{1, 2, 3, 4}representing one of four predefined schedules. Higher values reduce variation in the analog spin values during the solving process and are more likely to result in a better objective function value.num_samples: An integer specifying the number of independent solutions (samples) to be generated by the device.
# The solver needs the value for R, the schedule, and number of samples
response = solver.solve(
model,
sum_constraint=10,
relaxation_schedule=1,
num_samples=10
)solver = Dirac3ContinuousCloudSolver(url=api_url, api_token=api_token)
response = solver.solve(model, sum_constraint=10, relaxation_schedule=1, num_samples=20) # sum constraint is set to 10, 5 samples are collected
# Print the results
print(response["results"]["solutions"])
print(response["results"]["energies"])
print(response["results"]["counts"])2025-08-18 15:27:39 - Dirac allocation balance = 9645 s (unmetered)
2025-08-18 15:27:39 - Job submitted: job_id='68a37eab8060c9339796334d'
2025-08-18 15:27:39 - QUEUED
2025-08-18 15:27:42 - RUNNING
2025-08-18 15:27:44 - COMPLETED
2025-08-18 15:27:47 - Dirac allocation balance = 9645 s (unmetered)
[[1.8597407, 2.7621448, 5.3781147], [1.8901105, 2.7242651, 5.3856239], [1.8217838, 2.8303771, 5.3478389], [1.9145035, 2.722614, 5.3628826], [1.9317503, 2.7126865, 5.3555632], [1.7030318, 2.71842, 5.5785484], [1.942136, 2.6797423, 5.3781223], [1.9522564, 2.67962, 5.3681231], [1.6953294, 2.8521452, 5.4525251], [1.8016272, 2.5664244, 5.6319485], [1.6546638, 2.730413, 5.614924], [1.7797163, 2.9434371, 5.2768469], [1.6292859, 2.7642462, 5.6064677], [1.9134034, 2.8933036, 5.1932931], [1.8563873, 2.9376242, 5.2059884], [1.8356512, 2.972044, 5.1923056], [2.0200472, 2.779747, 5.2002058], [1.5742502, 2.8361845, 5.5895658], [1.7902411, 3.0133739, 5.1963849], [1.5251429, 2.9664829, 5.5083733]]
[54.5589104, 54.5657387, 54.5781403, 54.5817337, 54.5943718, 54.600769, 54.6019135, 54.6113892, 54.6219215, 54.6294937, 54.6514168, 54.6749268, 54.6783142, 54.6960449, 54.7001076, 54.7349739, 54.7378998, 54.7659225, 54.7781487, 54.9203987]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
print(response){'job_info': {'job_id': '68a37eab8060c9339796334d', 'job_submission': {'problem_config': {'normalized_qudit_hamiltonian_optimization': {'polynomial_file_id': '68a37eabacc178773e9a33f8'}}, 'device_config': {'dirac-3_normalized_qudit': {'num_samples': 20, 'relaxation_schedule': 1, 'sum_constraint': 10}}}, 'job_status': {'submitted_at_rfc3339nano': '2025-08-18T19:27:39.467Z', 'queued_at_rfc3339nano': '2025-08-18T19:27:39.468Z', 'running_at_rfc3339nano': '2025-08-18T19:27:39.802Z', 'completed_at_rfc3339nano': '2025-08-18T19:27:42.809Z'}, 'job_result': {'file_id': '68a37eaeacc178773e9a33fa', 'device_usage_s': 2}}, 'status': 'COMPLETED', 'results': {'counts': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'energies': [54.5589104, 54.5657387, 54.5781403, 54.5817337, 54.5943718, 54.600769, 54.6019135, 54.6113892, 54.6219215, 54.6294937, 54.6514168, 54.6749268, 54.6783142, 54.6960449, 54.7001076, 54.7349739, 54.7378998, 54.7659225, 54.7781487, 54.9203987], 'solutions': [[1.8597407, 2.7621448, 5.3781147], [1.8901105, 2.7242651, 5.3856239], [1.8217838, 2.8303771, 5.3478389], [1.9145035, 2.722614, 5.3628826], [1.9317503, 2.7126865, 5.3555632], [1.7030318, 2.71842, 5.5785484], [1.942136, 2.6797423, 5.3781223], [1.9522564, 2.67962, 5.3681231], [1.6953294, 2.8521452, 5.4525251], [1.8016272, 2.5664244, 5.6319485], [1.6546638, 2.730413, 5.614924], [1.7797163, 2.9434371, 5.2768469], [1.6292859, 2.7642462, 5.6064677], [1.9134034, 2.8933036, 5.1932931], [1.8563873, 2.9376242, 5.2059884], [1.8356512, 2.972044, 5.1923056], [2.0200472, 2.779747, 5.2002058], [1.5742502, 2.8361845, 5.5895658], [1.7902411, 3.0133739, 5.1963849], [1.5251429, 2.9664829, 5.5083733]]}}
4. Interpreting the Results¶
The solver returns a dictionary containing job information and results. The optimal solution is found by identifying the result with the lowest “energy” (objective function value).
The response['results'] key contains three important lists that correspond to each other by index:
solutions: A list of all potential solutions. Each item is a list of variable values (e.g.,[x1, x2, x3]).energies: A list of the objective function values (energies) for each corresponding solution.counts: A list of how many times each solution was found (typically 1 for unique solutions).
# Extract the results from the response
results = response["results"]
energies = results["energies"]
solutions = results["solutions"]
# Find the index of the best solution (minimum energy)
best_index = np.argmin(energies)
# Get the best solution and its energy
best_energy = energies[best_index]
best_solution = solutions[best_index]
# Print the results in a clear format
print("-- 📊 Best Solution Found --")
print(f"Optimal Objective (Energy): {best_energy:.4f}")
print(f"Variable Values: {np.round(best_solution, 4)}")
-- 📊 Best Solution Found --
Optimal Objective (Energy): 54.5477
Variable Values: [1.8302 2.6994 5.4704]
Integer Solver¶
The Dirac-3 can solver unconstrained optimization problem. In this mode, the resulting state vector consists of integers:
For integer problems, you must define the number of possible states (or levels) for each variable. For instance, to model a standard Qudratic Unconstrained Binary Optimization problem, you would set the number of levels for each variable to 2. This restricts the variables to two possible states, 0 and 1, which is equivalent to defining a binary variable with an upper bound of 1.
The solver is flexible, allowing for mixed encoding where some variables have 2 levels (binary) while others have more (up to 17 levels, for an upper bound of 16). If your problem requires more than 17 distinct states for any variable, the continuous solver is the recommended approach.
Let’s solve the simple QUBO problem given by the following objective function:
# 1. Create a concrete Pyomo model
m = pyo.ConcreteModel(name="Simple_QUBO")
# 2. Define the binary variables
# x[1] and x[2] can only take values of 0 or 1.
m.x = pyo.Var([1, 2], domain=pyo.Binary)
# 3. Define the objective function
# We use the property that for a binary variable x, x^2 = x.
# So, the objective E = -0.75*x1^2 - 0.25*x2^2 + 2*x1*x2 + 0.5*x1 becomes:
# E = -0.75*x1 - 0.25*x2 + 2*x1*x2
m.obj = pyo.Objective(
expr= -0.75 * m.x[1] - 0.25 * m.x[2] + 2 * m.x[1] * m.x[2] + 0.5 * m.x[1],
sense=pyo.minimize
)
# 4. Create a solver instance and solve the model
# This is an unconstrained problem, so no constraints are needed.
# We use a Mixed-Integer Programming (MINLP) solver like 'bonmin'.
solver = pyo.SolverFactory('bonmin')
results = solver.solve(m, tee=False)
# 5. Display the optimization results
print("\n" + "="*30)
print("-- 📊 Optimization Results --")
print(f"Solver Status: {results.solver.status}")
print(f"Termination Condition: {results.solver.termination_condition}")
print(f"Objective Value: {pyo.value(m.obj):.4f}")
print("\n-- 🏗️ Variable Values --")
for i in m.x:
print(f"x[{i}] = {int(pyo.value(m.x[i]))}")
print("="*30)
==============================
-- 📊 Optimization Results --
Solver Status: ok
Termination Condition: optimal
Objective Value: -0.2500
-- 🏗️ Variable Values --
x[1] = 1
x[2] = 0
==============================
Now let’s solve this problem using the Dirac-3 Integer solver.¶
The problem’s coefficients and indices are structured in the same way as they were for the continuous solver.
coefficients = [-0.75, -0.25, 2, 0.5] # Coefficients for the polynomial model
indices = [(1, 1), (2, 2), (1, 2), (0, 1)] # Create a polynomial model
model = PolynomialModel(coefficients, indices)
# add upper bounds for the variables
model.upper_bound = np.ones((2,)) # Bounds for the variablesFor integer and binary optimization problems, the sum_constraint is not necessary because Dirac3IntegerCloudSover operates in an unconstrained mode.
solver = Dirac3IntegerCloudSolver(url=api_url, api_token=api_token)
response = solver.solve(model, relaxation_schedule=1, num_samples=100)
# Print the results
print(response)
print(response["results"]["solutions"])
print(response["results"]["energies"])
print(response["results"]["counts"])2025-08-18 14:45:08 - Dirac allocation balance = 9645 s (unmetered)
2025-08-18 14:45:08 - Job submitted: job_id='68a374b48060c93397963349'
2025-08-18 14:45:08 - QUEUED
2025-08-18 14:45:11 - RUNNING
2025-08-18 14:45:32 - COMPLETED
2025-08-18 14:45:34 - Dirac allocation balance = 9645 s (unmetered)
{'job_info': {'job_id': '68a374b48060c93397963349', 'job_submission': {'problem_config': {'qudit_hamiltonian_optimization': {'polynomial_file_id': '68a374b4acc178773e9a33e8'}}, 'device_config': {'dirac-3_qudit': {'num_levels': [2, 2], 'num_samples': 100, 'relaxation_schedule': 1}}}, 'job_status': {'submitted_at_rfc3339nano': '2025-08-18T18:45:08.772Z', 'queued_at_rfc3339nano': '2025-08-18T18:45:08.772Z', 'running_at_rfc3339nano': '2025-08-18T18:45:09.505Z', 'completed_at_rfc3339nano': '2025-08-18T18:45:32.039Z'}, 'job_result': {'file_id': '68a374ccacc178773e9a33ea', 'device_usage_s': 15}}, 'status': 'COMPLETED', 'results': {'counts': [78, 22], 'energies': [-0.25, -0.25], 'solutions': [[0, 1], [1, 0]]}}
[[0, 1], [1, 0]]
[-0.25, -0.25]
[78, 22]
This code processes the solver’s results by counting the frequency of each unique solution and then uses matplotlib to create a bar chart visualizing this distribution.
def plot_solution_distribution(response):
"""
Processes a solver response and plots the distribution of unique solutions.
Args:
response (dict): The response dictionary from the Dirac-3 solver,
containing 'solutions' and 'counts' in its 'results' key.
"""
# --- Data Processing ---
# Extract the raw results from the response dictionary
results = response.get('results', {})
raw_solutions = results.get('solutions', [])
raw_counts = results.get('counts', [])
if not raw_solutions or not raw_counts:
print("Response object does not contain valid solutions or counts.")
return
# Aggregate counts for each unique solution
solution_counts = {}
for solution, count in zip(raw_solutions, raw_counts):
# Convert list to tuple to use it as a dictionary key
solution_tuple = tuple(solution)
solution_counts[solution_tuple] = solution_counts.get(solution_tuple, 0) + count
# Prepare data for plotting
# Create string labels for the x-axis from the solution tuples
plot_solutions_str = [str(list(s)) for s in solution_counts.keys()]
plot_counts = list(solution_counts.values())
# --- Plotting ---
plt.style.use('seaborn-v0_8-whitegrid')
fig, ax = plt.subplots(figsize=(8, 7)) # Increased height for rotated labels
# Create the bar plot
bars = ax.bar(plot_solutions_str, plot_counts, color='#4A90E2', width=0.5)
# Add labels and title for clarity
ax.set_xlabel("Solution", fontsize=12, labelpad=15)
ax.set_ylabel("Counts (Frequency)", fontsize=12)
ax.set_title("Distribution of Solutions", fontsize=14, fontweight='bold')
# Rotate x-axis labels to prevent overlap
plt.xticks(rotation=90)
# Ensure the y-axis only displays integer values
ax.yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
# Add count labels on top of each bar
for bar in bars:
yval = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2.0, yval + 0.1, int(yval), ha='center', va='bottom', fontsize=11)
# Set y-axis to start at 0 and give some space at the top
if plot_counts:
ax.set_ylim(0, max(plot_counts) * 1.15)
# Adjust layout to make room for rotated labels
plt.tight_layout()
plt.show()
# Plot the distribution of unique solutions
plot_solution_distribution(response)
Solving Quadratic Unconstrained Binary Optimization (QUBO) Problems via Dirac-3 Integer Solver¶
A Quadratic Unconstrained Binary Optimization (QUBO) problem is defined by the goal of minimizing the following objective function, E, over a set of binary variables:
where the variables are binary, i.e., , where are the linear coefficients, are the quadratic coefficients, and is an offset term.
The Dirac-3 Integer Solver is specifically designed to solve problems of this nature.
To submit a QUBO problem, you provide the C and J Hamiltonians to the QuadraticModel.
The constant offset is not passed to the model directly; instead, it should be stored separately and added to the final energy value returned by the solver to get the true objective function value.
Example¶
Suppose we want to solve the following linear integer problem via QUBO $$ \min_{\mathbf{x}} 2𝑥_0+4𝑥_1+4𝑥_2+4𝑥_3+4𝑥_4+4𝑥_5+5𝑥_6+4𝑥_7+5𝑥_8+6𝑥_9+5𝑥_{10} \ s.t. \begin{bmatrix} 1 & 0 & 0 & 1 & 1 & 1 & 0 & 1 & 1 & 1 & 1\ 0 & 1 & 0 & 1 & 0 & 1 & 1 & 0 & 1 & 1 & 1\ 0 & 0 & 1 & 0 & 1 & 0 & 1 & 1 & 1 & 1 & 1 \end{bmatrix}\mathbf{x}=
\mathbf{x} \in {0,1 }^{11} $$
First we would write this problem as a an unconstrained one by penalizing the linear constraints as quadratics in the objective. Let’s first define the problem parameters
A = np.array([[1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1],
[0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1],
[0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1]])
b = np.array([1, 1, 1])
c = np.array([2, 4, 4, 4, 4, 4, 5, 4, 5, 6, 5])
num_variables = A.shape[1]Given the parameters, we can solve the problem using classical methods.
However, we can also use the eqc_models library to solve this problem using the Dirac-3 Integer solver.
# Build a Pyomo model for the constrained linear integer program
m = pyo.ConcreteModel(name="Constrained_Linear_Integer_Program")
# Define the set of variable indices
m.I = pyo.RangeSet(0, num_variables - 1)
# Define the set of constraint indices
m.J = pyo.RangeSet(0, A.shape[0] - 1)
# Define the 11 binary variables
m.x = pyo.Var(m.I, domain=pyo.Binary)
# Define the Objective Function
# The objective is the original linear expression.
m.obj = pyo.Objective(
expr=sum(c[i] * m.x[i] for i in m.I),
sense=pyo.minimize
)
# Define the constraints
# We define a set of constraints Ax = b directly.
def ax_constraint_rule(model, j):
# For each row j, sum(A[j,i] * x[i]) must equal b[j]
return sum(A[j, i] * model.x[i] for i in model.I) == b[j]
m.constraints = pyo.Constraint(m.J, rule=ax_constraint_rule)
# Solve the model
# We use a Mixed-Integer Linear Programming (MILP) solver.
solver = pyo.SolverFactory('cbc')
results = solver.solve(m, tee=False)
# Display the optimization results
print("\n" + "="*40)
print("-- 📊 Optimization Results --")
print(f"Solver Status: {results.solver.status}")
print(f"Termination Condition: {results.solver.termination_condition}")
print(f"Objective Value: {pyo.value(m.obj):.4f}")
print("\n-- 🏗️ Variable Values --")
solution_vector = [int(pyo.value(m.x[i])) for i in m.I]
print(f"x = {solution_vector}")
========================================
-- 📊 Optimization Results --
Solver Status: ok
Termination Condition: optimal
Objective Value: 5.0000
-- 🏗️ Variable Values --
x = [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
In order to define the C and J matrix, we reformulate the linear problem
as follows:
where is a penalization factor that ensures the constraints are satisfied.
The term penalizes deviations from the constraints.
From this reformulation, we can derive the quadratic hamiltonian, linear hamiltonian and offset as follows:
For this problem in particular, one can prove the the penalization factor is given by , therefore we choose this bound + 1.
epsilon = 1 # small value to avoid division by zero
rho = np.sum(np.abs(c)) + epsilon # penalty multiplier
# Construct the quadratic Hamiltonian J
J = rho*np.matmul(A.T,A)
# Construct the linear Hamiltonian C
C = c.copy()
C -= rho*2*(np.matmul(b.T,A))
# Construct the offset term cQ
cQ = rho*np.matmul(b.T,b)
# --- Formatted Printing ---
print("\n" + "="*60)
print("--- QUBO Formulation Parameters ---")
# Set numpy print options for better readability
np.set_printoptions(precision=2, suppress=True)
print("\nQuadratic Hamiltonian (J):\n", J)
print("\n" + "-"*60)
print("\nLinear Hamiltonian (C):\n", C)
print("\n" + "-"*60)
print(f"\nOffset Term (cQ): {cQ:.2f}")
print("\n" + "="*60)
============================================================
--- QUBO Formulation Parameters ---
Quadratic Hamiltonian (J):
[[ 48 0 0 48 48 48 0 48 48 48 48]
[ 0 48 0 48 0 48 48 0 48 48 48]
[ 0 0 48 0 48 0 48 48 48 48 48]
[ 48 48 0 96 48 96 48 48 96 96 96]
[ 48 0 48 48 96 48 48 96 96 96 96]
[ 48 48 0 96 48 96 48 48 96 96 96]
[ 0 48 48 48 48 48 96 48 96 96 96]
[ 48 0 48 48 96 48 48 96 96 96 96]
[ 48 48 48 96 96 96 96 96 144 144 144]
[ 48 48 48 96 96 96 96 96 144 144 144]
[ 48 48 48 96 96 96 96 96 144 144 144]]
------------------------------------------------------------
Linear Hamiltonian (C):
[ -94 -92 -92 -188 -188 -188 -187 -188 -283 -282 -283]
------------------------------------------------------------
Offset Term (cQ): 144.00
============================================================
Since the QuadraticModel cannot handle the offset term directly, we will store it separately and add it to the final energy value returned by the solver to get the true objective function value.
model = QuadraticModel(C, J)
model.upper_bound = np.ones((11, )) # Set the upper bound for the variables
solver = Dirac3IntegerCloudSolver(url=api_url, api_token=api_token)
Q = model.qubo.Q
response = solver.solve(model, num_samples=10) # qubomodel
solution = model.decode(np.array(response["results"]["solutions"][0]), "qubo")
true_solution = model.evaluate(solution) + cQ # add the offset term to get the true objective value
print(f"Decoded Solution (x): {solution}")
print(f"Objective Value (E): {true_solution}")2025-08-18 14:46:21 - Dirac allocation balance = 9645 s (unmetered)
2025-08-18 14:46:21 - Job submitted: job_id='68a374fd8060c9339796334a'
2025-08-18 14:46:21 - QUEUED
2025-08-18 14:46:24 - RUNNING
2025-08-18 14:46:26 - COMPLETED
2025-08-18 14:46:29 - Dirac allocation balance = 9645 s (unmetered)
Decoded Solution (x): [0 0 0 0 0 0 0 0 0 0 1]
Objective Value (E): 5
# Plot the distribution of unique solutions
plot_solution_distribution(response)
Solving the Problem in Constrained Polynomial Form¶
We can now define the problem using the ConstrainedPolynomialModel class, which will automatically convert the linear constraints into a penalty function and combine them with the original objective. This approach avoids the need to manually construct a QUBO matrix.
To use this model, we provide the objective and constraints separately:
coefficientsandindices: These define the original linear objective function, . The coefficients variable holds the vector c of costs, while indices specifies the variable for each corresponding coefficient.lhsandrhs: These define the linear equality constraints, . Thelhsvariable is the left-hand-side matrixA, andrhsis the right-hand-side vectorb.
coefficients = c
num_vars = 11
indices = [[0, i + 1] for i in range(num_vars)]
lhs = A
rhs = b
# Create the constrained polynomial model
constraint_model = ConstrainedPolynomialModel(coefficients, indices, lhs, rhs)
constraint_model.upper_bound = np.ones(num_vars, ) # Set the upper bound for the variables
# Set the penalty multiplier for the constraints
# This is a crucial step to ensure the constraints are satisfied in the optimization process.
constraint_model.penalty_multiplier = np.sum(np.abs(c)) + 1 # Set the penalty multiplier
print(constraint_model.offset) # Offse term generated by the model
solver = Dirac3IntegerCloudSolver(url=api_url, api_token=api_token)
response = solver.solve(model, num_samples=10)
3
2025-08-18 14:46:30 - Dirac allocation balance = 9645 s (unmetered)
2025-08-18 14:46:30 - Job submitted: job_id='68a375068060c9339796334b'
2025-08-18 14:46:30 - QUEUED
2025-08-18 14:46:33 - RUNNING
2025-08-18 14:46:38 - COMPLETED
2025-08-18 14:46:41 - Dirac allocation balance = 9645 s (unmetered)
# plot the distribution of unique solutions
plot_solution_distribution(response)
# Extract the best solution and its objective value
best_solution = response["results"]["solutions"][0]
best_objective_value = response["results"]["energies"][0] + constraint_model.offset * constraint_model.penalty_multiplier
# Add the offset term to get the true objective value
print(f"Best Solution: {best_solution}")
print(f"Best Objective Value: {best_objective_value}")
Best Solution: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
Best Objective Value: 5
References¶
Nguyen, L., et al. (2024). Entropy Computing: A Paradigm for Optimization in an Open Quantum System. arXiv preprint arXiv:2407.04512. Retrieved from https://
arxiv .org /abs /2407 .04512 Quantum Computing Inc. (2024). Dirac-3 User Guide. Retrieved from https://
quantumcomputinginc .com /learn /support /user -guides /dirac -3 -user -guide