discopt MINLP Examples#

This notebook demonstrates solving Mixed-Integer Nonlinear Programs (MINLPs) using the discopt modeling API. discopt solves MINLPs via spatial Branch and Bound [Belotti et al., 2013, Land and Doig, 1960] with McCormick relaxations. We show:

  1. Pure NLP problems (continuous variables only) solved via the default IPM backend

  2. MINLP problems (with binary/integer variables) solved via spatial Branch & Bound

  3. MINLPLib instances loaded from .nl files

For each example, we verify the solution against known optimal values.

import discopt.modeling as dm
import numpy as np

print("discopt loaded successfully")
discopt loaded successfully

1. Unconstrained Quadratic (NLP)#

\[\min_{x,y} \; (x - 2)^2 + (y + 1)^2, \quad x, y \in [-5, 5]\]

Known optimal: \(x^* = 2, \; y^* = -1, \; f^* = 0\)

m = dm.Model("unconstrained_quadratic")
x = m.continuous("x", lb=-5, ub=5)
y = m.continuous("y", lb=-5, ub=5)

m.minimize((x - 2) ** 2 + (y + 1) ** 2)

result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: 0.0)")
print(f"x = {result.value(x):.6f}  (expected: 2.0)")
print(f"y = {result.value(y):.6f}  (expected: -1.0)")
print(f"Time:      {result.wall_time:.3f}s")
Status:    optimal
Objective: -0.00000000  (expected: 0.0)
x = 2.000000  (expected: 2.0)
y = -1.000000  (expected: -1.0)
Time:      0.038s

2. Rosenbrock Function (NLP)#

\[\min_{x,y} \; (1 - x)^2 + 100(y - x^2)^2, \quad x, y \in [-5, 5]\]

The classic banana-shaped valley. Known optimal: \(x^* = 1, \; y^* = 1, \; f^* = 0\)

m = dm.Model("rosenbrock")
x = m.continuous("x", lb=-5, ub=5)
y = m.continuous("y", lb=-5, ub=5)

m.minimize((1 - x) ** 2 + 100 * (y - x**2) ** 2)

result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: 0.0)")
print(f"x = {result.value(x):.6f}  (expected: 1.0)")
print(f"y = {result.value(y):.6f}  (expected: 1.0)")
print(f"Time:      {result.wall_time:.3f}s")
Status:    optimal
Objective: -0.00000000  (expected: 0.0)
x = 1.000000  (expected: 1.0)
y = 1.000000  (expected: 1.0)
Time:      15.341s

3. Constrained Quadratic with Inequality (NLP)#

\[\min_{x,y} \; x^2 + y^2 \quad \text{s.t.} \quad x + y \geq 1, \quad x, y \in [-5, 5]\]

Known optimal: \(x^* = y^* = 0.5, \; f^* = 0.5\)

m = dm.Model("constrained_quadratic")
x = m.continuous("x", lb=-5, ub=5)
y = m.continuous("y", lb=-5, ub=5)

m.minimize(x**2 + y**2)
m.subject_to(x + y >= 1, name="sum_lb")

result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: 0.5)")
print(f"x = {result.value(x):.6f}  (expected: 0.5)")
print(f"y = {result.value(y):.6f}  (expected: 0.5)")
print(f"Time:      {result.wall_time:.3f}s")
Status:    optimal
Objective: 0.50000000  (expected: 0.5)
x = 0.500000  (expected: 0.5)
y = 0.500000  (expected: 0.5)
Time:      0.003s

4. Nonlinear Equality Constraint (NLP)#

\[\min_{x,y} \; x^2 + y^2 \quad \text{s.t.} \quad xy = 1, \quad x, y \in [0.1, 5]\]

Known optimal: \(x^* = y^* = 1, \; f^* = 2\)

m = dm.Model("nonlinear_equality")
x = m.continuous("x", lb=0.1, ub=5)
y = m.continuous("y", lb=0.1, ub=5)

m.minimize(x**2 + y**2)
m.subject_to(x * y == 1, name="product_eq")

result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: 2.0)")
print(f"x = {result.value(x):.6f}  (expected: 1.0)")
print(f"y = {result.value(y):.6f}  (expected: 1.0)")
print(f"Time:      {result.wall_time:.3f}s")
Status:    optimal
Objective: 2.00000000  (expected: 2.0)
x = 1.000000  (expected: 1.0)
y = 1.000000  (expected: 1.0)
Time:      0.005s

5. Exponential NLP#

