binary_optimizer — Core DQA+QAE Solver¶
Primary entry point for the DQA+QAE pipeline. Contains BinaryNestedOptimizer,
the main solver class, and BernoulliA, the amplitude-estimation wrapper.
BernoulliA¶
qiskit_impl.binary_optimizer.BernoulliA ¶
Bases: QuantumCircuit
A circuit representing the Bernoulli A operator.
Source code in qiskit_impl/binary_optimizer.py
BinaryNestedOptimizer¶
qiskit_impl.binary_optimizer.BinaryNestedOptimizer ¶
BinaryNestedOptimizer(gas_costs: list[float], wind_costs: list[float], recourse_cost: float, pdf: dict[tuple[int], float], demand: int, is_uniform: bool)
Quantum + classical solver for the two-stage stochastic unit commitment (UC) problem.
Problem structure¶
First stage (x) : choose how much power the gas generator provides (integer, 0 <= x <= demand). Cost = x * gas_cost. Second stage (y) : given x and a realized wind scenario xi, choose which wind turbines to turn on (y in {0,1}^n_y, sum(y) = demand - x). Cost = sum_j c_y[j]y[j]xi[j] + recourse*(shortfall) Objective : minimize gas_cost(x) + E_xi[ min_y q(y, xi) ]
Quantum qubit layout (used consistently across ALL circuits)¶
wind_qubits [0 .. n_y-1] : y register -- binary turbine ON/OFF decisions pdf_qubits [n_y .. 2n_y-1] : xi register -- wind scenario, loaded with PDF amplitudes ancilla [2n_y] : oracle ancilla for QAE |0> -> sqrt(1-q?)|0> + sqrtq?|1> (cost encoded as amplitude) estimate [2*n_y+1 .. +m] : m QPE readout qubits (QAE output: integer b)
The DQA (Discrete Quantum Annealing) circuit evolves the system from the Dicke state (uniform superposition over feasible y) toward the optimal state by interpolating between the XY mixer and the cost operator.
Source code in qiskit_impl/binary_optimizer.py
Functions¶
gen_all_wind_outputs ¶
create a list of all bitstrings possible for the wind bitstrings
Source code in qiskit_impl/binary_optimizer.py
wind_scenario_cost ¶
Given a list of wind outputs and the corresponding scenario, return the cost of that scenario.
Compute second-stage cost q(y, xi) for one turbine decision and one wind scenario.
For each turbine j
effective_output[j] = wind_output[j] AND scenario[j] -> turbine j generates power only if it is ON (y[j]=1) AND wind blows (xi[j]=1) cost contribution = wind_costs[j] * effective_output[j]
If total effective output < wind_demand: shortfall = wind_demand - sum(effective_output) recourse = shortfall * recourse_cost <- expensive backup power purchase
This is q(x, y, xi) from Eq. (2) of the paper.
Source code in qiskit_impl/binary_optimizer.py
bstr_to_output ¶
prep_gs ¶
Create a statevector which is the minima for each scenario.
Build the ideal "ground-state" wavefunction |psi*> -- the state perfect DQA would produce.
For each scenario xi (with probability Pr[xi]): 1. Solve y(xi) = argmin_{sum(y)=wind_demand} q(y, xi) classically 2. Place amplitude sqrtPr[xi] on the basis state |y(xi)>|xi>
The resulting statevector satisfies
This is the benchmark target. Real DQA produces an approximation (residual temperature delta > 0 means some amplitude leaks to sub-optimal states).
Source code in qiskit_impl/binary_optimizer.py
min_scenario ¶
Get the minimization for a single wind scenario.
Classical brute-force second-stage solver for ONE fixed wind scenario xi.
Enumerate all turbine decisions y with sum(y) == wind_demand (the feasible set), evaluate q(y, xi) for each, and return the cheapest one:
y*(xi) = argmin_{y : sum(y)=wind_demand} q(y, xi)
Returns: (min_cost, optimal_turbine_decision_tuple)
Source code in qiskit_impl/binary_optimizer.py
brute_force_wind_demand_expectation_values ¶
Compute the classical exact expected cost for every possible wind demand.
For each integer wind_demand in [0, demand], enumerates all feasible
turbine decisions and scenarios to compute:
This is the classical brute-force ground truth used to benchmark DQA and QAE.
Returns:
| Type | Description |
|---|---|
|
List of floats |
Source code in qiskit_impl/binary_optimizer.py
brute_force_energy_surface ¶
Compute the full first-stage objective function over all gas levels.
For each gas commitment level x in [0, demand]:
where phi is from brute_force_wind_demand_expectation_values().
Returns:
| Type | Description |
|---|---|
|
Dict |
Source code in qiskit_impl/binary_optimizer.py
dicke_state_initialize ¶
Convenience wrapper around dicke_state_circuit() returning a gate object.
Returns a gate that prepares \(|D_n^k\rangle\) on num_wind_vars qubits
with weight excitations. Delegates to dicke_state_circuit().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weight
|
Integer \(k\) — number of turbines to turn on (Hamming weight). |
required |
Returns:
| Type | Description |
|---|---|
|
Gate object for the Dicke state preparation. |
Source code in qiskit_impl/binary_optimizer.py
dicke_state_circuit ¶
Prepare the Dicke state. Reference: https://arxiv.org/pdf/1904.07358.pdf
Prepare the Dicke state |D_n^k> with n = num_wind_vars qubits and k = weight ones.
The Dicke state is a uniform superposition over ALL bitstrings with exactly k ones:
|D_n^k> = 1/sqrtC(n,k) sum_{|y|=k} |y>
This is the constraint-respecting initial state for DQA: every basis state in the superposition already satisfies sum(y) = wind_demand.
Construction uses the recursive SCS (Splitting-Coherence-Splitting) gadget from: Baertschi & Eidenbenz, arXiv:1904.07358 (Fig. 1)
SCS(n, k) acts on k+1 qubits and "moves" one unit of weight downward: - CX + doubly-controlled RY: redistribute amplitude between |1...1 0> and |1...0 1> - Applied (n-k) times top-down, then (k-1) times bottom-up
Gate count: O(n*k) with no ancilla, depth O(n).
Source code in qiskit_impl/binary_optimizer.py
pdf_initialize ¶
Build a circuit that encodes the PDF as amplitudes on the xi register.
Prepares the state \(|\mathcal{P}\rangle = \sum_\xi \sqrt{\Pr[\xi]}|\xi\rangle\) so that measuring the xi register gives scenario \(\xi\) with probability \(\Pr[\xi]\).
Two cases:
is_uniform=True: \(n\) Hadamard gates — \(O(n)\) circuit depth.is_uniform=False: QiskitInitialize— \(O(2^n)\) gates after transpile.
Returns:
| Type | Description |
|---|---|
|
Gate object (via |
Source code in qiskit_impl/binary_optimizer.py
cost_operator ¶
Our cost operator.
constraint_amplitude will be recourse for the constraint-preserving mixer,
and a penalty weight otherwise.
Build the cost (phase) operator U_q(gamma) = exp(-i gamma H_C).
For each turbine j, applies TWO controlled-phase (CP) gates:
CP(gamma . c_y[j] / norm) on [pdf_qubit_j, wind_qubit_j] -> adds phase gamma.c_y[j]/norm when BOTH y[j]=1 AND xi[j]=1 (turbine ON and wind available -> incurs operational cost)
X(pdf_qubit_j) -> CP(gamma . recourse / norm) -> X(pdf_qubit_j) -> adds phase gamma.recourse/norm when y[j]=1 AND xi[j]=0 (turbine ON but no wind -> incurs recourse penalty)
The CP gate CP(theta)|11> = e^(itheta)|11> implements exp(i.theta.|1><1| (x) |1><1|), which is the diagonal cost Hamiltonian acting as a phase kick on computational basis states.
amplitude = gamma (annealing parameter, 0..1)
constraint_amplitude = recourse cost coefficient
norm = normalization factor (prevents phase wrapping)
Corresponds to U_q in Eq. (42) of the paper.
Source code in qiskit_impl/binary_optimizer.py
demand_constraint_preserving_mixer ¶
Mixing operator which preserves the demand constraint.
Build the XY mixer U_d(beta) = exp(-i beta H_XY) that preserves Hamming weight.
For every pair of qubits (j, k): applies SWAP^beta (a fractional SWAP gate).
SWAP^beta interpolates between identity (beta=0) and a full SWAP (beta=1):
SWAP^beta = exp(-i beta pi/2 . (XX + YY)/2)
The XX+YY part of the Hamiltonian is the XY model -- it moves excitations between qubits without changing the total number of |1>s. This ensures that mixing only connects states with the SAME sum(y) = wind_demand.
Combined with the Dicke state initializer, this keeps the entire DQA evolution within the feasible subspace {y : sum(y) = wind_demand}. Corresponds to U_d (Eq. 39) of the paper.
Source code in qiskit_impl/binary_optimizer.py
cost_scenario_operator ¶
Build the single-scenario cost (phase) operator for a fixed wind scenario.
Unlike cost_operator (which loops over all scenarios in superposition),
this version acts only on the y-register and encodes the cost for one
specific scenario xi deterministically.
For each turbine j:
- If
scenario[j] == 0(no wind): applyP(amplitude * recourse / norm)on qubit j. - If
scenario[j] == 1(wind): applyP(amplitude * c_y[j] / norm)on qubit j.
Used in adiabatic_evolution_scenario_circuit() for single-scenario DQA.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scenario
|
Tuple of 0/1 values encoding the wind scenario xi. |
required | |
amplitude
|
Annealing parameter gamma (0 to 1). |
required | |
constraint_amplitude
|
Recourse cost coefficient c_r. |
required | |
norm
|
Cost normalization factor (prevents phase wrapping). |
required |
Returns:
| Type | Description |
|---|---|
|
QuantumCircuit acting on |
Source code in qiskit_impl/binary_optimizer.py
adiabatic_evolution_circuit ¶
Adiabatic evolution; currently only using the constraint-preserving mixer.
Build the Discrete Quantum Annealing (DQA) circuit U_DQA(T).
Implements the alternating-operator ansatz (QAOA-style) with a LINEAR annealing schedule s(t) = t/T interpolating from mixer-dominated (t=0) to cost-dominated (t=T):
U_DQA = Prod_{t=0}^{T-1} U_q(s(t)) . U_d(1 - s(t))
At t=0: s=0 -> full mixing, no cost -> system stays in Dicke state (uniform) At t=T: s=1 -> full cost, no mixing -> system is "frozen" near cost minimum
The adiabatic theorem guarantees that if T is large enough relative to the inverse spectral gap, the final state is close to the ground state of H_C (i.e., the optimal turbine assignment for each scenario).
Initialization
- Dicke state |D_n^{wind_demand}> on wind_qubits
- PDF state |P> on pdf_qubits (encodes scenario probabilities)
Parameters¶
wind_demand : integer k (sum(y) must equal this) time : total annealing time T (scales schedule) time_steps : number of Trotter steps (circuit depth) norm : cost normalization (keeps phases in [0, 2pi])
Source code in qiskit_impl/binary_optimizer.py
adiabatic_evolution_scenario_circuit ¶
adiabatic evolution; constraint-preserving mixer and only a specific scenario
Source code in qiskit_impl/binary_optimizer.py
implemented_qae ¶
Build the canonical Quantum Amplitude Estimation (QAE) circuit. (Brassard et al. 2002, also Fig. 2 of the paper)
estimate a = E_xi[q?(y,xi)] (the normalized expected cost)
with error O(1/2^m) using 2^m Grover applications.
Qubit layout
[0 .. m-1] : m QPE estimate qubits (readout) [m .. m+2n_y-1] : system qubits (wind + pdf) [m+2n_y] : ancilla qubit
Circuit structure
- H(x)m on estimate qubits -> superposition |0>+|1>+...+|2^m-1>
- A = oracle . op applied once on system + ancilla Prepares: sqrt(1-a)|psi_bad>|0> + sqrta|psi_good>|1>
- Controlled-Grover repetitions: for i=0..m-1, controlled on estimate qubit i:
- S_psi0 : flip phase of ancilla=|1> states (X.CZ.X on ancilla)
- Adag : invert oracle then invert op
- S_0 : flip phase of |00...0> (X-flip all, MCZ, X-flip back)
- A : reapply op then oracle Each iteration of j applies this block 2^i times, encoding theta_a as a binary fraction in the QPE phase register.
- IQFT on estimate qubits -> phase -> readable integer b Measurement: b -> ? = sin^2(b.pi/2^m) -> phi?(x) = ?.norm
Source code in qiskit_impl/binary_optimizer.py
canonical_qae ¶
use qiskit tools to setup estimator NOTE still need to setup
Source code in qiskit_impl/binary_optimizer.py
exact_oracle ¶
Compute the exact oracle -- obviously untractable, but helpful for actually determining accuracy.
Build the exact oracle F_exact (Eq. 44 of paper).
For every (y, xi) basis state that satisfies sum(y) == ydemand:
F_exact |y>|xi>|0>_anc = |y>|xi> . (sqrt(1-q?)|0> + sqrtq?|1>)
where q? = q(y,xi)/norm is the normalized cost. After this operation Pr[ancilla = |1>] = q?.
Implementation -- "bit-flip sandwich" for each basis state: 1. Flip X on every qubit where the target bit pattern is 0 -> now ALL control qubits are |1> for this basis state only 2. Apply multi-controlled RY(2.arcsin(sqrtq?)) on the ancilla -> fires only when all controls are |1> = when system is in this exact state 3. Flip X back on the same qubits (uncompute)
This requires O(2^{2n_y}) gates and is impractical at scale, but gives the exact result for small n_y. Used only as a convergence benchmark.
Source code in qiskit_impl/binary_optimizer.py
single_oracle_sin_inconstraint ¶
Compute the oracle onto an ancilla qubit -- assuming our states are in-constraint, using the sin approximation.
Build the practical sin-approximation oracle F_sin (Eq. 45 of paper).
Uses the small-angle approximation sin(theta) ~= theta to decompose the oracle into O(n_y) doubly-controlled RY gates instead of O(2^n) multi-controlled ones.
For each turbine j, applies TWO CCRY (doubly-controlled RY) pairs:
CCRY(pi.c_y[j]/norm) controlled on [wind_qubit_j, pdf_qubit_j] -> ancilla -> fires when y[j]=1 AND xi[j]=1 (turbine ON, wind available) -> rotates ancilla by pi.c_y[j]/norm (encodes operational cost)
X(pdf_qubit_j) then CCRY(pi.c_r/norm) then X(pdf_qubit_j) -> fires when y[j]=1 AND xi[j]=0 (turbine ON, no wind) -> rotates ancilla by pi.c_r/norm (encodes recourse cost)
The approximation works because costs are a SUM over independent turbines, so each turbine's contribution can be added independently to the ancilla. Error grows with the magnitude of individual cost angles.
c = cost normalization scalar (controls scale = pi.c/norm)
norm = maximum cost (prevents ancilla from over-rotating past pi/2)
Source code in qiskit_impl/binary_optimizer.py
single_oracle_asin_inconstraint ¶
Build the arcsin oracle using exact RY angles (more accurate than sin oracle).
Applies CCRY rotations with angles \(2\arcsin(\sqrt{c/\text{norm}})\) instead
of the small-angle approximation \(\pi c/\text{norm}\) used by
single_oracle_sin_inconstraint().
For each turbine j:
theta_cy = 2*arcsin(sqrt(c_y[j]/norm))— exact rotation for operational costtheta_cr = 2*arcsin(sqrt(c_r/norm))— exact rotation for recourse cost
More accurate than F_sin at the cost of a different approximation error
profile (arcsin vs. linear). Requires c_y/norm <= 1 and c_r/norm <= 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
norm
|
Maximum cost; all costs must satisfy |
required | |
inverse
|
If |
False
|
Returns:
| Type | Description |
|---|---|
|
QuantumCircuit on |
Source code in qiskit_impl/binary_optimizer.py
execute_optimizer ¶
Execute the DQA circuit and return measurement results.
Two modes:
num_meas=None: Run with the statevector simulator (exact). Returns a probability dictionary{bitstring: probability}.num_meas=N: Appendmeasure_all()and run N shots. Returns a normalized count dictionary{bitstring: count/N}.
GPU Tier 1
Replace Aer.get_backend('statevector_simulator') with
AerSimulator(method='statevector', device='GPU') for H100 speedup.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qc
|
The DQA |
required | |
num_meas
|
Number of measurement shots, or |
None
|
Returns:
| Type | Description |
|---|---|
|
Dict mapping bitstrings to probabilities (exact) or normalized counts (sampled). |
Source code in qiskit_impl/binary_optimizer.py
execute_optimizer_oracle ¶
Execute the DQA + oracle circuit and return the ancilla amplitude.
Runs the circuit that is DQA followed by the oracle, then marginalizes over the ancilla qubit to recover the amplitude \(a = \Pr[\text{ancilla}=|1\rangle]\). This \(a\) is the normalized expected cost \(\bar{\phi}(x)\).
Two modes:
num_meas=None: Exact statevector — sums probability of all states with ancilla=1.num_meas=N: Sampled — returnscounts['1'] / N.
GPU Tier 1
Replace Aer.get_backend('aer_simulator_statevector') with
AerSimulator(method='statevector', device='GPU').
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qc
|
Circuit comprising DQA followed by the oracle (ancilla is MSB). |
required | |
num_meas
|
Number of shots, or |
None
|
Returns:
| Type | Description |
|---|---|
|
Float in [0, 1] — the amplitude on the ancilla |
Source code in qiskit_impl/binary_optimizer.py
execute_qae ¶
Execute the full QAE circuit and return the QPE readout distribution.
Runs the canonical QAE circuit (built by implemented_qae()) and returns
measurement outcomes of the \(m\) QPE estimate qubits.
Post-processing (done by caller):
b_str = max(b_counts, key=b_counts.get) # most probable readout
b_int = int(b_str, 2)
a_tilde = np.sin(b_int * np.pi / (2**m))**2
phi_tilde = a_tilde * norm
Two modes:
num_meas=None: Exact statevector, marginalizes over the system qubits.num_meas=N: Sampled, applies measurement gates on the \(m\) estimate qubits only.
GPU Tier 1 + Tier 3
Replace backend with AerSimulator(method='statevector', device='GPU').
For m >= 7, also enable cuStateVec_enable=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qc
|
The full QAE |
required | |
m
|
Number of QPE estimate qubits (determines resolution: error ~ 1/2^m). |
required | |
num_meas
|
Number of shots, or |
None
|
Returns:
| Type | Description |
|---|---|
|
Dict |
Source code in qiskit_impl/binary_optimizer.py
1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 | |
execute_optimizer_batched ¶
GPU-batched variant of execute_optimizer for multiple circuits at once.
Submits all circuits in a single simulator.run() call, which amortises
GPU kernel launch overhead and maximises H100 occupancy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuits
|
list
|
List of DQA QuantumCircuits (one per x-value). |
required |
num_meas
|
int
|
Shots per circuit, or |
None
|
Returns:
| Type | Description |
|---|---|
list
|
List of probability/count dicts, one per input circuit. |
Source code in qiskit_impl/binary_optimizer.py
execute_qae_batched ¶
GPU-batched variant of execute_qae for multiple QAE circuits at once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuits
|
list
|
List of full QAE QuantumCircuits (one per x-value). |
required |
m
|
int
|
Number of QPE estimate qubits. |
required |
num_meas
|
int
|
Shots per circuit, or |
None
|
Returns:
| Type | Description |
|---|---|
list
|
List of QPE outcome dicts |
Source code in qiskit_impl/binary_optimizer.py
process_expectation_value_optimizer ¶
Convert DQA measurement counts to the classical expected cost.
Post-processes the output of execute_optimizer() into an expected cost
estimate by evaluating wind_scenario_cost() for each measured bitstring
and weighting by its probability:
where the sum is over all bitstrings in counts and \(p\) is the
normalized measurement frequency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
wind_demand
|
Integer \(k\) — required Hamming weight of turbine decisions. |
required | |
counts
|
Dict |
required |
Returns:
| Type | Description |
|---|---|
|
Float — estimated expected second-stage cost \(\tilde{\phi}(x)\). |
Source code in qiskit_impl/binary_optimizer.py
Helper Functions¶
qiskit_impl.binary_optimizer.sample_wvfn ¶
given a probability vector (ie wvfn squared), return one measurement
GPU Backend Helper¶
qiskit_impl.binary_optimizer._get_simulator ¶
Return an AerSimulator targeting GPU if available, else CPU.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
Aer simulation method ('statevector' or 'tensor_network'). |
'statevector'
|
shots_mode
|
bool
|
If True, add max_parallel_shots=0 for GPU shot parallelism. |
False
|
n_qubits
|
int
|
Circuit width hint. When >= 20 on GPU, enables cuStateVec (NVIDIA cuQuantum) for large-statevector acceleration. |
0
|