Expressions#
What is an Expression?#
Let’s consider binary operations or unary operations on integer or real variables. For example, \(x+y\) or \(x^2\). Corresponding operations are also possible for variables defined in jijmodeling
, and the results are called “expressions”.
import jijmodeling as jm
x = jm.BinaryVar("x")
y = jm.BinaryVar("y")
z = x + y
w = x ** 2
x
and y
are BinaryVar
objects, and z
and w
are “expressions”. Note that these are “dependent variables” and are different from what we call “decision variables”.
Built-in Operations#
Python’s built-in operators (e.g., +
) can be used for both decision variables (e.g., BinaryVar
) and Placeholder
.
x = jm.BinaryVar("x")
y = jm.BinaryVar("y")
z = x + y
repr(z)
"BinaryVar(name='x', shape=[]) + BinaryVar(name='y', shape=[])"
Such operations are algebraic processing (i.e., constructing expression trees). You can check the contents of the expression tree using Python’s built-in repr
function. In a Jupyter environment, you can also get a more beautiful display using LaTeX.
Degree of Expressions
Since built-in operations are not limited to linear operations, a function is_linear
is provided to determine whether an expression is linear.
x = jm.BinaryVar("x")
jm.is_linear(x) # True
w = x ** 2
jm.is_linear(w) # False
Functions such as is_quadratic
and is_higher_order
are also provided to check the degree of expressions.
Comparison Operations#
Equality operator ==
and other comparison operators (e.g., <=
) can also be used to construct equality and inequality constraints. Note that to check if two expression trees are the same, you should use the is_same
function instead of the equality operator ==
.
x = jm.BinaryVar("x")
y = jm.BinaryVar("y")
repr(x == y)
"BinaryVar(name='x', shape=[]) == BinaryVar(name='y', shape=[])"
jm.is_same(x, y)
False
Indexing and Summation#
Similar to Python’s built-in list
and numpy.ndarray
, jijmodeling
supports indexing elements of multi-dimensional decision variables and parameters.
x = jm.BinaryVar("x", shape=(3, 4))
x[0, 2]
Note
x[0][2]
and x[0, 2]
refer to the same thing.
x[0, 2]
is also an expression. This is similar to the expression tree of x
with the unary operator **2
applied. x[0]
is the expression tree of x
with the unary operator [0]
applied to take the 0th element. Also, if the expression does not contain decision variables, it can be specified as an index.
x = jm.BinaryVar("x", shape=(3, 4))
n = jm.Placeholder("n")
x[2*n, 3*n]
There is a class Element
for indexing. This can be used to represent variables \(n\) in summation.
In jijmodeling
, three steps are required to represent summation.
N = jm.Placeholder("N")
x = jm.BinaryVar("x", shape=(N,))
# Step1. Introduce variable $n$ in summation with range [0, N-1]
n = jm.Element('n', belong_to=(0, N))
# Step2. Create expression $x_n$ using index access
xn = x[n]
# Step3. Sum $x_n$ along variable $n$
jm.sum(n, xn)
Tip
Simple summations like the above can be written in shorthand.
N = jm.Placeholder('N')
x = jm.BinaryVar(name='x', shape=(N,))
sum_x = x[:].sum()
Since Element
objects themselves can be treated as expressions, you can also write \(\sum_n n x_n\) as follows.
jm.sum(n, n * x[n])
The result of jm.sum
is also an expression. Therefore, you can easily describe models that include the same expression as follows.
n = jm.Element('n', belong_to=(0, N))
sum_x = jm.sum(n, x[n])
sum_x * (1 - sum_x)
Summation over a Set#
In mathematical models, summation over a set \(V\) is often performed. For example, the following summation.
In this summation, non-continuous indices (e.g., \([1, 4, 5, 9]\) or \([2, 6]\)) are used along the set \(V\).
Tip
Summation over a set is often used in one-hot constraints for a given set \(V\).
All expressions explained on this page can also be used as the second argument of the Constraint object.
N = jm.Placeholder('N')
x = jm.BinaryVar('x', shape=(N,))
# Define set $V$ as a Placeholder object
V = jm.Placeholder('V', ndim=1)
# Define variable $v$ moving along $V$
v = jm.Element('v', belong_to=V)
# Sum along $V$
sum_v = jm.sum(v, x[v])
Also, the actual data for the set \(V\) needs to be specified as follows.
import jijmodeling_transpiler as jmt
problem = jm.Problem('Iterating over a Set')
problem += sum_v
instance_data = { "N": 10, "V": [1, 4, 5, 9]}
compiled_model = jmt.core.compile_model(problem, instance_data)
Jagged Arrays#
Sometimes, we need to consider groups of sets like \(C_\alpha\). For example, when setting K-hot constraints.
These sets \(C_\alpha\) may have different numbers of elements. For example, as follows.
You can represent such “jagged” arrays using Placeholder
objects.
N = jm.Placeholder('N')
x = jm.BinaryVar('x', shape=(N,))
# Define a 2-dimensional Placeholder object
C = jm.Placeholder('C', ndim=2)
# Define the number of K-hot constraints
# Note that the length in the 0th dimension cannot be obtained because it is a "jagged" array
M = C.len_at(0, latex="M")
K = jm.Placeholder('K', ndim=1)
# Define the index $/alpha$
a = jm.Element(name='a', belong_to=(0, M), latex=r"\alpha")
# Define the variable moving along each $C_/alpha$
i = jm.Element(name='i', belong_to=C[a])
# Define the K-hot constraint
k_hot = jm.Constraint('k-hot_constraint', jm.sum(i, x[i]) == K[a], forall=a)
Also, the actual data for \(C\) needs to be passed as follows.
problem = jm.Problem('K-hot')
problem += k_hot
instance_data = {
"N": 4,
"C": [[1, 4, 5, 9],
[2, 6],
[3, 7, 8]],
"K": [1, 1, 2],
}
compiled_model = jmt.core.compile_model(problem, instance_data)
Summation over Multiple Indices#
Consider a case where multiple summations are included, such as the following.
Such a case can be implemented in jijmodeling
as follows.
# Define variables
Q = jm.Placeholder('Q', ndim=2)
I = Q.shape[0]
J = Q.shape[1]
x = jm.BinaryVar('x', shape=(I, J))
i = jm.Element(name='i', belong_to=(0, I))
j = jm.Element(name='j', belong_to=(0, J))
# Sum along $i$ and $j$
sum_ij = jm.sum([i, j], Q[i, j]*x[i, j])
When there are multiple summations, you can specify the first argument of jm.sum
as a list [subscript1, subscript2, ...]
instead of using jm.sum
multiple times. Of course, this results in the same mathematical model as using jm.sum
multiple times because \(\sum_{i, j} = \sum_{i} \sum_{j}\).
sum_ij = jm.sum(i, jm.sum(j, Q[i, j]*x[i, j]))
Conditional Summation#
Consider a case where summation is taken over indices that satisfy a specific condition, such as the following.
Such a case can be implemented using jijmodeling
as follows.
# Define variables
I = jm.Placeholder('I')
x = jm.BinaryVar('x', shape=(I,))
i = jm.Element(name='i', belong_to=(0, I))
U = jm.Placeholder('U')
# Sum over indices that satisfy $i<U$
sum_i = jm.sum((i, i<U), x[i])
To sum over indices that satisfy a specific condition, you need to specify a tuple (index, condition)
as the first argument of jm.sum
.
Comparison operators <
, <=
, >=
, >
, ==
, !=
and logical operators &
, |
and their combinations can be used as conditions. For example,
can be implemented as follows.
# Define variables
d = jm.Placeholder('d', ndim=1)
I = d.shape[0]
x = jm.BinaryVar('x', shape=(I,))
U = jm.Placeholder('U')
N = jm.Placeholder('N')
i = jm.Element(name='i', belong_to=(0, I))
# Sum over indices that satisfy $i<U$ and $i≠N$
sum_i = jm.sum((i, (i<U)&(i!=N)), d[i]*x[i])
Conditional Summation with Multiple Conditions#
Consider a case where there are conditions on multiple indices in the summation, such as the following.
Such a case can be implemented in jijmodeling
as follows.
# Define variables
R = jm.Placeholder('R', ndim=2)
I = R.shape[0]
J = R.shape[1]
x = jm.BinaryVar('x', shape=(I, J))
i = jm.Element(name='i', belong_to=(0, I))
j = jm.Element(name='j', belong_to=(0, J))
N = jm.Placeholder('N')
L = jm.Placeholder('L')
# Sum over indices that satisfy $i>L$, $i≠N$, and $j<i$
sum_ij = jm.sum([(i, (i>L)&(i!=N)), (j, j<i)], R[i, j]*x[i, j])
You need to specify the first argument of jm.sum
as [(index 1, condition of index 1), (index 2, condition of index 2), ...]
. By specifying it as [(index1, condition of index1), (index2, condition of index2), ...]
, you can describe multiple summation operations with conditions on each index.
Caution
[(j, j<i), (i, (i>L)&(i!=N))]
will cause an error because \(i\) is not defined at the point of \(j<i\). This corresponds to the fact that you can write \(\sum_{\substack{i>L \\ i!=N}} \left( \sum_{j<i} \cdots \right)\) in an expression, but you cannot write \(\sum_{j<i} \left( \sum_{\substack{i>L \\ i!=N}} \cdots \right)\). Be careful about the order in which you impose conditions on indices in multiple summations.