Skip to content

API Reference

Unified Python API for Integer Linear Programming (ILP) solvers.

ilpy provides a consistent interface that abstracts over different ILP backends (currently Gurobi and SCIP), and lets you build problems either imperatively (via Objective / Constraint) or as natural Python expressions (via Variable and Expression).

Classes:

Name Description
Constraint

A linear (or quadratic) constraint of the form Ax [<=|=|>=] b.

Constraints

An ordered collection of Constraint objects.

Expression

Base class for all expression nodes.

Objective

A linear (or quadratic) objective function to minimize or maximize.

Preference

Preference for a solver backend.

Relation

Relation used in a linear constraint.

Sense

Direction of an objective function.

Solution

The result of solving an optimization problem.

Solver

High-level wrapper around an ILP solver backend.

SolverBackend

Abstract base class implemented by each concrete solver backend.

SolverStatus

Normalized solver status across backends.

Variable

A variable.

VariableType

Type of a decision variable in an optimization problem.

Functions:

Name Description
solve

Solve an objective subject to constraints.

Constraint

A linear (or quadratic) constraint of the form Ax [<=|=|>=] b.

Methods:

Name Description
__init__

Create an empty <= 0 constraint with no coefficients.

from_coefficients

Build a Constraint from coefficients, a relation, and a value.

get_coefficients

Return the linear coefficients as a mapping from variable index to value.

get_quadratic_coefficients

Return the quadratic coefficients, keyed by variable-index pairs.

get_relation

Return the relation of this constraint.

get_value

Return the right-hand-side value of this constraint.

is_violated

Return True if solution violates this constraint.

set_coefficient

Set the linear coefficient of variable i.

set_quadratic_coefficient

Set the quadratic coefficient for the term x_i * x_j.

set_relation

Set the relation (<=, =, >=) used by this constraint.

set_value

Set the right-hand-side value of this constraint.

Source code in src/ilpy/_components.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
class Constraint:
    """A linear (or quadratic) constraint of the form `Ax [<=|=|>=] b`."""

    def __init__(self) -> None:
        """Create an empty `<= 0` constraint with no coefficients."""
        self._coefs: dict[int, float] = {}
        self._quad_coefs: dict[tuple[int, int], float] = {}
        self._relation: Relation = Relation.LessEqual
        self._value: float = 0.0

    def set_coefficient(self, i: SupportsIndex, value: float) -> None:
        """Set the linear coefficient of variable `i`."""
        if value == 0:
            self._coefs.pop(int(i), None)
        else:
            self._coefs[int(i)] = value

    def get_coefficients(self) -> Mapping[int, float]:
        """Return the linear coefficients as a mapping from variable index to value."""
        return MappingProxyType(self._coefs)

    def set_quadratic_coefficient(
        self, i: SupportsIndex, j: SupportsIndex, value: float
    ) -> None:
        """Set the quadratic coefficient for the term `x_i * x_j`."""
        key = (int(i), int(j))
        if value == 0:
            self._quad_coefs.pop(key, None)
        else:
            self._quad_coefs[key] = value

    def get_quadratic_coefficients(self) -> Mapping[tuple[int, int], float]:
        """Return the quadratic coefficients, keyed by variable-index pairs."""
        return MappingProxyType(self._quad_coefs)

    def set_relation(self, relation: Relation) -> None:
        """Set the relation (`<=`, `=`, `>=`) used by this constraint."""
        self._relation = relation

    def get_relation(self) -> Relation:
        """Return the relation of this constraint."""
        return self._relation

    def set_value(self, value: float) -> None:
        """Set the right-hand-side value of this constraint."""
        self._value = value

    def get_value(self) -> float:
        """Return the right-hand-side value of this constraint."""
        return self._value

    def is_violated(self, solution: Solution) -> bool:
        """Return True if `solution` violates this constraint."""
        total = sum(coef * solution[var] for var, coef in self._coefs.items())
        if self._relation == Relation.LessEqual:
            return total > self._value
        elif self._relation == Relation.GreaterEqual:
            return total < self._value
        elif self._relation == Relation.Equal:
            return total != self._value
        return False

    @classmethod
    def from_coefficients(
        cls,
        coefficients: LinearCoeffs = (),
        quadratic_coefficients: QCoeffs = (),
        relation: Relation = Relation.LessEqual,
        value: float = 0,
    ) -> Constraint:
        """Build a `Constraint` from coefficients, a relation, and a value."""
        constraint = cls()
        iter_coeffs = (
            coefficients.items()
            if isinstance(coefficients, Mapping)
            else enumerate(coefficients)
        )
        for i, coeff in iter_coeffs:
            constraint.set_coefficient(i, coeff)
        iter_quadratic_coeffs = (
            quadratic_coefficients.items()
            if isinstance(quadratic_coefficients, Mapping)
            else quadratic_coefficients
        )
        for (i, j), coeff in iter_quadratic_coeffs:
            constraint.set_quadratic_coefficient(i, j, coeff)

        constraint.set_relation(relation)
        constraint.set_value(value)
        return constraint

__init__()

Create an empty <= 0 constraint with no coefficients.

Source code in src/ilpy/_components.py
22
23
24
25
26
27
def __init__(self) -> None:
    """Create an empty `<= 0` constraint with no coefficients."""
    self._coefs: dict[int, float] = {}
    self._quad_coefs: dict[tuple[int, int], float] = {}
    self._relation: Relation = Relation.LessEqual
    self._value: float = 0.0

from_coefficients(coefficients=(), quadratic_coefficients=(), relation=Relation.LessEqual, value=0) classmethod

Build a Constraint from coefficients, a relation, and a value.

Source code in src/ilpy/_components.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@classmethod
def from_coefficients(
    cls,
    coefficients: LinearCoeffs = (),
    quadratic_coefficients: QCoeffs = (),
    relation: Relation = Relation.LessEqual,
    value: float = 0,
) -> Constraint:
    """Build a `Constraint` from coefficients, a relation, and a value."""
    constraint = cls()
    iter_coeffs = (
        coefficients.items()
        if isinstance(coefficients, Mapping)
        else enumerate(coefficients)
    )
    for i, coeff in iter_coeffs:
        constraint.set_coefficient(i, coeff)
    iter_quadratic_coeffs = (
        quadratic_coefficients.items()
        if isinstance(quadratic_coefficients, Mapping)
        else quadratic_coefficients
    )
    for (i, j), coeff in iter_quadratic_coeffs:
        constraint.set_quadratic_coefficient(i, j, coeff)

    constraint.set_relation(relation)
    constraint.set_value(value)
    return constraint

get_coefficients()

Return the linear coefficients as a mapping from variable index to value.

Source code in src/ilpy/_components.py
36
37
38
def get_coefficients(self) -> Mapping[int, float]:
    """Return the linear coefficients as a mapping from variable index to value."""
    return MappingProxyType(self._coefs)

get_quadratic_coefficients()

Return the quadratic coefficients, keyed by variable-index pairs.

Source code in src/ilpy/_components.py
50
51
52
def get_quadratic_coefficients(self) -> Mapping[tuple[int, int], float]:
    """Return the quadratic coefficients, keyed by variable-index pairs."""
    return MappingProxyType(self._quad_coefs)

