The CUTEst Interface#

CUTEst – the Constrained and Unconstrained Testing Environment with safe threads – is the standard collection of nonlinear optimization test problems used to benchmark NLP solvers [Gould et al., 2015]. It descends from the earlier CUTE [Bongartz et al., 1995] and CUTEr [Gould et al., 2003] environments, and today bundles well over a thousand continuous problems ranging from the classic two-variable Rosenbrock function and the Hock–Schittkowski [Hock and Schittkowski, 1981] test set through to large-scale sparse instances drawn from real engineering applications.

Each problem is described in a SIF (Standard Input Format) file, decoded by SIFDecode, compiled against the CUTEst Fortran library, and exposed to Python through PyCUTEst [Fowkes et al., 2022]. CUTEst supplies analytic first and second derivatives – gradients, Jacobians, and Hessians – so a solver under test is measured on its own merits rather than on the noise of finite differences.

discopt’s CUTEst interface lives in discopt.interfaces.cutest. It wraps PyCUTEst so that any CUTEst problem can be loaded, inspected, and – crucially – handed to discopt’s NLP solver backends through the same NLPEvaluator interface used everywhere else in the package. This makes the entire CUTEst library available as a benchmark suite for POUNCE, cyipopt/Ipopt [Wächter and Biegler, 2006], and the differentiable relaxation machinery.

Installation#

PyCUTEst is an optional dependency. Unlike the pure-Python parts of discopt, it cannot be installed from a wheel alone: it compiles each problem from Fortran source at import time, so it needs a full CUTEst toolchain in place.

pip install discopt[cutest]   # pulls in pycutest

In addition you must provide, outside of pip:

  • a Fortran compiler (gfortran),

  • the CUTEst library and SIFDecode (and the MASTSIF problem archive),

  • the environment variables CUTEST, SIFDECODE, MASTSIF, and MYARCH pointing at that installation.

The canonical setup instructions are maintained with PyCUTEst [Fowkes et al., 2022] at https://jfowkes.github.io/pycutest/. Because this build environment has none of that toolchain, the cells below that actually load a CUTEst problem are shown for reference but are not executed here – they will run unchanged once PyCUTEst is available. The cells that only touch discopt’s wrapper (imports, the public API surface, the availability probe) run normally.

Importing the interface and inspecting the API#

The module itself imports cleanly with or without PyCUTEst installed – the optional dependency is probed lazily, so import discopt.interfaces.cutest never fails. This lets you write code against the interface and discover whether the backing toolchain is present at runtime.

import discopt.interfaces.cutest as cutest

public_api = [
    "CUTEstProblem",
    "CUTEstProblemInfo",
    "NLPEvaluatorFromCUTEst",
    "list_cutest_problems",
    "load_cutest_problem",
]
# Confirm each name is actually exported by the module.
for name in public_api:
    print(f"{name:24s} {'OK' if hasattr(cutest, name) else 'MISSING'}")
CUTEstProblem            OK
CUTEstProblemInfo        OK
NLPEvaluatorFromCUTEst   OK
list_cutest_problems     OK
load_cutest_problem      OK

The five public names form the whole interface:

Name

Kind

Purpose

load_cutest_problem(name, sif_params=None)

function

Load a problem by name, returning a CUTEstProblem.

list_cutest_problems(objective=, constraints=, regular=, max_n=, max_m=, userN=)

function

Discover/filter problems by classification and size.

CUTEstProblem

class

Wraps a PyCUTEst problem: metadata, bounds, starting point, evaluator factory.

CUTEstProblemInfo

dataclass

Structured metadata (n, m, objective/constraint type, degree, …).

NLPEvaluatorFromCUTEst

class

Adapter exposing CUTEst callbacks as a discopt NLPEvaluator.

Checking availability gracefully#

Because PyCUTEst is optional, robust code should check before relying on it. The discovery and loading helpers call an internal _require_pycutest() guard that raises a clear, actionable ImportError when the toolchain is missing – rather than a cryptic failure deep inside Fortran bindings. The cell below catches that error and reports the situation truthfully for the current environment.

def cutest_available() -> bool:
    """Return True if a working PyCUTEst toolchain is importable."""
    try:
        cutest.load_cutest_problem("ROSENBR")
        return True
    except ImportError as exc:
        print("CUTEst is not available in this environment:")
        print(f"  {type(exc).__name__}: {str(exc).splitlines()[0]}")
        print("  -> install with `pip install discopt[cutest]` plus the CUTEst toolchain.")
        return False
    except Exception as exc:  # pragma: no cover - only with a partial install
        print(f"CUTEst probe raised {type(exc).__name__}: {exc}")
        return False


AVAILABLE = cutest_available()
print(f"\nCUTEst available: {AVAILABLE}")
CUTEst is not available in this environment:
  ImportError: pycutest is required for CUTEst support. Install with:
  -> install with `pip install discopt[cutest]` plus the CUTEst toolchain.

CUTEst available: False

Loading a problem#

Note

The remaining code cells require a working PyCUTEst toolchain and are therefore not executed in this rendered book. With CUTEst installed they run exactly as written.

