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:
Pure NLP problems (continuous variables only) solved via the default IPM backend
MINLP problems (with binary/integer variables) solved via spatial Branch & Bound
MINLPLib instances loaded from
.nlfiles
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)#
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)#
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)#
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)#
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#
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)#
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#
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#
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#
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")