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

Objectives

An objective is the part of a model’s Hamiltonian you write directly, as opposed to the part a constraint instruction adds on your behalf – see Quadratic Models for what the Hamiltonian \(H\) is and why a solver minimising it is the whole mechanism. In XQCP terms, an objective is whatever you assign or add to model.linear[i] and model.quadratic[i, j] before any constraint method runs.

Solvers Minimise. Say So.

Every backend XQuad targets minimises. If your problem is a maximisation, you have to negate it, and getting this backwards produces a model that runs, verifies, and confidently returns the worst answer instead of the best one – nothing checks that you meant to maximise.

Knapsack maximises total value. examples/knapsack/runner.py writes the objective as:

# Objective: minimise -sum(v_i * x_i)
with problem.range(0, num_items) as i:
    vi = problem.stow("vi", values_in.get(i))
    problem.model.linear[i].add(-vi)

x_i = 1 means item i is selected, and -vi is the sign flip: instead of rewarding selection with +v_i, the objective penalises it with -v_i, so minimising H picks large values, not small ones. Isolating just this loop – no capacity constraint – and running the encoder with num_items=5, values=[5, 4, 18, 3, 19], then computing ENERGY against the sample [1, 1, 1, 0, 1] (items 0, 1, 2, 4 selected, values 5 + 4 + 18 + 19 = 46) gives:

objective-only energy: -46
expected: -(sum of selected values) = -46

Energy is exactly the negative of total value, for any selection, because that is what linear[i] = -v_i means: drop the - and the same solver would minimise total value instead, selecting nothing.

Assign or Accumulate

model.linear[i] and model.quadratic[i, j] are CoefficientRef proxies, not values. Three things you can do with one:

SyntaxOpcodeEffect
model.linear[i] = wSETLINECoefficient becomes w, replacing whatever was there
model.linear[i].add(w)ADDLINECoefficient becomes current + w
x = model.linear[i]GETLINERead the current coefficient as an expression

The same three exist for model.quadratic[i, j], via SETQUAD/ADDQUAD/ GETQUAD. The distinction matters the moment more than one term touches the same coefficient – knapsack’s .add(-vi) runs once per item, each touching a different linear[i], so assign and accumulate would agree there. They stop agreeing as soon as two terms share a coefficient: two .add() calls against the same linear[0] with weights 5 and 7 leave it at 12; two = assignments with the same weights leave it at 7, the second overwriting the first outright. Reach for .add() any time a coefficient might accumulate contributions from more than one place, which for an objective built inside a loop – the usual shape – is the common case; reach for = when you know this is the coefficient’s only source.

Quadratic Terms

A quadratic coefficient couples two variables, and the same accumulate pattern applies. Max-Cut’s objective is the DSL rendition of the Max-Cut coefficient rule Quadratic Models derives by hand – for each weighted edge (i, j, w):

problem.model.linear[i].add(-w)
problem.model.linear[j].add(-w)
problem.model.quadratic[i, j].add(w * 2)

three .add() calls, one per term of \(\text{linear}[i] \mathrel{+}= -w,\ \text{linear}[j] \mathrel{+}= -w,\ \text{quadratic}[i,j] \mathrel{+}= 2w\). .add() is what makes this loop correct: a node touched by several edges accumulates a -w contribution from each one, and = would let the last edge silently overwrite every edge before it. Running examples/maxcut/runner.py --n 5 --seed 42 confirms the sign on the result – energy: -354 against cut_weight: 354 – matching the same “minimise the negative” pattern knapsack uses, for the same reason.

Coefficient access on a 2D model accepts (row, col) tuples in place of flat indices, for both linear and quadratic coordinates independently – see Inputs and Model Shape for the flattening XQCP applies; nothing about objective assignment changes once a coordinate is a tuple instead of an int.

Beyond Quadratic: model.reduce()

Everything above stops at degree two: one or two variables per term. model.reduce(var_a, var_b, p_aux) -> RegLoad is the only way past that. It performs a Rosenberg degree reduction: allocate one fresh auxiliary variable w at model.size, grow model.size by one, and add enforcement terms so w behaves as var_a AND var_b: p_aux * (x_a*x_b - 2*x_a*w - 2*x_b*w + 3*w), which is 0 when w == x_a AND x_b and strictly positive otherwise. It returns w as a RegLoad, an ordinary variable index you can use in a further quadratic term, or feed into a second reduce() call to reach one degree higher still:

w = problem.model.reduce(ti, tj, p_aux)
problem.model.quadratic[w, tk].add(coeff)   # coeff * x_i * x_j * x_k

examples/cubic_opt/runner.py builds a cubic term this way, one reduce() call per term inside a problem.range() loop; max3sat and portfolio_opt use the same one-call pattern for their own cubic terms. examples/quartic_opt/runner.py chains two reduce() calls to reach a quartic term – the reason reduce() returns a RegLoad rather than nothing.

p_aux is a penalty in its own right, separate from any constraint penalty in the same model: it has to be large enough that violating the w == x_a AND x_b relationship is never worth it, the same reasoning Constraints applies to constraint penalties. Each reduce() call that actually executes grows model.size by one, so the final variable count is size plus the number of reduce() calls executed – loops included. cubic_opt allocates one auxiliary variable per cubic term inside its loop, not one for the whole model.

reduce() hangs off model, like every constraint form, but it is not one: XQCP keeps it out of the DSL’s constraint bookkeeping deliberately, since it is a structural transformation of the model, not a domain rule about a solution. That is also why it lives on this page and not Constraints.

How Large an Objective Gets

An objective’s magnitude is bounded by what its coefficients can sum to. Knapsack’s linear coefficients are -values, so its objective ranges from 0 (nothing selected) down to -49 for values = [5, 4, 18, 3, 19] (everything selected, which for this instance fits the capacity: the weights [2, 1, 5, 4, 4] sum to 16, under the capacity of 18) – a range fixed entirely by the problem’s own numbers, before any constraint enters the picture. Constraints picks a penalty weight relative to a range like this one; this page stops at producing the range, not sizing anything against it.

With an objective in place, the next step most problems need is a rule the objective alone cannot express – see Constraints for folding one into the same Hamiltonian.