Skip to main content

Tips

Adding terms to an objective function

Objective functions in Problem are not mutable. Attempting to add another objective function replaces the previous one.

import jijmodeling as jm

c = jm.Placeholder("c", ndim=1)
N = c.len_at(0)
x = jm.BinaryVar("x", shape=(N,))
i = jm.Element("i", (0, N))
problem += jm.sum(i, c[i] * x[i]) # sets our objective function

# if we later want this to have another variable, we need to write out
# a new expression as the new objective function
d = jm.Placeholder("d", ndim=1)
M = d.len_at(0)
y = jm.BinaryVar("y", shape=(M,))
j = jm.Element("j", (0, M))
problem += jm.sum(i, c[i] * x[i]) + jm.sum(j, d[j] * y[j])

In cases where you want to build up a more complex objective function from simpler terms, you can simply store them in variables before writing out the final expression. For example:

sum_of_xs = jm.sum(i, c[i] * x[i])
sum_of_ys = jm.sum(j, d[j] * y[j])
problem += sum_of_xs + sum_of_ys

Note that jijmodeling does not support multiple objective functions in a single problem. Models must be adapted to work with a single objective function.

Using specific sets of numbers with Element

Some users might expect to be able to create an index i{0,1,2}i \in \{0, 1, 2\} using Python sets:

import jijmodeling as jm
i = jm.Element("i", {0,1,2}) # does not work

However, the above is not supported. We can use a range-based notation to define the same index (note that because this range is half-open, we need to write (0, 3) to get {0,1,2}\{0, 1, 2\}):

i = jm.Element("i", (0, 3))

For more complex use-cases where your index isn’t just a simple interval of integers, there are two options.

One of them is to create a one-dimensional Placeholder representing your set. This is particularly useful for arbitrary sets of numbers, say E={2,4,10,35,36}E = \{2, 4, 10, 35, 36\}. This makes Element work very much like the set notation above, but the values within the set are not specified as part of the model directly. To write an Element representing eEe \in E:

E = jm.Placeholder("E", ndim=1)
e = jm.Element("e", E)

Then the actual values of E become part of the instance data when converting your model.

The other alternative is to use a condition to restrict the valid values within a range. This is useful when which integers you want follow a logical rule. You can see this other tip for how to do this.

Using conditions with Element

Often in models you’ll have some additional conditions for an index, such as iji \neq j. These conditions are specified when defining your summation or constraint, not when creating the element. In the index parameter of sum you can give a tuple (<element>, <condition>). That <element> will be used as the index, but <condition> will be applied and only values for which it is true will be used.

For example, to sum over all xix_i with even ii:

import jijmodeling as jm
i = jm.Element("i", (0, 100))
x = jm.BinaryVar("x", shape=(100,))

sum_over_even_is = jm.sum((i, i % 2 == 0), x[i])

Conditions can refer to the value of other Elements being used in the same index. Note that they must be in order, that is, you can only refer to elements that come before it in the list. So to write a sum over two indices i,ji, j where iji \neq j, you can write:

import jijmodeling as jm

i = jm.Element("i", (0, 100))
j = jm.Element("j", (0, 100))
x = jm.BinaryVar("x", shape=(100, 100))

jm.sum([i, (j, j!= i)], x[i, j])

The same conditional tuple notation can used in the forall list given to a constraint:

jm.Constraint("c1", x[i, j] - x[j, i] >= 0, forall=[i, (j, j != i)])

To apply more complex conditions to an index, conditional expressions can be combined with the operators & (logical AND), | (logical OR), ^ (logical XOR):

jm.sum((i, (i % 2 == 0) | (i % 5 == 0)), x[i, 0])

Handling two-sided constraints

In mathematical models it’s common to have two-sided constraints, with inequalities like lx+yul \leq x + y \leq u. These are not directly supported in jijmodeling, and attempting to create an inequality like this should result in an exception saying Converting <class> to boolean is unsupported.

Instead, you should separate this into two constraints, each with one inequality.

import jijmodeling as jm

l, u = jm.Placeholder("l"), jm.Placeholder("u")
x = jm.IntegerVar("x", lower_bound=0, upper_bound=10)
y = jm.IntegerVar("y", lower_bound=5, upper_bound=20)

problem = jm.Problem("problem")
problem += jm.Constraint("greater than l", l <= x + y)
problem += jm.Constraint("less than u", x + y <= u)
problem

I want to use dependent variables

Let’s suppose we want to write constraints such as:

constraint:yici{0,,N1}whereyi=aixi+b\begin{array}{cccc} & \text{constraint:} & \displaystyle y_{i} \leq c & \forall i \in \left\{0,\ldots,N - 1\right\} \\ \end{array}\quad \text{where}\quad y_{i} = a_{i} x_{i} + b

Here yiy_i is a dependent variable. It’s easy enough to write such dependent expressions, and we can even use set_latex to force it to display as yiy_i if we wish:

import jijmodeling as jm

a = jm.Placeholder("a", ndim=1)
b = jm.Placeholder("b")
N = a.len_at(0, latex="N")
x = jm.BinaryVar("x", shape=(N,))
i = jm.Element("i", belong_to=N)
c = jm.Placeholder("c")

y = a[i] * x[i] + b
y.set_latex("y_i")
jm.Constraint("constraint", y <= c, forall=i)

However, the above lacks some flexibility. y is defined explicitly using the index i. If this expressions shows up all throughout our model using different indices we’d need to define all combinations separately. In this situation, we can take advantage of of Python functions and lambdas.

y = lambda e: a[e] * x[e] + b
jm.Constraint("constraint", y(i) <= c, forall=i)

# or, for more complex definitions and/or specifying the latex representation
def y(e: jm.Element):
y = a[e] * x[e] + b
y.set_latex("y_{e.name}")
return y
jm.Constraint("constraint", y(i) <= c, forall=i)

Multi-dimensional variables with different bounds

When defining a multi-dimensional decision variable, one normally specifies a lower_bound and upper_bound as scalar values, applying the same bound to all member variables.

N = jm.Placeholder("N")
M = jm.Placeholder("M")
x = jm.IntegerVar("x", shape=(N, M), lower_bound=0, upper_bound=5)

In the above, we have N * M variables, all of which have a lower bound of 0 and an upper bound of 5, represented by the 2-dimensional x. But what if we want them to have different bounds, while still treating them all as one single 2-dimensional variable?

In the above, we have N * M variables, all of which have a lower bound of 0 and an upper bound of 5, represented by the 2-dimensional x. But what if we want them to have different bounds, while still treating them all as one single 2-dimensional variable?

We can use Placeholders as the parameters lower_bound and upper_bound. For scalar placeholders (with ndim=0), this works just like a regular number literal. But we can also use a Placeholder that has the same number of dimensions as the decision variable, and the elements of that placeholder will be used to specify the bounds.

Here's how to set the upper bound of variable xi,jx_{i,j} to be the value of ubi,jub_{i,j}:

import jijmodeling as jm

ub = jm.Placeholder("ub", ndim=2)
N = ub.len_at(0, latex="N")
M = ub.len_at(1, latex="M")
# all will have a lower bound of 0, but the upper bound is determined using `ub`:
x = jm.IntegerVar("x", shape=(N,M), lower_bound=0, upper_bound=ub)

Note that this only works if the dimensionality matches. It’s also important to be sure all decision variables have valid bounds. This is done in the above code by defining N and M based on ub.

If for some reason you want the bounds be specified as the inverted version of the placeholder, or some other scheme of matching axes, we support a special syntax using subscripts. To be clear, this is if you want the upper bound of variable xi,jx_{i,j} to be the value of ubj,iub_{j,i}, with the indices flipped. To do this you can define Elements matching the axes, and then specify the upper bound as ub[j, i] like so:

ub = jm.Placeholder("ub", ndim=2)
N = ub.len_at(0, latex="N")
M = ub.len_at(1, latex="M")
i = jm.Element("i", N)
j = jm.Element("j", M)
x = jm.IntegerVar("x", shape=(N, M), lower_bound=0, upper_bound=ub[j, i])

You must still ensure that all variables have valid bounds, so in this case it only makes sense if N equals M. Note also that this currently has a downside where you will only be able to use x in sums and constraints with i and j.

Summation over lists of indices with varying sizes

In this tip we’ll look over a use case which may seem somewhat niche, but it helps illustrate some techniques related to multi-dimensional placeholders and indexing, which can be more generally applicable.

Say we have a two-dimensional decision variable xx and we want to write a constraint like the one below:

aAnxn,a=0,n{0,...,N1}\sum_{a \in A_{n}} x_{n, a} = 0,\quad \forall n \in \{0,..., N-1\}

Where AA is a 2-dimensional jagged array with NN rows. That is, AA can be thought of a “list of lists”, whose elements we want to use as the indices to point at specific decision variables within that row.

To be clear on what this constraint means, consider as an example A = [[1, 2, 3], [0, 1, 4, 5], [2, 3, 5]]. The constraint would be expanded to:

a{1,2,3}x0,a=0  a{0,1,4,5}x1,a=0  a{2,3,5}x2,a=0\sum_{a \in \{ 1, 2, 3 \}} x_{0, a} = 0 \ \land \ \sum_{a \in \{ 0, 1, 4, 5 \}} x_{1, a} = 0 \ \land \ \sum_{a \in \{ 2, 3, 5 \}} x_{2, a} = 0

We can write that constraint as:

import jijmodeling as jm

A = jm.Placeholder("A", ndim=2)
N = A.len_at(0, latex="N")
n = jm.Element("n", N) # number of rows in the jagged array
a = jm.Element("a", A[n]) # elements in a given row n
x = jm.BinaryVar("x", shape=(3,6))

jm.Constraint("constraint", jm.sum(a, x[n, a]) == 0, forall=n)

In the example above I defined the shape of x with arbitrary numbers (or rather, based on our previous example). In a real model you’ll likely want the shape to also be a parameter, or defined in relation to other parameters to avoid indexing errors. If you just want to make sure there are enough xs for this constraint to be valid you can try to define the shape in relation to A. (3,6) would be replaced by (number of rows in A, maximum value in A). The number of rows is N, but we don’t know the largest value in A during model construction. We can do this by defining an additional placeholder, then just making sure its value in the instance data is obtained from what is actually in A.

# same A, N, and a as before
# an additional parameter, meaning the greatest number found in A
max_A = jm.Placeholder("max_A")
x = jm.BinaryVar("x", shape=(N, max_A + 1))

problem = jm.Problem("problem")
problem += jm.Constraint("constraint", jm.sum(a, x[n, a]) == 0, forall=n)

When defining your instance data for use with JijModeling-Transpiler or JijZept, you can write:

# jagged array
data_A = [
[1, 2, 3],
[0, 1, 4, 5],
[2, 3, 5],
# ...
]
# the largest value in `data_A`
data_max_A = max(max(An) for An in data_A)

instance_data = {
"A": data_A,
"max_An": data_max_A,
}