Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Entropy Computing via QCI (Python)

Maintained by the JuliaQUBO organization
SECQUOIA  ·  PSR Energy

Open In Colab

Setup

Google Colab

Click the badge above. The hidden setup cell installs the QCi stack and HiGHS.

Local installation

From the repository root, use the isolated, locked QCi environment:

uv sync --locked --project notebooks_py/environments/qci
uv run --locked --project notebooks_py/environments/qci jupyter lab

Choose the Python kernel from this environment. For a fresh-kernel check of the whole notebook, run make verify-qci-python-local from the repository root. The local path needs no QCi account or separately installed solver binaries.

Cloud examples require an explicit QUBONOTEBOOKS_QCI_ENABLE_CLOUD=1 opt-in and QCI_TOKEN. Published cloud results are retained historical examples; default execution skips submissions and their result displays, while constructing and validating every model locally. Verification writes new results to .nbverify/.

Learning objectives

By the end of this notebook you will be able to:

  1. Describe how QCi’s Dirac solvers represent constrained optimization problems.

  2. Formulate a constrained quadratic or polynomial model with objective coefficients and linear constraints.

  3. Submit a model to Dirac-3 solvers when credentials are available and decode returned solutions.

  4. Compare manual QUBO penalty construction with ConstrainedPolynomialModel handling of constraints and offsets.

Prerequisites

Mathematical background: Binary optimization, QUBO penalties, matrix multiplication, and linear equality constraints.
Prior notebooks: Notebook 2 (QUBO) and Notebook 5 (Benchmarking) for solver comparison context.
Accounts required: QCi account credentials and an API token for cloud solver execution.
Python version: Python 3.11–3.12 with the isolated QCi environment.

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:

  1. Structure the problem using QCI’s eqc_models library.

  2. Submit the model to the Dirac3IntegerCloudSolver and Dirac3ContinuousSolver for processing.

  3. Analyze the high-quality candidate solutions returned by the solver to find the optimal result.

Notebook Cell
WARNING:root:eqc-direct package not available
Notebook Cell

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.

QCI API token

QCi cloud execution is an explicit opt-in. Set QUBONOTEBOOKS_QCI_ENABLE_CLOUD=1 before running the notebook, then provide QCI_TOKEN through your shell environment or a Colab Secret. A token alone never enables submission; the default path does not read Colab Secrets.

In Colab, opt in with os.environ["QUBONOTEBOOKS_QCI_ENABLE_CLOUD"] = "1" before running the next cell. Sign in to the QCi portal to obtain a token. Missing credentials or an opted-in submission failure stop execution.

Solving Problems using Dirac-3

Dirac-3 can solve combinatorial optimization problems using the following objective function, E, that has the following high-order polynomial structure:

E=∑i=1NCiVi+∑i,j=1N,NJijViVj+∑i,j,k=1N,N,NTijkViVjVk+∑i,j,k,l=1N,N,N,NQijklViVjVkVl+∑i,j,k,l,m=1N,N,N,N,NPijklmViVjVkVlVmE = \sum_{i=1}^{N} C_i V_i + \sum_{i,j=1}^{N,N} J_{ij} V_i V_j + \sum_{i,j,k=1}^{N,N,N} T_{ijk} V_i V_j V_k + \sum_{i,j,k,l=1}^{N,N,N,N} Q_{ijkl} V_i V_j V_k V_l + \sum_{i,j,k,l,m=1}^{N,N,N,N,N} P_{ijklm} V_i V_j V_k V_l V_m

ViV_i refers to the value of nonnegative variable, and CiC_i, JijJ_{ij}, TijkT_{ijk}, QijklQ_{ijkl}, and PijklmP_{ijklm} 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

R=∑i=1NVi,R∈[1,10000].R = \sum_{i=1}^{N} V_i, \quad R \in [1, 10000].

Note that RR 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:

min⁡E=3x12+2x22+x32s.t.x1+x2+x3=10,x1,x2,x3∈[0,10].\begin{align*} \min \quad & E = 3 x_1^2 + 2 x_2^2 + x_3^2 \\ \text{s.t.} \quad & x_1 + x_2 + x_3 = 10, \\ & x_1, x_2, x_3 \in [0, 10]. \end{align*}

==============================
-- 📊 Optimization Results --
Solver Status: ok
Termination Condition: optimal
Objective Value (E): 54.5455

-- 🏗️ Variable Values --
x[1] = 1.8182
x[2] = 2.7273
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., 1 for x1x_1, 2 for x2x_2).

    # (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:

E=3⏟coef[0]⋅x1x1⏟idx[0]+2⏟coef[1]⋅x2x2⏟idx[1]+1⏟coef[2]⋅x3x3⏟idx[2]E = \underbrace{3}_{\text{coef[0]}} \cdot \underbrace{x_1 x_1}_{\text{idx[0]}} + \underbrace{2}_{\text{coef[1]}} \cdot \underbrace{x_2 x_2}_{\text{idx[1]}} + \underbrace{1}_{\text{coef[2]}} \cdot \underbrace{x_3 x_3}_{\text{idx[2]}}

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 the model object after it has been created.

    # Sets the upper bound for all 3 variables to 10
    model.upper_bound = 10 * np.ones((3,))

3. 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 (R=∑ViR = \sum V_i) is a required input for the solver.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
)
QCI job completed; provider identifiers are omitted from notebook output.
[[1.8590674, 2.5134315, 5.6275015], [1.9934049, 2.7398567, 5.2667379], [1.8994547, 2.8885841, 5.2119608], [1.9604021, 2.820755, 5.2188425], [1.639825, 2.6989937, 5.6611805], [1.6354562, 2.7102983, 5.6542454], [1.9995629, 2.7769594, 5.2234774], [2.0278194, 2.7187715, 5.2534094], [1.5045189, 2.7742901, 5.7211914], [1.5001835, 2.7959194, 5.7038975], [1.568614, 2.5883961, 5.8429899], [1.7180657, 3.1012127, 5.1807213], [1.4948952, 2.8680477, 5.6370568], [1.5316118, 3.0429969, 5.4253912], [1.5206749, 3.0626678, 5.4166574], [1.6410959, 3.1415131, 5.2173905], [1.5717465, 3.1131096, 5.3151441], [1.6082671, 2.4509151, 5.9408183], [1.7679406, 3.1595535, 5.0725055], [1.5454257, 3.1976578, 5.256916]]
[54.6718445, 54.6731453, 54.6761551, 54.6791649, 54.6851768, 54.6860733, 54.7024803, 54.7178993, 54.9161339, 54.9204292, 54.9217682, 54.9301643, 54.931942, 54.9920311, 55.0374031, 55.0389595, 55.0448189, 55.0668602, 55.0727119, 55.2502213]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
{'solutions': [[1.8590674, 2.5134315, 5.6275015], [1.9934049, 2.7398567, 5.2667379], [1.8994547, 2.8885841, 5.2119608], [1.9604021, 2.820755, 5.2188425], [1.639825, 2.6989937, 5.6611805], [1.6354562, 2.7102983, 5.6542454], [1.9995629, 2.7769594, 5.2234774], [2.0278194, 2.7187715, 5.2534094], [1.5045189, 2.7742901, 5.7211914], [1.5001835, 2.7959194, 5.7038975], [1.568614, 2.5883961, 5.8429899], [1.7180657, 3.1012127, 5.1807213], [1.4948952, 2.8680477, 5.6370568], [1.5316118, 3.0429969, 5.4253912], [1.5206749, 3.0626678, 5.4166574], [1.6410959, 3.1415131, 5.2173905], [1.5717465, 3.1131096, 5.3151441], [1.6082671, 2.4509151, 5.9408183], [1.7679406, 3.1595535, 5.0725055], [1.5454257, 3.1976578, 5.256916]], 'energies': [54.6718445, 54.6731453, 54.6761551, 54.6791649, 54.6851768, 54.6860733, 54.7024803, 54.7178993, 54.9161339, 54.9204292, 54.9217682, 54.9301643, 54.931942, 54.9920311, 55.0374031, 55.0389595, 55.0448189, 55.0668602, 55.0727119, 55.2502213], 'counts': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

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).

