Constructing Expressions#

In this section, we describe various ways to write expressions in JijModeling. JijModeling expressions are classified into several kinds (types). JijModeling provides type via Python type hints (stub files) as well as a custom, more sophisticated type checker, which can detect common modeling mistakes during construction. Below, we first summarize the overview of types in JijModeling, then learn typical patterns of expression building.

Tip

We focus on basic common patterns here. For a complete list of expressions, see the API reference for the Expression class and top-level functions in the jijmodeling module.

The Cheat Sheet also provides more complex examples, so it is worth checking after reading this section.

import jijmodeling as jm

What is an expression?#

JijModeling separates model definitions from input data to achieve various features and efficiency. As a result, modeling in JijModeling does not directly construct a concrete formula. Instead, you first build a “program that becomes a concrete mathematical model only after input data is given”, then compile it into a specific instance by providing data. JijModeling calls this “program” an expression.

More precisely, JijModeling expressions do not store concrete values, but keep an abstract syntax tree (AST) built from decision variables, placeholders, constants, and operations. Consider the following example:

@jm.Problem.define("Test Problem")
def ast_examples(problem: jm.DecoratedProblem):
    N = problem.Length()
    x = problem.BinaryVar()
    y = problem.IntegerVar(lower_bound=0, upper_bound=42, shape=(N,))

    z = x + y[0]
    w = jm.sum(y[i] for i in N)
    display(repr(z))
    display(repr(w))
'Expression(x + y[0])'
'Expression(sum(N.map(lambda i: y[i])))'
Python variables can bind arbitrary expressions and variables. Expressions are represented as syntax trees with operators as nodes and constants or parameters as leaves.

Fig. 5 Decision variables, placeholders, and syntax trees bound to Python variables#

Figure 5 visualizes the definition of Test Problem. Decision variables and placeholders in the model such as \(x, y, N\) correspond to Python variables x, y, N. This illustrates an ambiguity: when we say “variable”, it can mean either a parameter in the model or a Python variable that temporarily binds it. Expressions like z = x + y[0] and w = jm.sum(y[i] for i in N) are represented as symbolic ASTs that reference these variables.

Function calls and method calls are equivalent for expressions

For an Expression object A, unary operations can be written as prefix function calls like jm.log(A) or as postfix method calls like A.log(). Both construct exactly the same expression, so use whichever you prefer. The same applies to DecisionVar and Placeholder. However, Python builtin numbers do not support method calls, so for such cases you must use function calls like jm.log(2).

Types of expressions in JijModeling#

In JijModeling, expressions are classified by type and validated as needed. You can use JijModeling without understanding the type system in detail. Still, it is useful to know how the type checks are performed when you formulate models. This section gives a brief overview.

JijModeling actually performs type checks in two stages:

  1. Editor assistance and static checking via Python type hints

  2. A built-in type checker in JijModeling during model construction

(1) is bundled as Python code in the library and enables editor completion and static checks with tools like Pyright, ty, and pyrefly. However, Python type hints cannot express all constraints (for example, validating array index sizes). To compensate, JijModeling includes (2), its own more expressive type checker.

The checker in (2) is not invoked directly by users. It is called when you add constraints or objective terms, declare shape for decision variables/placeholders, and so on, and it validates modeling mistakes before any data is provided. In other words, editor checks are “coarser” than the true JijModeling type system, while finer checks happen during construction. At the Python type level, the only distinction is whether something is an Expression, but JijModeling checks much more detail internally.

There are several expression types in JijModeling; representative ones are listed below:

Kind

Notation (example)

Textual example

Description

Numeric types

\(\mathbb{N}, \mathbb{Z}, \mathbb{R}\)

natural, int, float

Natural numbers, integers, real-valued scalars, and related numeric types.

Category label types

\(L\)

CategoryLabel("L")

Sets of labels provided later by users.

Higher-dimensional array types

\(\mathrm{Array}[N_1 \times \cdots \times N_k; \mathbb{R}]\)

Array[N1, .., Nk; float]

Arrays with an element type and a shape.

Dictionary types

\(\mathrm{TotalDict}[K; V]\) / \(\mathrm{PartialDict}[K; V]\)

TotalDict[K; V], PartialDict[K; V]

Dictionaries with key type \(K\) and value type \(V\).

