Quantum implementation
How one day of battery decisions becomes a quantum circuit, and how that circuit runs, first on a simulator and then on IBM's real machines. Each step shows the actual code and what it does, so you can follow the whole path without opening GitHub.
Before you start
Everything on this page except the very last step runs on an ordinary laptop, for free. You need Python 3 and two quantum libraries from IBM: Qiskit (for building circuits) and Qiskit Aer (the simulator). The install command below brings in both.
terminal
git clone https://github.com/austinamissah/quantum-solar
cd quantum-solar
pip install -e .Step 1
A day is a small bundle of numbers: for each time slot, the electricity price, the solar generation, and the household's use, plus the battery's size, how much energy it can take in or give out in one slot, and how full it starts. To see every step clearly, I will use a tiny made-up day with just 3 slots. (The real Golden, Colorado day goes through exactly the same steps; it just has more slots.)
python
from quantum_solar import synthetic_instance
problem = synthetic_instance(num_slots=3, seed=1)
print(problem.price) # $/kWh in each slot
print(problem.load) # kWh the home uses
print(problem.generation) # kWh the panels produceStep 2
Each slot gets two yes/no bits: “charge this hour?” and “discharge this hour?” The day's total cost then becomes one formula over those bits, called a QUBO. Rules are enforced by penalties baked into the same formula: breaking one (charging and discharging at once, or overfilling the battery) adds so much cost that no rule-breaking plan can ever look cheapest. Keeping the battery inside its limits needs a few extra bookkeeping bits, called slack bits. For this 3-slot day that makes 10 bits in total: 6 decision bits plus 4 slack bits.
python
from quantum_solar import build_qubo, default_weights
qubo = build_qubo(problem, default_weights(problem))
print(qubo.num_vars) # 10 bits for the 3-slot dayStep 3
Quantum hardware does not minimize formulas; it minimizes energy. Luckily there is a standard, exact translation: each bit becomes a qubit, and the QUBO becomes an energy rule (physicists call it an Ising Hamiltonian) where the cheapest schedule is the lowest-energy state. Nothing is lost in translation; it is the same problem in the machine's native language.
python
from quantum_solar import qubo_to_ising
hamiltonian, constant = qubo_to_ising(qubo)Step 4
The circuit is QAOA, and it has a simple shape. Reading it like a recipe:
One round only nudges the odds a little. Each extra layer nudges them again, so in theory more layers make the cheap schedules come out more often when you measure. But layers have two prices. Each one adds dials that have to be tuned by trial and error, so a badly tuned deep circuit can lose to a well-tuned shallow one; my hardware results showed exactly that, with the ordering reflecting tuning rather than depth. And on a real quantum computer, every layer adds physical operations, each adding a little noise, so past some point extra layers hurt more than they help. (On a simulator, extra layers only cost computing time, because a simulator makes no errors.)
Each pair of layers has two dials (angles). The circuit does nothing useful until the dials are tuned; that is the next step. Qiskit builds the whole circuit from the energy rule in one line:
python
from qiskit.circuit.library import QAOAAnsatz
ansatz = QAOAAnsatz(cost_operator=hamiltonian, reps=2)
print(ansatz.num_parameters) # 4 dials: two per layerStep 5
Tuning is a loop between a quantum circuit and an ordinary optimizer: run the circuit with the current dial settings, estimate the average energy of what comes out, let the optimizer (COBYLA, a standard numerical method) nudge the dials, and repeat, up to 200 times, restarting from 5 random starting points to avoid getting stuck. My solver runs that whole loop with one call, on the free Aer simulator, on a laptop.
A simulator is an ordinary computer doing the math a perfect, noise-free quantum computer would do: it gives the ideal answer, but the work doubles with every qubit. A real machine is fast per run but noisy, so some of the ideal answer is lost; how much is exactly what the hardware run in Step 9 measured. The results below, through Step 8, are all simulator results. The technical page explains both machines in more depth.
python
from quantum_solar import QAOASolver
solver = QAOASolver(reps=2, n_starts=5, shots=4096, seed=1234)
result = solver.solve(problem, qubo)
print(f"cost ${result.true_energy:.3f} feasible={result.feasible}")
# cost $-0.026 feasible=TrueStep 6
Measuring a quantum circuit gives one random schedule each time (each measurement is called a shot), so the solver measures 4,096 times and gets back a tally: each observed bit-pattern and how often it appeared. Every pattern decodes to a schedule; the solver scores them all against the real cost formula and keeps the best one that obeys the rules. A well-tuned circuit makes the cheap schedules appear far more often than chance, which is exactly what the quantum-vs-random-guessing chart on the project page measures.
python
# result.counts is the raw tally from 4,096 measurements
for bits, count in sorted(result.counts.items(),
key=lambda kv: -kv[1])[:3]:
print(bits, count) # the three most frequent patternsStep 7
Because the day is small, two classical solvers can find the guaranteed best plan: brute force tries every plan, and dynamic programming computes it instantly. The quantum answer is graded against them. On this day, all three agree.
python
import numpy as np
from quantum_solar import brute_force_solve, dp_solve
brute = brute_force_solve(problem, qubo)
dp = dp_solve(problem)
assert np.isclose(brute.true_energy, dp.true_energy)
assert np.isclose(result.true_energy, dp.true_energy)
print("all three solvers agree")Step 8
Everything so far used the made-up 3-slot day. One call replaces it with the real Golden, Colorado day: real sunlight from NREL's PVWatts, real Xcel Energy time-of-use prices from the Utility Rate Database, and the real ResStock household profile (the sources and preparation steps are listed on the project page). You need one thing first: a free NREL API key from developer.nlr.gov, set as an environment variable.
terminal (one time)
export NREL_API_KEY=your_key_herepython
from quantum_solar.data import load_nrel_instance
# Golden, CO; day 172 of the year is near June 21
real = load_nrel_instance(lat=39.74, lon=-105.18, day=172)The result is the same kind of problem object as Step 1, so every step above works on it unchanged; it just has 24 hourly slots instead of 3, which means more qubits. Responses are cached on disk, so the network is only touched once. This is exactly the day behind the bill and the chart on the project page.
Step 9
The only step that leaves the laptop. It is free, and it is deliberately hard to trigger by accident.
First, the account. IBM Quantum's free Open Plan gives anyone about ten minutes of real machine time every month. Sign up at IBM Quantum's website, copy your API token (a long code that identifies your account), and save it once on your computer; Qiskit remembers it from then on.
python (one time)
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(token="YOUR_TOKEN")Then the run itself, which my project splits into three deliberate stages so the free minutes cannot be wasted:
terminal
# (a) tune the dials on the simulator; saves them to a file
python scripts/experiment_hardware.py optimize
# (b) dry run: shows the chosen machine, the circuits, the
# shot count, and the estimated seconds of machine time.
# Spends nothing.
python scripts/experiment_hardware.py submit
# (c) the real thing. Only this flag spends the free minutes.
python scripts/experiment_hardware.py submit --yes-spend-qpuThree details worth knowing. The script asks IBM for the least busy machine that is currently up, so there is no hand-picking. Only the final sampling happens on the real machine; all the tuning stayed on the simulator, so the hardware job is short. And the results come back in exactly the same tally format as Step 6, so the same analysis applies: this time comparing the simulator's tally against the real machine's, to measure how much physical noise changes the answer.
Where this stands
As of July 10, 2026: the dials are tuned and saved in the repository, the dry run works, the analysis was rehearsed end to end on stand-in data, and my predictions are written down. The one thing not yet done is typing the final command. When that happens, the results will appear here and in the progress notes.
Keep going