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
The instance for JijPresolve, a wrapper of |
Functions
|
Execute MILP presolve using PaPILO library and return the presolved instance with statistics. |
|
Execute presolve in a preset order. |
Package Contents
- class jijpresolve.Instance
The instance for JijPresolve, a wrapper of
ommx::v1::Instancein OMMX Rust SDK rather than Python SDK.You need to convert OMMX Python SDK’s
ommx.v1.Instanceto this classjijpresolve.Instancefor 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_constraintsfield.removed_reason.variable_idcontains 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_valuefield 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_constraintsfield.>>> 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_idcontains 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
Instancefor 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 presolvedual_reductions(str, optional): Level of dual reductions to apply. Must be one of following:disable: Disable all dual reductionsweak: Preserving the set of optimal solutionsstrong: Preserving the optimal objective value (default)
threads(int, optional): Number of threads to use for presolving (default:1)0: Automatic detection1: 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().