Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Integer Scaling

Every XQMX coefficient is an i64, and every arithmetic operation on one is exact – Quadratic Models and Energy and Precision both rest on that fact. Real problem data rarely arrives as integers: prices, weights in kg, fractional returns. This page covers turning that data into i64 coefficients without losing precision the problem needs, and what a scale factor costs once it is chosen. No example in this repository has fractional inputs to scale, so everything below is a fresh, executed worked instance rather than a citation.

Choosing the Scale Factor

Multiply every fractional value by a factor large enough that the result is exactly an integer, and use that integer as the coefficient. The factor has to be large enough for every value in the problem, not just the ones that look round. A four-item knapsack with quarter-kilogram weights and cent-precision values:

weights_f = [2.50, 3.75, 1.25, 4.00]
values_f = [10.20, 15.75, 8.40, 20.00]
capacity_f = 7.50

def scale(values, factor):
    scaled = [round(v * factor) for v in values]
    for v, s in zip(values, scaled):
        assert abs(v * factor - s) < 1e-9, (v, factor, s)   # catch silent rounding
    return scaled

Scaling by 4 – enough for the weights, which are all quarters – fails the assertion the moment it reaches the values: 10.2 * 4 = 40.8 is not integral (rounds to 41), so scale() raises before the demo builds a model from this factor:

AssertionError: (10.2, 4, 41)

The values are cent-precision but not all quarters (10.20, 8.40 are not multiples of 0.25), so 4 is not enough. The minimal factor that is exact for every value here is the least common multiple of every value’s decimal denominator – 20 (every one of these numbers is a multiple of 0.05), not the naive 100 a “two decimal places” guess would reach for:

from fractions import Fraction
from math import lcm
denoms = [Fraction(v).limit_denominator(10000).denominator
          for v in weights_f + values_f + [capacity_f]]
lcm(*denoms)   # 20

At factor = 20: weights = [50, 75, 25, 80], values = [204, 315, 168, 400], capacity = 150. Building this through xqcp (the same SLACK + EQUALITY shape Selection Under Budget covers, at the penalty weight of 4 derived in Coefficient Magnitude below), compiling, running the encoder on the Rust VM, solving with SolverDWaveCPU, and dividing the decoded totals back by the scale factor:

selection=[1, 1, 1, 0] total_weight=7.5 total_value=34.35 capacity_ok=True energy=-90687

7.5 and 34.35 are exact – not rounded back, computed by an integer sum divided by 20 with no remainder, since 20 was chosen to be exact for every input. xquad verify accepts the compiled encoder (43 instructions), and the same selection, {0, 1, 2}, comes back identically across five different solver seeds.

What Scaling Costs: Slack Bits

SLACK’s bit count is \(S = \lfloor \log_2(\text{capacity}) \rfloor + 1\) (SLACK) – logarithmic in the capacity, but the capacity that matters is the scaled one. The naive factor = 100 scaling of the same instance needs 10 slack bits (capacity = 750); the minimal exact factor = 20 needs 8 (capacity = 150). Scaling five times more finely than the data requires costs two extra variables here, and the gap widens as the capacity grows – an unnecessarily large scale factor is not free, even before its effect on coefficient magnitude below.

What Scaling Costs: Coefficient Magnitude, and Two Backends That Disagree

A penalty weight multiplies every constraint coefficient (High-Level Constraints), so a scale factor and a penalty compound. Using the loose safe penalty Constraints gives as a fallback – one more than the sum of absolute linear coefficients, 1088 for the values above, since the rule is penalty greater than that sum – against the factor = 20 model:

loose_penalty=1088: max |coefficient| = 23,953,408
fits MAX_NATURAL_COEFFICIENT (2,147,483): False

Quip Network confirms the exact bound directly from xqsa.quip_codec:

from xqsa.quip_codec import MILLI_SCALE, MAX_NATURAL_COEFFICIENT
print(MILLI_SCALE, MAX_NATURAL_COEFFICIENT)
# 1000 2147483

The loose bound, safe on its own terms, overflows SolverQuip’s milli-scale i32 encoding more than tenfold at this scale factor. Enumerating this specific instance the way Constraints enumerates its own – sixteen subsets, tightest violator {0, 2, 3} at weight 155, value gap 85 over an excess of 5 – gives a tight threshold of 85 / 5^2 = 3.4, so the smallest safe integer penalty is 4, not 1088:

tight penalty=4: max |coefficient| = 88,064
fits MAX_NATURAL_COEFFICIENT: True

4 clears the tight threshold and the solver still returns the correct optimum, {0, 1, 2}, identically across five seeds – the two hundred seventy-two-fold gap between the loose and tight penalty is exactly what made the difference between overflowing SolverQuip’s encoding and fitting it comfortably. This is Constraints’s own warning about the loose bound being loose, made concrete by a scale factor large enough to expose it: a bound that is merely safe at natural scale can become the deciding factor once a scale factor multiplies every coefficient it touches.

The same coefficient growth affects metal-gpu’s float32 search differently from cuda-gpu’s float64 one, the way Energy and Precision covers for an unscaled model. At the loose penalty above, the resulting solved energy (-24,480,687, order \(10^7\)) is past float32’s roughly seven decimal digits of resolution for a difference of 1, but not for a difference of 100:

import numpy as np
e = -24480687   # this model's actual solved energy at the loose penalty
np.float32(e) != np.float32(e - 1)     # False -- the 1-unit difference is lost
np.float32(e) != np.float32(e - 100)   # True -- a 100-unit difference still resolves

A scaling choice that looks safe by natural-scale reasoning can cost metal-gpu resolution cuda-gpu keeps, at the identical model – check the actual coefficient magnitudes your factor produces against the target backend, not just against the input data’s precision.

Cost in Variables

\(S = \lfloor \log_2(\text{scaled capacity}) \rfloor + 1\) slack bits on top of the item count, same formula as Selection Under Budget, with the scaled capacity – not the original – as the input.

Failure Mode

Picking a scale factor from “how many decimal places does this look like” rather than the actual least common multiple of every value’s denominator either drops precision silently (a factor too small, caught here only because the demo asserts the rounding was exact) or costs variables and coefficient headroom for no reason (a factor larger than any value needs, the 100 vs. 20 case above). Compute the minimal exact factor once, from every value the problem uses, rather than guessing a round number.