Energy and Precision
Every backend in this chapter returns the same kind of number, computed the same way, no matter what found the sample: an integer. This page covers how that number is computed and compared, and where precision is lost or preserved as a model moves from an exact integer formula onto real hardware.
The Formula
A sample’s energy with respect to a model is:
$$E = \sum_i \text{linear}[i] \cdot x_i ;+; \sum_{i \le j} \text{quadratic}[i,j] \cdot x_i \cdot x_j$$
identical to the model’s Hamiltonian from
Quadratic Models, evaluated at one
concrete assignment x, and identical to the XQVM ENERGY opcode (see
Energy Evaluation). A verifier program
run through XQVM and xqsa’s own recomputation compute the same
formula two independent ways.
The Precision Contract
SolverResult.energy must equal model.energy(sample)
exactly, an integer-to-integer comparison with zero tolerance for
drift. Every XQMX
coefficient and every variable assignment is an integer, so the energy
formula is a sum of integer products, not a float that merely rounds to
the right answer. It is an exact integer whenever it is representable at
all, and representable is a real condition: XqmxModel.energy checks every
term product and every partial sum against the i64 range and raises
ArithmeticOverflow rather than returning a wrapped number, so a model
whose energy leaves that range has no energy this contract can report.
Solver._recompute_energy()
returns model.energy(sample); every backend in this
chapter uses it instead of trusting whatever float its underlying
library reports. That raw float, where one exists, survives only as
metadata["params"]["raw_energy"], for diagnostics – never as the
authoritative value.
A verifier program never has to take a solver’s word for its own answer.
It recomputes the same integer independently with ENERGY. There are
three outcomes, not two: the comparison holds, the comparison fails, or
ENERGY raises ArithmeticOverflow and the verifier produces no verdict
at all. The third is not a solver disagreement and must not be read as
one – it says the model’s energy left the i64 range on the way to
being computed. XqmxModel.energy is the function ENERGY itself
calls, so the two accumulate in the same sorted key order, check the
same intermediates, and agree on which models fall into it.
A Large Penalty Does Not Corrupt the Model
Choosing a Penalty Weight
argues that a large \(P\) cannot corrupt a model, because energy
differences between feasible assignments survive exactly however large
\(P\) gets. Take that page’s three-node Max-Cut model with a one-hot
penalty \(P \cdot (x_0 + x_2 - 1)^2\) added on top, and score two
feasible assignments and one infeasible one through XqmxModel.energy()
at three wildly different weights:
P= 2 | E(feasible_a)= -8 E(feasible_b)= -6 E(infeasible)= -8 diff= 2
P= 1000 | E(feasible_a)= -1006 E(feasible_b)= -1004 E(infeasible)= -8 diff= 2
P= 2000000000 | E(feasible_a)= -2000000006 E(feasible_b)= -2000000004 E(infeasible)= -8 diff= 2
The difference between the two feasible assignments stays exactly 2
at every weight. The penalty term is 0 on both, so what separates them
is the underlying objective alone, and integer arithmetic never lets it
drift.
The infeasible assignment’s own energy does not move either – it sits
at -8 at every \(P\) above. What moves is the two feasible
energies, and they fall without bound as \(P\) grows. That is the
missing +P constant, not the penalty term itself: the true,
unstorable Hamiltonian adds +P to every assignment that violates the
constraint and +0 to every assignment that satisfies it, so dropping
that constant subtracts a uniform P from every stored energy. On the
infeasible sample the missing +P exactly cancels the penalty’s own
+P, leaving it unchanged; on a feasible sample there is nothing to
cancel, so the stored energy falls by P.
What a Large Penalty Costs Instead
Nothing in the model degrades, but two things outside it do, and both matter to backends in this chapter.
Fixed-precision hardware loses resolution. The Metal engine (SolverGPU(engine="metal")) computes
coefficients in float32 on the GPU, not the integer arithmetic
ENERGY uses – result.energy is still recomputed in exact integers
afterward, but the search that finds the sample runs at float32
precision. The CUDA engine does not share this: its kernels take
const double* throughout, its acceptance draws stay float64, and
result.energy is recomputed the same way as every other backend. See
Local Solvers for the
per-backend split. The rest of this section applies to the Metal engine and
to fixed-precision annealing hardware, not to every GPU backend.
A float32 mantissa carries about seven decimal digits. Once a term in
the combined Hamiltonian is large enough that a difference of 2 no
longer changes the float, the Metal engine’s search cannot distinguish the
two assignments that difference separates, even though ENERGY still
resolves them exactly afterward:
import numpy as np
np.float32(-2000000006) == np.float32(-2000000004) # True -- the 2 vanished, at the Metal engine's width
np.float64(-2000000006) == np.float64(-2000000004) # False -- still resolved, at the CUDA engine's width
np.float32(-1006) == np.float32(-1004) # False -- still resolved
At P = 2000000000 the two feasible assignments above are
computationally indistinguishable to the Metal engine’s float32 search even
though XqmxModel.energy() still tells them apart exactly; at
P = 1000 they are not. The CUDA engine’s float64 search keeps this pair
distinguishable at this scale, though a large enough \(P\) would
eventually exhaust its wider but still finite mantissa too. Neither
GPU backend’s correctness ever suffers – the returned energy is
always the exact integer – but the Metal engine’s search can lose the
signal, at penalty weights where the CUDA engine’s still keeps it visible.
Real annealing hardware rescales, and the Quip network encodes at
fixed precision. A physical D-Wave annealer maps coefficients onto
its analog range with a monotonic, optimum-preserving rescaling, so an
oversized \(P\) is not rejected there – it costs usable range on the
device instead. SolverQuip goes further: it scales every coefficient
by MILLI_SCALE = 1000 into the chain’s i32 fields, so an oversized
coefficient can overflow i32 and raise EncodingError, and one finer
than 1/1000 is rounded to the nearest milli, with the largest rounding
error reported. MAX_NATURAL_COEFFICIENT = 2_147_483 is the
exact bound only for a SPIN model; a BINARY model – the domain every
worked example on this page uses – goes through a basis change first
that shifts the bound in both directions, and can overflow well before
any single coefficient looks anywhere near that large. See
Quip Network for the full
derivation.
A large penalty also raises the barrier between feasible regions, independent of any hardware precision question: a heuristic search such as simulated annealing has to cross that barrier to leave the first feasible region it reaches, and a larger \(P\) makes crossing less likely within a fixed number of sweeps. Sizing \(P\) in practice, and reading a solver’s output to tell which of these failure modes you hit, belongs to Constraints.
An Energy Ceiling on the Quip Network
SolverQuip’s max_energy order option (see Quality
floors) is an energy ceiling, and it is
in model units: the same units as XqmxModel.energy and
result.energy. The chain never sees your model, only its spin fields
and couplings at milli scale, so the client converts the ceiling before
sending it.
For a SPIN model the conversion is a change of scale: the ceiling times
1000, rounded down, so the chain admits exactly the energies at or below
it. A BINARY model also carries a constant. The basis change
s = 2x - 1 (see Quip Network)
turns the model into spin fields and couplings plus an offset that no
spin multiplies, and the chain, which stores only the fields and
couplings, computes energies without it. The client subtracts the offset
first:
chain ceiling (milli) = floor((max_energy - offset) * 1000)
A value within a millionth of a milli of the grid snaps to it before the
floor, so float noise such as -1.2500000000001 does not lower the
ceiling to -1251.
Take E = -2*x0 + x0*x1. Its spin form has fields h0 = -0.75 and
h1 = 0.25, a coupling of 0.25, and an offset of -0.75. The
assignment x0 = 1, x1 = 0 has model energy -2; on the chain, with
spins s0 = +1, s1 = -1, its energy is -0.75 - 0.25 - 0.25 = -1.25.
So max_energy=-2 goes on chain as -1250 milli, and the order box
shows both values:
│ Floors energy <= -2 (chain -1.25) │
The chain applies the ceiling to the model it holds. When the model’s
coefficients were rounded to the milli grid (see What a Large Penalty
Costs Instead), the chain’s
energies differ slightly from yours, so an answer near the ceiling can
land on either side of it, and create_order() warns. A ceiling below
the lowest energy the placed model can reach is refused, and one at or
above the highest warns that it filters nothing; Quality
floors lists every check.