Nonlinear Functions
PyQBPP supports the absolute value qbpp.abs(), ReLU qbpp.relu(), maximum qbpp.max(), and minimum qbpp.min() directly inside expressions. The solvers bundled with QUBO++ handle the function values directly and search efficiently — there is no need to design auxiliary variables or penalty polynomials by hand. These functions can also be combined with Native Integer Variables.
Absolute value: abs
qbpp.abs(f) denotes $|f|$ and qbpp.abs(f, 2) denotes $|f|^2$ (the exponent is 1 or 2). The following program minimizes $|x + y - 13| + |x - y - 3|$:
import pyqbpp as qbpp
x = qbpp.var("x", integer=(0, 10))
y = qbpp.var("y", integer=(0, 10))
f = qbpp.abs(x + y - 13) + qbpp.abs(x - y - 3)
f.simplify_as_binary()
solver = qbpp.ExhaustiveSolver(f)
sol = solver.search()
print(f"x = {sol(x)}, y = {sol(y)}")
print(f"f = {sol.energy}")
The output of the program is as follows:
x = 8, y = 5
f = 0
ReLU: relu
qbpp.relu(f) denotes $\max(0, f)$ and qbpp.relu(f, 2) denotes $\max(0, f)^2$; they are convenient for penalizing only the excess over a threshold. The following program maximizes the profit $4x + 7y$ while quadratically penalizing the workload $2x + 3y$ beyond 36 and imposing the native constraint $x + y \le 12$:
import pyqbpp as qbpp
x = qbpp.var("x", integer=(0, 20))
y = qbpp.var("y", integer=(0, 20))
profit = 4 * x + 7 * y
overtime = qbpp.relu(2 * x + 3 * y - 36, 2)
f = -profit + overtime + 100 * qbpp.cons(x + y <= 12)
f.simplify_as_binary()
solver = qbpp.ExhaustiveSolver(f)
sol = solver.search()
print(f"x = {sol(x)}, y = {sol(y)}")
print(f"profit = {sol(profit)}")
The output of the program is as follows:
x = 0, y = 12
profit = 84
Maximum and minimum: max / min
qbpp.max(f, g) and qbpp.min(f, g) denote the maximum and minimum of two expressions. The following program assigns four jobs to two machines and minimizes the larger load (the makespan):
import pyqbpp as qbpp
a = qbpp.var("a", 4)
p = [5, 3, 7, 4]
load1 = qbpp.Expr(0)
load2 = qbpp.Expr(0)
for i in range(4):
load1 += p[i] * a[i]
load2 += p[i] * (1 - a[i])
f = qbpp.max(load1, load2)
f.simplify_as_binary()
solver = qbpp.ExhaustiveSolver(f)
sol = solver.search()
print(f"makespan = {sol.energy}")
The output of the program is as follows:
makespan = 10
Support and limitations
- The bundled solvers (EasySolver, ABS3 Solver, and Exhaustive Solver) support
absandrelu(exponents 1 and 2) as well asmaxandmin. - Among the MIP wrappers,
GurobiSolversupportsabs/relu(exponents 1, 2) and the convex directions ofmax/min(minimizingw*max, maximizing min); coefficients must be positive there. The other MIP wrappers do not support nonlinear functions. - Any constant coefficient works, including negative ones (a negative coefficient rewards a large function value). Expressions containing nonlinear functions do not support
expand_cons()orreduce(). - The value
f(sol)of the expression at a solutionsolalways matches the energy reported by the solvers.
Relation to cons()
qbpp.cons() of Native Constraints is, value-wise, a member of the same family of nonlinear functions. An equality constraint has the same value as
and a range constraint has the same value as
\[\operatorname{cons}(l \le f \le u) = \operatorname{relu}(l - f,\, 2) + \operatorname{relu}(f - u,\, 2)\](at most one side can be violated, so the two relu terms are never positive at the same time).
The difference is the semantics. cons() declares the expression as a constraint: it participates in the violation count (Viol) and in the feasibility / target_energy decisions. abs() and relu() are pure objective terms with no constraint semantics. Use cons() for conditions that must be satisfied, and abs() / relu() for quantities whose value itself is a cost.