Baby steps, starting it simple
Scenario Details
We have a set of N power plants (sources), and M consumers.
We have 3 Sources: ["A1", "A2", "A3"]
We have 4 Consumers: ["B1", "B2", "B3", "B4"]
Desired outcome
The goal is to supply power to all consumers, meeting the constraints of the power plants, and minimize the total cost of supplying power.
Discussion Points:
Step #1: The Mathematic Representation
Step #2: Articulating the problem
Step #3: Solving with Quantum (Simulation)
Step #4: Solving with Quantum (Real deal)
Quick admin - install the libraries
from itertools import product
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
from qiskit import *
from qiskit.visualization import plot_histogram
from qiskit_algorithms import QAOA
from qiskit_optimization.algorithms import MinimumEigenOptimizer
from qiskit_algorithms.optimizers import COBYLA
from qiskit_optimization import QuadraticProgram
from qiskit_aer.primitives import Sampler
from qiskit_aer import Aer
As you can see, we are using IBM's qiskit quantum library
Articulating the Problem
cost_matrix = np.array(
[[0.5, 1.0, 1.0, 2.1], [1.0, 0.6, 1.4, 1.0], [1.0, 1.4, 0.4, 2.3]]
)
Sources = [“A1”, “A2”, “A3”]
Consumers = [“B1”, “B2”, “B3”, “B4”]
N = len(Sources)
M = len(Consumers)
The cost_matrix defines the per-unit transmission costs from each power plant (source) to each consumer. Specifically, each row corresponds to a source (A1, A2, A3) and each column corresponds to a consumer (B1, B2, B3, B4). For example, the cost from source A1 to consumer B1 is 0.5, while the cost from source A1 to consumer B4 is 2.1.
Solving with Quantum (Simulation)
1. Define the Optimization Problem
problem = QuadraticProgram()
for i in range(N):
for j in range(M):
problem.binary_var(name=f"x_{i}_{j}")
problem = QuadraticProgram()
This creates an empty optimization problem where you'll later add variables, constraints, and an objective.
The loop over i (from 0 to N-1) and nested loop over j (from 0 to M-1) create a grid of decision variables.
For each combination of i and j, a binary variable is added to the problem.
The variable name is set to "x_{i}_{j}" (using an f-string for string interpolation), which helps identify its corresponding source (i) and consumer (j).
These variables typically represent decisions—in this case, whether to supply power from source A_i to consumer B_j—and can only take the values 0 or 1. This structure is useful in problems like assignment or network flow where a binary decision is needed at each pairing.
2. Build and Set the Objective
Build the objective as a dictionary mapping variable names to their cost coefficients
objective_linear = {}
for i in range(N):
for j in range(M):
objective_linear[f"x_{i}_{j}"] = cost_matrix[i, j]
problem.minimize(linear=objective_linear)
objective_linear = {}
This dictionary will store each decision variable (by name) paired with its cost coefficient.
for i in range(N):
for j in range(M):
objective_linear[f"x_{i}_{j}"] = cost_matrix[i, j]
Two nested loops iterate over all combinations of i (for sources) and j (for consumers). For each pair, the code creates a key "x_{i}_{j}" (corresponding to the binary variable defined earlier) and assigns it the value from cost_matrix[i, j]. This maps each decision variable to its associated cost.
objective_linear = {'x_0_0': 0.5,
'x_0_1': 1.0,
'x_0_2': 1.0,
'x_0_3': 2.1,
'x_1_0': 1.0,
'x_1_1': 0.6,
'x_1_2': 1.4,
'x_1_3': 1.0,
'x_2_0': 1.0,
'x_2_1': 1.4,
'x_2_2': 0.4,
'x_2_3': 2.3}
problem.minimize(linear=objective_linear)
This tells the optimizer that the goal is to minimize the total cost as represented by the sum of the costs times the decision variables. In other words, it adds up each variable's cost coefficient (multiplied by the value of that variable, which will be 0 or 1) to form the objective.
3.1 Consumer Supply Constraint
for j in range(M):
problem.linear_constraint(
linear={f"x_{i}_{j}": 1 for i in range(N)},
sense='==',
rhs=1,
name=f"cons_{j}"
)
The loop iterates once for each consumer (indexed by j). For every consumer, a constraint will be added.
The coefficient 1 for each variable indicates that every decision variable (whether a link from a source to consumer j) is equally weighted in the constraint.
For each consumer (each value of j), this code adds a constraint that collects all decision variables for that consumer (one for each source) and requires their sum to be exactly 1. This models the requirement that every consumer must be supplied by exactly one source.
3.2 Source Capacity Constraint
for i in range(N):
problem.linear_constraint(
linear={f"x_{i}_{j}": 1 for j in range(M)},
sense='<=',
rhs=2,
name=f"source_{i}")
Where each consumer is assumed to consume 1 unit of power e.g. a source can only supply up to two consumers
4. Set Up the Quantum Algorithm
sampler = Sampler()
sampler.options.backend = Aer.get_backend("qasm_simulator")
optimizer_cobyla = COBYLA()
qaoa = QAOA(sampler=sampler, optimizer=optimizer_cobyla, reps=1)
# Wrap QAOA in the MinimumEigenOptimizer
min_eigen_optimizer = MinimumEigenOptimizer(qaoa)
sampler = Sampler()
sampler.options.backend = Aer.get_backend(“qasm_simulator”)
The Sampler() object is used to sample outcomes from the quantum circuit.
The backend is set to IBM’s Aer QASM simulator, which simulates quantum measurements on a quantum computer.
optimizer_cobyla = COBYLA()
COBYLA() is a classical optimizer that is often used with quantum algorithms to adjust circuit parameters iteratively.
qaoa = QAOA(sampler=sampler, optimizer=optimizer_cobyla, reps=1)
QAOA (Quantum Approximate Optimization Algorithm) is a hybrid quantum-classical algorithm designed to solve optimization problems. Basically magic
It is configured with the quantum sampler and the classical optimizer.
The parameter reps=1 sets the depth (number of repetitions) of the QAOA circuit; increasing this value can sometimes improve solution quality at the cost of longer execution time.
min_eigen_optimizer = MinimumEigenOptimizer(qaoa)
The MinimumEigenOptimizer wraps the QAOA instance so that it can be used to solve optimization problems formulated as quadratic programs.
Essentially, it leverages QAOA to find the minimum eigenvalue (solution) of the problem’s cost function, thereby determining the optimal (or near-optimal) assignment for the decision variables.
5. Execute Multiple Runs and Collect Results
num_runs = 10
results = []
for _ in range(num_runs):
result = min_eigen_optimizer.solve(problem)
best_cost = result.fval
results.append(best_cost)
The loop iterates 10 times.
In each iteration, the min_eigen_optimizer.solve(problem) call attempts to solve the optimization problem using the quantum algorithm (QAOA).
The result.fval accesses the final cost (objective function value) of the solution.
That cost is then appended to the results list.
Step #4: Solving with Quantum (Real deal)
Pending free time