-- 📊 Best Solution Found --
Optimal Objective (Energy): 54.6718
Variable Values: [1.8591 2.5134 5.6275]

Integer Solver

The Dirac-3 can solver unconstrained optimization problem. In this mode, the resulting state vector ViV_i consists of integers:

Vi∈N,0≤Vi≤16V_i \in \mathbb{N}, \quad 0 \leq V_i \leq 16

For integer problems, you must define the number of possible states (or levels) for each variable. For instance, to model a standard Quadratic 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:

min⁡E=−0.75x12−0.25x22+2x1x2+0.5x1s.t.x1,x2∈{0,1}\begin{align*} \min \quad & E = -0.75 x_1^2 - 0.25 x_2^2 + 2 x_1 x_2 + 0.5 x_1 \\ \text{s.t.} \quad & x_1, x_2 \in \{0,1\} \\ \end{align*}

==============================
-- Enumeration Results --
x[1] = 0, x[2] = 0, objective = 0.0000
x[1] = 0, x[2] = 1, objective = -0.2500
x[1] = 1, x[2] = 0, objective = -0.2500
x[1] = 1, x[2] = 1, objective = 1.5000
Optimal Objective: -0.2500
Variable Values: x = [0, 1]
==============================

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.

For integer and binary optimization problems, the sum_constraint is not necessary because Dirac3IntegerCloudSover operates in an unconstrained mode.

QCI job completed; provider identifiers are omitted from notebook output.
[[0, 1], [1, 0]]
[-0.25, -0.25]
[84, 16]

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.

Image produced in Jupyter

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:

min⁡xE(x)=∑iCixi+∑ijJijxixj+cQ\min_x \quad E(x)=\sum_i C_i x_i + \sum_{ij} J_{ij} x_i x_j + c_Q

where the variables xix_i are binary, i.e., xi∈{0,1}x_i \in \{0,1\}, where CiC_i are the linear coefficients, JijJ_{ij} are the quadratic coefficients, and cQc_Q 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 cQc_Q 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}=

[111]\begin{bmatrix} 1\\ 1\\ 1 \end{bmatrix}

\mathbf{x} \in {0,1 }^{11} $$

First, we rewrite this problem as an unconstrained one by adding quadratic penalties for the linear constraints. Let’s define the problem parameters.

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.


========================================
-- 📊 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]

To define the C and J matrices, we reformulate the linear problem

min⁡xc⊤xs.t.Ax=bx∈{0,1}11\min_{\mathbf{x}} \mathbf{c}^\top \mathbf{x}\\ s.t. \mathbf{A}\mathbf{x}=\mathbf{b} \\ \mathbf{x} \in \{0,1 \}^{11}

as follows:

min⁡xc⊤x+ρ(Ax−b)⊤(Ax−b)x∈{0,1}11\min_{\mathbf{x}} \mathbf{c}^\top \mathbf{x} + \rho(\mathbf{A}\mathbf{x}-\mathbf{b})^\top (\mathbf{A}\mathbf{x}-\mathbf{b}) \\ \mathbf{x} \in \{0,1 \}^{11}

where ρ\rho is a penalization factor that ensures the constraints are satisfied.

The term ρ(Ax−b)⊤(Ax−b)\rho(\mathbf{A}\mathbf{x}-\mathbf{b})^\top (\mathbf{A}\mathbf{x}-\mathbf{b}) penalizes deviations from the constraints.

