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 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 ):
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 . 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 :
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 . 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 with even :
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 Element
s 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 where , 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 . 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:
Here 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 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 Placeholder
s 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 to be the value of :
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 to be the value of , with the indices flipped. To do this you can define Element
s 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 and we want to write a constraint like the one below:
Where is a 2-dimensional jagged array with rows. That is, 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:
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 x
s 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,
}