Transforms
Transforms reduce or change a QUBO Instance before solving — fixing variables, or eliminating
negative off-diagonal coefficients — and record enough history to map a Solution of the
transformed problem back to the original one. Each is callable directly, or chained automatically
by Solver/SolverConfig via preprocessing=True.
- API reference:
qubosolver.transforms
Apply and lift
Section titled “Apply and lift”Every transform exposes the same pair of functions:
applytakes anInstanceand returns a wrapperInstance— a subclass that keeps a reference to the parent instance and records what was changed (fixed variables, flipped bits, or zeroed edges).lifttakes aSolutionobtained by solving the wrapper instance, together with that wrapper instance, and reconstructs aSolutionfor the original problem: bitstrings are mapped back to their original length and ordering, and costs are recomputed against the original QUBO matrix.
Code example
Section titled “Code example”from qubosolver import Instance, transforms, solving, matrix, bitstrings
instance = Instance(matrix.tensor([[-2.0, 1.0], [1.0, -2.0]]))
reduced_instance = transforms.variable_fixing.apply_recursively(instance)reduced_solution = solving.brute_force.solve(reduced_instance)solution = transforms.variable_fixing.lift(reduced_solution, reduced_instance)
print(f"Fixed indices: {reduced_instance.fixed_indices}")print(f"Full-size bitstrings: {bitstrings.to_strings(solution.bitstrings)}")print(f"Costs: {solution.costs}")Fixed indices: [{0: 1, 1: 1}]Full-size bitstrings: ['11']Costs: tensor([-2.])Transforms can be chained: apply one to the output of another, solve the final instance, then
lift back through each transform in reverse order. Solver does this for you (see
The Solver shortcut).
Reduction with variable fixing
Section titled “Reduction with variable fixing”Variable fixing deterministically fixes variables to 0 or 1 when the structure of the QUBO matrix alone already settles their optimal value, without introducing any approximation or randomness — every fixation is guaranteed to be optimal with respect to the original QUBO. It is particularly effective for sparse or structured matrices, where many variables can often be fixed early.
The transform lives in qubosolver.transforms.variable_fixing and applies fixation rules
— functions that inspect the QUBO matrix and return the variables they can fix. apply runs each
rule once, in order, reducing the matrix after each rule fixes variables; apply_recursively
repeats that until a full pass fixes no additional variables.
The default (and currently only) rule, hansen_fixing, is based on the diagonal and off-diagonal
entries of the QUBO matrix. For each variable , it computes a lower bound
and an upper bound
on that variable's contribution to the objective; the variable is fixed to when the lower bound is already non-negative (it cannot improve the objective by being ) and to when the upper bound is already non-positive (it can only improve it by being ). Several such rules can be applied in sequence until convergence, reducing the QUBO instance before it is passed to the solver.
- API reference:
qubosolver.transforms.variable_fixing
Code example
Section titled “Code example”from qubosolver import Instance, transforms, solving, matrix, bitstrings
Q = Instance(matrix.tensor([[-2.0, 1.0], [1.0, -2.0]]))
reduced = transforms.variable_fixing.apply_recursively(Q)reduced_solution = solving.brute_force.solve(reduced)solution = transforms.variable_fixing.lift(reduced_solution, reduced)
print(f"Fixed indices: {reduced.fixed_indices}")print(f"Reduced size: {reduced.size}")print(f"Full-size bitstrings: {bitstrings.to_strings(solution.bitstrings)}")Fixed indices: [{0: 1, 1: 1}]Reduced size: 0Full-size bitstrings: ['11']If no variables were fixed, lift returns a deep copy of the reduced solution unchanged.
References
Section titled “References”- Hansen, P. (1979). Method of non-linear 0-1 programming. Annals of Discrete Mathematics, 5:53–70.
Negative coefficients bitflip
Section titled “Negative coefficients bitflip”Quantum (Rydberg) solvers cannot embed attractive (negative off-diagonal) interactions, so a QUBO must have non-negative off-diagonal coefficients to be embeddable — see Embedding. Bit-flip preprocessing searches for a change of variables that removes as much negative off-diagonal weight as possible while preserving the QUBO objective exactly.
For each variable, a flip vector entry of 0 keeps it unchanged and 1 complements it
(x_i -> 1 - x_i); the transformed QUBO is equivalent to the original problem up to this change of
variables. The current implementation uses GLPK to solve an integer linear program that selects
the flip vector minimizing the remaining negative off-diagonal weight — exact, which is useful for
validation and small QUBOs, but not expected to scale to large industrial instances.
The transform lives in qubosolver.transforms.negative_bitflip. apply solves the ILP, applies
the flips, and returns an Instance that records the flip vector, status, and metrics.
- API reference:
qubosolver.transforms.negative_bitflip
Code example
Section titled “Code example”import jsonfrom qubosolver import Instance, transforms, matrix
Q_hard = Instance( matrix.tensor( [ [0.0, -2.0, 1.0, 1.0], [-2.0, 0.0, -2.0, 1.0], [1.0, -2.0, 0.0, -2.0], [1.0, 1.0, -2.0, 0.0], ] ))
reduced_hard = transforms.negative_bitflip.apply(Q_hard, time_limit_s=60.0)
print(f"Flip vector: {reduced_hard.flips}")print(f"Bitflip status: {reduced_hard.status}")print(f"Bitflip metrics: {json.dumps(reduced_hard.metrics, indent=4)}")Flip vector: tensor([1, 0, 1, 0], dtype=torch.int8)Bitflip status: OPTIMALBitflip metrics: { "n_edges": 6, "neg_count_before": 3, "neg_count_after": 1, "neg_count_reduction_pct": 66.66666666666667, "neg_weight_before": 6.0, "neg_weight_after": 1.0, "neg_weight_reduction_pct": 83.33333333333333, "objective_value": -5.0}Bit-flip preprocessing reduces the negative off-diagonal coefficients, but — as in the example above — it may not remove all of them.
Negative coefficients zeroing
Section titled “Negative coefficients zeroing”If negative off-diagonal coefficients remain after bit-flip preprocessing, zeroing.apply sets
each of them to zero, applied on top of the bit-flip result. This makes the QUBO embeddable, but
it changes the QUBO objective, unlike bit-flip preprocessing — so it should be used only when this
approximation is acceptable, as a last resort.
The transform lives in qubosolver.transforms.zeroing. apply returns a new Instance; it does
not modify its argument in place.
- API reference:
qubosolver.transforms.zeroing
Code example
Section titled “Code example”from qubosolver import Instance, transforms, matrix
Q_hard = Instance( matrix.tensor( [ [0.0, -2.0, 1.0, 1.0], [-2.0, 0.0, -2.0, 1.0], [1.0, -2.0, 0.0, -2.0], [1.0, 1.0, -2.0, 0.0], ] ))
reduced_hard = transforms.negative_bitflip.apply(Q_hard, time_limit_s=60.0)zeroed_hard = transforms.zeroing.apply(reduced_hard)
print(f"Zeroed edges:\n{zeroed_hard.zeroed_edges}")print(f"Negative matrix:\n{zeroed_hard.negative_matrix}")Zeroed edges:tensor([[0, 3]])Negative matrix:tensor([[ 0., 0., 0., -1.], [ 0., 0., 0., 0.], [ 0., 0., 0., 0.], [-1., 0., 0., 0.]])Zeroing is only ever applied automatically by the Solver/SolverConfig shortcut below, and only
when solving with a QuantumSolvingConfig — regardless of whether preprocessing is enabled.
Before embedding, the final QUBO is always checked, and if negative off-diagonal coefficients
remain, the quantum solver zeros them out automatically (logging a message) so it can be embedded
— there is no separate flag to request this, and no flag to opt out of it or make it raise
instead. Classical solvers have no such restriction and never trigger this fallback.
The Solver shortcut
Section titled “The Solver shortcut”Going through the Solver dispatcher with a QuantumSolvingConfig chains the transforms above
for you:
- if
SolverConfig.preprocessing=True, variable-fixing reduction (transforms.variable_fixing.apply_recursively) followed by negative-coefficients bit-flip preprocessing (transforms.negative_bitflip.apply); - regardless of
preprocessing, if negative off-diagonal coefficients remain, automatic zeroing (transforms.zeroing.apply) — classical solvers skip this step entirely.
After solving, Solver lifts the solution back through each transform in reverse order, so the
returned Solution always refers to the bitstrings and costs of the original instance you
passed in.
Code example
Section titled “Code example”from qubosolver import Instance, Solver, SolverConfig, QuantumSolvingConfig, matrix
Q_hard = Instance( matrix.tensor( [ [0.0, -2.0, 1.0, 1.0], [-2.0, 0.0, -2.0, 1.0], [1.0, -2.0, 0.0, -2.0], [1.0, 1.0, -2.0, 0.0], ] ))
config = SolverConfig( solving=QuantumSolvingConfig(), preprocessing=True,)
# Bit-flip preprocessing cannot remove every negative off-diagonal coefficient here;# the quantum solver zeros out the remainder automatically before embedding (see the# logged message).solution = Solver(Q_hard, config).solve()
print(f"Solution bitstrings shape: {solution.bitstrings.shape}")print(f"Original instance size: {Q_hard.size}")Solution bitstrings shape: torch.Size([5, 4])Original instance size: 4