Tuple types

\(T \times U\)

Tuple[int, float]

Fixed-length tuples with per-component types.

With these in mind, let’s look at operations that commonly appear in modeling.

When errors are raised

JijModeling’s built-in type checking is performed not right after an expression is created, but at the following times:

  1. When a term is added to a problem’s objective

  2. When a constraint is declared via Problem.Constraint()

  3. When it appears in ndim, shape, or dict_keys

  4. When compiling to an instance via Problem.eval() or Compiler

  5. When type inference is explicitly triggered via Problem.infer()

This is because expression types are determined only when placed in context. So even if an expression is “invalid”, it does not necessarily throw an error at construction time.

Below, we use Problem.infer() to show valid and invalid examples. This method infers the type of a given expression based on the decision variables and placeholders defined in the Problem, and it raises a type error for invalid expressions. Let’s look at an example. Here, we add a binary variable \(x\) and an integer \(N\), so \(x + N\) is inferred as an integer-type expression \(\mathbb{Z}\).

problem = jm.Problem("Type Inference Example")
x = problem.BinaryVar("x", description="Scalar decision variable")
N = problem.Integer("N")

problem.infer(x + N)  # OK! (scalar addition)
\[\mathbb{Z}\]

On the other hand, a scalar value cannot be added to a string, so the following example raises an error.

try:
    # ERROR! (string and scalar cannot be added)
    problem.infer(x + "hoge")
except Exception as e:
    print(e)
Traceback (most recent last):
    while inferring the type of expression `x + Located { inner: "hoge", src_span: NoSrcSpan }',
        defined at File "/tmp/ipykernel_676/594888127.py", line 3, col 19-29
    while inferring the type of expression `x + Located { inner: "hoge", src_span: NoSrcSpan }',
        defined at File "/tmp/ipykernel_676/594888127.py", line 3, col 19-29
    while checking if types `binary!' and `Literal["hoge"]' can be combined with numeric operator `Add',
        defined at File "/tmp/ipykernel_676/594888127.py", line 3, col 19-29

File "/tmp/ipykernel_676/594888127.py", line 3, col 19-29:

    3  |      problem.infer(x + "hoge")
                            ^^^^^^^^^^

Type Error: Instance for binary operator Add not found for type binary! and Literal["hoge"]

What is the relationship between Expression and ExpressionLike / ExpressionFunction?

In the API reference and editor completions/docs, you may see type names such as ExpressionLike and ExpressionFunction. These are dummy shorthand types that do not exist in the library implementation, and are used to represent types that can be converted to Expression, or functions from Expression to Expression. Specifically, you can think of them as follows:

Type name

Description

ExpressionLike

A type that can be converted to Expression. Depending on the context, this includes Expression itself, Placeholder, DecisionVar, DependentVar, as well as Python numbers, strings, tuples, lists, dictionaries, NumPy arrays, and so on.

ExpressionFunction

A function that takes one or more Expression objects and returns a Expression. In Python type hints, only up to 5 arguments are enumerated, but in practice there is no limit on the number of arguments.

Placeholders and decision variables as expressions#

As described in the previous section, decision variables and placeholders are defined with methods like Problem.BinaryVar and Problem.Placeholder. These methods return DecisionVar and Placeholder objects that hold metadata, but when used in expression building they are automatically converted into Expression objects. In the Test Problem example, Python variables x and y are DecisionVar objects, but in z = x + y[0], they are converted to expressions that represent a decision variable and an array of decision variables. Constants like 0 are plain Python numbers, but they are also automatically converted when they appear in expressions.

Arithmetic operations#

Python’s builtin arithmetic operators (add/subtract/multiply/divide/mod: +, -, *, /, %, etc.) can be used with JijModeling expressions. Besides numeric scalars, you can also apply these operations to (multidimensional) arrays or to TotalDict objects with matching key sets, subject to some conditions. Specifically, the following combinations (left or right) are supported:

  1. Scalar with scalar

  2. Scalar with multidimensional array

  3. Scalar with dictionary

  4. Arrays with the same shape

  5. Total dictionaries with the same key set

Broadcasting in JijModeling

(2)-(4) correspond to broadcasting in libraries like NumPy. NumPy supports more general shape combinations (for example, \((N, M, L)\) with \((M, L)\)). While such generalized broadcasting can be concise, it often makes the intent ambiguous. For this reason, JijModeling intentionally restricts broadcasting and only supports cases that should be unambiguous.