get_relation()

Return the relation of this constraint.

Source code in src/ilpy/_components.py
58
59
60
def get_relation(self) -> Relation:
    """Return the relation of this constraint."""
    return self._relation

get_value()

Return the right-hand-side value of this constraint.

Source code in src/ilpy/_components.py
66
67
68
def get_value(self) -> float:
    """Return the right-hand-side value of this constraint."""
    return self._value

is_violated(solution)

Return True if solution violates this constraint.

Source code in src/ilpy/_components.py
70
71
72
73
74
75
76
77
78
79
def is_violated(self, solution: Solution) -> bool:
    """Return True if `solution` violates this constraint."""
    total = sum(coef * solution[var] for var, coef in self._coefs.items())
    if self._relation == Relation.LessEqual:
        return total > self._value
    elif self._relation == Relation.GreaterEqual:
        return total < self._value
    elif self._relation == Relation.Equal:
        return total != self._value
    return False

set_coefficient(i, value)

Set the linear coefficient of variable i.

Source code in src/ilpy/_components.py
29
30
31
32
33
34
def set_coefficient(self, i: SupportsIndex, value: float) -> None:
    """Set the linear coefficient of variable `i`."""
    if value == 0:
        self._coefs.pop(int(i), None)
    else:
        self._coefs[int(i)] = value

set_quadratic_coefficient(i, j, value)

Set the quadratic coefficient for the term x_i * x_j.

Source code in src/ilpy/_components.py
40
41
42
43
44
45
46
47
48
def set_quadratic_coefficient(
    self, i: SupportsIndex, j: SupportsIndex, value: float
) -> None:
    """Set the quadratic coefficient for the term `x_i * x_j`."""
    key = (int(i), int(j))
    if value == 0:
        self._quad_coefs.pop(key, None)
    else:
        self._quad_coefs[key] = value

set_relation(relation)

Set the relation (<=, =, >=) used by this constraint.

Source code in src/ilpy/_components.py
54
55
56
def set_relation(self, relation: Relation) -> None:
    """Set the relation (`<=`, `=`, `>=`) used by this constraint."""
    self._relation = relation

set_value(value)

Set the right-hand-side value of this constraint.

Source code in src/ilpy/_components.py
62
63
64
def set_value(self, value: float) -> None:
    """Set the right-hand-side value of this constraint."""
    self._value = value

Constraints

An ordered collection of Constraint objects.

Methods:

Name Description
__init__

Create an empty collection of constraints.

add

Append a Constraint (or an Expression convertible to one).

add_all

Append every constraint from another Constraints instance.

clear

Remove all constraints from this collection.

Source code in src/ilpy/_components.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
class Constraints:
    """An ordered collection of `Constraint` objects."""

    def __init__(self) -> None:
        """Create an empty collection of constraints."""
        self._constraints: list[Constraint] = []

    def clear(self) -> None:
        """Remove all constraints from this collection."""
        self._constraints.clear()

    def add(self, constraint: Constraint | Expression) -> None:
        """Append a `Constraint` (or an `Expression` convertible to one)."""
        if isinstance(constraint, Expression):
            self._constraints.append(constraint.as_constraint())
        else:
            self._constraints.append(constraint)

    def add_all(self, constraints: Constraints) -> None:
        """Append every constraint from another `Constraints` instance."""
        self._constraints.extend(constraints._constraints)

    def __len__(self) -> int:
        return len(self._constraints)

    def __iter__(self) -> Iterator[Constraint]:
        return iter(self._constraints)

__init__()

Create an empty collection of constraints.

Source code in src/ilpy/_components.py
114
115
116
def __init__(self) -> None:
    """Create an empty collection of constraints."""
    self._constraints: list[Constraint] = []

add(constraint)

Append a Constraint (or an Expression convertible to one).

Source code in src/ilpy/_components.py
122
123
124
125
126
127
def add(self, constraint: Constraint | Expression) -> None:
    """Append a `Constraint` (or an `Expression` convertible to one)."""
    if isinstance(constraint, Expression):
        self._constraints.append(constraint.as_constraint())
    else:
        self._constraints.append(constraint)

add_all(constraints)

Append every constraint from another Constraints instance.

Source code in src/ilpy/_components.py
129
130
131
def add_all(self, constraints: Constraints) -> None:
    """Append every constraint from another `Constraints` instance."""
    self._constraints.extend(constraints._constraints)

clear()

Remove all constraints from this collection.

Source code in src/ilpy/_components.py
118
119
120
def clear(self) -> None:
    """Remove all constraints from this collection."""
    self._constraints.clear()

Expression

Bases: expr

Base class for all expression nodes.

Expressions allow ilpy to represent mathematical expressions in an intuitive syntax, and then convert to a native Constraint object.

This class provides all of the operators and methods needed to build expressions. For example, to create the expression 2 * x - y >= 0, you can write 2 * Variable('x') - Variable('y') >= 0.

!!! tip

you can use `ast.dump` to see the AST representation of an expression.
Or, use `print(expr)` to see the string representation of an expression.

Methods:

Name Description
__str__

Serialize this expression to string form.

as_constraint

Create an ilpy.Constraint object from this expression.

as_objective

Create a linear objective from this expression.

Source code in src/ilpy/expressions.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class Expression(ast.expr):
    """Base class for all expression nodes.

    Expressions allow ilpy to represent mathematical expressions in an
    intuitive syntax, and then convert to a native Constraint object.

    This class provides all of the operators and methods needed to build
    expressions. For example, to create the expression `2 * x - y >= 0`, you can
    write `2 * Variable('x') - Variable('y') >= 0`.

    !!! tip

        you can use `ast.dump` to see the AST representation of an expression.
        Or, use `print(expr)` to see the string representation of an expression.
    """

    def as_constraint(self) -> Constraint:
        """Create an [ilpy.Constraint][] object from this expression."""
        from ._components import Constraint

        l_coeffs, q_coeffs, value = _get_coeff_indices(self)
        return Constraint.from_coefficients(
            coefficients=l_coeffs,
            quadratic_coefficients=q_coeffs,
            relation=_get_relation(self) or Relation.LessEqual,
            value=-value,  # negate value to convert to RHS form
        )

    def as_objective(self, sense: Sense = Sense.Minimize) -> Objective:
        """Create a linear objective from this expression."""
        if _get_relation(self) is not None:  # pragma: no cover
            # TODO: may be supported in the future, eg. for piecewise objectives?
            raise ValueError(f"Objective function cannot have comparisons: {self}")
        from ._components import Objective

        l_coeffs, q_coeffs, value = _get_coeff_indices(self)
        return Objective.from_coefficients(
            coefficients=l_coeffs,
            quadratic_coefficients=q_coeffs,
            constant=value,
            sense=sense,
        )

    @staticmethod
    def _cast(obj: Any) -> Expression:
        """Cast object into an Expression."""
        return obj if isinstance(obj, Expression) else Constant(obj)

    def __str__(self) -> str:
        """Serialize this expression to string form."""
        return str(_ExprSerializer(self))

    # comparisons

    def __lt__(self, other: Expression | float) -> Compare:
        return Compare(self, [ast.Lt()], [other])

    def __le__(self, other: Expression | float) -> Compare:
        return Compare(self, [ast.LtE()], [other])

    def __eq__(self, other: Expression | float) -> Compare:  # type: ignore
        return Compare(self, [ast.Eq()], [other])

    def __ne__(self, other: Expression | float) -> Compare:  # type: ignore
        return Compare(self, [ast.NotEq()], [other])

    def __gt__(self, other: Expression | float) -> Compare:
        return Compare(self, [ast.Gt()], [other])

    def __ge__(self, other: Expression | float) -> Compare:
        return Compare(self, [ast.GtE()], [other])

    # binary operators
    # (note that __and__ and __or__ are reserved for boolean operators.)

    def __add__(self, other: Expression | Number) -> BinOp:
        return BinOp(self, ast.Add(), other)

    def __radd__(self, other: Expression | Number) -> BinOp:
        return BinOp(other, ast.Add(), self)

    def __sub__(self, other: Expression | Number) -> BinOp:
        return BinOp(self, ast.Sub(), other)

    def __rsub__(self, other: Expression | Number) -> BinOp:
        return BinOp(other, ast.Sub(), self)

    def __mul__(self, other: Any) -> BinOp | Constant:
        return BinOp(self, ast.Mult(), other)

    def __rmul__(self, other: Number) -> BinOp | Constant:
        if not isinstance(other, (int, float)):  # pragma: no cover
            raise TypeError("Right multiplication must be with a number")
        return Constant(other) * self

    def __truediv__(self, other: Number) -> BinOp:
        return BinOp(self, ast.Div(), other)

    def __rtruediv__(self, other: Number) -> BinOp:
        return BinOp(other, ast.Div(), self)

    # unary operators

    def __neg__(self) -> UnaryOp:
        return UnaryOp(ast.USub(), self)

    def __pos__(self) -> UnaryOp:
        # usually a no-op
        return UnaryOp(ast.UAdd(), self)

