jijpresolve

JijPresolve: OMMX-based presolvers for optimization problems.

This module provides presolving tools for optimization problems with automatic OpenTelemetry tracing support. When a TracerProvider is configured, Rust-side tracing spans are automatically connected to Python’s tracing infrastructure.

Tracing is lazily initialized when the first presolve function is called, automatically detecting Python’s TracerProvider configuration.

Submodules

Classes

Instance

The instance for JijPresolve, a wrapper of ommx::v1::Instance in OMMX Rust SDK rather than Python SDK.

Functions

milp_presolve(→ tuple[Any, dict])

Execute MILP presolve using PaPILO library and return the presolved instance with statistics.

presolve(→ Any)

Execute presolve in a preset order.

Package Contents

class jijpresolve.Instance

The instance for JijPresolve, a wrapper of ommx::v1::Instance in OMMX Rust SDK rather than Python SDK.

You need to convert OMMX Python SDK’s ommx.v1.Instance to this class jijpresolve.Instance for executing presolve.

Example

We here demonstrate the convert_single_linear_inequality_into_bounds() method to show how to use this class. It converts \(a x + b \leq 0\) formed constraints (we call it ‘single linear’) into the upper (\(x \leq -b/a\)) or lower (\(x \geq -b/a\)) bound of \(x\) according to the sign of \(a\).

First, import required modules.

>>> import jijpresolve
>>> from ommx.v1 import DecisionVariable, Instance, Function, Bound

Create an OMMX Python SDK’s instance.

>>> x = [DecisionVariable.continuous(i, lower=0.0, upper=3.0) for i in range(3)]
>>> ommx_instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints=[
...         (2 * x[0] <= 1).set_id(0),  # single linear inequality
...         (x[0] + x[1] + x[2] <= 1).set_id(1),
...     ],
...     sense=Instance.MAXIMIZE,
... )

Convert the OMMX Python SDK’s instance to jijpresolve’s instance.

>>> presolve_instance = jijpresolve.Instance(ommx_instance)

Execute presolve.

>>> presolve_instance.convert_single_linear_inequality_into_bounds()

Restore the OMMX Python SDK’s instance.

>>> instance: Instance = presolve_instance.to_ommx()
>>> instance.decision_variables_df.dropna(axis=1, how="all")
          kind  lower  upper subscripts
id
0   Continuous    0.0    0.5         []
1   Continuous    0.0    3.0         []
2   Continuous    0.0    3.0         []
>>> instance.constraints_df.dropna(axis=1, how="all")
   equality    type   used_ids subscripts
id
1       <=0  Linear  {0, 1, 2}         []

The single linear inequality \(2x_0 \leq 1\) is converted to the upper bound \(x_0 \in [0, 0.5]\).

>>> print(instance.removed_constraints_df[["removed_reason", "removed_reason.variable_id"]].to_string(max_colwidth=100))
                                 removed_reason  removed_reason.variable_id
id
0   convert_single_linear_inequality_into_bounds                          0

The converted constraint is stored in the removed_constraints field. removed_reason.variable_id contains the variable id whose bound is applied to.

to_ommx() Any

Convert to OMMX Python SDK’s ommx.v1.Instance.

static from_bytes(bytes: Instance.from_bytes.bytes) Instance

Decode from the protobuf-encoded OMMX Message. You should use __init__() in most cases.

to_bytes() bytes

Encode to the protobuf-encoded OMMX Message. You should use to_ommx() in most cases.

convert_single_linear_equality_into_fixed_variables(atol: float = 1e-06) None

Convert single linear equality \(a x + b = 0\) into fixed variable \(x = -b/a\).

Example

Ready to use the OMMX Python SDK’s instance.

>>> x = [DecisionVariable.continuous(i, lower=0.0, upper=3.0) for i in range(3)]
>>> ommx_instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints=[
...         (2 * x[0] == 1).set_id(0),  # single linear equality
...         (x[0] + x[1] + x[2] <= 1).set_id(1),
...     ],
...     sense=Instance.MAXIMIZE,
... )

Call this method

>>> presolve_instance = jijpresolve.Instance(ommx_instance)
>>> presolve_instance.convert_single_linear_equality_into_fixed_variables()
>>> instance: Instance = presolve_instance.to_ommx()

The fixed value is stored in the substituted_value field of the decision variable.

>>> instance.decision_variables_df.dropna(axis=1, how="all")
          kind  lower  upper subscripts substituted_value
id
0   Continuous    0.0    3.0         []               0.5
1   Continuous    0.0    3.0         []              <NA>
2   Continuous    0.0    3.0         []              <NA>

The fixed variable \(x_0 = 0.5\) is substituted to objective and constraints.

>>> instance.objective
Function(x1 + x2 + 0.5)
>>> instance.get_constraint_by_id(1)
Constraint(x1 + x2 - 0.5 <= 0)

The removed constraint is stored in the removed_constraints field.

>>> print(instance.removed_constraints_df[["removed_reason", "removed_reason.variable_id"]].to_string(max_colwidth=100))
                                        removed_reason removed_reason.variable_id
id
0  convert_single_linear_equality_into_fixed_variables                          0

The removed_reason.variable_id contains the variable id which has been fixed via this equality.

convert_single_linear_inequality_into_bounds(atol: float = 1e-06) None

Converts single linear inequality \(a x + b \leq 0\) into the upper (\(x \leq -b/a\)) or lower (\(x \geq -b/a\)) bound of \(x\) according to the sign of \(a\).

See the example in this class Instance for more details.

find_fixed_variables_from_bounds(atol: float = 1e-06) None

Find fixed variables from degenerated bounds \([a, a]\).

This also truncates the bounds of integer variables \([a, b]\) to \([\lceil a \rceil, \lfloor b \rfloor]\), and then check degeneration.

Example

Ready to use the OMMX Python SDK’s instance.

>>> x = [
...     DecisionVariable.continuous(0, lower=3.0, upper=3.0),  # fixed to 3
...     DecisionVariable.integer(1, lower=0.9, upper=1.1),  # Rounded to [1, 1], and fixed to 1
...     DecisionVariable.integer(2, lower=0.1, upper=3.1),  # Rounded to [1, 3]
... ]
>>> ommx_instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints=[
...         (x[0] + x[1] + x[2] <= 1).set_id(0),
...     ],
...     sense=Instance.MAXIMIZE,
... )

Call this method

>>> presolve_instance = jijpresolve.Instance(ommx_instance)
>>> presolve_instance.find_fixed_variables_from_bounds()
>>> instance: Instance = presolve_instance.to_ommx()

The bounds for the integer variable are rounded to integers, and degenerate bounds are fixed.

>>> instance.decision_variables_df.dropna(axis=1, how="all")
          kind  lower  upper subscripts substituted_value
id
0   Continuous    3.0    3.0         []               3.0
1      Integer    1.0    1.0         []               1.0
2      Integer    1.0    3.0         []              <NA>

The fixed variables are substituted to objective and constraints.

>>> instance.objective
Function(x2 + 4)
>>> instance.get_constraint_by_id(0)
Constraint(x2 + 3 <= 0)

This does not remove any constraints.

evaluate_constraint_bounds(atol: float = 1e-06) None

Evaluate the bound of \(f(x)\) for the constraints \(f(x) \leq 0\) and \(f(x) = 0\) to remove trivial constraints and detect infeasibility.

Example

Ready to use the OMMX Python SDK’s instance.

>>> x = [DecisionVariable.continuous(i, lower=0.0, upper=2.0) for i in range(3)]
>>> ommx_instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints=[
...         (x[0] + x[1] >= 0).set_id(0),  # Trivial inequality
...         (x[0] + x[1] + x[2] <= 6).set_id(1),  # Trivial inequality
...         (x[0] + x[2] <= 0).set_id(2),  # Convert to equality
...     ],
...     sense=Instance.MAXIMIZE,
... )

Call this method

>>> presolve_instance = jijpresolve.Instance(ommx_instance)
>>> presolve_instance.evaluate_constraint_bounds(1e-7)
>>> instance: Instance = presolve_instance.to_ommx()

The trivial constraints are removed, and inequality \(f(x) \leq 0\) with evaluated bound \(f(x) \geq 0\) is converted to equality \(f(x) = 0\).

>>> instance.constraints_df.dropna(axis=1, how="all")
   equality    type used_ids subscripts
id
2        =0  Linear   {0, 2}         []
>>> instance.removed_constraints_df.dropna(axis=1, how="all")
   equality    type   used_ids subscripts              removed_reason
id
0       <=0  Linear     {0, 1}         []  evaluate_constraint_bounds
1       <=0  Linear  {0, 1, 2}         []  evaluate_constraint_bounds
Infeasible detection

Let’s create an infeasible inequality \(x_0 + x_1 \leq -1\) for \(x_0, x_1 \in [0, 2]\).

>>> x = [DecisionVariable.continuous(i, lower=0.0, upper=2.0) for i in range(3)]
>>> ommx_instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints=[(x[0] + x[1] <= -1).set_id(0)], # x[0] + x[1] must be positive
...     sense=Instance.MAXIMIZE,
... )

When infeasible, this method raises a RuntimeError.

>>> presolve_instance = jijpresolve.Instance(ommx_instance)
>>> presolve_instance.evaluate_constraint_bounds(1e-7)
Traceback (most recent call last):
...
RuntimeError: The bound of `f(x)` in the inequality `f(x) <= 0` (ID=0) is inconsistent: Bound[1, 5]

Similarly, for equality \(f(x) = 0\) the bound of \(f(x)\) does not contains \(0\), this raises a RuntimeError.

milp_presolve(dual_reductions: str = 'strong', threads: int = 1, randomseed: int = 0) dict

Perform MILP presolve using PaPILO library and modify the instance in place.

This method applies a comprehensive set of presolve techniques to the MILP problem, including domain propagation, constraint aggregation, and variable elimination.

For detailed parameter descriptions, examples, and usage patterns, see milp_presolve().

jijpresolve.milp_presolve(instance: Any, dual_reductions: str = 'strong', threads: int = 1, randomseed: int = 0) tuple[Any, dict]

Execute MILP presolve using PaPILO library and return the presolved instance with statistics.

This function applies a comprehensive set of presolve techniques to the MILP problem, including domain propagation, constraint aggregation, and variable elimination. Unlike Instance.milp_presolve() which modifies the instance in place, this function returns a new presolved instance along with statistics.

Parameters:

  • instance (ommx.v1.Instance): The OMMX instance to presolve

  • dual_reductions (str, optional): Level of dual reductions to apply. Must be one of following:

    • disable: Disable all dual reductions

    • weak: Preserving the set of optimal solutions

    • strong: Preserving the optimal objective value (default)

  • threads (int, optional): Number of threads to use for presolving (default: 1)

    • 0: Automatic detection

    • 1: Single-threaded (default)

    • N > 1: Use N threads for parallel presolving

  • randomseed (int, optional): Random seed value for reproducibility (default: 0)

Returns:

tuple[ommx.v1.Instance, dict]: A tuple containing:

  • Presolved OMMX instance

  • Statistics dictionary with keys such as: presolve_time, rounds, deleted_columns, deleted_rows, and more

Example

Basic usage with a simple instance

>>> from ommx.v1 import DecisionVariable, Instance
>>> import jijpresolve
>>> x = [DecisionVariable.integer(i, lower=0, upper=10) for i in range(3)]
>>> ommx_instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints=[
...         (x[0] + x[1] + x[2] <= 5).set_id(0),
...     ],
...     sense=Instance.MAXIMIZE,
... )
>>> presolved_instance, stats = jijpresolve.milp_presolve(ommx_instance)
>>> stats['rounds']
2
>>> stats['deleted_columns']
3

The original instance is not modified:

>>> ommx_instance.stats()['decision_variables']['by_usage']['fixed']
0
>>> presolved_instance.stats()['decision_variables']['by_usage']['fixed']
3

Using MIPLIB instance

>>> import jijpresolve
>>> from ommx import dataset
>>> ommx_instance = dataset.miplib2017("g503inf")
>>> presolved_instance, stats = jijpresolve.milp_presolve(ommx_instance)
>>> stats['rounds']
7
>>> stats['deleted_columns']
10
>>> stats['deleted_rows']
6

Compare before and after:

>>> ommx_instance.stats()['decision_variables']['total']
48
>>> ommx_instance.stats()['constraints']['active']
41
>>> presolved_instance.stats()['constraints']['active']
35
>>> presolved_instance.stats()['constraints']['removed']
6
>>> presolved_instance.stats()['decision_variables']['total']
48
>>> presolved_instance.stats()['decision_variables']['by_usage']['fixed']
8

This demonstrates the effectiveness of MILP presolve on larger problem instances.

jijpresolve.presolve(instance: Any, atol: float = 1e-07) Any

Execute presolve in a preset order.

Currently this function executes the following presolve steps:

Important

The algorithm of this function is not guaranteed to be stable. This function is designed as a better choice for users who do not want to handle the details of presolve and are open to algorithm improvements.

Example

Ready the OMMX Python SDK’s instance.

>>> from ommx.v1 import DecisionVariable, Instance
>>> x = [DecisionVariable.continuous(i, lower=0.0, upper=3.0) for i in range(3)]
>>> ommx_instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints=[
...         (2 * x[0] <= 1).set_id(0),
...         (x[0] + x[1] + x[2] <= 1).set_id(1),
...     ],
...     sense=Instance.MAXIMIZE,
... )

Call this method

>>> import jijpresolve
>>> presolved_instance = jijpresolve.presolve(ommx_instance)

The input instance is not modified, and a new presolved instance is returned.

>>> presolved_instance.decision_variables_df.dropna(axis=1, how="all")
          kind  lower  upper subscripts
id
0   Continuous    0.0    0.5         []
1   Continuous    0.0    3.0         []
2   Continuous    0.0    3.0         []
>>> presolved_instance.constraints_df.dropna(axis=1, how="all")
   equality    type   used_ids subscripts
id
1       <=0  Linear  {0, 1, 2}         []

The single linear inequality \(2x_0 \leq 1\) is converted to the upper bound \(x_0 \in [0, 0.5]\) by convert_single_linear_inequality_into_bounds().