Let’s look at examples.

problem = jm.Problem("Arithmetic Operations")
x = problem.BinaryVar("x", description="Scalar decision variable")
N = problem.Length("N")
M = problem.Length("M")
y = problem.IntegerVar(
    "y",
    lower_bound=0,
    upper_bound=10,
    shape=(N, M),
    description="2D array decision variable",
)
z = problem.ContinuousVar(
    "z",
    lower_bound=-1,
    upper_bound=42,
    shape=(N, M, N),
    description="3D array decision variable",
)
S = problem.TotalDict(
    "S", dtype=float, dict_keys=N, description="Scalar total dictionary"
)
s = problem.ContinuousVar("s", lower_bound=0, upper_bound=10, dict_keys=N)
W = problem.Float("w", shape=(N, M))

problem
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Arithmetic Operations}\\\displaystyle \min &\displaystyle 0\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}s&\in \mathop{\mathrm{TotalDict}}\left[N;\mathbb{R}\right]\;\left(0\leq s\leq 10\right)&\quad &\text{a dictionary of }\text{continuous}\text{ variables with key }N\\x&\in \left\{0, 1\right\}&\quad &0\text{-dim binary variable}\\&&&\text{Scalar decision variable}\\&&&\\y&\in \mathop{\mathrm{Array}}\left[N\times M;\mathbb{Z}\right]\;\left(0\leq y\leq 10\right)&\quad &2\text{-dim integer variable}\\&&&\text{2D array decision variable}\\&&&\\z&\in \mathop{\mathrm{Array}}\left[N\times M\times N;\mathbb{R}\right]\;\left(-1\leq z\leq 42\right)&\quad &3\text{-dim continuous variable}\\&&&\text{3D array decision variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}M&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\S&\in \mathop{\mathrm{TotalDict}}\left[N;\mathbb{R}\right]&\quad &\text{A total dictionary of placeholders with keys }N\text{, values in }\mathbb{R}\\&&&\text{Scalar total dictionary}\\&&&\\w&\in \mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]&\quad &2\text{-dimensional array of placeholders with elements in }\mathbb{R}\\\end{alignedat}\end{array} \end{split}\]

Allowed examples#

problem.infer(x + 1)  # OK! (scalar addition)
\[\mathbb{N}\]
problem.infer(y - x)  # OK! (array minus scalar)
\[\mathop{\mathrm{Array}}\left[N\times M;\mathbb{Z}\right]\]
problem.infer(S * x)  # OK! (scalar times dictionary)
\[\mathop{\mathrm{TotalDict}}\left[N;\mathbb{R}\right]\]
problem.infer(y / W)  # OK! (division of arrays with the same shape (N, M))
\[\mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]\]
problem.infer(S + s)  # OK! (addition of total dictionaries with the same key set)
\[\mathop{\mathrm{TotalDict}}\left[N;\mathbb{R}\right]\]

Disallowed examples#

try:
    # ERROR! (dictionary times array)
    problem.infer(S * y)
except Exception as e:
    print(e)
Traceback (most recent last):
    while inferring the type of expression `S * y',
        defined at File "/tmp/ipykernel_676/2932800859.py", line 3, col 19-24
    while inferring the type of expression `S * y',
        defined at File "/tmp/ipykernel_676/2932800859.py", line 3, col 19-24
    while checking if types `TotalDict[N; float]' and `Array[N, M; int!]' can be combined with numeric operator `Mul',
        defined at File "/tmp/ipykernel_676/2932800859.py", line 3, col 19-24

File "/tmp/ipykernel_676/2932800859.py", line 3, col 19-24:

    3  |      problem.infer(S * y)
                            ^^^^^

Type Error: Instance for binary operator Mul not found for type TotalDict[N; float] and Array[N, M; int!]
try:
    # ERROR! (arrays with different shapes)
    problem.infer(y + z)
except Exception as e:
    print(e)
Traceback (most recent last):
    while inferring the type of expression `y + z',
        defined at File "/tmp/ipykernel_676/3762455632.py", line 3, col 19-24
    while inferring the type of expression `y + z',
        defined at File "/tmp/ipykernel_676/3762455632.py", line 3, col 19-24
    while checking if types `Array[N, M; int!]' and `Array[N, M, N; float!]' can be combined with numeric operator `Add',
        defined at File "/tmp/ipykernel_676/3762455632.py", line 3, col 19-24