\[\min_{x,y} \; e^x + y^2 \quad \text{s.t.} \quad x + y \geq 1, \quad x, y \in [-2, 2]\]

From KKT conditions: \(e^x = 2y\) and \(x + y = 1\), giving \(f^* \approx 1.8395\).

m = dm.Model("exp_nlp")
x = m.continuous("x", lb=-2, ub=2)
y = m.continuous("y", lb=-2, ub=2)

m.minimize(dm.exp(x) + y**2)
m.subject_to(x + y >= 1, name="sum_lb")

result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: ~1.8395)")
print(f"x = {result.value(x):.6f}")
print(f"y = {result.value(y):.6f}")
print(f"Time:      {result.wall_time:.3f}s")
Status:    optimal
Objective: 1.83948430  (expected: ~1.8395)
x = 0.314923
y = 0.685077
Time:      0.004s

6. Simple MINLP (Branch & Bound)#

\[\min_{x_1, x_2, x_3} \; x_1^2 + x_2^2 + x_3 \quad \text{s.t.} \quad x_1 + x_2 \geq 1, \; x_1^2 + x_2 \leq 3, \; x_3 \in \{0, 1\}, \; x_1, x_2 \in [0, 5]\]

The binary variable \(x_3\) is minimized, so \(x_3^* = 0\). The continuous part reduces to a constrained quadratic.

Known optimal: \(x_1^* = x_2^* = 0.5, \; x_3^* = 0, \; f^* = 0.5\)

m = dm.Model("simple_minlp")
x1 = m.continuous("x1", lb=0, ub=5)
x2 = m.continuous("x2", lb=0, ub=5)
x3 = m.binary("x3")

m.minimize(x1**2 + x2**2 + x3)
m.subject_to(x1 + x2 >= 1, name="sum_lb")
m.subject_to(x1**2 + x2 <= 3, name="quad_ub")

result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: 0.5)")
print(f"x1 = {result.value(x1):.6f}  (expected: 0.5)")
print(f"x2 = {result.value(x2):.6f}  (expected: 0.5)")
print(f"x3 = {result.value(x3):.6f}  (expected: 0.0)")
print(f"Time:      {result.wall_time:.3f}s | Nodes: {result.node_count}")
OA: generating OA cuts for 1 of 2 rows (2 convex; the rest are either nonconvex or already exact in the master)
Status:    optimal
Objective: 0.50000000  (expected: 0.5)
x1 = 0.500000  (expected: 0.5)
x2 = 0.500000  (expected: 0.5)
x3 = 0.000000  (expected: 0.0)
Time:      0.006s | Nodes: 0

7. Binary Knapsack-like MINLP#

\[\min_{x, y_1, y_2} \; (x - 3)^2 + 2 y_1 + 3 y_2 \quad \text{s.t.} \quad x \leq 5 y_1, \; x \leq 4 y_2, \; x \in [0, 5], \; y_1, y_2 \in \{0, 1\}\]

Known optimal: \(x^* = 3, \; y_1^* = 1, \; y_2^* = 1, \; f^* = 5.0\)

m = dm.Model("binary_knapsack_minlp")
x = m.continuous("x", lb=0, ub=5)
y1 = m.binary("y1")
y2 = m.binary("y2")

m.minimize((x - 3) ** 2 + 2 * y1 + 3 * y2)
m.subject_to(x <= 5 * y1, name="link_y1")
m.subject_to(x <= 4 * y2, name="link_y2")

result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: 5.0)")
print(f"x  = {result.value(x):.6f}  (expected: 3.0)")
print(f"y1 = {result.value(y1):.6f}  (expected: 1.0)")
print(f"y2 = {result.value(y2):.6f}  (expected: 1.0)")
print(f"Time:      {result.wall_time:.3f}s | Nodes: {result.node_count}")
OA: generating OA cuts for 0 of 2 rows (2 convex; the rest are either nonconvex or already exact in the master)
Status:    optimal
Objective: 5.00000000  (expected: 5.0)
x  = 3.000000  (expected: 3.0)
y1 = 1.000000  (expected: 1.0)
y2 = 1.000000  (expected: 1.0)
Time:      0.011s | Nodes: 0

8. Integer Quadratic#

\[\min_{n, x} \; (x - 2.7)^2 + (n - 3)^2 \quad \text{s.t.} \quad x + n \leq 6, \; x \in [0, 5], \; n \in \{0, 1, \ldots, 5\}\]

Known optimal: \(x^* = 2.7, \; n^* = 3, \; f^* = 0.0\)

