Skip to content

Repository files navigation

IpoptNet

.NET NuGet

A modern .NET interface for IPOPT (Interior Point OPTimizer), a software library for large-scale nonlinear optimization. This library provides both a high-level modeling API with automatic differentiation and a low-level native wrapper.

Installation

Install the package via NuGet:

dotnet add package ipopt-net

The package includes native binaries for IPOPT 3.14.20 (with MUMPS 5.9.0):

  • Windows (x64) — ipopt-3.dll
  • Linux (x64) — libipopt-3.so

Both are a single self-contained library: MUMPS, MKL Pardiso, BLAS/LAPACK and the GCC/Fortran runtimes are statically linked, so no other DLLs or shared objects are needed — and on Windows, no Visual C++ redistributable either.

Both are built from the same sources by the scripts in build/, which document the process. The Windows DLL is cross-compiled from WSL with mingw-w64, so the two platforms stay on matching IPOPT and MUMPS versions.

Features

  • Modeling API: Define nonlinear optimization problems using C# expressions with natural syntax
  • Automatic Differentiation: Gradients and Hessians computed automatically via reverse-mode AD
  • Intelligent Matrix Caching: Automatically detects and pre-computes constant matrices for LP/QP/QCP problems
  • Automatic Partitioning: Detects models that split into independent sub-problems and solves each separately
  • High-level Wrapper: Clean, disposable IpoptSolver class for direct API access
  • Native Performance: Uses .NET 10 LibraryImport for efficient C API calls
  • Expression Support: Arithmetic, trigonometric, exponential, logarithmic, and power operations
  • Flexible Constraints: Equality, inequality, and bound constraints

Quick Start (Modeling API)

The modeling API allows you to define optimization problems with automatic differentiation:

using IpoptNet.Modelling;

// Create a model
var model = new Model();

// Configure IPOPT (optional)
model.Options.LinearSolver = LinearSolver.PardisoMkl;
model.Options.HessianApproximation = HessianApproximation.LimitedMemory;

// Add variables with bounds and optional initial guesses
var x = model.AddVariable(1, 5);
var y = model.AddVariable(1, 5) { Start = 3.7 };
var z = model.AddVariable(1, 5);
var w = model.AddVariable(1, 5);

// Set objective: minimize x*w*(x+y+z) + z (expressions can be built incrementally)
var expr = x * (x + y + z);
expr *= w;
model.SetObjective(expr + z);

// Add constraints
model.AddConstraint(x * y * z * w >= 25);
model.AddConstraint(x*x + y*y + z*z + w*w == 40);

// Solve
var result = model.Solve();

if (result.Status == ApplicationReturnStatus.SolveSucceeded)
{
    Console.WriteLine($"x = {result.Solution[x]:F3}");
    Console.WriteLine($"y = {result.Solution[y]:F3}");
    Console.WriteLine($"z = {result.Solution[z]:F3}");
    Console.WriteLine($"w = {result.Solution[w]:F3}");
    Console.WriteLine($"Objective = {result.ObjectiveValue:F3}");
}

Output:

x = 1.000
y = 4.743
z = 3.821
w = 1.379
Objective = 17.014

Supported Operations

The expression system supports:

  • Arithmetic: +, -, *, /, unary -
  • Power: Expr.Pow(x, n), Expr.Sqrt(x)
  • Trigonometric: Expr.Sin(x), Expr.Cos(x), Expr.Tan(x)
  • Exponential/Log: Expr.Exp(x), Expr.Log(x)
  • Constraints: >=, <=, ==

Partitioning

Many models decompose into independent sub-problems that share no variable through any constraint, implicit block, or objective term. Because IPOPT's linear-algebra cost grows superlinearly with problem size, solving each sub-problem separately is both exact and considerably cheaper.

This feature is enabled by default. result.Partitions exposes the individual sub-problems when a model does decompose:

var model = new Model();
// ... build the model ...
var result = model.Solve();

foreach (var partition in result.Partitions)
    Console.WriteLine($"{partition.Status}: {partition.ObjectiveValue}");

Set Model.EnablePartitioning = false to force a single whole-model solve, skipping the decomposition analysis entirely.

Status, Solution, ObjectiveValue and Statistics on the returned ModelResult are model-level aggregates, so a partitioned solve reads the same as an unpartitioned one; the individual sub-problem results are on Partitions. Every partition is always attempted, so one failing sub-problem never suppresses the others.

Iteration callback