File "/tmp/ipykernel_676/3762455632.py", line 3, col 19-24:

    3  |      problem.infer(y + z)
                            ^^^^^

Type Error: Instance for binary operator Add not found for type Array[N, M; int!] and Array[N, M, N; float!]

Division by decision variables

At the modeling stage, decision variables can appear on either side of arithmetic operators. However, when compiling to an instance, expressions with a decision variable in the denominator (like N / x above) currently raise an error. Some solvers can support division by decision variables with special encodings, so the syntax is allowed, but JijModeling and OMMX do not yet support such encodings. In the future, they may allow these encodings and compile some cases successfully.

Elementary transcendental functions

JijModeling expressions support not only arithmetic but also elementary transcendental functions such as trigonometric functions (sin(), cos(), tan()) and logarithms (log2(), log10(), ln()). These functions can be applied regardless of whether the expression contains decision variables, but if they do, compilation to an instance currently raises an error.

Comparison operators#

Equality operators (==, !=) and order comparison operators (<, <=, >, >=) can also be used with JijModeling expressions.

If neither side contains decision variables, the result is a Boolean expression (Bool). If at least one side can contain decision variables, the result is a special comparison type. This is because constraints must compare expressions that include decision variables, while comprehension filters require fully determined Boolean expressions.

Currently, comparison operators can be applied to scalars and category labels, or arrays/dictionaries of those. The conditions for arrays and dictionaries are the same as the arithmetic overload rules.

problem.infer(x == y)  # OK! (scalar vs array equality)
\[\mathop{\mathrm{Comparison}}\left[\left\{0, 1\right\},\mathop{\mathrm{Array}}\left[N\times M;\mathbb{Z}\right]\right]\]
problem.infer(N <= N)  # OK! (scalar order comparison)
\[\mathrm{Bool}\]
problem.infer(y > W)  # OK! (comparison of arrays with the same shape)
\[\mathop{\mathrm{Comparison}}\left[\mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right],\mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]\right]\]

Indexing arrays and dictionaries#

Element access and images by indexing#

Like Python lists, dictionaries, or numpy.ndarray, JijModeling expressions support multi-dimensional indexing such as x[i, j]. Specifically, you can index expressions of the following types:

  1. (Multidimensional) arrays

    • Allowed indices: natural-number expressions that do not include decision variables

  2. Dictionaries

    • Allowed indices: category labels in the dictionary key set, or arbitrary integer expressions (including decision variables)

  3. Tuples

    • Allowed indices: natural-number expressions (within the tuple length) that do not include decision variables

Indices can only include natural numbers, integers, or category labels that do not include decision variables. You can write multiple indices at once, like x[i, j, k]. Using too many indices (more than the tuple length, array dimension, or dictionary tuple length) results in a type error.

Array indexing also supports slicing syntax such as x[:, 1]. In this case, x[:, 1] keeps all elements along the 0th dimension and selects index 1 on the 1st dimension. If x is 2D, the result is a 1D array; if x has dimension \(N \ge 3\), the result is \((N-1)\)-dimensional. If x is 1D or scalar, it is a type error. Slices with step and end indices, like x[1, 1:N:2], are also supported. For details on slice syntax, see the Python docs on “Slicings”.

Getting the index set of array/dictionary expressions#

For array and dictionary expressions, you can obtain their index sets. For arrays, use indices(); for dictionaries, use keys(). For example, you can define a dictionary decision variable with the same domain as a PartialDict placeholder as follows:

problem = jm.Problem("Index and Keys Example")
N = problem.Length("N")
L = problem.CategoryLabel("L")
S = problem.PartialDict("S", dtype=float, dict_keys=(N, L))
x = problem.BinaryVar("x", dict_keys=S.keys())
problem
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Index and Keys Example}\\\displaystyle \min &\displaystyle 0\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{TotalDict}}\left[\mathop{\mathtt{keys}}\left(S\right);\left\{0, 1\right\}\right]&\quad &\text{a dictionary of }\text{binary}\text{ variables with key }\mathop{\mathtt{keys}}\left(S\right)\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\S&\in \mathop{\mathrm{PartialDict}}\left[N\times L;\mathbb{R}\right]&\quad &\text{A partial dictionary of placeholders with keys }N\times L\text{, values in }\mathbb{R}\\\end{alignedat}\\&\\&\text{Category Labels:}\\&\qquad \begin{array}{rl} L&\text{Category Label}\end{array} \end{array} \end{split}\]

