Getting started

A model is built in two stages, mirroring the backend: a Core accumulates variables, parameters, objectives and constraints, and a Model is built from it.

import examodels as exa

core = exa.Core()

Variables

N = 10
x = exa.add_var(core, N, start=[-1.2 if i % 2 == 0 else 1.0 for i in range(N)])

start, lvar and uvar take a number or an array. A dimension may be an integer or a range, and there may be several:

T = 4
y = exa.add_var(core, T, N)               # index as y[t, i]
z = exa.add_var(core, range(2, 11))       # indices 2..10

Indices are 0-based, and an index is a number: x[i] - i means what it says.

Objective

exa.add_obj(core, lambda i: 100 * (x[i-1]**2 - x[i])**2 + (x[i-1] - 1)**2,
            over=range(1, N))

Terms are summed; call it more than once to add more.

Constraints

con = exa.add_con(core, lambda i: 3 * x[i+1]**3 + 2 * x[i+2] - 5
            + exa.sin(x[i+1] - x[i+2]) * exa.sin(x[i+1] + x[i+2])
            + 4 * x[i+1] - x[i] * exa.exp(x[i] - x[i+1]) - 3,
            over=range(0, N - 2))

Constraints are two-sided; lcon and ucon default to zero, and take a number or an array. Use -inf or inf for a one-sided constraint.

Solving

model = exa.Model(core)
sol = model.solve()

print(sol.status)            # 'first_order'
print(sol.objective)         # 6.232458632
print(sol[x])                # the values of block x, as a numpy array
print(sol.multipliers(con))  # the duals of that constraint block

Without solver=, Ipopt is used on the host and MadNLP on an accelerator — Ipopt cannot take device arrays. Install one with exa.install_solver("ipopt").

Two ways to write the same call

Everything is available both as a function taking the core first, matching the backend’s argument order, and as a method:

exa.add_obj(core, lambda i: x[i]**2, over=range(N))
core.add_obj(lambda i: x[i]**2, over=range(N))

A generator expression also works, and reads closest to the backend’s macros. As a bare argument it must be the only one, so it suits the method form:

core.add_obj(x[i]**2 for i in range(N))     # `i` is bound by the comprehension

Naming

Passing name= registers a block, retrievable from either the core or the model:

x = exa.add_var(core, N, name="x")
model = exa.Model(core)
model.get_start(model.x)