// before: Func<SolveStatistics, bool>
model.IntermediateCallback = (stats, partition) =>
{
    // stats always describes the whole model: ObjectiveValue accounts for partitions already
    // solved, the one currently iterating, and the ones not yet started; IterationCount is
    // cumulative. So best-so-far tracking needs no changes.
    // partition.Index / .Count identify the sub-problem; partition.LocalStatistics has the raw
    // per-partition numbers. With partitioning off these are 0 and 1.
    return !cancelled;
};

Limits and budgets

Iteration and time limits are treated differently, on purpose:

Option Scope Why
MaxIterations per partition It guards against one sub-problem spinning forever.
MaxWallTime, MaxCpuTime model-wide These are deadlines. Each partition is handed what remains of the budget, so N partitions cannot take N times as long as you allowed.

Elapsed wall time is measured exactly. Elapsed CPU time is taken from the process total, which over-counts when other threads in your application are busy — it therefore errs toward stopping sooner, never toward overrunning the budget.

Best Iterate

IPOPT returns its final iterate, which is not always its best one. A run that ends on MaximumIterationsExceeded, RestorationFailed, or a caller-requested stop can finish somewhere worse than it passed through earlier. Every solve therefore records the best point it saw:

var result = model.Solve();

var best = result.BestIterate;
if (best is not null && best.IsFeasible)
    Console.WriteLine($"best objective {best.ObjectiveValue} at iteration {best.IterationCount}");

BestIterate.Solution covers every variable, implicit-block-eliminated ones included, and under partitioning it is the whole model's — no partition bookkeeping required.

"Best" is feasibility-first, not lowest-objective: the lowest-objective iterate whose constraint violation is within ConstraintViolationTolerance, falling back to the least-infeasible point (with IsFeasible false) when nothing feasible was ever seen.

Automatic Elimination

A variable defined by an equality it appears in linearly can be moved out of IPOPT's decision vector and computed from that equality instead. Model.FindEliminableVariables() reports what qualifies without changing anything:

foreach (var c in model.FindEliminableVariables())
    Console.WriteLine($"x[{c.Variable.Index}] could be defined by its constraint (coefficient {c.Coefficient})");

model.EnableAutomaticElimination = true;   // off by default

A pair qualifies when the constraint is an equality of the form expression == 0, the variable's partial derivative of it is a non-zero constant, and the variable is unbounded.

This is off by default. Unlike partitioning it is not a free win: the reduced problem has the same optimum in exact arithmetic but is a different problem for IPOPT to walk, with different conditioning, and each eliminated variable enters it nonlinearly through its block. Measure before adopting it.

Performance Optimization

The solver automatically detects problem structure and optimizes matrix computations:

Constant Matrix Detection

For certain problem types, derivative matrices remain constant throughout the solution process. The library automatically detects these cases and pre-computes matrices once:

Problem Type Constant Matrices Description
Linear Programming (LP) Gradient, Jacobian All derivatives are constant coefficients
Quadratic Programming (QP) Jacobian, Hessian Linear constraints have constant gradients; quadratic terms have constant second derivatives
Quadratically Constrained (QCP) Hessian contributions Quadratic constraints contribute constant Hessian terms

Example - Linear Program:

var model = new Model();
var x = model.AddVariable(0, 10);
var y = model.AddVariable(0, 10);

// Linear objective and constraints - matrices computed once
model.SetObjective(2*x + 3*y);
model.AddConstraint(x + 2*y <= 10);
model.AddConstraint(3*x + y <= 12);

var result = model.Solve();

Example - Quadratic Program:

var model = new Model();
var x = model.AddVariable();
var y = model.AddVariable();

// Quadratic objective, linear constraints - Hessian and Jacobian computed once
model.SetObjective(x*x + y*y - 4*x - 6*y);
model.AddConstraint(x + y <= 5);

var result = model.Solve();

This optimization is completely automatic - no code changes required. The solver analyzes the expression structure and applies the appropriate strategy.

More Examples

Rosenbrock Function (Unconstrained)

var model = new Model();
var x = model.AddVariable();
var y = model.AddVariable();

// Minimize (1-x)^2 + 100*(y-x^2)^2
model.SetObjective(Expr.Pow(1 - x, 2) + 100 * Expr.Pow(y - x*x, 2));

var result = model.Solve();
// Converges to x=1, y=1

Constrained Optimization

var model = new Model();
var x = model.AddVariable();
var y = model.AddVariable();

// Minimize x^2 + y^2
model.SetObjective(x*x + y*y);