Set operations and comprehensions for sum/product#

“Sets” in JijModeling#

JijModeling supports the concept of a set, which represents “a sequence of values of a specific type”. The indices() and keys() mentioned above actually return expressions that represent index sets. Sets are used to iterate over index ranges, compute sums/products, and define indexed constraints.

Sets in JijModeling are streams

As in other modelers, JijModeling calls them “sets”, but mathematically a set has no duplicates and no ordering. By contrast, JijModeling “sets” allow duplicates and preserve order. Strictly speaking, JijModeling sets correspond to streams or iterators in general programming terms.

Some values are automatically converted to sets. For example, a multi-dimensional array becomes a set that scans elements in row-major order, a natural number \(N\) becomes the set \(\{0, 1, \ldots, N-1\}\), and a category label L becomes the set of all values of L given at compile time.

Change from JijModeling 1: arrays as “sets”

In JijModeling 1, when a multi-dimensional array appeared in belong_to= or forall=, it behaved like a set that iterates over rows. That is, if A had shape (N, M), iterating over A produced a set of N elements, each a 1D array of length M.

In JijModeling 2, this behavior was removed, and arrays now iterate over elements in order. If you want the old behavior, explicitly convert with rows(): use jm.rows(A) or A.rows().

Conversion to sets is usually automatic, but you can explicitly convert via set().

Sum and product over sets#

Indices become especially powerful when combined with sums/products. Below we introduce several ways to write sums and products.

Note

For simplicity we show sums using jm.sum() (or Expression.sum()), but products using jm.prod() or Expression.prod() are analogous.

With the Decorator API, sums/products can be written using intuitive comprehensions.

For example, the sum of products of decision variables and placeholders can be written as:

@jm.Problem.define("Sum Example")
def sum_example(problem: jm.DecoratedProblem):
    N = problem.Length()
    a = problem.Float(shape=(N,))
    x = problem.BinaryVar(shape=(N,))
    problem += jm.sum(a[i] * x[i] for i in N)


sum_example
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Sum Example}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{{a}_{i}\cdot {x}_{i}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N;\left\{0, 1\right\}\right]&\quad &1\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}a&\in \mathop{\mathrm{Array}}\left[N;\mathbb{R}\right]&\quad &1\text{-dimensional array of placeholders with elements in }\mathbb{R}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\\end{alignedat}\end{array} \end{split}\]

Caution: Do not use Python’s built-in sum

To write sums with comprehensions, you can only use JijModeling’s jm.sum() function or Expression.sum() method. If you accidentally use Python’s built-in sum(), or call jm.sum() outside the Decorator API, you will get an error like the following:

try:

    @jm.Problem.define("Wrong Sum Example")
    def wrong_sum_example(problem: jm.DecoratedProblem):
        N = problem.Length()
        a = problem.Float(shape=(N,))
        x = problem.BinaryVar(shape=(N,))
        # ERROR! Using Python's builtin sum instead of jm.sum()
        problem += sum(a[i] * x[i] for i in N)
except Exception as e:
    print(e)
Invalid comprehension syntax detected! Perhaps you used comprehension syntax outside decorator API, or used Python's builtin `sum` function etc., instead of `jijmodeling.sum`?

JijModeling provides jijmodeling.map(), corresponding to Python’s builtin map(), so you can write the same thing using only the Plain API as follows:

sum_example_plain = jm.Problem("Sum Example (Plain)")
N = sum_example_plain.Length("N")
a = sum_example_plain.Float("a", shape=(N,))
x = sum_example_plain.BinaryVar("x", shape=(N,))
sum_example_plain += jm.sum(jm.map(lambda i: a[i] * x[i], N))