load_cutest_problem takes a CUTEst problem name (and, optionally, sif_params for variable-dimension problems) and returns a CUTEstProblem. Here we load HS71 – problem 71 from the Hock–Schittkowski set [Hock and Schittkowski, 1981], a small constrained NLP that is a common smoke test for interior-point solvers.

# Requires PyCUTEst -- not executed in the rendered book.
prob = cutest.load_cutest_problem("HS71")

print(prob)                       # CUTEstProblem('HS71', n=..., m=..., classification=...)
print("variables   :", prob.n)
print("constraints :", prob.m)
print("x0          :", prob.x0)
print("lower bounds:", prob.bl)
print("upper bounds:", prob.bu)

info = prob.info                  # CUTEstProblemInfo dataclass
print("objective type :", info.objective_type)
print("constraint type:", info.constraint_type)
print("classification :", info.classification)

Plugging into discopt’s NLP evaluator interface#

The key integration point is CUTEstProblem.to_evaluator(), which returns an NLPEvaluatorFromCUTEst. This adapter implements the same interface as discopt’s native NLPEvaluator and the .nl-file evaluator: evaluate_objective, evaluate_gradient, evaluate_hessian/evaluate_lagrangian_hessian, evaluate_constraints, evaluate_jacobian, the sparse variants, and the n_variables / n_constraints / variable_bounds / constraint_bounds properties.

All derivatives are CUTEst’s analytic values, so the evaluator drops straight into any solver that consumes an NLPEvaluator – for example discopt’s Ipopt bridge [Wächter and Biegler, 2006].

# Requires PyCUTEst -- not executed in the rendered book.
evaluator = prob.to_evaluator()

x0 = prob.x0
print("n_variables   :", evaluator.n_variables)
print("n_constraints :", evaluator.n_constraints)
print("f(x0)         :", evaluator.evaluate_objective(x0))
print("grad f(x0)    :", evaluator.evaluate_gradient(x0))
print("c(x0)         :", evaluator.evaluate_constraints(x0))
print("Jac c(x0)     :\n", evaluator.evaluate_jacobian(x0))

# The evaluator exposes the same surface as discopt's native NLPEvaluator,
# so it can be solved directly with discopt's Ipopt bridge:
from discopt.solvers.nlp_ipopt import solve_nlp

result = solve_nlp(evaluator, x0)
print("objective     :", result.objective)
print("status        :", result.status)

Discovering problems with list_cutest_problems#

list_cutest_problems filters the CUTEst archive by classification and size. The objective and constraints arguments accept either CUTEst single-letter codes ("U", "Q", "L", …) or the full PyCUTEst strings ("unconstrained", "quadratic", …); max_n / max_m cap the variable and constraint counts; regular and userN filter on regularity and variable-dimension problems respectively.

# Requires PyCUTEst -- not executed in the rendered book.

# Small unconstrained problems (<= 10 variables).
small_uncon = cutest.list_cutest_problems(constraints="U", max_n=10)
print(f"{len(small_uncon)} small unconstrained problems")
print(small_uncon[:10])

# Small constrained problems with a quadratic objective.
small_qp = cutest.list_cutest_problems(objective="Q", constraints="L", max_n=20, max_m=20)
print(f"\n{len(small_qp)} small quadratic/linear-constrained problems")
print(small_qp[:10])

# Build a tiny benchmark batch and report sizes.
for name in small_uncon[:5]:
    p = cutest.load_cutest_problem(name)
    print(f"{name:12s} n={p.n:4d} m={p.m:4d}  {p.info.classification}")
    p.close()

Connecting CUTEst to discopt’s benchmark suite#

CUTEst is the lingua franca of NLP benchmarking: results reported against it – usually summarized with performance and data profiles [Dolan and Moré, 2002] – are directly comparable across the literature. The discopt.interfaces.cutest wrapper makes that whole corpus available to discopt’s own validation framework in discopt_benchmarks.

The bridge is CUTEstProblem.to_instance_info(), which converts a loaded problem into the benchmark runner’s InstanceInfo record (variable/constraint counts, nonlinear-constraint count, a cutest_<class> problem class, and source="cutest"). A typical workflow is:

  1. list_cutest_problems(...) to select a size- and class-controlled subset of the archive;

  2. load_cutest_problem(name).to_evaluator() to obtain an analytic-derivative NLPEvaluator for each instance;

  3. solve with each backend under test (POUNCE, cyipopt/Ipopt [Wächter and Biegler, 2006], …);

  4. feed the per-instance objective, status, and timing through benchmarks/metrics.py to produce performance profiles [Dolan and Moré, 2002] and check the phase-gate correctness criteria.

Because the CUTEst evaluator is interface-compatible with the native .nl-file and modeling-API evaluators, the same benchmark harness measures discopt on hand-built models, AMPL/.nl instances [Fourer et al., 2003], and the standard CUTEst collection without any solver-side changes – giving an apples-to-apples comparison against the externally reported numbers for BARON, Couenne, SCIP, and Ipopt.