Declaring a mathematical model#

In JijModeling, variables, constraints, and other elements are always registered and tied to a specific mathematical model. Before we dive into the individual elements, this section briefly explains how to declare a model.

Creating a Problem object that represents a model#

In JijModeling, a specific model is represented by a Problem object, which you typically declare first when constructing a model. First, import the JijModeling library under the name jm.

import jijmodeling as jm

Creating a Problem object with the Plain API#

There are two ways to create a Problem: with the Plain API and with the Decorator API. The first way is to directly construct a Problem object using the Plain API.

plain_problem = jm.Problem(
    "Empty Problem",
    sense=jm.ProblemSense.MAXIMIZE,
    description="An optimization problem with no objective or constraints, for demonstration",
)

The first argument is required and specifies the name of the model. The remaining keyword arguments, sense and description, are both optional. sense specifies whether the model is a maximization problem (jm.ProblemSense.MAXIMIZE) or a minimization problem (jm.ProblemSense.MINIMIZE); if omitted, it defaults to minimization. description is a human-readable description of the purpose of model, used in LaTeX output or OMMX metadata. You can display the object to check such metadata within Jupyter:

plain_problem
\[\begin{array}{rl} \text{Problem}\colon &\text{Empty Problem}\\&\text{An optimization problem with no objective or constraints, for demonstration}\\\displaystyle \max &\displaystyle 0\end{array} \]

Since no objective has been set yet, \(0\) is shown as the objective at this stage.

Writing mathematical notation in description

In mathematical displays such as Jupyter Notebook output, description is interpreted as a normal string. Therefore, when you write mathematical notation inside description, enclose the mathematical part in $..$. For example, writing description="when x_i = 1, ..." makes _ invalid as a LaTeX string, so write description="when $x_i = 1$, ..." to have it treated as mathematics.

This note also applies to description values on objects other than Problem.

Creating a Problem object with the Decorator API#

Here is the same model defined with the Decorator API using @jm.Problem.define():

@jm.Problem.define(
    "Empty Problem",
    sense=jm.ProblemSense.MAXIMIZE,
    description="An optimization problem with no objective or constraints, for demonstration",
)
def deco_problem(problem: jm.DecoratedProblem):
    pass  # do nothing


deco_problem
\[\begin{array}{rl} \text{Problem}\colon &\text{Empty Problem}\\&\text{An optimization problem with no objective or constraints, for demonstration}\\\displaystyle \max &\displaystyle 0\end{array} \]

@jm.Problem.define() takes the same arguments as jm.Problem(), but instead of binding directly to a variable, it decorates a function definition (here, def deco_problem(...)). With @jm.Problem.define, when the function definition ends, the actual Problem instance is bound to a variable with the same name as the function (here, deco_problem). In the example above, after the function definition, we can print-out deco_problem to check its definition. A function definition preceded by an expression starting with @ is called a decorated function. Inside such a decorated function, you will call various methods on the first argument problem to update the model.

What is a DecoratedProblem object?

Note that the first argument of a decorated function is not a Problem object but a DecoratedProblem object. DecoratedProblem is a dummy class that only appears inside decorated functions. It is provided with type hints tailored to the Decorator API, so you can benefit from editor completion and type checking.

As we haven’t made any updates on the problem, this style may look a bit verbose. However, inside a function decorated by @jm.Problem.define, you can use natural and intuitive Decorator API syntax, such as omitting variable names or using comprehensions for sums and products, which becomes very convenient in real model definitions.

Also, you can treat models defined with either API in the same way, so you never need to care which API was used. In fact, both plain_problem and deco_problem above are identified as the same model:

jm.is_same(plain_problem, deco_problem)
True

Updating a Problem object#

We created almost empty Problem objects above, but in practice you update the Problem incrementally as you build a model, adding decision variables, constraints, and objectives to the model. Regardless of how a model is defined, you can always update it with the Plain API, and you can also update an existing Problem object problem using the Decorator API via the @problem.update decorator. You can also mix the two styles freely. Let’s add variables to the two problems we defined earlier.

# Update the previously Plain API-defined `plain_problem` using the Decorator API:
@plain_problem.update
def _(problem: jm.DecoratedProblem):
    # Define a new binary decision variable `x` and add it to the objective.
    x = problem.BinaryVar()  # Name can be omitted if it matches the Python variable.
    problem += x


# Now add another binary decision variable `y` using the Plain API.
y = plain_problem.BinaryVar("y")  # Plain API requires the name.
plain_problem += y
plain_problem
\[\begin{array}{rl} \text{Problem}\colon &\text{Empty Problem}\\&\text{An optimization problem with no objective or constraints, for demonstration}\\\displaystyle \max &\displaystyle x+y\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \left\{0,1\right\}&\qquad &\text{a binary decision variable}\\&&&\\y&\in \left\{0,1\right\}&\qquad &\text{a binary decision variable}\\\end{alignedat}\end{array} \]
# Conversely, update a Decorator API-defined `deco_problem` using only the Plain API.
x = deco_problem.BinaryVar("x")
y = deco_problem.BinaryVar("y")
deco_problem += x + y

deco_problem
\[\begin{array}{rl} \text{Problem}\colon &\text{Empty Problem}\\&\text{An optimization problem with no objective or constraints, for demonstration}\\\displaystyle \max &\displaystyle x+y\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \left\{0,1\right\}&\qquad &\text{a binary decision variable}\\&&&\\y&\in \left\{0,1\right\}&\qquad &\text{a binary decision variable}\\\end{alignedat}\end{array} \]

We use _ as the function name in the @problem.update example – the function name has no effect on the result, so you can choose any name you like.

Variable rebinding in the Decorator API#

Python variables defined inside functions decorated with @jm.Problem.define() or @problem.update cannot be accessed directly from outside the function. More precisely, while the model-level variables and constraints are registered in the corresponding Problem object, the Python variables that refer to them stay inside the function scope.

To make such cases easy to handle, since JijModeling 2.7.0, the second and subsequent arguments of a update() function can bind variables already defined in the Problem, including placeholders, category labels, decision variables, and named expressions. Starting with the second argument, declare arguments with the same names as the items you want to obtain and use the corresponding type annotations below.

Item to obtain

Type annotation

Placeholder

jm.Placeholder

Category label

jm.CategoryLabel

Decision variable

jm.DecisionVar

Named expression

jm.NamedExpr

Each argument name must match the name passed as the first argument to the corresponding constructor. If the name was omitted through the Decorator API, it matches the Python variable name. When type annotations are omitted, the appropriate variables are still matched by name, but specifying annotations is recommended whenever possible because it enables more precise type checking and editor completion.

import jijmodeling as jm

@jm.Problem.define("Updated Problem")
def updated_problem(problem: jm.DecoratedProblem):
    N = problem.Length()
    L = problem.CategoryLabel()
    x = problem.BinaryVar(shape=N)
    total = problem.NamedExpr(x.sum())

@updated_problem.update
def _(
    problem: jm.DecoratedProblem,
    N: jm.Placeholder,
    total: jm.NamedExpr,
):
    problem += problem.Constraint("select", total <= N)

As this example shows, the additional arguments to @problem.update need only include the variables required by that update, rather than every variable defined in the Problem.

If you are using a version earlier than 2.7.0, you can achieve the same result by retrieving items directly from the metadata held by the Problem, such as placeholders, and binding them manually as described in the following sections.

Now, let’s move on to the concrete features you need to build models in the next sections.

Tip

At this point the benefits of the Decorator API may not be obvious, but they will become clear as you go through the following sections.