m = dm.Model("integer_quadratic")
x = m.continuous("x", lb=0, ub=5)
n = m.integer("n", lb=0, ub=5)

m.minimize((x - 2.7) ** 2 + (n - 3) ** 2)
m.subject_to(x + n <= 6, name="sum_ub")

result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: 0.0)")
print(f"x = {result.value(x):.6f}  (expected: 2.7)")
print(f"n = {result.value(n):.6f}  (expected: 3.0)")
print(f"Time:      {result.wall_time:.3f}s | Nodes: {result.node_count}")
OA: generating OA cuts for 0 of 1 rows (1 convex; the rest are either nonconvex or already exact in the master)
Status:    optimal
Objective: 0.00000000  (expected: 0.0)
x = 2.700000  (expected: 2.7)
n = 3.000000  (expected: 3.0)
Time:      0.004s | Nodes: 0

9. Logarithmic NLP#

\[\min_{x,y} \; -\log(x) - \log(y) \quad \text{s.t.} \quad x + y \leq 1, \; x, y \in [0.01, 0.99]\]

Known optimal: \(x^* = y^* = 0.5, \; f^* = 2 \ln(2) \approx 1.3863\)

import math

m = dm.Model("log_nlp")
x = m.continuous("x", lb=0.01, ub=0.99)
y = m.continuous("y", lb=0.01, ub=0.99)

m.minimize(-dm.log(x) - dm.log(y))
m.subject_to(x + y <= 1, name="simplex")

expected = 2 * math.log(2)
result = m.solve()
print(f"Status:    {result.status}")
print(f"Objective: {result.objective:.8f}  (expected: {expected:.8f})")
print(f"x = {result.value(x):.6f}  (expected: 0.5)")
print(f"y = {result.value(y):.6f}  (expected: 0.5)")
print(f"Time:      {result.wall_time:.3f}s")
Status:    optimal
Objective: 1.38629436  (expected: 1.38629436)
x = 0.500000  (expected: 0.5)
y = 0.500000  (expected: 0.5)
Time:      0.003s

10. MINLPLib Instances (from .nl files)#

discopt can also load standard .nl format files from the MINLPLib [Bussieck et al., 2003] benchmark library. These are parsed by the Rust backend and solved through the same B&B engine.

Below we solve several MINLPLib instances and compare against published optimal values.

from pathlib import Path

# Known optima from MINLPLib (verified by BARON/ANTIGONE/SCIP)
minlplib_instances = {
    "ex1221": {"opt": 7.66718007, "type": "MINLP"},
    "ex1226": {"opt": -17.0, "type": "MINLP"},
    "st_e13": {"opt": 2.0, "type": "NLP"},
    "st_e15": {"opt": 7.66718007, "type": "NLP"},
    "st_e27": {"opt": 2.0, "type": "NLP"},
    "nvs03": {"opt": 16.0, "type": "MINLP"},
    "nvs04": {"opt": 0.72, "type": "MINLP"},
    "nvs07": {"opt": 4.0, "type": "MINLP"},
    "nvs15": {"opt": 1.0, "type": "MINLP"},
    "nvs16": {"opt": 0.70312500, "type": "MINLP"},
    "prob03": {"opt": 10.0, "type": "MINLP"},
    "gear": {"opt": 0.0, "type": "MINLP"},
}

# The notebook is executed from its own directory (jupyter-book) but may also be
# run from the repo root, so resolve against both rather than guessing one. The
# previous path, "../python/tests/data/minlplib", resolved to docs/python/... from
# here -- it has never existed, and the exists() guard below turned that into
# twelve lines of "SKIP  .nl file not found": a benchmark table that solved
# nothing and read like a result.
NL_DIRS = [
    Path("../../python/tests/data/minlplib"),  # from docs/notebooks/
    Path("python/tests/data/minlplib"),  # from the repo root
]
nl_dir = next((d for d in NL_DIRS if d.is_dir()), None)
if nl_dir is None:
    raise FileNotFoundError(
        "MINLPLib test data not found in any of: " + ", ".join(str(d) for d in NL_DIRS)
    )
header = (
    f"{'Instance':<12s} {'Type':<8s} {'Status':<10s} "
    f"{'Objective':>14s} {'Expected':>14s} "
    f"{'Error':>10s} {'Time':>8s}"
)
print(header)
print("-" * len(header))