__str__()

Serialize this expression to string form.

Source code in src/ilpy/expressions.py
84
85
86
def __str__(self) -> str:
    """Serialize this expression to string form."""
    return str(_ExprSerializer(self))

as_constraint()

Create an ilpy.Constraint object from this expression.

Source code in src/ilpy/expressions.py
52
53
54
55
56
57
58
59
60
61
62
def as_constraint(self) -> Constraint:
    """Create an [ilpy.Constraint][] object from this expression."""
    from ._components import Constraint

    l_coeffs, q_coeffs, value = _get_coeff_indices(self)
    return Constraint.from_coefficients(
        coefficients=l_coeffs,
        quadratic_coefficients=q_coeffs,
        relation=_get_relation(self) or Relation.LessEqual,
        value=-value,  # negate value to convert to RHS form
    )

as_objective(sense=Sense.Minimize)

Create a linear objective from this expression.

Source code in src/ilpy/expressions.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def as_objective(self, sense: Sense = Sense.Minimize) -> Objective:
    """Create a linear objective from this expression."""
    if _get_relation(self) is not None:  # pragma: no cover
        # TODO: may be supported in the future, eg. for piecewise objectives?
        raise ValueError(f"Objective function cannot have comparisons: {self}")
    from ._components import Objective

    l_coeffs, q_coeffs, value = _get_coeff_indices(self)
    return Objective.from_coefficients(
        coefficients=l_coeffs,
        quadratic_coefficients=q_coeffs,
        constant=value,
        sense=sense,
    )

Objective

A linear (or quadratic) objective function to minimize or maximize.

Methods:

Name Description
__init__

Create an empty minimizing objective with size zero coefficients.

from_coefficients

Build an Objective from coefficients, a constant, and a sense.

get_coefficients

Return a copy of the linear coefficients.

get_constant

Return the constant term of the objective.

get_quadratic_coefficients

Return the quadratic coefficients, keyed by variable-index pairs.

get_sense

Return the sense of this objective.

resize

Resize the objective function. New coefficients are set to 0.

set_coefficient

Set the linear coefficient of variable i, growing storage as needed.

set_constant

Set the constant term added to the objective.

set_quadratic_coefficient

Set the quadratic coefficient for the term x_i * x_j.

set_sense

Set the sense (Sense.Minimize or Sense.Maximize).

Source code in src/ilpy/_components.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
class Objective:
    """A linear (or quadratic) objective function to minimize or maximize."""

    def __init__(self, size: int = 0) -> None:
        """Create an empty minimizing objective with `size` zero coefficients."""
        self._sense = Sense.Minimize
        self._constant = 0.0
        self._coeffs: list[float] = []
        self._quad_coeffs: dict[tuple[int, int], float] = {}

    def set_constant(self, value: float) -> None:
        """Set the constant term added to the objective."""
        self._constant = value

    def get_constant(self) -> float:
        """Return the constant term of the objective."""
        return self._constant

    def resize(self, size: int) -> None:
        """Resize the objective function. New coefficients are set to 0."""
        if size < 0:
            raise ValueError("Size must be non-negative.")
        current_size = len(self)
        if current_size < size:
            self._coeffs.extend([0] * (size - current_size))
        elif current_size > size:
            self._coeffs = self._coeffs[:size]

    def set_coefficient(self, i: SupportsIndex, value: float) -> None:
        """Set the linear coefficient of variable `i`, growing storage as needed."""
        i = int(i)
        if i >= len(self):
            self.resize(i + 1)
        self._coeffs[i] = value

    def get_coefficients(self) -> list[float]:
        """Return a copy of the linear coefficients."""
        return list(self._coeffs)

    def __iter__(self) -> Iterator[float]:
        return iter(self._coeffs)

    def set_quadratic_coefficient(
        self, i: SupportsIndex, j: SupportsIndex, value: float
    ) -> None:
        """Set the quadratic coefficient for the term `x_i * x_j`."""
        i, j = int(i), int(j)
        if i >= len(self) or j >= len(self):
            self.resize(max(i, j) + 1)
        if value == 0:
            self._quad_coeffs.pop((i, j), None)
        else:
            self._quad_coeffs[(i, j)] = value

    def get_quadratic_coefficients(self) -> Mapping[tuple[int, int], float]:
        """Return the quadratic coefficients, keyed by variable-index pairs."""
        return MappingProxyType(self._quad_coeffs)

    def set_sense(self, sense: Sense) -> None:
        """Set the sense (`Sense.Minimize` or `Sense.Maximize`)."""
        self._sense = sense

    def get_sense(self) -> Sense:
        """Return the sense of this objective."""
        return self._sense

    def __len__(self) -> int:
        return len(self._coeffs)

    @classmethod
    def from_coefficients(
        cls,
        coefficients: LinearCoeffs = (),
        quadratic_coefficients: QCoeffs = (),
        constant: float = 0,
        sense: Sense = Sense.Minimize,
    ) -> Objective:
        """Build an `Objective` from coefficients, a constant, and a sense."""
        obj = cls()
        iter_coeffs = (
            coefficients.items()
            if isinstance(coefficients, Mapping)
            else enumerate(coefficients)
        )
        for i, coeff in iter_coeffs:
            obj.set_coefficient(i, coeff)
        iter_quadratic_coeffs = (
            quadratic_coefficients.items()
            if isinstance(quadratic_coefficients, Mapping)
            else quadratic_coefficients
        )
        for (i, j), coeff in iter_quadratic_coeffs:
            obj.set_quadratic_coefficient(i, j, coeff)

        obj.set_constant(constant)
        obj.set_sense(sense)
        return obj