sum_example_plain
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Sum Example (Plain)}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{{a}_{i}\cdot {x}_{i}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N;\left\{0, 1\right\}\right]&\quad &1\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}a&\in \mathop{\mathrm{Array}}\left[N;\mathbb{R}\right]&\quad &1\text{-dimensional array of placeholders with elements in }\mathbb{R}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\\end{alignedat}\end{array} \end{split}\]

For simple sums, you can also pass the domain and the function to jm.sum() directly:

sum_example_plain_alt = jm.Problem("Sum Example (Plain, Alt)")
N = sum_example_plain_alt.Length("N")
a = sum_example_plain_alt.Float("a", shape=(N,))
x = sum_example_plain_alt.BinaryVar("x", shape=(N,))
sum_example_plain_alt += jm.sum(N, lambda i: a[i] * x[i])

sum_example_plain_alt
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Sum Example (Plain, Alt)}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{{a}_{i}\cdot {x}_{i}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N;\left\{0, 1\right\}\right]&\quad &1\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}a&\in \mathop{\mathrm{Array}}\left[N;\mathbb{R}\right]&\quad &1\text{-dimensional array of placeholders with elements in }\mathbb{R}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\\end{alignedat}\end{array} \end{split}\]

When using the Plain API without the Decorator API, you need Python lambda expressions to build indexed expressions.

Tip

When jm.sum() / jm.prod() is called as a single-argument function or method, it computes the sum/product over a set. So if you simply want the sum of elements in x, you can write jm.sum(x) or x.sum(). With the limited broadcasting described earlier, you can also write jm.sum(a * x) as above. This also works when x is a multi-dimensional array.

Conditional sums/products#

Comprehensions in the Decorator API allow if, so you can take a sum only over even indices like this:

@jm.Problem.define("Even Sum Example")
def even_sum_example(problem: jm.DecoratedProblem):
    N = problem.Length()
    a = problem.Float(shape=(N,))
    x = problem.BinaryVar(shape=(N,))
    problem += jm.sum(a[i] * x[i] for i in N if (i % 2) == 0)


even_sum_example
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Even Sum Example}\\\displaystyle \min &\displaystyle \sum _{\substack{i=0\\i\bmod 2=0}}^{N-1}{{a}_{i}\cdot {x}_{i}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N;\left\{0, 1\right\}\right]&\quad &1\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}a&\in \mathop{\mathrm{Array}}\left[N;\mathbb{R}\right]&\quad &1\text{-dimensional array of placeholders with elements in }\mathbb{R}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\\end{alignedat}\end{array} \end{split}\]

JijModeling also provides jm.filter() corresponding to Python’s builtin filter, so the same model in the Plain API is:

even_sum_example_plain = jm.Problem("Even Sum Example (Plain)")
N = even_sum_example_plain.Length("N")
a = even_sum_example_plain.Float("a", shape=(N,))
x = even_sum_example_plain.BinaryVar("x", shape=(N,))
even_sum_example_plain += jm.sum(
    N.filter(lambda i: (i % 2) == 0),
    lambda i: a[i] * x[i],
)

even_sum_example_plain
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Even Sum Example (Plain)}\\\displaystyle \min &\displaystyle \sum _{\substack{i=0\\i\bmod 2=0}}^{N-1}{{a}_{i}\cdot {x}_{i}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N;\left\{0, 1\right\}\right]&\quad &1\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}a&\in \mathop{\mathrm{Array}}\left[N;\mathbb{R}\right]&\quad &1\text{-dimensional array of placeholders with elements in }\mathbb{R}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\\end{alignedat}\end{array} \end{split}\]

Sums/products over multiple indices#

Comprehensions support nested for and if, so sums over multiple indices are easy to write by stacking for clauses:

@jm.Problem.define("Double Sum Example")
def double_sum_example(problem: jm.DecoratedProblem):
    N = problem.Length()
    M = problem.Length()
    Q = problem.Float(shape=(N, M))
    x = problem.BinaryVar(shape=(N, M))
    problem += jm.sum(Q[i, j] for i in N for j in M)


double_sum_example
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Double Sum Example}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{\sum _{j=0}^{M-1}{{Q}_{i,j}}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N\times M;\left\{0, 1\right\}\right]&\quad &2\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}M&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\Q&\in \mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]&\quad &2\text{-dimensional array of placeholders with elements in }\mathbb{R}\\\end{alignedat}\end{array} \end{split}\]