n_correct = 0
n_total = 0
for name, info in minlplib_instances.items():
    nl_path = nl_dir / f"{name}.nl"
    if not nl_path.exists():
        raise FileNotFoundError(nl_path)

    model = dm.from_nl(str(nl_path))
    result = model.solve(time_limit=60, max_nodes=10000)
    n_total += 1

    expected = info["opt"]
    if result.objective is not None:
        error = abs(result.objective - expected)
        tol = 1e-4 + 1e-3 * abs(expected)
        correct = error <= tol
        if correct:
            n_correct += 1
        error_str = f"{error:.2e}"
    else:
        error_str = "N/A"
        correct = False

    marker = " *" if not correct else ""
    obj_str = f"{result.objective:.6f}" if result.objective is not None else "None"
    print(
        f"{name:<12s} {info['type']:<8s} "
        f"{result.status:<10s} {obj_str:>14s} "
        f"{expected:>14.6f} {error_str:>10s} "
        f"{result.wall_time:>7.2f}s{marker}"
    )

print(f"\nCorrect: {n_correct}/{n_total}")

# A table that solved nothing prints an empty table and a cheerful "0/0". Assert
# both that every instance ran and that every one matched its published optimum.
assert n_total == len(minlplib_instances), f"only {n_total} instance(s) were solved"
assert n_correct == n_total, f"{n_total - n_correct} instance(s) missed their optimum"
Instance     Type     Status          Objective       Expected      Error     Time
----------------------------------------------------------------------------------
ex1221       MINLP    optimal          7.667180       7.667180   1.32e-09    0.29s
ex1226       MINLP    optimal        -17.000000     -17.000000   2.88e-13    1.60s
st_e13       NLP      optimal          2.000000       2.000000   2.52e-09    0.01s
OA: generating OA cuts for 1 of 2 rows (2 convex; the rest are either nonconvex or already exact in the master)
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1.968516895119089e-6 unscaled_kkt_error=0.006412063201096259 scaled_nlp_error=1.2622254743929374e-8 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1.5504319224258128e-5 unscaled_kkt_error=9.717506991061491e-5 scaled_nlp_error=1.5066333045337743e-9 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=5.863399825452219e-7 unscaled_kkt_error=0.00011228824157759562 scaled_nlp_error=6.583908560664108e-11 threshold=0.0001
OA: generating OA cuts for 0 of 1 rows (1 convex; the rest are either nonconvex or already exact in the master)
st_e15       NLP      optimal          7.667180       7.667180   1.32e-09    0.29s
st_e27       NLP      optimal          2.000000       2.000000   3.11e-15    0.03s
nvs03        MINLP    optimal         16.000000      16.000000   0.00e+00    0.01s
nvs04        MINLP    optimal          0.720000       0.720000   6.00e-15    0.05s
nvs07        MINLP    optimal          4.000000       4.000000   1.33e-15    0.02s
nvs15        MINLP    optimal          1.000000       1.000000   0.00e+00    0.01s
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=1.2544302837886403e-5 scaled_nlp_error=1.2544302837886402e-13 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.7261067102032894e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=1.2544302837886403e-5 scaled_nlp_error=1.2544302837886402e-13 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189889412895032 scaled_nlp_error=1.2544302837886402e-13 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.770914984903148e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189889412895032 scaled_nlp_error=1.2544302837886402e-13 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189896853074708 scaled_nlp_error=1.2544302845983725e-13 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189894035458565 scaled_nlp_error=4.816410922487705e-14 threshold=0.0001
 INFO pounce_algorithm::conv_check::opt_error: refusing a termination certificate masked by an extreme objective scale; continuing toward the true minimum (obj_scale_certificate_threshold=0 disables) obj_scale=1e-8 unscaled_kkt_error=0.00018189896853074708 scaled_nlp_error=1.2544302845983725e-13 threshold=0.0001
nvs16: reporting this model's objective at the returned point (0.703125) rather than the solver's internal value (0.703125155); they differ by -1.550e-07, which means the solve ran on a reformulated model whose optimum does not transfer at this tolerance.
nvs16        MINLP    optimal          0.703125       0.703125   0.00e+00    0.31s
prob03       MINLP    optimal         10.000000      10.000000   0.00e+00    0.00s
gear         MINLP    optimal          0.000000       0.000000   3.78e-08    0.04s

Correct: 12/12

11. Direct Comparison: discopt vs. cyipopt#

For continuous NLP problems, we can compare discopt’s solution against a raw cyipopt (Ipopt [Wächter and Biegler, 2006]) solve to verify correctness.

import cyipopt