__init__(size=0)

Create an empty minimizing objective with size zero coefficients.

Source code in src/ilpy/_components.py
143
144
145
146
147
148
def __init__(self, size: int = 0) -> None:
    """Create an empty minimizing objective with `size` zero coefficients."""
    self._sense = Sense.Minimize
    self._constant = 0.0
    self._coeffs: list[float] = []
    self._quad_coeffs: dict[tuple[int, int], float] = {}

from_coefficients(coefficients=(), quadratic_coefficients=(), constant=0, sense=Sense.Minimize) classmethod

Build an Objective from coefficients, a constant, and a sense.

Source code in src/ilpy/_components.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
@classmethod
def from_coefficients(
    cls,
    coefficients: LinearCoeffs = (),
    quadratic_coefficients: QCoeffs = (),
    constant: float = 0,
    sense: Sense = Sense.Minimize,
) -> Objective:
    """Build an `Objective` from coefficients, a constant, and a sense."""
    obj = cls()
    iter_coeffs = (
        coefficients.items()
        if isinstance(coefficients, Mapping)
        else enumerate(coefficients)
    )
    for i, coeff in iter_coeffs:
        obj.set_coefficient(i, coeff)
    iter_quadratic_coeffs = (
        quadratic_coefficients.items()
        if isinstance(quadratic_coefficients, Mapping)
        else quadratic_coefficients
    )
    for (i, j), coeff in iter_quadratic_coeffs:
        obj.set_quadratic_coefficient(i, j, coeff)

    obj.set_constant(constant)
    obj.set_sense(sense)
    return obj

get_coefficients()

Return a copy of the linear coefficients.

Source code in src/ilpy/_components.py
175
176
177
def get_coefficients(self) -> list[float]:
    """Return a copy of the linear coefficients."""
    return list(self._coeffs)

get_constant()

Return the constant term of the objective.

Source code in src/ilpy/_components.py
154
155
156
def get_constant(self) -> float:
    """Return the constant term of the objective."""
    return self._constant

get_quadratic_coefficients()

Return the quadratic coefficients, keyed by variable-index pairs.

Source code in src/ilpy/_components.py
194
195
196
def get_quadratic_coefficients(self) -> Mapping[tuple[int, int], float]:
    """Return the quadratic coefficients, keyed by variable-index pairs."""
    return MappingProxyType(self._quad_coeffs)

get_sense()

Return the sense of this objective.

Source code in src/ilpy/_components.py
202
203
204
def get_sense(self) -> Sense:
    """Return the sense of this objective."""
    return self._sense

resize(size)

Resize the objective function. New coefficients are set to 0.

Source code in src/ilpy/_components.py
158
159
160
161
162
163
164
165
166
def resize(self, size: int) -> None:
    """Resize the objective function. New coefficients are set to 0."""
    if size < 0:
        raise ValueError("Size must be non-negative.")
    current_size = len(self)
    if current_size < size:
        self._coeffs.extend([0] * (size - current_size))
    elif current_size > size:
        self._coeffs = self._coeffs[:size]

set_coefficient(i, value)

Set the linear coefficient of variable i, growing storage as needed.

Source code in src/ilpy/_components.py
168
169
170
171
172
173
def set_coefficient(self, i: SupportsIndex, value: float) -> None:
    """Set the linear coefficient of variable `i`, growing storage as needed."""
    i = int(i)
    if i >= len(self):
        self.resize(i + 1)
    self._coeffs[i] = value

set_constant(value)

Set the constant term added to the objective.

Source code in src/ilpy/_components.py
150
151
152
def set_constant(self, value: float) -> None:
    """Set the constant term added to the objective."""
    self._constant = value

set_quadratic_coefficient(i, j, value)

Set the quadratic coefficient for the term x_i * x_j.

Source code in src/ilpy/_components.py
182
183
184
185
186
187
188
189
190
191
192
def set_quadratic_coefficient(
    self, i: SupportsIndex, j: SupportsIndex, value: float
) -> None:
    """Set the quadratic coefficient for the term `x_i * x_j`."""
    i, j = int(i), int(j)
    if i >= len(self) or j >= len(self):
        self.resize(max(i, j) + 1)
    if value == 0:
        self._quad_coeffs.pop((i, j), None)
    else:
        self._quad_coeffs[(i, j)] = value

set_sense(sense)

Set the sense (Sense.Minimize or Sense.Maximize).

Source code in src/ilpy/_components.py
198
199
200
def set_sense(self, sense: Sense) -> None:
    """Set the sense (`Sense.Minimize` or `Sense.Maximize`)."""
    self._sense = sense

Preference

Bases: IntEnum

Preference for a solver backend.

A "full" Gurobi license means one that is not the size-limited license bundled with the gurobipy pip wheel. See https://support.gurobi.com/hc/en-us/articles/360051597492

  • Any: Use Gurobi if a full license is available, otherwise fall back to SCIP. The bundled size-limited license is treated as "no license" to avoid silent "Model too large" failures on problems with >2000 variables.
  • Scip: Use SCIP. Raises if pyscipopt is not installed.
  • Gurobi: Use Gurobi; requires a full license. Raises otherwise. Use GurobiRestricted if you only have the bundled pip license.
  • GurobiRestricted: Use Gurobi with whatever license resolves (including the bundled size-limited pip license). Suitable for small problems (<2000 variables); larger ones will fail at solve time.
Source code in src/ilpy/solver_backends/__init__.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Preference(IntEnum):
    """Preference for a solver backend.

    A "full" Gurobi license means one that is *not* the size-limited license
    bundled with the `gurobipy` pip wheel. See
    https://support.gurobi.com/hc/en-us/articles/360051597492

    - `Any`: Use Gurobi if a full license is available, otherwise fall back
      to SCIP. The bundled size-limited license is treated as "no license" to
      avoid silent "Model too large" failures on problems with >2000 variables.
    - `Scip`: Use SCIP. Raises if `pyscipopt` is not installed.
    - `Gurobi`: Use Gurobi; requires a full license. Raises otherwise. Use
      `GurobiRestricted` if you only have the bundled pip license.
    - `GurobiRestricted`: Use Gurobi with whatever license resolves
      (including the bundled size-limited pip license). Suitable for small
      problems (<2000 variables); larger ones will fail at solve time.
    """

    Any = auto()
    Scip = auto()
    Gurobi = auto()
    GurobiRestricted = auto()

Relation

Bases: IntEnum

Relation used in a linear constraint.

Attributes:

Name Type Description
Equal

Left-hand side equals the right-hand side.

GreaterEqual

Left-hand side is greater than or equal to the right-hand side.

LessEqual

Left-hand side is less than or equal to the right-hand side.

Source code in src/ilpy/_constants.py
26
27
28
29
30
31
32
33
34
class Relation(IntEnum):
    """Relation used in a linear constraint."""

    LessEqual = auto()
    """Left-hand side is less than or equal to the right-hand side."""
    Equal = auto()
    """Left-hand side equals the right-hand side."""
    GreaterEqual = auto()
    """Left-hand side is greater than or equal to the right-hand side."""

