Variable and Expression Classes
qbpp::Var, qbpp::Term, and qbpp::Expr classes
QUBO++ provides the following fundamental classes:
qbpp::Var: Represents a variable symbolically and is associated with a string used for display. Internally, a 32-bit unsigned integer is used as its identifier.qbpp::Term: Represents a product term consisting of an integer coefficient and one or moreqbpp::Varobjects. The coefficient type isqbpp::coeff_t(defaultint32_t); it is selected at build time via one of theINTEGER_TYPE_*macros (see below). Eachqbpp::Termstores its variables using a static array (inline buffer of 2 elements) combined with dynamic allocation for higher-degree terms, allowing terms of arbitrary degree with no upper limit in the default variable-length mode (see the fixed-length VarArray modes below).qbpp::Expr: Represents an expanded expression consisting of an integer constant term and zero or moreqbpp::Termobjects. The constant-term type isqbpp::energy_t(defaultint64_t), also selected at build time via theINTEGER_TYPE_*macros.
In the following program, x and y are qbpp::Var objects, t is a qbpp::Term object, and f is a qbpp::Expr object:
#include <qbpp/qbpp.hpp>
int main() {
auto x = qbpp::var("x");
auto y = qbpp::var("y");
auto t = 2 * x * y;
auto f = t - x + 1;
std::cout << "x = " << x << std::endl;
std::cout << "y = " << y << std::endl;
std::cout << "t = " << t << std::endl;
std::cout << "f = " << f << std::endl;
}
This program produces the following output:
x = x
y = y
t = 2*x*y
f = 1 +2*x*y -x
If the data types are to be explicitly specified, the program can be rewritten as follows:
#include <qbpp/qbpp.hpp>
int main() {
qbpp::Var x = qbpp::var("x");
qbpp::Var y = qbpp::var("y");
qbpp::Term t = 2 * x * y;
qbpp::Expr f = t - x + 1;
std::cout << "x = " << x << std::endl;
std::cout << "y = " << y << std::endl;
std::cout << "t = " << t << std::endl;
std::cout << "f = " << f << std::endl;
}
qbpp::Var objects are immutable and cannot be updated after creation. In contrast, qbpp::Term and qbpp::Expr objects are mutable and can be updated via assignment.
For example, as shown in the following program, compound assignment operators can be used to update qbpp::Term and qbpp::Expr objects:
#include <qbpp/qbpp.hpp>
int main() {
qbpp::Var x = qbpp::var("x");
qbpp::Var y = qbpp::var("y");
qbpp::Term t = 2 * x * y;
qbpp::Expr f = t - x + 1;
std::cout << "t = " << t << std::endl;
std::cout << "f = " << f << std::endl;
t *= 3 * x;
f += 2 * y;
std::cout << "t = " << t << std::endl;
std::cout << "f = " << f << std::endl;
}
This program prints the following output:
t = 2*x*y
f = 1 +2*x*y -x
t = 6*x*y*x
f = 1 +2*x*y -x +2*y
Aliasing and Copying
C++ uses value semantics by default. Assigning one qbpp::Term or qbpp::Expr to another performs a deep copy, producing two independent objects:
qbpp::Expr f = x + 1;
qbpp::Expr g = f; // independent copy
f += y;
std::cout << "f = " << f << std::endl; // f = 1 +x +y
std::cout << "g = " << g << std::endl; // g = 1 +x (unaffected)
f = f + x and f += x produce the same observable result — both update f and leave any other object alone. Their difference is purely in performance: the compiler picks an in-place rvalue overload for binary + to avoid unnecessary copies when the left-hand side is a temporary.
The Python frontend (PyQBPP) follows different rules due to Python’s reference semantics — see C++ vs Python for a side-by-side comparison.
In most cases, there is no need to explicitly use qbpp::Term objects. They should only be used when maximum performance optimization is required.
However, note that auto type deduction may create a qbpp::Term object, which cannot store general expressions. For example, the following program results in a compilation error because an expression is assigned to a qbpp::Term object:
#include <qbpp/qbpp.hpp>
int main() {
auto x = qbpp::var("x");
auto y = qbpp::var("y");
auto t = 2 * x * y;
t = x + 1;
}
If a qbpp::Expr object is intended, qbpp::toExpr() can be used to explicitly construct one, as shown below:
#include <qbpp/qbpp.hpp>
int main() {
auto x = qbpp::var("x");
auto y = qbpp::var("y");
auto t = qbpp::toExpr(2 * x * y);
auto f = qbpp::toExpr(1);
t += x + 1;
f += t;
std::cout << "t = " << t << std::endl;
std::cout << "f = " << f << std::endl;
}
In this program, both t and f are qbpp::Expr objects and can store general expressions. In particular, f is created as a qbpp::Expr object containing only a constant term with value 1 and no product terms.
qbpp::toExpr() also accepts an array, returning an array of expressions with the same shape, e.g. auto f = qbpp::toExpr(qbpp::var("x", 3));. This is required by operations that are not defined for arrays of variables, such as division by an integer or compound assignments like += (division by an integer also works on arrays of terms) — see Basic Operators and Functions for Arrays.
Integer Ranges: coeff_t and energy_t
The type aliases qbpp::coeff_t and qbpp::energy_t determine the data types used for coefficients and energy values in expressions. qbpp::energy_t is also the data type of the integer constant term of a qbpp::Expr object. Operations that add a constant (x + c, e += c, qbpp::Expr(c)) and constraint values (e == c, e <= c, …) take energy_t, so a value beyond coeff_t (such as 1LL << 40 by default) can be written as is. A number multiplying a variable (c * x) is coeff_t. The following types can be chosen:
| Type | Range | Large constant syntax |
|---|---|---|
int32_t | ±2.1×10⁹ | 12345 (integer literal) |
int64_t | ±9.2×10¹⁸ | 1234567890123456789LL |
qbpp::int128_t | ±1.7×10³⁸ | qbpp::integer("12345678901234567890") |
qbpp::cpp_int | unlimited | qbpp::integer("...") |
The type qbpp::cpp_int represents an integer with an arbitrary number of digits. The helper function qbpp::integer("...") parses a decimal string into the current coeff_t type, so the same source code works unchanged for any build from int32_t through cpp_int.
By default, coeff_t is int32_t and energy_t is int64_t. To use a different type, define one of the following macros before including the header (or pass as a compiler flag -D...):
| Macro | coeff_t | energy_t |
|---|---|---|
INTEGER_TYPE_C32E32 | int32_t | int32_t |
INTEGER_TYPE_C32E64 (default) | int32_t | int64_t |
INTEGER_TYPE_C64E64 | int64_t | int64_t |
INTEGER_TYPE_C64E128 | int64_t | int128_t |
INTEGER_TYPE_C128E128 | int128_t | int128_t |
INTEGER_TYPE_CPP_INT | cpp_int | cpp_int |
Real (double) Coefficients
Coefficients and energy values can also be double. Define one of the following macros before including the header (or pass it as a compiler flag -D...):
| Macro | Solved with |
|---|---|
DOUBLE_TYPE (default double) | 64-bit integer solver |
DOUBLE_TYPE_C64E64 | 64-bit integer solver |
DOUBLE_TYPE_C64E128 | 128-bit integer solver (higher precision, recommended) |
DOUBLE_TYPE_C128E128 | 128-bit integer solver (widest dynamic range) |
#define DOUBLE_TYPE
#include <qbpp/qbpp.hpp>
auto x = qbpp::var("x");
auto y = qbpp::var("y");
qbpp::Expr f = -1.5 * x - 2.5 * y + 4.0 * x * y; // real (double) coefficients
Expressions are built and simplified in double. When a problem is solved, QUBO++ automatically scales the coefficients to integers, solves with the integer solver, and returns the energy as a double (for example, sol.energy() returns a double) — so you work entirely in double without dealing with the integer backend. Dyadic coefficients (1, 1/2, 1/4, …) are represented exactly.
A coefficient that is vastly smaller than the largest one may fall below the scaling precision; it is then treated as 0 and its term is dropped (QUBO++ prints a short notice rather than failing). A variable left without any term stays in the model as a free variable with no effect on the objective; reading it from the solution (sol(x), sol(x[i])) returns 0. A variable that cancels out during simplify_as_binary, on the other hand, is not in the model, and reading it from the solution is an error; sol.has(x[i]) reports whether a variable is in the model. A genuine overflow of the energy range is still reported as an error.
Choosing a macro
Start with the default DOUBLE_TYPE. For most models it keeps essentially all of the precision of the original double coefficients.
Precision runs out when the spread between your smallest and largest coefficients is large compared to the size of the whole model. Consider DOUBLE_TYPE_C64E128 when:
- the model has very many terms (10^7 and up is a useful rule of thumb),
- constraint penalty weights are orders of magnitude larger than the objective coefficients, or
- coefficients within one model span many orders of magnitude.
DOUBLE_TYPE_C64E128 keeps far more coefficient precision while searching at essentially the same speed as DOUBLE_TYPE; it uses somewhat more memory. DOUBLE_TYPE_C128E128 handles a wider dynamic range still, but reaches worse solutions in the same time, so use it only when coefficients do not fit with DOUBLE_TYPE_C64E128.
Inspecting the expression the solver solves: qbpp::quantize()
qbpp::quantize() returns the integer conversion that happens when a problem is solved: the expression the solver actually solves, with integer coefficients. You can pass it to Easy Solver, Exhaustive Solver, or ABS3 Solver instead of the original expression; both solve the same problem.
#define DOUBLE_TYPE
#include <qbpp/qbpp.hpp>
#include <qbpp/easy_solver.hpp>
#include <cmath>
int main() {
auto x = qbpp::var("x", 3);
auto f = 0.3 * x[0] + 0.5 * x[1] + 0.4 * x[2] +
1000 * qbpp::cons(0.06 * x[0] + 0.05 * x[1] + 0.07 * x[2] >= 0.1);
f.simplify_as_binary();
auto g = qbpp::quantize(f);
std::cout << "scale = 2^" << std::log2(g.scale()) << std::endl;
std::cout << g << std::endl;
auto sol = qbpp::EasySolver(g).search();
std::cout << sol << std::endl;
std::cout << "g.energy(sol) = " << g.energy(sol)
<< ", g.cons(sol) = " << g.cons(sol) << std::endl;
}
The output of this program is:
scale = 2^58
86469112845513520*x[0] +144115188075855872*x[1] +115292150460684704*x[2]
+1024000*cons(1006633*x[0] +838861*x[1] +1174405*x[2] >= 1677722)
0.7:{{x[0],1},{x[1],0},{x[2],1}}
g.energy(sol) = 0.7, g.cons(sol) = 0
- The objective coefficients are multiplied by
g.scale()(a power of two, 258 here) and rounded to integers. - A
qbpp::cons()constraint has its body and bounds made integral with a separate factor (224 here), and its weight is converted so that the penalty is in the same units as the objective (1000 × 258 / (224)2 = 1024000). Values such as 0.06 and 0.1 that have no finite binary form are rounded with relative error at most 2−20 (0.1 becomes 1677722 / 224). A constraint that cannot keep this precision is an error that suggestsDOUBLE_TYPE_C64E128or writing the coefficients and bounds as integers. - For a model with integer coefficients,
g.scale()is 1 andgis the original expression.
The expression g returned by qbpp::quantize() provides:
| Function | Description |
|---|---|
g.scale() | The factor applied to the coefficients |
g.energy(sol) | Energy of a solution in the original units; equals sol.energy() for a solution returned by a solver |
g.energy_int(sol) | Energy of a solution in integer units (g.scale() times g.energy(sol)) |
g.cons(sol) | Number of violated constraints (the same test as the solver) |
g.is_feasible(sol) | true if every constraint is satisfied |
g.violations(sol) | Value, bounds and violation of each constraint (rounded values, in the original units) |
g.expr() | The expression as a qbpp::Expr with integer coefficients |
Evaluating the original expression, f(sol), gives the double value before rounding. It can differ slightly from g.energy(sol), and for a solution exactly on a constraint boundary f.cons(sol) and g.cons(sol) can disagree. Evaluate with g when you need the same judgement as the solver. To accept a tiny violation, pass a tolerance such as f.cons(sol, 1e-6) (see Nonlinear functions and native constraints).
VarArray Mode
The MAXDEG macro controls how variables within each term are stored internally. Fixed-length modes eliminate heap allocation and improve performance when the maximum degree is known:
| Macro | Max degree | Description |
|---|---|---|
MAXDEG0 (default) | unlimited | Variable-length (heap allocation for degree 3+) |
MAXDEG2 | 2 | Fixed-length, QUBO only (no heap allocation, fastest) |
MAXDEG4 | 4 | Fixed-length, up to degree 4 (no heap allocation) |
Example — selecting both type and VarArray mode:
#define INTEGER_TYPE_C32E32
#define MAXDEG2
#include <qbpp/qbpp.hpp>
The appropriate library is automatically loaded at runtime based on the specified macros.
Large constants: qbpp::integer()
Constant values that exceed the 64-bit integer range are specified by passing a decimal string to the helper function qbpp::integer("..."). It parses the string into the current coeff_t type (int32_t through cpp_int), so the same source code works for every build. If the value does not fit in coeff_t, std::out_of_range is thrown.
Small values such as qbpp::integer("0") and qbpp::integer("-1") are accepted as well. However, the string-to-value conversion happens at runtime, so for values that fit in int64_t (±9.2×10¹⁸) it is more efficient to use ordinary integer literals (e.g., 12345, 1234567890123456789LL). When the same string appears inside a hot loop, bind it once to a variable to avoid repeated parse overhead:
const auto K = qbpp::integer("1000000000000"); // parsed once
for (int i = 0; i < n; ++i) f += K * x[i];
Note: Standard integer literals (e.g.,
12345) and 64-bit literals with theLLsuffix can be used directly with any type via implicit conversion, as long as the value fits in the type that receives it. A number multiplied by a variable is acoeff_t, so with the default (coeff_tisint32_t) a value that does not fit incoeff_t, such as(1LL << 40) * x, is silently truncated (this example becomes0*x; the compiler only issues a-Woverflowwarning). To use such coefficients, choose a type whosecoeff_tis 64 bits or wider (INTEGER_TYPE_C64E64, etc.).qbpp::integer()is only needed when the value exceeds theint64_trange.
Example with 128-bit integers
The following program creates a qbpp::Expr object with coefficients exceeding 64-bit range:
#define INTEGER_TYPE_C128E128
#include <qbpp/qbpp.hpp>
int main() {
auto x = qbpp::var("x");
auto y = qbpp::var("y");
auto f = qbpp::integer("12345678901234567890") * x +
qbpp::integer("98765432109876543210") * y;
std::cout << "f = " << f << std::endl;
}
This program produces the following output:
f = 12345678901234567890*x +98765432109876543210*y
Example with arbitrary-precision integers (cpp_int)
The following program creates a qbpp::Expr object with very large coefficient and constant terms:
#define INTEGER_TYPE_CPP_INT
#include <qbpp/qbpp.hpp>
int main() {
auto x = qbpp::var("x");
auto f = qbpp::integer("123456789012345678901234567890") * x +
qbpp::integer("987654321098765432109876543210");
std::cout << "f = " << f << std::endl;
}
This program produces the following output:
f = 987654321098765432109876543210 +123456789012345678901234567890*x