# ===== Solve with discopt =====
m = dm.Model("comparison")
x = m.continuous("x", lb=-5, ub=5)
y = m.continuous("y", lb=-5, ub=5)
m.minimize((x - 2) ** 2 + (y + 1) ** 2 + x * y)
m.subject_to(x + y >= 0.5, name="sum_lb")
m.subject_to(x - y <= 3, name="diff_ub")

discopt_result = m.solve()
print("=== discopt ===")
print(f"Status:    {discopt_result.status}")
print(f"Objective: {discopt_result.objective:.10f}")
print(f"x = {discopt_result.value(x):.8f}")
print(f"y = {discopt_result.value(y):.8f}")
print(f"Time:      {discopt_result.wall_time:.4f}s")
print()


# ===== Solve with raw cyipopt =====
class NLPProblem:
    def objective(self, x):
        return (x[0] - 2) ** 2 + (x[1] + 1) ** 2 + x[0] * x[1]

    def gradient(self, x):
        return np.array(
            [
                2 * (x[0] - 2) + x[1],
                2 * (x[1] + 1) + x[0],
            ]
        )

    def constraints(self, x):
        return np.array(
            [
                x[0] + x[1],  # >= 0.5
                x[0] - x[1],  # <= 3
            ]
        )

    def jacobian(self, x):
        return np.array(
            [
                1.0,
                1.0,  # d(c1)/d(x0), d(c1)/d(x1)
                1.0,
                -1.0,  # d(c2)/d(x0), d(c2)/d(x1)
            ]
        )


nlp = cyipopt.Problem(
    n=2,
    m=2,
    problem_obj=NLPProblem(),
    lb=np.array([-5.0, -5.0]),
    ub=np.array([5.0, 5.0]),
    cl=np.array([0.5, -1e20]),
    cu=np.array([1e20, 3.0]),
)
nlp.add_option("print_level", 0)
x_sol, info = nlp.solve(np.array([0.0, 0.0]))

print("=== raw cyipopt ===")
print(f"Status:    {info['status_msg']}")
print(f"Objective: {info['obj_val']:.10f}")
print(f"x = {x_sol[0]:.8f}")
print(f"y = {x_sol[1]:.8f}")
print()

# ===== Compare =====
obj_diff = abs(discopt_result.objective - info["obj_val"])
x_diff = abs(float(discopt_result.value(x)) - x_sol[0])
y_diff = abs(float(discopt_result.value(y)) - x_sol[1])
print("=== Comparison ===")
print(f"Objective difference: {obj_diff:.2e}")
print(f"x difference:        {x_diff:.2e}")
print(f"y difference:        {y_diff:.2e}")
print(f"Match: {'YES' if obj_diff < 1e-6 else 'NO'}")
=== discopt ===
Status:    optimal
Objective: -2.0833333308
x = 1.83333334
y = -1.16666666
Time:      0.0039s

=== raw cyipopt ===
Status:    b'Algorithm terminated successfully at a locally optimal point, satisfying the convergence tolerances (can be specified by options).'
Objective: -2.0833333783
x = 1.83333336
y = -1.16666667


******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit https://github.com/coin-or/Ipopt
******************************************************************************

=== Comparison ===
Objective difference: 4.75e-08
x difference:        1.84e-08
y difference:        1.32e-08
Match: YES

12. Summary of the discopt API#

The discopt.modeling API provides a clean, Pythonic interface for MINLP formulation:

import discopt.modeling as dm

# Create model
m = dm.Model("my_problem")

# Variables
x = m.continuous("x", shape=(3,), lb=0, ub=10)
y = m.binary("y", shape=(2,))
n = m.integer("n", lb=0, ub=100)

# Objective
m.minimize(dm.sum(x) + dm.exp(x[0]) + 10*dm.sum(y))

# Constraints (named for clarity)
m.subject_to(x[0] + x[1] >= 1, name="lb")
m.subject_to(x[0] * x[1] <= 5 * y[0], name="bilinear")  # bilinear
m.subject_to(dm.log(x[2] + 1) <= 2, name="log_ub")       # nonlinear

# Solve
result = m.solve(time_limit=60)
print(result.status)      # 'optimal', 'feasible', 'infeasible'
print(result.objective)   # optimal objective value
print(result.value(x))    # solution value for variable x
print(result.value(y))    # solution value for variable y
print(result.wall_time)   # solve time in seconds
print(result.node_count)  # B&B nodes explored (0 for pure NLP)

Available nonlinear functions: dm.exp, dm.log, dm.sqrt, dm.sin, dm.cos, dm.tan, dm.abs

Load from files: dm.from_nl("problem.nl")