Equal = auto() class-attribute instance-attribute

Left-hand side equals the right-hand side.

GreaterEqual = auto() class-attribute instance-attribute

Left-hand side is greater than or equal to the right-hand side.

LessEqual = auto() class-attribute instance-attribute

Left-hand side is less than or equal to the right-hand side.

Sense

Bases: IntEnum

Direction of an objective function.

Attributes:

Name Type Description
Maximize

Maximize the objective.

Minimize

Minimize the objective.

Source code in src/ilpy/_constants.py
17
18
19
20
21
22
23
class Sense(IntEnum):
    """Direction of an objective function."""

    Minimize = auto()
    """Minimize the objective."""
    Maximize = auto()
    """Maximize the objective."""

Maximize = auto() class-attribute instance-attribute

Maximize the objective.

Minimize = auto() class-attribute instance-attribute

Minimize the objective.

Solution dataclass

The result of solving an optimization problem.

Attributes:

Name Type Description
variable_values Sequence[float]

The values assigned to each variable by the solver.

objective_value float

The value of the objective at the returned solution.

status SolverStatus

Normalized status reported by the solver.

time float

Wall-clock time (seconds) spent solving.

native_status Any

The backend-specific status object, for callers who need more detail than SolverStatus provides.

Methods:

Name Description
get_status

Return the solver status as a string (the enum member name).

get_value

Return the objective value at this solution.

Source code in src/ilpy/_solver.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@dataclass
class Solution:
    """The result of solving an optimization problem.

    Attributes
    ----------
    variable_values : Sequence[float]
        The values assigned to each variable by the solver.
    objective_value : float
        The value of the objective at the returned solution.
    status : SolverStatus
        Normalized status reported by the solver.
    time : float
        Wall-clock time (seconds) spent solving.
    native_status : Any
        The backend-specific status object, for callers who need more detail
        than `SolverStatus` provides.
    """

    variable_values: Sequence[float]
    objective_value: float
    status: SolverStatus
    time: float
    native_status: Any = None

    def __array__(
        self, dtype: npt.DTypeLike | None = None, copy: bool | None = None
    ) -> np.ndarray:
        import numpy as np

        return np.asarray(self.variable_values, dtype=dtype, copy=copy)

    def __iter__(self) -> Iterator[float]:
        return iter(self.variable_values)

    def get_value(self) -> float:
        """Return the objective value at this solution."""
        return self.objective_value

    def __getitem__(self, key: int) -> float:
        return self.variable_values[key]

    def __setitem__(self, key: int, value: float) -> None:
        self.variable_values[key] = value  # type: ignore

    def get_status(self) -> str:
        """Return the solver status as a string (the enum member name)."""
        return self.status.name

get_status()

Return the solver status as a string (the enum member name).

Source code in src/ilpy/_solver.py
65
66
67
def get_status(self) -> str:
    """Return the solver status as a string (the enum member name)."""
    return self.status.name

get_value()

Return the objective value at this solution.

Source code in src/ilpy/_solver.py
55
56
57
def get_value(self) -> float:
    """Return the objective value at this solution."""
    return self.objective_value

Solver

High-level wrapper around an ILP solver backend.

Methods:

Name Description
__init__

Create a solver with num_variables decision variables.

add_constraint

Add a single constraint (or an Expression convertible to one).

native_model

Return the backend's native model object (e.g. a gurobipy Model).

set_constraints

Replace the current constraint set.

set_event_callback

Set (or clear) a callback invoked on backend progress events.

set_num_threads

Set the number of threads the backend may use.

set_objective

Set the objective, converting from an Expression if needed.

set_optimality_gap

Set the optimality gap at which the solver may stop.

set_timeout

Set a wall-clock time limit (in seconds) for solving.

set_verbose

Enable or disable solver log output.

solve

Solve the problem and return a Solution.

Source code in src/ilpy/_solver.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class Solver:
    """High-level wrapper around an ILP solver backend."""

    def __init__(
        self,
        num_variables: int,
        default_variable_type: VariableType,
        variable_types: dict[int, VariableType] | None = None,
        preference: Preference = Preference.Any,
    ) -> None:
        """Create a solver with `num_variables` decision variables.

        Parameters
        ----------
        num_variables : int
            The number of decision variables in the problem.
        default_variable_type : VariableType
            The type used for variables not listed in `variable_types`.
        variable_types : dict[int, VariableType], optional
            Per-variable overrides for the default variable type.
        preference : Preference
            Backend preference.  `Preference.Any` picks the first available.
        """
        vtpes: dict[int, VariableType] = dict(variable_types) if variable_types else {}
        self._backend: SolverBackend = create_solver_backend(preference)
        self._num_variables = num_variables
        self._backend.initialize(num_variables, default_variable_type, vtpes)

    def set_objective(self, objective: Objective | Expression) -> None:
        """Set the objective, converting from an `Expression` if needed."""
        if isinstance(objective, Expression):
            objective = objective.as_objective()
        self._backend.set_objective(objective)

    def set_constraints(self, constraints: Constraints) -> None:
        """Replace the current constraint set."""
        self._backend.set_constraints(constraints)

    def add_constraint(self, constraint: Constraint | Expression) -> None:
        """Add a single constraint (or an `Expression` convertible to one)."""
        if isinstance(constraint, Expression):
            constraint = constraint.as_constraint()
        self._backend.add_constraint(constraint)

    def set_timeout(self, timeout: float) -> None:
        """Set a wall-clock time limit (in seconds) for solving."""
        self._backend.set_timeout(timeout)

    def set_optimality_gap(self, gap: float, absolute: bool = False) -> None:
        """Set the optimality gap at which the solver may stop.

        If `absolute` is True, `gap` is the absolute gap; otherwise it is
        interpreted as a relative gap.
        """
        self._backend.set_optimality_gap(gap, absolute)

    def set_num_threads(self, num_threads: int) -> None:
        """Set the number of threads the backend may use."""
        self._backend.set_num_threads(num_threads)

    def set_verbose(self, verbose: bool) -> None:
        """Enable or disable solver log output."""
        self._backend.set_verbose(verbose)

    def set_event_callback(self, callback: Callable[[EventData], None] | None) -> None:
        """Set (or clear) a callback invoked on backend progress events."""
        self._backend.set_event_callback(callback)

    def solve(self) -> Solution:
        """Solve the problem and return a `Solution`."""
        return self._backend.solve()

    def native_model(self) -> Any:
        """Return the backend's native model object (e.g. a gurobipy Model)."""
        return self._backend.native_model()

__init__(num_variables, default_variable_type, variable_types=None, preference=Preference.Any)

Create a solver with num_variables decision variables.

Parameters:

Name Type Description Default
num_variables int

The number of decision variables in the problem.

required
default_variable_type VariableType

The type used for variables not listed in variable_types.

required
variable_types dict[int, VariableType]

Per-variable overrides for the default variable type.

None
preference Preference

Backend preference. Preference.Any picks the first available.

Any
Source code in src/ilpy/_solver.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __init__(
    self,
    num_variables: int,
    default_variable_type: VariableType,
    variable_types: dict[int, VariableType] | None = None,
    preference: Preference = Preference.Any,
) -> None:
    """Create a solver with `num_variables` decision variables.

    Parameters
    ----------
    num_variables : int
        The number of decision variables in the problem.
    default_variable_type : VariableType
        The type used for variables not listed in `variable_types`.
    variable_types : dict[int, VariableType], optional
        Per-variable overrides for the default variable type.
    preference : Preference
        Backend preference.  `Preference.Any` picks the first available.
    """
    vtpes: dict[int, VariableType] = dict(variable_types) if variable_types else {}
    self._backend: SolverBackend = create_solver_backend(preference)
    self._num_variables = num_variables
    self._backend.initialize(num_variables, default_variable_type, vtpes)

add_constraint(constraint)

Add a single constraint (or an Expression convertible to one).

Source code in src/ilpy/_solver.py
108
109
110
111
112
def add_constraint(self, constraint: Constraint | Expression) -> None:
    """Add a single constraint (or an `Expression` convertible to one)."""
    if isinstance(constraint, Expression):
        constraint = constraint.as_constraint()
    self._backend.add_constraint(constraint)

native_model()

Return the backend's native model object (e.g. a gurobipy Model).

Source code in src/ilpy/_solver.py
142
143
144
def native_model(self) -> Any:
    """Return the backend's native model object (e.g. a gurobipy Model)."""
    return self._backend.native_model()

set_constraints(constraints)

Replace the current constraint set.

Source code in src/ilpy/_solver.py
104
105
106
def set_constraints(self, constraints: Constraints) -> None:
    """Replace the current constraint set."""
    self._backend.set_constraints(constraints)

set_event_callback(callback)

Set (or clear) a callback invoked on backend progress events.

Source code in src/ilpy/_solver.py
134
135
136
def set_event_callback(self, callback: Callable[[EventData], None] | None) -> None:
    """Set (or clear) a callback invoked on backend progress events."""
    self._backend.set_event_callback(callback)

set_num_threads(num_threads)

Set the number of threads the backend may use.

Source code in src/ilpy/_solver.py
126
127
128
def set_num_threads(self, num_threads: int) -> None:
    """Set the number of threads the backend may use."""
    self._backend.set_num_threads(num_threads)

set_objective(objective)

Set the objective, converting from an Expression if needed.

Source code in src/ilpy/_solver.py
 98
 99
100
101
102
def set_objective(self, objective: Objective | Expression) -> None:
    """Set the objective, converting from an `Expression` if needed."""
    if isinstance(objective, Expression):
        objective = objective.as_objective()
    self._backend.set_objective(objective)

set_optimality_gap(gap, absolute=False)

Set the optimality gap at which the solver may stop.

If absolute is True, gap is the absolute gap; otherwise it is interpreted as a relative gap.

Source code in src/ilpy/_solver.py
118
119
120
121
122
123
124
def set_optimality_gap(self, gap: float, absolute: bool = False) -> None:
    """Set the optimality gap at which the solver may stop.

    If `absolute` is True, `gap` is the absolute gap; otherwise it is
    interpreted as a relative gap.
    """
    self._backend.set_optimality_gap(gap, absolute)

set_timeout(timeout)

Set a wall-clock time limit (in seconds) for solving.

Source code in src/ilpy/_solver.py
114
115
116
def set_timeout(self, timeout: float) -> None:
    """Set a wall-clock time limit (in seconds) for solving."""
    self._backend.set_timeout(timeout)

set_verbose(verbose)

Enable or disable solver log output.

Source code in src/ilpy/_solver.py
130
131
132
def set_verbose(self, verbose: bool) -> None:
    """Enable or disable solver log output."""
    self._backend.set_verbose(verbose)

solve()

Solve the problem and return a Solution.

Source code in src/ilpy/_solver.py
138
139
140
def solve(self) -> Solution:
    """Solve the problem and return a `Solution`."""
    return self._backend.solve()

SolverBackend

Bases: ABC

Abstract base class implemented by each concrete solver backend.

Methods:

Name Description
add_constraint

Add a single constraint to the problem.

emit_event_data

Dispatch data to the registered event callback (no-op if none).

initialize

Initialize the backend with decision variables and their types.

native_model

Return the underlying native model object for this backend.

set_constraints

Replace the backend's current constraint set.

set_event_callback

Set (or clear) a callback invoked on solver progress events.

set_num_threads

Set the number of threads the backend may use.

set_objective

Set the objective function for the problem.

set_optimality_gap

Set the optimality gap (absolute or relative) for early termination.

set_timeout

Set the wall-clock time limit (in seconds) for solving.

set_verbose

Enable or disable backend log output.

solve

Solve the problem and return a Solution.

Source code in src/ilpy/solver_backends/_base.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class SolverBackend(ABC):
    """Abstract base class implemented by each concrete solver backend."""

    def __init__(self) -> None:
        self._event_callback: Callable[[EventData], None] | None = None

    def set_event_callback(self, callback: Callable[[EventData], None] | None) -> None:
        """Set (or clear) a callback invoked on solver progress events."""
        self._event_callback = callback

    def emit_event_data(self, data: EventData) -> None:
        """Dispatch `data` to the registered event callback (no-op if none)."""
        if self._event_callback:
            self._event_callback(data)

    @abstractmethod
    def initialize(
        self,
        num_variables: int,
        default_variable_type: VariableType,
        variable_types: Mapping[int, VariableType],
    ) -> None:
        """Initialize the backend with decision variables and their types."""

    @abstractmethod
    def set_objective(self, objective: Objective) -> None:
        """Set the objective function for the problem."""

    @abstractmethod
    def set_constraints(self, constraints: Constraints) -> None:
        """Replace the backend's current constraint set."""

    @abstractmethod
    def add_constraint(self, constraint: Constraint) -> None:
        """Add a single constraint to the problem."""

    @abstractmethod
    def set_timeout(self, timeout: float) -> None:
        """Set the wall-clock time limit (in seconds) for solving."""

    @abstractmethod
    def set_optimality_gap(self, gap: float, absolute: bool) -> None:
        """Set the optimality gap (absolute or relative) for early termination."""

    @abstractmethod
    def set_num_threads(self, num_threads: int) -> None:
        """Set the number of threads the backend may use."""

    @abstractmethod
    def set_verbose(self, verbose: bool) -> None:
        """Enable or disable backend log output."""

    @abstractmethod
    def solve(self) -> Solution:
        """Solve the problem and return a `Solution`."""

    @abstractmethod
    def native_model(self) -> Any:
        """Return the underlying native model object for this backend."""

add_constraint(constraint) abstractmethod

Add a single constraint to the problem.

Source code in src/ilpy/solver_backends/_base.py
47
48
49
@abstractmethod
def add_constraint(self, constraint: Constraint) -> None:
    """Add a single constraint to the problem."""

emit_event_data(data)

Dispatch data to the registered event callback (no-op if none).

Source code in src/ilpy/solver_backends/_base.py
25
26
27
28
def emit_event_data(self, data: EventData) -> None:
    """Dispatch `data` to the registered event callback (no-op if none)."""
    if self._event_callback:
        self._event_callback(data)

initialize(num_variables, default_variable_type, variable_types) abstractmethod

Initialize the backend with decision variables and their types.

Source code in src/ilpy/solver_backends/_base.py
30
31
32
33
34
35
36
37
@abstractmethod
def initialize(
    self,
    num_variables: int,
    default_variable_type: VariableType,
    variable_types: Mapping[int, VariableType],
) -> None:
    """Initialize the backend with decision variables and their types."""

native_model() abstractmethod

Return the underlying native model object for this backend.

Source code in src/ilpy/solver_backends/_base.py
71
72
73
@abstractmethod
def native_model(self) -> Any:
    """Return the underlying native model object for this backend."""

set_constraints(constraints) abstractmethod

Replace the backend's current constraint set.

Source code in src/ilpy/solver_backends/_base.py
43
44
45
@abstractmethod
def set_constraints(self, constraints: Constraints) -> None:
    """Replace the backend's current constraint set."""

set_event_callback(callback)

Set (or clear) a callback invoked on solver progress events.

Source code in src/ilpy/solver_backends/_base.py
21
22
23
def set_event_callback(self, callback: Callable[[EventData], None] | None) -> None:
    """Set (or clear) a callback invoked on solver progress events."""
    self._event_callback = callback

set_num_threads(num_threads) abstractmethod

Set the number of threads the backend may use.

Source code in src/ilpy/solver_backends/_base.py
59
60
61
@abstractmethod
def set_num_threads(self, num_threads: int) -> None:
    """Set the number of threads the backend may use."""

set_objective(objective) abstractmethod

Set the objective function for the problem.

Source code in src/ilpy/solver_backends/_base.py
39
40
41
@abstractmethod
def set_objective(self, objective: Objective) -> None:
    """Set the objective function for the problem."""

set_optimality_gap(gap, absolute) abstractmethod

Set the optimality gap (absolute or relative) for early termination.

Source code in src/ilpy/solver_backends/_base.py
55
56
57
@abstractmethod
def set_optimality_gap(self, gap: float, absolute: bool) -> None:
    """Set the optimality gap (absolute or relative) for early termination."""

set_timeout(timeout) abstractmethod

Set the wall-clock time limit (in seconds) for solving.

Source code in src/ilpy/solver_backends/_base.py
51
52
53
@abstractmethod
def set_timeout(self, timeout: float) -> None:
    """Set the wall-clock time limit (in seconds) for solving."""

set_verbose(verbose) abstractmethod

Enable or disable backend log output.

Source code in src/ilpy/solver_backends/_base.py
63
64
65
@abstractmethod
def set_verbose(self, verbose: bool) -> None:
    """Enable or disable backend log output."""

solve() abstractmethod

Solve the problem and return a Solution.

Source code in src/ilpy/solver_backends/_base.py
67
68
69
@abstractmethod
def solve(self) -> Solution:
    """Solve the problem and return a `Solution`."""

SolverStatus

Bases: Enum

Normalized solver status across backends.

Attributes:

Name Type Description
INFEASIBLE

Model proven infeasible.

INF_OR_UNBOUNDED

Model infeasible or unbounded.

NODELIMIT

Node limit reached before proving optimality.

NUMERIC

Numerical issues encountered by the solver.

OPTIMAL

Optimal solution found.

OTHER

Status not otherwise classified.

SOLUTIONLIMIT

Solution limit reached.

SUBOPTIMAL

A feasible but not provably optimal solution was returned.

TIMELIMIT

Time limit reached before proving optimality.

UNBOUNDED

Model proven unbounded.

UNKNOWN

Model loaded but not optimized yet.

USERINTERRUPT

Optimization was interrupted by the user.

Source code in src/ilpy/_constants.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class SolverStatus(Enum):
    """Normalized solver status across backends."""

    UNKNOWN = "unknown"
    """Model loaded but not optimized yet."""
    OPTIMAL = "optimal"
    """Optimal solution found."""
    INFEASIBLE = "infeasible"
    """Model proven infeasible."""
    UNBOUNDED = "unbounded"
    """Model proven unbounded."""
    INF_OR_UNBOUNDED = "infeasible_or_unbounded"
    """Model infeasible or unbounded."""
    TIMELIMIT = "time_limit"
    """Time limit reached before proving optimality."""
    NODELIMIT = "node_limit"
    """Node limit reached before proving optimality."""
    SOLUTIONLIMIT = "solution_limit"
    """Solution limit reached."""
    USERINTERRUPT = "user_interrupt"
    """Optimization was interrupted by the user."""
    NUMERIC = "numeric_issue"
    """Numerical issues encountered by the solver."""
    SUBOPTIMAL = "suboptimal"
    """A feasible but not provably optimal solution was returned."""
    OTHER = "other"
    """Status not otherwise classified."""

INFEASIBLE = 'infeasible' class-attribute instance-attribute

Model proven infeasible.

INF_OR_UNBOUNDED = 'infeasible_or_unbounded' class-attribute instance-attribute

Model infeasible or unbounded.

NODELIMIT = 'node_limit' class-attribute instance-attribute

Node limit reached before proving optimality.

NUMERIC = 'numeric_issue' class-attribute instance-attribute

Numerical issues encountered by the solver.

OPTIMAL = 'optimal' class-attribute instance-attribute

Optimal solution found.

OTHER = 'other' class-attribute instance-attribute

Status not otherwise classified.

SOLUTIONLIMIT = 'solution_limit' class-attribute instance-attribute

Solution limit reached.

SUBOPTIMAL = 'suboptimal' class-attribute instance-attribute

A feasible but not provably optimal solution was returned.

TIMELIMIT = 'time_limit' class-attribute instance-attribute

Time limit reached before proving optimality.

UNBOUNDED = 'unbounded' class-attribute instance-attribute

Model proven unbounded.

UNKNOWN = 'unknown' class-attribute instance-attribute

Model loaded but not optimized yet.

USERINTERRUPT = 'user_interrupt' class-attribute instance-attribute

Optimization was interrupted by the user.

Variable

Bases: Expression, Name

A variable.

id holds the index as a string (because ast.Name requires a string).

The special attribute index is added here for the purpose of storing the index of a variable in a solver's variable list: Variable('u', index=0)

Source code in src/ilpy/expressions.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
class Variable(Expression, ast.Name):
    """A variable.

    `id` holds the index as a string (because ast.Name requires a string).

    The special attribute `index` is added here for the purpose of storing
    the index of a variable in a solver's variable list: `Variable('u', index=0)`
    """

    def __init__(self, id: str, index: int | None = None) -> None:
        self.index = index
        super().__init__(str(id), ctx=ast.Load())

    def __pow__(self, other: Number) -> Expression:
        if not isinstance(other, (int, float)):  # pragma: no cover
            raise TypeError("Exponent must be a number")
        if other == 2:
            return BinOp(self, ast.Mult(), self)
        elif other == 1:
            return self
        raise ValueError("Only quadratic variables are supported")  # pragma: no cover

    def __hash__(self) -> int:
        # allow use as dict key
        return id(self)

    def __int__(self) -> int:
        if self.index is None:  # pragma: no cover
            raise TypeError(f"Variable {self!r} has no index")
        return int(self.index)

    __index__ = __int__

    def __repr__(self) -> str:
        return f"ilpy.Variable({self.id!r}, index={self.index!r})"

VariableType

Bases: IntEnum

Type of a decision variable in an optimization problem.

Attributes:

Name Type Description
Binary

A variable restricted to 0 or 1.

Continuous

A real-valued variable.

Integer

An integer-valued variable.

Source code in src/ilpy/_constants.py
 6
 7
 8
 9
10
11
12
13
14
class VariableType(IntEnum):
    """Type of a decision variable in an optimization problem."""

    Continuous = auto()
    """A real-valued variable."""
    Integer = auto()
    """An integer-valued variable."""
    Binary = auto()
    """A variable restricted to 0 or 1."""

Binary = auto() class-attribute instance-attribute

A variable restricted to 0 or 1.

Continuous = auto() class-attribute instance-attribute

A real-valued variable.

Integer = auto() class-attribute instance-attribute

An integer-valued variable.

solve(objective, constraints, sense=Sense.Minimize, variable_type=VariableType.Continuous, verbose=False, preference=Preference.Any, on_event=None)

Solve an objective subject to constraints.

This is a functional interface to the solver. It creates a solver instance and sets the objective and constraints, then solves the problem and returns the solution.

Parameters:

Name Type Description Default
objective Sequence[float] | Expression | Objective

The objective to solve. If a sequence of floats is provided, it is interpreted as the coefficients of the objective. For example, the objective 2x + 3y would be provided as [2, 3]. Alternatively, an ilpy.Expression or ilpy.Objective can be provided.

required
constraints Iterable[ConstraintTuple | Expression | Constraint]

The constraints to satisfy. May be provided as a sequence of Expression or Constraint objects, or as a sequence of tuples of the form (coefficients, relation, value), where coefficients is a sequence of floats, relation is an ilpy.Relation or a string in {"<=", ">=", "="}, and value is a float. For example, the constraint 2x + 3y <= 5 would be provided as ([2, 3], Relation.LessEqual, 5).

required
sense Sense | Literal['minimize', 'maximize']

The sense of the objective, either Sense.Minimize or Sense.Maximize. Alternatively, a string in {"minimize", "maximize"} can be provided. By default, Sense.Minimize.

Minimize
variable_type VariableType | Literal['continuous', 'binary', 'integer']

The type of the variables, either an ilpy.VariableType, or a string in {"continuous", "binary", "integer"}. By default, VariableType.Continuous.

Continuous
verbose bool

Whether to print the solver output, by default False.

False
preference Preference | Literal['any', 'cplex', 'gurobi', 'scip']

Backend preference, either an ilpy.Preference or a string in {"any", "cplex", "gurobi", "scip"}. By default, Preference.Any.

Any
on_event Callable[[EventData], None]

A callback function that is called when an event occurs, by default None. The callback function should accept a dict which will contain statics about the solving or presolving process. You can import ilpy.EventData from ilpy and use it to provide dict key hints in your IDE, but EventData is not available at runtime. See SCIP and Gurobi documentation for details what each value means.

For example

import ilpy

if TYPE_CHECKING:
    from ilpy import EventData


def callback(data: EventData) -> None:
    if data["backend"] == "gurobi" and data["event_type"] == "MIP":
        print(data["gap"])


ilpy.solve(..., on_event=callback)
None

Returns:

Type Description
Solution

The solution to the problem.

Source code in src/ilpy/_functional.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def solve(
    objective: Sequence[float] | Expression | Objective,
    constraints: Iterable[ConstraintTuple | Expression | Constraint],
    sense: SenseType = Sense.Minimize,
    variable_type: VariableTypeType = VariableType.Continuous,
    verbose: bool = False,
    preference: PreferenceType = Preference.Any,
    on_event: Callable[[EventData], None] | None = None,
) -> Solution:
    """Solve an objective subject to constraints.

    This is a functional interface to the solver. It creates a solver instance
    and sets the objective and constraints, then solves the problem and returns
    the solution.

    Parameters
    ----------
    objective : Sequence[float] | Expression | Objective
        The objective to solve.  If a sequence of floats is provided, it is
        interpreted as the coefficients of the objective. For example, the objective
        2x + 3y would be provided as [2, 3].
        Alternatively, an [`ilpy.Expression`][] or [`ilpy.Objective`][] can be provided.
    constraints : Iterable[ConstraintTuple  |  Expression  |  Constraint]
        The constraints to satisfy.  May be provided as a sequence of Expression or
        Constraint objects, or as a sequence of tuples of the form
        (coefficients, relation, value), where coefficients is a sequence of floats,
        relation is an [`ilpy.Relation`][] or a string in {"<=", ">=", "="}, and value
        is a float.  For example, the constraint 2x + 3y <= 5 would be provided as
        `([2, 3], Relation.LessEqual, 5)`.
    sense : Sense | Literal["minimize", "maximize"]
        The sense of the objective, either `Sense.Minimize` or `Sense.Maximize`.
        Alternatively, a string in {"minimize", "maximize"} can be provided.
        By default, `Sense.Minimize`.
    variable_type : VariableType | Literal["continuous", "binary", "integer"]
        The type of the variables, either an [`ilpy.VariableType`][], or a string in
        {"continuous", "binary", "integer"}.  By default, `VariableType.Continuous`.
    verbose : bool, optional
        Whether to print the solver output, by default `False`.
    preference : Preference | Literal["any", "cplex", "gurobi", "scip"]
        Backend preference, either an [`ilpy.Preference`][] or a string in
        {"any", "cplex", "gurobi", "scip"}.  By default, `Preference.Any`.
    on_event : Callable[[EventData], None], optional
        A callback function that is called when an event occurs, by default None. The
        callback function should accept a dict which will contain statics about the
        solving or presolving process. You can import `ilpy.EventData` from ilpy and use
        it to provide dict key hints in your IDE, but `EventData` is not available at
        runtime. See SCIP and Gurobi documentation for details what each value means.

        For example

        ```python
        import ilpy

        if TYPE_CHECKING:
            from ilpy import EventData


        def callback(data: EventData) -> None:
            if data["backend"] == "gurobi" and data["event_type"] == "MIP":
                print(data["gap"])


        ilpy.solve(..., on_event=callback)
        ```

    Returns
    -------
    Solution
        The solution to the problem.
    """
    if isinstance(sense, str):
        sense = Sense[sense.title()]
    if isinstance(variable_type, str):
        variable_type = VariableType[variable_type.title()]
    if isinstance(preference, str):
        preference = Preference[preference.title()]

    if isinstance(objective, Expression):
        obj = objective.as_objective(sense)
    elif isinstance(objective, Objective):
        obj = objective
    else:
        obj = Objective.from_coefficients(coefficients=objective, sense=sense)

    solver = Solver(len(obj), variable_type, preference=preference)
    solver.set_verbose(verbose)
    solver.set_objective(obj)

    for constraint in constraints:
        if isinstance(constraint, Expression):
            const = constraint.as_constraint()
        elif isinstance(constraint, Constraint):
            const = constraint
        else:
            coeff, relation, value = constraint
            const = Constraint.from_coefficients(
                coefficients=coeff, relation=_op_map[relation], value=value
            )
        solver.add_constraint(const)

    solver.set_event_callback(on_event)
    solution = solver.solve()
    return solution