Alternatively, you can use jijmodeling.product() to form the Cartesian product \(A_1 \times \ldots \times A_n\):

@jm.Problem.define("Double Sum Example (Alt)")
def double_sum_example_alt(problem: jm.DecoratedProblem):
    N = problem.Length()
    M = problem.Length()
    Q = problem.Float(shape=(N, M))
    x = problem.BinaryVar(shape=(N, M))
    problem += jm.sum(Q[i, j] for (i, j) in jm.product(N, M))


double_sum_example_alt
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Double Sum Example (Alt)}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{\sum _{j=0}^{M-1}{{Q}_{i,j}}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N\times M;\left\{0, 1\right\}\right]&\quad &2\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}M&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\Q&\in \mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]&\quad &2\text{-dimensional array of placeholders with elements in }\mathbb{R}\\\end{alignedat}\end{array} \end{split}\]

With if, you can build more complex examples:

@jm.Problem.define("Filtered Double Sum Example")
def filtered_double_sum_example(problem: jm.DecoratedProblem):
    N = problem.Length()
    M = problem.Length()
    Q = problem.Float(shape=(N, M))
    x = problem.BinaryVar(shape=(N, M))
    problem += jm.sum(
        Q[i, j]
        for i in N
        for j in M
        if (i + j) % 2 == 0  # sum is even
    )


filtered_double_sum_example
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Filtered Double Sum Example}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{\sum _{\substack{j=0\\\left(i+j\right)\bmod 2=0}}^{M-1}{{Q}_{i,j}}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N\times M;\left\{0, 1\right\}\right]&\quad &2\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}M&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\Q&\in \mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]&\quad &2\text{-dimensional array of placeholders with elements in }\mathbb{R}\\\end{alignedat}\end{array} \end{split}\]

In the Plain API, this becomes:

filtered_double_sum_example_plain = jm.Problem("Filtered Double Sum Example (Plain)")
N = filtered_double_sum_example_plain.Length("N")
M = filtered_double_sum_example_plain.Length("M")
Q = filtered_double_sum_example_plain.Float("Q", shape=(N, M))
x = filtered_double_sum_example_plain.BinaryVar("x", shape=(N, M))
filtered_double_sum_example_plain += jm.sum(
    jm.product(N, M).filter(lambda i, j: (i + j) % 2 == 0), lambda i, j: Q[i, j]
)

filtered_double_sum_example_plain
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Filtered Double Sum Example (Plain)}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{\sum _{\substack{j=0\\\left(i+j\right)\bmod 2=0}}^{M-1}{{Q}_{i,j}}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N\times M;\left\{0, 1\right\}\right]&\quad &2\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}M&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\N&\in \mathbb{N}&\quad &\text{A scalar placeholder in }\mathbb{N}\\Q&\in \mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]&\quad &2\text{-dimensional array of placeholders with elements in }\mathbb{R}\\\end{alignedat}\end{array} \end{split}\]

Or you can use jm.flat_map() (or the method form Expression.flat_map()) to map with functions that return sets:

jm.sum(
    N.flat_map(
        lambda i: jm.map(lambda j: (i, j), M),
    ).filter(lambda i, j: (i + j) % 2 == 0),
    lambda i, j: Q[i, j],
)
\[\begin{split}\sum _{i=0}^{N-1}{\sum _{\substack{j=0\\\left(i+j\right)\bmod 2=0}}^{M-1}{{Q}_{i,j}}}\end{split}\]

In principle, you can write any model without the Decorator API, but it becomes complex and hard to read, so we recommend using the Decorator API.

Logical operations on conditional expressions and sets#

So far, conditions in comprehensions and filter() were simple, but in practice you often want logical expressions like “and” or “or”. Python’s and, or, and not cannot be overloaded, so JijModeling uses bitwise operators: & (and), | (or), ~ (not), or the functions jijmodeling.band(), jijmodeling.bor(), jijmodeling.bnot().

Be careful with operator precedence in bitwise logic

Unlike and/or, & and | have lower precedence than == and !=. For example, a == b & c == d is parsed as a == (b & c) == d. Therefore, when using & or |, always parenthesize each comparison, e.g., (a >= b) & (c == d).

Logical operations can also be used on set expressions: union is |, and intersection is &. Complement is not supported because it may be infinite; instead, use jijmodeling.diff() to take set differences.