ρ(Ax−b)⊤(Ax−b)=ρ(x⊤(A⊤A)x−2b⊤Ax+b⊤b)\rho(\mathbf{A}\mathbf{x}-\mathbf{b})^\top (\mathbf{A}\mathbf{x}-\mathbf{b}) = \rho( \mathbf{x}^\top (\mathbf{A}^\top \mathbf{A}) \mathbf{x} - 2\mathbf{b}^\top \mathbf{A} \mathbf{x} + \mathbf{b}^\top \mathbf{b} )

From this reformulation, we can derive the quadratic hamiltonian, linear hamiltonian and offset as follows:

C=c−2ρA⊤bC = \mathbf{c} - 2\rho\mathbf{A}^\top \mathbf{b}
J=ρA⊤AJ = \rho\mathbf{A}^\top \mathbf{A}
cQ=ρ(b⊤b)c_Q = \rho(\mathbf{b}^\top \mathbf{b})

Penalty parameter rationale

The penalty term must be large enough that any infeasible assignment is worse than the objective improvement it might gain by violating the constraint. For a binary constraint A x = b, the residual A x - b is integer-valued; the smallest nonzero violation has squared penalty at least 1. A conservative sufficient bound is rho = sum(abs(c)) + epsilon, because changing binary variables can improve the linear objective by at most sum(abs(c_i)).

Worked example: if sum(abs(c_i)) = 6 and rho = 5.9, an infeasible assignment with violation 1 can gain 6 objective units while paying only 5.9 penalty units, so it can look 0.1 units better than a feasible assignment. Choosing rho = 6 + epsilon closes that gap for this bounded binary model.

This is a sufficient bound for this model family, not a universal rule. Constraints with non-binary variables, non-integer residuals, or a larger objective range need a problem-specific penalty analysis. See Glover, Kochenberger, and Du (2019), “A Tutorial on Formulating and Using QUBO Models.”


============================================================
--- 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 cQc_Q 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.

QCI job completed; provider identifiers are omitted from notebook output.
Decoded Solution (x): [0 0 0 0 0 0 0 0 1 0 0]
Objective Value (E): 5
Image produced in Jupyter

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:

  • coefficients and indices: These define the original linear objective function, cTx\mathbf{c}^T \mathbf{x} . The coefficients variable holds the vector c of costs, while indices specifies the variable for each corresponding coefficient.

  • lhs and rhs: These define the linear equality constraints, Ax=b\mathbf{A} \mathbf{x}=\mathbf{b}. The lhs variable is the left-hand-side matrix A, and rhs is the right-hand-side vector b.

3
QCI job completed; provider identifiers are omitted from notebook output.
Image produced in Jupyter
Best Solution: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
Best Objective Value: 5

Practice checkpoints

Use these checkpoints during the workshop to test the main ideas before moving on.

Notebook Cell
{'target_sum': 2, 'feasible_candidates': [(0, 1, 1), (1, 0, 1), (1, 1, 0)]}
Notebook Cell
((0, 1), -1)
Notebook Cell
{'local': ['imports', 'model construction', 'local objective evaluation'], 'cloud': ['submit job', 'poll job', 'download cloud result']}

Summary

In this notebook we:

  • Introduced QCi’s entropy-computing workflow and the Dirac-3 solver interface for constrained optimization.

  • Built constrained quadratic and polynomial model representations from linear objective and constraint data.

  • Submitted models to QCi solvers when credentials are available and decoded the returned solution vectors and energies.

  • Compared manual QUBO offset handling with the constrained polynomial model abstraction.

Learning objectives met: You practiced describing the Dirac modeling workflow, constructing constrained models, decoding solver responses, and comparing explicit QUBO penalties with higher-level constraint handling.

Next steps: This is the final Python notebook in the current sequence; use the benchmarking notebook to design fair comparisons when applying QCi models to your own optimization instances.

Further reading:

References

  • David E. Bernal Neira — Davidson School of Chemical Engineering, Purdue University

  • Albert Lee — Davidson School of Chemical Engineering, Purdue University; Graduate Research Assistant

Acknowledgments

This notebook was developed by: