Skip to main content

A learning-to-optimize framework for Mixed-Integer Nonlinear Programming (MINLP)

Project description

REINS: Relaxation-Enforced Integer Neural Neighbourhood Search for Parametric MINLP with Feasibility Guarantees

Framework

Based on the paper: "Learning to Optimize for Mixed-Integer Nonlinear Programming with Feasibility Guarantees"

Overview

REINS solves parametric MINLP: given a family of optimization problems that share the same structure but differ in parameter values (e.g., constraint right-hand sides), it learns a neural network that maps parameters directly to high-quality integer solutions, without invoking a traditional solver at inference time.

The key components are differentiable integer correction layers (for rounding continuous relaxations to integers) and gradient-based feasibility projection (for enforcing feasibility post-hoc). Training is self-supervised: only sampled parameter values are needed, not expensive optimal solutions. The framework scales to problems with tens of thousands of variables at subsecond inference with fast training.

Citation

@article{tang2024learning,
  title={Learning to optimize for mixed-integer non-linear programming with feasibility guarantees},
  author={Tang, Bo and Khalil, Elias B and Drgo{\v{n}}a, J{\'a}n},
  journal={arXiv preprint arXiv:2410.11061},
  year={2024}
}

Slides

Our talk at ICS 2025. View the slides here.

Installation

pip install -e .

Requirements: Python >= 3.10, PyTorch, NeuroMANCER

Tutorial

This tutorial walks through building a learnable solver for a parametric integer quadratic program:

$$\min_{x} \quad \frac{1}{2} x^\top Q x + c^\top x \quad \text{s.t.} \quad Ax \leq b, \quad x \in \mathbb{Z}^n$$

Here $Q$, $c$, $A$ are fixed problem coefficients, while $b$ is the varying parameter. Different values of $b$ define different problem instances. The goal of REINS is to learn a neural network mapping $b \mapsto x^*$ so that, given any new $b$, the network efficiently predicts a high-quality integer solution.

The training pipeline:

  1. Sample a dataset of parameter values ${b^{(i)}}$ (no optimal solutions needed)
  2. Train the network end-to-end with a self-supervised penalty loss (objective + constraint violations)
  3. Predict solutions for unseen parameters via a single forward pass

Step 1: Define Variables

Use TypeVariable for decision variables with type metadata (tells rounding layers which indices need integrality enforcement), and Variable for parameters (continuous inputs like constraint RHS).

from reins import TypeVariable, Variable, VarType

# Pure integer decision variable
x = TypeVariable("x", num_vars=5, var_types=VarType.INTEGER)

# Equivalent using index-based specification
x = TypeVariable("x", num_vars=5, integer_indices=[0, 1, 2, 3, 4])

# Mixed-integer: indices 0,1 are integer, index 2 is binary, rest continuous
y = TypeVariable("y", num_vars=5, integer_indices=[0, 1], binary_indices=[2])
# Equivalent using explicit type list
var_types=[
    VarType.INTEGER, VarType.INTEGER, VarType.BINARY,
    VarType.CONTINUOUS, VarType.CONTINUOUS,
]
y = TypeVariable("y", var_types=var_types)

# Parameter variable (continuous, no type metadata)
b = Variable("b")

Step 2: Define Loss (Objectives + Constraints)

Define objectives and constraints symbolically via operator overloading, then combine into a PenaltyLoss. Use the same x and b from Step 1 so that the loss and rounding layer share the same variable objects.

import torch
import numpy as np
from reins import PenaltyLoss

num_var = 5
num_ineq = 5

# Fixed problem coefficients
rng = np.random.RandomState(17)
Q = torch.from_numpy(0.01 * np.diag(rng.random(size=num_var))).float()
c = torch.from_numpy(0.1 * rng.random(num_var)).float()
A = torch.from_numpy(rng.normal(scale=0.1, size=(num_ineq, num_var))).float()

# Objective: minimize (1/2) x^T Q x + c^T x
f = 0.5 * torch.sum((x @ Q) * x, dim=1) + torch.sum(c * x, dim=1)
obj = f.minimize(weight=1.0, name="obj")

# Constraint: Ax <= b
penalty_weight = 100
con = penalty_weight * (x @ A.T <= b)

loss = PenaltyLoss(objectives=[obj], constraints=[con])

Step 3: Build Relaxation Network

The relaxation network learns the mapping $b \mapsto x_{\text{rel}}$.

from reins import MLPBnDrop
from reins.node import RelaxationNode

# set a neural network for relaxation
rel_net = MLPBnDrop(
    insize=num_ineq,
    outsize=num_var,
    hsizes=[64] * 4,
    dropout=0.2,      # dropout rate
    bnorm=True,       # batch normalization
)

# b -> rel_net -> x_rel
rel = RelaxationNode(rel_net, [b], [x])

Step 4: Choose a Rounding Layer

Rounding layers convert continuous relaxations to integer solutions. Set stochastic=True to inject Gumbel-Softmax noise during training for better exploration; at eval time the layer is always deterministic.

from reins.node.rounding import AdaptiveSelectionRounding, DynamicThresholdRounding

# set a neural network for rounding
rnd_net = MLPBnDrop(
    insize=num_ineq + num_var,
    outsize=num_var,
    hsizes=[64] * 3,
    dropout=0.2,      # dropout rate
    bnorm=True,       # batch normalization
)

# Adaptive Selection (AS): (b, x_rel) -> rnd_net -> x
rnd = AdaptiveSelectionRounding(rnd_net, [b], [x], stochastic=True)

# Dynamic Thresholding (DT): (b, x_rel) -> rnd_net -> x
rnd = DynamicThresholdRounding(rnd_net, [b], [x])

Step 5: Assemble the Solver

LearnableSolver composes the relaxation node, the rounding node, and the loss (with a default projection).

from reins import LearnableSolver

# Default: GradientProjection enabled (10000 steps with decay) for feasibility enforcement at inference
solver = LearnableSolver(
    relaxation_node=rel,
    rounding_node=rnd,
    loss=loss)

# Disable projection
solver = LearnableSolver(
    relaxation_node=rel,
    rounding_node=rnd,
    loss=loss,
    projection_steps=0)

Step 6: Prepare Data & Train

Training data consists of sampled parameter values only — no optimal solutions needed.

from torch.utils.data import DataLoader
from reins import DictDataset

num_data = 10000
b_samples = torch.from_numpy(
    np.random.uniform(-1, 1, size=(num_data, num_ineq))
).float()

data_train = DictDataset({"b": b_samples[:8000]}, name="train")
data_val   = DictDataset({"b": b_samples[8000:9000]}, name="val")
data_test  = DictDataset({"b": b_samples[9000:]}, name="test")

loader_train = DataLoader(data_train, batch_size=64, shuffle=True,
                          collate_fn=data_train.collate_fn)
loader_val   = DataLoader(data_val, batch_size=64, shuffle=False,
                          collate_fn=data_val.collate_fn)

optimizer = torch.optim.AdamW(solver.problem.parameters(), lr=1e-3)
solver.train(
    loader_train,
    loader_val,
    optimizer,
    epochs=200,      # max epochs
    patience=20,     # early stopping patience
    warmup=20,       # warmup epochs before early stopping
    device="cuda",
)

Step 7: Predict

b_test = data_test.datadict["b"].to("cuda")
result = solver.predict({"b": b_test, "name": "test"})

print(result["x"])       # integer solution
print(result["x_rel"])   # continuous relaxation

Package Structure

src/reins/                       # Core package
├── __init__.py                  # Public API
├── variable.py                  # VarType enum & TypeVariable class
├── blocks.py                    # MLPBnDrop (MLP with BatchNorm + Dropout)
├── loss.py                      # PenaltyLoss (sum-reduced penalty loss)
├── solver.py                    # LearnableSolver wrapper
├── node/                        # Node components
│   ├── relaxation.py            # RelaxationNode (relaxation solution)
│   └── rounding/                # Integer rounding layers
│       ├── functions.py         # Differentiable STE primitives
│       ├── base.py              # RoundingNode abstract base class
│       ├── ste.py               # STERounding
│       ├── selection.py         # AdaptiveSelectionRounding
│       └── threshold.py         # DynamicThresholdRounding
├── projection/                  # Feasibility projection
│   └── gradient.py              # GradientProjection
experiments/                     # Benchmark experiments (not part of the package)
├── quadratic.py                 # Integer Quadratic Programming (IQP)
├── nonconvex.py                 # Integer Non-Convex Programming (INP)
├── rosenbrock.py                # Mixed-Integer Rosenbrock (MIRB)
├── heuristic.py                 # Rounding heuristics (naive_round, floor_round)
└── math_solver/                 # Pyomo-based exact solvers for evaluation
    ├── abc_solver.py            # Abstract parametric solver base class
    ├── quadratic.py             # IQP solver (Gurobi)
    ├── nonconvex.py             # INP solver (SCIP)
    └── rosenbrock.py            # MIRB solver (SCIP)
tests/                           # pytest test suite

Methodology

Integer Correction Layers

Two learnable correction layers enforce integrality in neural network output:

example for RC example for RT
  • Adaptive Selection (AS) / AdaptiveSelectionRounding: Learns a classification strategy to determine rounding directions for integer variables.
  • Dynamic Thresholding (DT) / DynamicThresholdRounding: Learns a threshold value for each integer variable to decide whether to round up or down.

Integer Feasibility Projection

A gradient-based projection iteratively refines infeasible solutions. The figure below illustrates how the projection step adjusts a solution over multiple iterations.

Feasibility Projection Iterations

By integrating feasibility projection with the correction layers, we extend AS and DT into AS-P and DT-P, respectively.

Performance

Our learning-based methods (AS & DT) achieve comparable or superior performance to exact solvers (EX) while being orders of magnitude faster:

Penalty Effect on IQP Penalty Effect on MIRB

With properly tuned penalty weights, the approach attains comparable or better objective values within sub-seconds, while exact solvers require up to 1000 seconds.

Reproducibility

Three benchmark problems are included:

  • Integer Quadratic Problems (IQP): Convex quadratic objective with linear constraints and integer variables.
  • Integer Non-Convex Problems (INP): Non-convex variant with trigonometric terms.
  • Mixed-Integer Rosenbrock Problems (MIRB): Rosenbrock function with linear and non-linear constraints.
python run_qp.py --size 5
python run_nc.py --size 10 --penalty 1 --project
python run_rb.py --size 100 --penalty 10 --project

Arguments: --size (problem size), --penalty (constraint violation weight), --project (enable feasibility projection).

License

Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

reins-0.1.0.tar.gz (48.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

reins-0.1.0-py3-none-any.whl (25.7 kB view details)

Uploaded Python 3

File details

Details for the file reins-0.1.0.tar.gz.

File metadata

  • Download URL: reins-0.1.0.tar.gz
  • Upload date:
  • Size: 48.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for reins-0.1.0.tar.gz
Algorithm Hash digest
SHA256 db0865f487da2c3b7c40e643fe9ba274a71980d10e0249bb9d2cfe58c18de150
MD5 62bf87eebdf987dbedb1cbabf8bfec7a
BLAKE2b-256 32cb079393731af283ad5f1cb77e178fb8bb5f5a656b27a4398d8d25d79008d8

See more details on using hashes here.

File details

Details for the file reins-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: reins-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 25.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for reins-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a56d16a910edf9f96142214b767fe1753b2216df086e0704aa3eb184ddee57da
MD5 25a1985fdce1562ba0c2999d930ccfcb
BLAKE2b-256 2bd41dcde89839e55071b557522ce7d71d95edc399d74f49d6b8ffa4d181641b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page