// Subject to x + y = 4
model.AddConstraint(x + y == 4);

var result = model.Solve();
// Solution: x=2, y=2, objective=8

Trigonometric Optimization

var model = new Model();
var x = model.AddVariable(-Math.PI, Math.PI);

// Minimize -sin(x)
model.SetObjective(-Expr.Sin(x));

var result = model.Solve();
// Converges to x=π/2

Configuring IPOPT Options

The modeling API exposes all IPOPT configuration options through a strongly-typed API with enums:

var model = new Model();

// Configure solver options using enums (type-safe with IntelliSense)
model.Options.LinearSolver = LinearSolver.PardisoMkl;  // Use Intel MKL Pardiso
model.Options.HessianApproximation = HessianApproximation.Exact;
model.Options.MuStrategy = MuStrategy.Adaptive;

// Configure termination criteria
model.Options.Tolerance = 1e-7;
model.Options.MaxIterations = 100;
model.Options.MaxWallTime = 60.0;  // seconds

// Configure output verbosity
model.Options.PrintLevel = 5;  // 0=no output, 5=default, 12=verbose
model.Options.OutputFile = "ipopt.log";

// Configure NLP scaling
model.Options.NlpScalingMethod = NlpScalingMethod.GradientBased;

// Use custom options for advanced features
model.Options.SetCustomOption("bound_push", 0.01);
model.Options.SetCustomOption("acceptable_tol", 1e-5);

// Define and solve your problem...
// ...
var result = model.Solve();

Available Linear Solvers

  • LinearSolver.Mumps - Default, included with IPOPT
  • LinearSolver.PardisoMkl - Intel MKL Pardiso, included with IPOPT
  • LinearSolver.PardisoProject - Pardiso from pardiso-project.org (often faster, requires external library)
  • LinearSolver.Ma27, Ma57, Ma77, Ma86, Ma97 - HSL solvers (require external library)
  • LinearSolver.Wsmp - Watson Sparse Matrix Package (requires external library)
  • LinearSolver.Spral - Sparse Parallel Robust Algorithms Library (requires external library)

Common Options

  • Termination: Tolerance, MaxIterations, MaxWallTime, MaxCpuTime
  • Output: PrintLevel, OutputFile, PrintUserOptions
  • Algorithm: LinearSolver, HessianApproximation, MuStrategy
  • Scaling: NlpScalingMethod, LinearSystemScaling
  • Tolerances: ConstraintViolationTolerance, DualInfeasibilityTolerance

Low-level API

For advanced users who want direct control over the IPOPT solver:

using IpoptNet;

// Define callback functions
EvalFCallback evalF = (n, x, newX, objValue, userData) =>
{
    *objValue = x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2];

    // Note: If a callback cannot be evaluated at a given point (e.g. division by zero), 
    // it should return false. IPOPT will then attempt to backtrack to a valid point.
    // If it cannot recover, the solve will terminate with InvalidNumberDetected.
    return true;
};

// Define gradient, constraint, Jacobian, and Hessian callbacks...

// Create solver
using var solver = new IpoptSolver(
    n: 4, xL, xU,
    m: 2, gL, gU,
    jacobianNonZeros, hessianNonZeros,
    evalF, evalGradF, evalG, evalJacG, evalH);

// Set options
solver.SetOption("print_level", 5);
solver.SetOption("tol", 1e-7);

// Solve
var x = new double[] { 1, 5, 5, 1 };
var status = solver.Solve(x, out var objValue);

Problem Formulation

IPOPT solves nonlinear optimization problems of the form:

minimize    f(x)
subject to  g_L ≤ g(x) ≤ g_U
            x_L ≤ x ≤ x_U

where:

  • f(x) is the objective function
  • g(x) are constraint functions
  • x are the optimization variables
  • Bounds can be infinite for unconstrained dimensions

References

License

This .NET wrapper is provided as-is. IPOPT itself is released under the Eclipse Public License (EPL).

Acknowledgments

IPOPT is developed and maintained by the COIN-OR project. This wrapper provides a convenient .NET interface with automatic differentiation capabilities.

The native binaries bundled with this package include statically-linked Intel oneAPI Math Kernel Library (MKL) components (mkl_intel_lp64, mkl_sequential, mkl_core) redistributed under the Intel Simplified Software License.

About

A modern .NET interface for IPOPT (Interior Point OPTimizer), a software library for large-scale nonlinear optimization. This library provides both a high-level modeling API with automatic differentiation and a low-level native wrapper.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages