Tips#

Adding Terms to the Objective Function#

The objective function of Problem is designed to be overwritten. Therefore, be careful that adding a new objective function will overwrite the existing 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.Problem("my problem")
problem += jm.sum(i, c[i] * x[i]) # Set the objective function

# If you want to add another variable or expression later,
# you need to start by creating a 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])

If you want to construct a more complex objective function, it is recommended to assemble simple terms or expressions 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

Caution

Currently, jijmodeling does not support mathematical models with multiple objective functions.

Defining Element Along a Set#

Some users may want to create an index \(i \in \{0, 1, 2\}\) using Python’s set Set as follows:

import jijmodeling as jm
i = jm.Element("i", {0,1,2}) # This code will cause an error

However, the above notation is not supported in jijmodeling. In this example, you need to express it as an interval. (Since the interval of Element is a half-open interval, you need to write (0, 3) to represent \(\{0, 1, 2\}\))

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

For more complex use cases (such as when the index is not just a continuous integer), consider whether it can be expressed in one of the following two ways:

The first option is to create a one-dimensional Placeholder representing the set. This option is useful when expressing a set containing discontinuous integers like \(E = \{2, 4, 10, 35, 36\}\). This allows Element to function as if it were using a set. Using this method, an Element representing \(e \in E\) can be defined as follows:

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

However, note that the specific values within the set do not appear in the mathematical model. You need to specify the actual values of E when converting the mathematical model to an instance.

Another option is to use conditions that restrict valid values within the range. This option is useful when using a set of integers that follow a rule. For how to apply conditions, refer to here.

Handling Conditional Element#

Often, the index of a sum in a mathematical model is conditional. For example, conditions like \(i \neq j\). These conditions are specified when defining the sum or constraint, not when creating the index. In jijmodeling, you can define a conditional index for sum as a tuple (<element>, <condition>) in the first argument. In this case, <element> is used as the index, and <condition> is set as the condition, and only those that satisfy <condition> are summed.

For example, consider the sum of \(x_i\) for even \(i\). In jijmodeling, it can be defined as follows:

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])

It is also possible to compare with other Element used in the same sum. Additionally, the first argument of sum can be input in the form of [[index 1, condition of index 1), (index 2, condition of index 2), ...]]. Note that condition of index 1 cannot use index 2, so be careful about the order of indices and conditions. As a specific example, the following shows a sum for two indices \(i, j\) where \(i \neq j\) in jijmodeling.

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])
\[\begin{split}\displaystyle \sum_{i = 0}^{99} \sum_{\substack{j = 0\\j \neq i}}^{99} x_{i, j}\end{split}\]

Similarly, the tuple notation can also be used in the forall argument of Constraint.

jm.Constraint("c1", x[i, j] - x[j, i] >= 0, forall=[i, (j, j != i)])
\[\begin{split}\begin{array}{cccc} & \text{c1} & \displaystyle x_{i, j} - x_{j, i} \geq 0 & \forall i \in \left\{0,\ldots,99\right\} \forall j \in \left\{j \in \left\{0,\ldots,99\right\} \mid j \neq i \right\} \\ \end{array}\end{split}\]

Additionally, logical operations can be applied to indices, not just comparison operations. Here, logical operations refer to logical AND &, logical OR |, and exclusive OR ^.

jm.sum((i, (i % 2 == 0) | (i % 5 == 0)), x[i, 0])
\[\begin{split}\displaystyle \sum_{\substack{i = 0\\\left(i \bmod 2\right) = 0 \lor \left(i \bmod 5\right) = 0}}^{99} x_{i, 0}\end{split}\]

Handling Constraints Bounded by Inequalities on Both Sides#

In mathematical optimization, there are cases where you want to handle constraints bounded by inequalities on both sides, such as \(l \leq x + y \leq u\). These constraints are not directly supported in jijmodeling, and attempting to implement them as is will result in an exception Converting <class> to boolean is unsupported.

Therefore, in jijmodeling, you need to split such constraints into two and describe each as a single 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
\[\begin{split}\begin{array}{cccc}\text{Problem:} & \text{problem} & & \\& & \min \quad \displaystyle 0 & \\\text{{s.t.}} & & & \\ & \text{greater than l} & \displaystyle l \leq x + y & \\ & \text{less than u} & \displaystyle x + y \leq u & \\\text{{where}} & & & \\& x & 0\text{-dim integer variable}\\ & & \text{lower bound: }0 & \\ & & \text{upper bound: }10 & \\& y & 0\text{-dim integer variable}\\ & & \text{lower bound: }5 & \\ & & \text{upper bound: }20 & \\\end{array}\end{split}\]

Using Dependent Variables#

Suppose you want to write a constraint like the following in jijmodeling:

\[\begin{split} \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 \end{split}\]

This constraint has \(y_i\) as a dependent variable. In jijmodeling, it is not difficult to write such dependent variables. Additionally, if necessary, you can display it as \(y_i\) in LaTeX using set_latex.

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)
\[\begin{split}\begin{array}{cccc} & \text{constraint} & \displaystyle y_i \leq c & \forall i \in \left\{0,\ldots,N - 1\right\} \\ \end{array}\end{split}\]

However, the above code lacks flexibility. This is because \(y\) is explicitly defined using the index \(i\), and it cannot be reused if a similar expression is used in other parts of the mathematical model. To ensure this flexibility, it is better to use Python functions or lambda expressions.

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

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)
\[\begin{split}\begin{array}{cccc} & \text{constraint} & \displaystyle y_{e.name} \leq c & \forall i \in \left\{0,\ldots,N - 1\right\} \\ \end{array}\end{split}\]

Handling Multi-dimensional Variables with Different Lower and Upper Bounds#

When defining multi-dimensional decision variables, you usually specify lower_bound and upper_bound as scalar values. In this case, all decision variables are set with the same lower and upper bounds.

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

In the above code, N * M integer variables x are defined with a lower bound of 0 and an upper bound of 5. So, what if you want to define them with different lower and upper bounds?

One way to solve this problem is to specify lower_bound and upper_bound with Placeholder. For a 0-dimensional Placeholder, this works the same as when you provide a normal numeric literal. On the other hand, you can also use a Placeholder with the same dimensions as the decision variables, and specify the lower and upper bounds for each variable with that Placeholder.

Below is an example of setting the upper bound of the variable \(x_{i,j}\) individually to \(ub_{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")
# Define integer variable x with lower bound 0 and upper bound ub
x = jm.IntegerVar("x", shape=(N,M), lower_bound=0, upper_bound=ub)

Note that the dimensions and shapes of the decision variables and Placeholder must match. Also, make sure that all decision variables have valid lower and upper bounds. In the above code, N and M are defined based on ub, so it is guaranteed that their shapes match.

Additionally, if you want to use the transpose of Placeholder or specify other axes as the lower and upper bounds for some reason, you can use a special syntax with Element. For example, if you want to set the upper bound of the variable \(x_{i,j}\) to the value of \(ub_{j,i}\) by reversing the indices, you can do this by defining an Element that matches the axes and specifying the upper bound as ub[j, i] as follows:

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])

As mentioned earlier, make sure that all variables have valid lower and upper bounds. Additionally, note that the above code only makes sense when N and M are equal. Also, note that this syntax cannot be used in sums and constraints.

Handling Sums for Indices of Different Sizes#

In this tip, we will explain methods related to multi-dimensional Element and indexing, which may seem a bit niche.

Suppose you have a 2-dimensional decision variable \(x\) and want to write a constraint like the following:

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

Here, \(A\) is a 2-dimensional “irregular array” with \(N\) rows. Therefore, you can interpret \(A\) as a “list of lists” and use each list as an index.

To clarify what this constraint means, let’s consider an example with \(A = [[1, 2, 3], [0, 1, 4, 5], [2, 3, 5]]\). Specifically, the constraint can be written as follows:

\[ \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 \]

In jijmodeling, you can write this constraint as follows:

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 "irregular" array
a = jm.Element("a", A[n]) # Index for each row
x = jm.BinaryVar("x", shape=(3,6))

jm.Constraint("constraint", jm.sum(a, x[n, a]) == 0, forall=n)
\[\begin{split}\begin{array}{cccc} & \text{constraint} & \displaystyle \sum_{a \in A_{n}} x_{n, a} = 0 & \forall n \in \left\{0,\ldots,N - 1\right\} \\ \end{array}\end{split}\]

In the above example, we defined the shape of x with specific numbers. In an actual mathematical model, it is recommended to define the shape as a Placeholder or related to other parameters to avoid index errors. To correctly define such constraints, it is better to define enough decision variables x. Specifically, you can replace (3, 6) in the above code with (number of rows in A, maximum number of columns in A). However, there is a problem that the maximum number of columns in A is unknown at the stage of constructing the mathematical model. Therefore, it is better to define an additional Placeholder and make it possible to specify it as instance data.

# Define a Placeholder that means `maximum number of columns 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)

It is recommended to construct instance data for use with jijmodeling_transpiler or JijZept as follows.

# Create an "irregular" array
data_A = [
    [1, 2, 3],
    [0, 1, 4, 5],
    [2, 3, 5],
    # ...
]
# Get the maximum number of columns in the "irregular" array
data_max_A = max(max(An) for An in data_A)

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