Symbolic Math

dimod’s support for symbolic math can simplify your coding of problems. For example, consider a problem of finding the rectangle with the greatest area for a given perimeter, which you can formulate mathematically as,

\[ \begin{align}\begin{aligned}\textrm{Objective: } \quad &\max_{i,j} \quad ij\\\textrm{Constraint:} \quad &2i+2j \le 8\end{aligned}\end{align} \]

where the components are,

  • Variables: \(i\) and \(j\) are the lengths of two sides of the rectangle and the length of the perimeter is arbitrarily selected to be 8.

  • Objective: maximize the area, which is given by the standard geometric formula \(ij\).

  • Constraint: subject to not exceeding the given perimeter length; that is, \(2i+2j\), the summation of the rectangle’s four sides, is constrained to a maximum value of 8.

dimod’s symbolic math enables an intuitive coding of such problems:

>>> i, j = dimod.Integers(["i", "j"])
>>> objective = -i * j
>>> constraint = 2 * i + 2 * j <= 8
>>> print(objective.to_polystring())
-i*j
>>> print(constraint.to_polystring())
2*i + 2*j <= 8

Here variables i,j are of type integer, perhaps representing the number of tiles laid horizontally and vertically in creating a rectangular floor, and the coded objective is set to negative because D-Wave samplers minimize rather than maximize.

The foundation for this symbolic representation is single-variable models.

Variables as Models

To symbolically represent an objective or constraint, you first need symbolic representations of variables.

Quadratic models with a single variable can represent your variables; for example, if the type of variable you need is integer:

>>> i = dimod.Integer('i')
>>> i
QuadraticModel({'i': 1.0}, {}, 0.0, {'i': 'INTEGER'}, dtype='float64')

Here, variable i is a QuadraticModel that contains one variable with the label 'i'.

This works because quadratic models (QMs) have the form,

\[\sum_i a_i x_i + \sum_{i \le j} b_{i, j} x_i x_j + c\]

where \(\{ x_i\}_{i=1, \dots, N}\) can be integer variables (\(a_{i}, b_{ij}, c\) are real values). If you set \(a_1=1\) and all remaining coefficients to zero, the model represents a single variable, \(x_1\).

When your variable is used in a linear term of a polynomial, such as \(3.75i\), the coefficient (\(3.75\)) is represented in this same model by the value of the linear bias on the 'i'–labeled variable:

>>> 3.75 * i
QuadraticModel({'i': 3.75}, {}, 0.0, {'i': 'INTEGER'}, dtype='float64')

Similarly, when your variable is in a quadratic term, such as \(2.2i^2\), the coefficient (\(2.2\)) is represented in this same model by the value of the quadratic bias, \(b_{1, 1} = 2.2\):

>>> 3.75 * i + 2.2 * i * i
QuadraticModel({'i': 3.75}, {('i', 'i'): 2.2}, 0.0, {'i': 'INTEGER'}, dtype='float64')

You can see the various methods of creating such variables in the Generators reference documentation.

Typically, you have more than a single variable, and your variables interact.

Operations on Variables

Consider a simple problem of a NOT operation between two binary variables. For \(\{-1, 1\}\)–valued binary variables, the NOT operation is equivalent to multiplication of the two variables:

>>> s1, s2 = dimod.Spins(["s1", "s2"])
>>> bqm_not = s1*s2
>>> bqm_not
BinaryQuadraticModel({'s1': 0.0, 's2': 0.0}, {('s2', 's1'): 1.0}, 0.0, 'SPIN')
>>> print(dimod.ExactSolver().sample(bqm_not))
  s1 s2 energy num_oc.
1 +1 -1   -1.0       1
3 -1 +1   -1.0       1
0 -1 -1    1.0       1
2 +1 +1    1.0       1
['SPIN', 4 rows, 4 samples, 2 variables]

The symbolic multiplication between variables above executes a multiplication between the models representing each variable. Binary quadratic models (BQMs) are of the form:

\[\sum_{i=1} a_i v_i + \sum_{i<j} b_{i,j} v_i v_j + c \qquad\qquad v_i \in\{-1,+1\} \text{ or } \{0,1\}\]

where \(a_{i}, b_{ij}, c\) are real values. The multiplication of two such models, with linear terms \(a_1 = 1\), reduces to \(\sum_{i=1} 1 v_1 * \sum_{i=1} 1 u_1 = v_1 u_1\), a multiplication of two variables.

In this NOT example, because all the variables are the same Vartype, dimod represents each binary variable, and their multiplication, with BinaryQuadraticModel objects.

>>> bqm_not.vartype is dimod.Vartype.SPIN
True

If an operation includes more than one type of variable, the representation is always a QuadraticModel and the Vartype is per variable:

>>> qm = bqm_not + 3.75 * i
>>> print(type(qm))
<class 'dimod.quadratic.quadratic_model.QuadraticModel'>
>>> qm.vartype("s1") == dimod.Vartype.SPIN
True
>>> qm.vartype("i") == dimod.Vartype.INTEGER
True

Note

An important distinction is that x = dimod.Binary('x'), for example, instantiates a model with a variable label 'x' and not a free-floating variable labeled x. Consequently, you can add x to another model by adding the two models,

>>> x = dimod.Binary("x")
>>> bqm = dimod.BinaryQuadraticModel('BINARY')
>>> bqm += x

which adds the variable labeled 'x' in the single-variable BQM, x, to model bqm. You cannot add x to a model—as though it were variable 'x'—by doing bqm.add_variable(x).

Representing Constraints

Many real-world problems include constraints. Typically constraints are either equality or inequality, in the form of a left-hand side(lhs), right-hand-side (rhs), and the dimod.sym.Sense (\(\le\), \(\ge\), or \(==\)). For example, the constraint of the rectangle problem above,

\[\textrm{s.t.} \quad 2i+2j \le P\]

has a lhs of \(2i+2j\) less or equal to a rhs of a some real number (\(8\)):

>>> print(constraint.lhs.to_polystring(), constraint.sense.value, constraint.rhs)  
2*i + 2*j <= 8

You can create such an equality or inequality symbolically, and it is shown with the model:

>>> print(type(3.75 * i <= 4))
<class 'dimod.sym.Le'>
>>> 3.75 * i <= 4
Le(QuadraticModel({'i': 3.75}, {}, 0.0, {'i': 'INTEGER'}, dtype='float64'), 4)

Performance

dimod’s symbolic math is very useful for small models used for experimenting and formulating problems. It also offers some more performant functionality; for example, methods such as IntegerArray() for creating multiple variables with NumPy arrays or quicksum() as a replacement for the Python sum().

See the examples of BinaryArray(), IntegerArray(), and SpinArray() for usage.