Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added possibility to use Gurobi Solver directly via the API, not using Google OR-Tools.

### Changed
- Usage of AdditionalSolverParmateters. Changed list of string to list of key-value pairs.

## [2.7.0] - 2025-xx-xx

### Added
Expand Down
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![](https://img.shields.io/nuget/v/Anexia.MathematicalProgram "NuGet version badge")](https://www.nuget.org/packages/Anexia.MathematicalProgram)
[![](https://github.com/anexia/dotnetcore-mathematical-program/actions/workflows/test.yml/badge.svg?branch=main "Test status")](https://github.com/anexia/dotnetcore-mathematical-program/actions/workflows/test.yml)
[![codecov.io](https://codecov.io/github/Anexia/dotnetcore-mathematical-program/coverage.svg?branch=main "Code coverage")](https://codecov.io/github/Anexia/dotnetcore-mathematical-program/coverage.svg?branch=main)
[![codecov.io](https://codecov.io/github/Anexia/dotnetcore-mathematical-program/coverage.svg?branch=main "Code coverage")](https://codecov.io/github/anexia/dotnetcore-mathematical-program/coverage.svg?branch=main)
This library allows you to build and solve linear programs and integer linear programs in a very handy way.
For linear programs, either [SCIP](https://www.scipopt.org/) or Google's [GLOP](https://developers.google.com/optimization/lp/lp_example) solver can be used.
For integer linear programs, SCIP, Gurobi and the Coin-OR CBC branch and cut
Expand Down Expand Up @@ -66,13 +66,46 @@ var result = SolverFactory.SolverFor(IlpSolverType.Scip).Solve(optimizationModel

Further detailed examples can be found in the [examples folder](examples).

## Solver parameters (SolverParameter)

You can control solver behavior using the SolverParameter record in Anexia.MathematicalProgram.SolverConfiguration. Common fields:

- EnableSolverOutput: toggles solver console logs.
- TimeLimitInMilliseconds: overall time limit.
- NumberOfThreads: caps thread usage when supported by the solver.
- RelativeGap: early stopping gap (when supported by the solver).
- AdditionalSolverSpecificParameters: extra key/value pairs passed straight to the underlying solver.
- ExportModelFilePath: path to export the model (MPS or solver-specific format depending on backend).

Examples:

Use with native Gurobi API (GurobiNativeSolver):
```
var native = new GurobiNativeSolver();
var result = native.Solve(optimizationModel,
new SolverParameter(
new EnableSolverOutput(true),
NumberOfThreads: new NumberOfThreads(8),
RelativeGap: RelativeGap.EMinus7,
AdditionalSolverSpecificParameters: new[]
{
("MIPFocus", "1"),
("Heuristics", "0.05")
},
ExportModelFilePath: "model.mps"
)
);
```

Notes:
- For Gurobi parameters, see https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#secparameterreference
- The AdditionalSolverSpecificParameters are forwarded as-is.
- NumberOfThreads, TimeLimitInMilliseconds, and RelativeGap is mapped to the solver’s native time limit.

## Contributing

Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information.

## Licensing

This project is licensed under MIT License. See [LICENSE](LICENSE) for more information.



Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

<ItemGroup>
<PackageReference Update="Google.OrTools" Version="9.14.6206" />
<PackageReference Include="Gurobi.Optimizer" Version="12.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
</ItemGroup>

Expand Down
13 changes: 10 additions & 3 deletions src/Anexia.MathematicalProgram/Model/Expression/Constraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ public readonly record struct
IConstraint<TVariable, TVariableCoefficient, TInterval>
where TVariable : IVariable<TInterval>
where TInterval : IAddableScalar<TInterval, TInterval>
where TVariableCoefficient : IAddableScalar<TVariableCoefficient,TVariableCoefficient>
where TVariableCoefficient : IAddableScalar<TVariableCoefficient, TVariableCoefficient>
{
internal Constraint(IWeightedSum<TVariable, TVariableCoefficient, TInterval> weightedSum,
IInterval<TInterval> interval)
IInterval<TInterval> interval, string? name = null)
{
WeightedSum = weightedSum;
Interval = interval;
Name = name;
}

/// <summary>
Expand All @@ -41,7 +42,13 @@ internal Constraint(IWeightedSum<TVariable, TVariableCoefficient, TInterval> wei
/// </summary>
public IInterval<TInterval> Interval { get; }

/// <summary>
/// The constraint's name.
/// </summary>
public string? Name { get; }

/// <inheritdoc/>
[ExcludeFromCodeCoverage]
public override string ToString() => $"{Interval.LowerBound} <= {WeightedSum} <= {Interval.UpperBound}";
public override string ToString() =>
$"{Name ?? ""}: {Interval.LowerBound} <= {WeightedSum} <= {Interval.UpperBound}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ public interface IConstraint<out TVariable, out TCoefficient, out TInterval> whe
/// The constraint's interval.
/// </summary>
public IInterval<TInterval> Interval { get; }

/// <summary>
/// The constraint's name.
/// </summary>
public string? Name { get; }
}
66 changes: 66 additions & 0 deletions src/Anexia.MathematicalProgram/Result/ResultHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Anexia.MathematicalProgram.Solve;
using Google.OrTools.ModelBuilder;
using Google.OrTools.Sat;
using Gurobi;

namespace Anexia.MathematicalProgram.Result;

Expand Down Expand Up @@ -60,6 +61,34 @@ internal static ISolverResult<TVariable, TCoefficient, TVariableInterval>
};
}

internal static ISolverResult<TVariable, TCoefficient, TVariableInterval>
Handle<TVariable, TCoefficient, TVariableInterval>(int resultStatus,
bool switchedToDefaultSolver,
ISolutionValues<TVariable, TCoefficient, TVariableInterval>? solutionValues = null,
double? objectiveValue = null,
double? bestBound = null) where TVariable : IVariable<TVariableInterval>
where TVariableInterval : IAddableScalar<TVariableInterval, TVariableInterval>
{
return resultStatus switch
{
GRB.Status.OPTIMAL => objectiveValue is null
? throw new MathematicalProgramException("Mathematical program could not be solved.")
: SolverResult(SolverResultStatus.Optimal, switchedToDefaultSolver, solutionValues, objectiveValue,
bestBound, true, true),
GRB.Status.INFEASIBLE => SolverResult<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.Infeasible, switchedToDefaultSolver),
GRB.Status.UNBOUNDED => SolverResult<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.Unbounded, switchedToDefaultSolver),
GRB.Status.INTERRUPTED => SolverResult<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.CancelledByUser, switchedToDefaultSolver),
GRB.Status.INF_OR_UNBD => SolverResult<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.InfOrUnbound, switchedToDefaultSolver),
GRB.Status.TIME_LIMIT => SolverResult<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.Timelimit, switchedToDefaultSolver),
_ => throw new MathematicalProgramException($"Unknown result status in solver. {resultStatus}")
};
}

internal static ISolverResult<TVariable, TCoefficient, TVariableInterval> Handle<TVariable, TCoefficient,
TVariableInterval>(CpSolverStatus resultStatus,
ISolutionValues<TVariable, TCoefficient, TVariableInterval>? solutionValues = null,
Expand Down Expand Up @@ -90,6 +119,43 @@ internal static ISolverResult<TVariable, TCoefficient, TVariableInterval>
};
}

internal static ISolverResult<TVariable, TCoefficient, TVariableInterval> HandleGurobi<TVariable, TCoefficient,
TVariableInterval>(int resultStatus,
ISolutionValues<TVariable, TCoefficient, TVariableInterval>? solutionValues = null,
double? objectiveValue = null,
double? bestBound = null) where TVariableInterval : IAddableScalar<TVariableInterval, TVariableInterval>
where TVariable : IVariable<TVariableInterval>
{
return resultStatus switch
{
GRB.Status.OPTIMAL => objectiveValue is null || bestBound is null
? throw new MathematicalProgramException("Mathematical program could not be solved.")
: SolverResult(SolverResultStatus.Optimal, false, solutionValues, objectiveValue,
bestBound, true, true),
GRB.Status.SUBOPTIMAL => objectiveValue is null || bestBound is null
? throw new MathematicalProgramException("Mathematical program could not be solved.")
: SolverResult(SolverResultStatus.Feasible, false, solutionValues, objectiveValue,
bestBound, true),
GRB.Status.TIME_LIMIT => objectiveValue is null || bestBound is null
? throw new MathematicalProgramException("Mathematical program could not be solved.")
: SolverResult(SolverResultStatus.Timelimit, false, solutionValues, objectiveValue,
bestBound, true),
GRB.Status.INTERRUPTED => objectiveValue is null || bestBound is null
? throw new MathematicalProgramException("Mathematical program could not be solved.")
: SolverResult(SolverResultStatus.CancelledByUser, false, solutionValues, objectiveValue,
bestBound, true),
GRB.Status.MEM_LIMIT => objectiveValue is null || bestBound is null
? throw new MathematicalProgramException("Mathematical program could not be solved.")
: SolverResult(SolverResultStatus.UnknownStatus, false, solutionValues, objectiveValue,
bestBound, true),
GRB.Status.UNBOUNDED => objectiveValue is null || bestBound is null
? throw new MathematicalProgramException("Mathematical program could not be solved.")
: SolverResult<TVariable, TCoefficient, TVariableInterval>(SolverResultStatus.Unbounded, false),

_ => throw new MathematicalProgramException($"Unknown result status in solver. {resultStatus}")
};
}

private static ISolverResult<TVariable, TCoefficient, TVariableInterval>
SolverResult<TVariable, TCoefficient, TVariableInterval>(SolverResultStatus resultStatus,
bool switchedToDefaultSolver,
Expand Down
4 changes: 3 additions & 1 deletion src/Anexia.MathematicalProgram/Result/SolverResultStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ public enum SolverResultStatus
ModelInvalid,
InvalidSolverParameters,
SolverTypeUnavailable,
IncompatibleOptions
IncompatibleOptions,
InfOrUnbound,
Timelimit
}
109 changes: 109 additions & 0 deletions src/Anexia.MathematicalProgram/Solve/GurobiNativeSolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// ------------------------------------------------------------------------------------------
// <copyright file = "GurobiNativeSolver.cs" company = "ANEXIA® Internetdienstleistungs GmbH">
// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved.
// </copyright>
// ------------------------------------------------------------------------------------------

using Anexia.MathematicalProgram.Model;
using Anexia.MathematicalProgram.Model.Scalar;
using Anexia.MathematicalProgram.Model.Variable;
using Anexia.MathematicalProgram.Result;
using Anexia.MathematicalProgram.SolverConfiguration;
using Gurobi;
using Microsoft.Extensions.Logging;

namespace Anexia.MathematicalProgram.Solve;

public sealed class GurobiNativeSolver(
ILogger<IlpSolver>? logger = null)
: MemberwiseEquatable<IlpSolver>,
IOptimizationSolver<IIntegerVariable<IRealScalar>, IRealScalar, IRealScalar, RealScalar>
{
public ISolverResult<IIntegerVariable<IRealScalar>, RealScalar, IRealScalar> Solve(
ICompletedOptimizationModel<IIntegerVariable<IRealScalar>, IRealScalar, IRealScalar> model,
SolverParameter solverParameter)
{
try
{
var env = new GRBEnv(true);
foreach (var (key, value) in solverParameter.ToSolverSpecificParametersList(IlpSolverType
.GurobiIntegerProgramming))
{
env.Set(key, value);
}

if (solverParameter.TimeLimitInMilliseconds is not null)
env.TimeLimit = solverParameter.TimeLimitInMilliseconds.AsSeconds;

env.LogToConsole = solverParameter.EnableSolverOutput.Value ? 1 : 0;

env.Start();
var gurobiModel = new GRBModel(env);

var variables = model.Variables.ToDictionary(
item => item, item => item switch
{
IntegerVariable<IRealScalar> or IntegerVariable<IIntegerScalar> or
IntegerVariable<RealScalar> or IntegerVariable<IntegerScalar> => gurobiModel.AddVar(
item.Interval.LowerBound.Value,
item.Interval.UpperBound.Value, 0, GRB.INTEGER, item.Name),
BinaryVariable or IntegerVariable<IBinaryScalar> => gurobiModel.AddVar(
item.Interval.LowerBound.Value,
item.Interval.UpperBound.Value, 0, GRB.BINARY, item.Name),
_ => throw new ArgumentOutOfRangeException(nameof(item), item, "Variable type not supported.")
});

var constraintNumber = 1;
foreach (var constraint in model.Constraints)
{
var termsExpression = constraint.WeightedSum
.Aggregate(
new GRBLinExpr(), (expression, term) =>
{
expression.AddTerm(term.Coefficient.Value, variables[term.Variable]);
return expression;
});

gurobiModel.AddConstr(constraint.Interval.LowerBound.Value, GRB.LESS_EQUAL, termsExpression,
constraint.Name ?? $"{constraintNumber++}");
gurobiModel.AddConstr(termsExpression, GRB.LESS_EQUAL, constraint.Interval.UpperBound.Value,
constraint.Name ?? $"{constraintNumber++}");
}

gurobiModel.SetObjective(model.ObjectiveFunction.WeightedSum
.Aggregate(
new GRBLinExpr(model.ObjectiveFunction.Offset?.Value ?? 0), (expression, term) =>
{
expression.AddTerm(term.Coefficient.Value, variables[term.Variable]);
return expression;
}), model.ObjectiveFunction.Maximize ? GRB.MAXIMIZE : GRB.MINIMIZE);

gurobiModel.Optimize();

if (solverParameter.ExportModelFilePath is not null)
{
logger?.LogInformation("Exporting model to {ExportModelFilePath}", solverParameter.ExportModelFilePath);

gurobiModel.Write(solverParameter.ExportModelFilePath);
}

if (gurobiModel.SolCount == 0)
return ResultHandling.Handle<IIntegerVariable<IRealScalar>, RealScalar, IRealScalar>(gurobiModel.Status,
false);

var solutionValues = new SolutionValues<IIntegerVariable<IRealScalar>, RealScalar, IRealScalar>(
variables.ToDictionary(
variable => variable.Key,
variable => new RealScalar(gurobiModel.GetVarByName(variable.Key.Name).X)).AsReadOnly());

return ResultHandling.HandleGurobi(gurobiModel.Status,
solutionValues, gurobiModel.ObjVal,
gurobiModel.ObjBound);
}
catch (Exception exception)
{
logger?.LogError(exception, "An error occurred during solving the model: {EMessage}", exception.Message);
throw new MathematicalProgramException(exception);
}
}
}
20 changes: 19 additions & 1 deletion src/Anexia.MathematicalProgram/Solve/IlpSolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
private ILogger<IlpSolver>? Logger { get; } = logger;

/// <summary>
/// Solves the given optimization model. Switches solver to SCIP, then the given type is not available.
/// Solves the given optimization model. Switches solver to SCIP, when the given type is not available.
/// </summary>
/// <param name="completedOptimizationModel">The model to be solved.</param>
/// <param name="solverParameter">Parameters to be passed to the underlying solver.</param>
/// <returns>Solver result containing solution information.</returns>
public ISolverResult<IIntegerVariable<IRealScalar>, RealScalar, IRealScalar> Solve(

Check warning on line 34 in src/Anexia.MathematicalProgram/Solve/IlpSolver.cs

View workflow job for this annotation

GitHub Actions / test

All 'Solve' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)
ICompletedOptimizationModel<IIntegerVariable<IRealScalar>, IRealScalar, IRealScalar>
completedOptimizationModel,
SolverParameter solverParameter)
Expand All @@ -40,8 +40,8 @@

if (configuredSolver is null)
{
return FallbackSolver == IlpSolverType.CbcIntegerProgramming

Check warning on line 43 in src/Anexia.MathematicalProgram/Solve/IlpSolver.cs

View workflow job for this annotation

GitHub Actions / test

'IlpSolverType.CbcIntegerProgramming' is obsolete: 'CBC Solver should not be used anymore, switch to SCIP instead.'
? new IlpCbcSolver().Solve(completedOptimizationModel, solverParameter)

Check warning on line 44 in src/Anexia.MathematicalProgram/Solve/IlpSolver.cs

View workflow job for this annotation

GitHub Actions / test

'IlpCbcSolver' is obsolete: 'CBC Solver is not supported anymore, please switch to other Solvers.'
: new SolverResult<IIntegerVariable<IRealScalar>, RealScalar, IRealScalar>();
}

Expand Down Expand Up @@ -88,6 +88,24 @@
configuredSolver.BestObjectiveBound);
}

/// <summary>
/// Solves the given optimization model directly using the specified solver API. Switches solver to specified default solver, when the given type is not available.
/// </summary>
/// <param name="completedOptimizationModel">The model to be solved.</param>
/// <param name="solverParameter">Parameters to be passed to the underlying solver.</param>
/// <returns>Solver result containing solution information.</returns>
public ISolverResult<IIntegerVariable<IRealScalar>, RealScalar, IRealScalar> SolveWithoutORTools(
ICompletedOptimizationModel<IIntegerVariable<IRealScalar>, IRealScalar, IRealScalar>
completedOptimizationModel,
SolverParameter solverParameter) =>
SolverType switch
{
IlpSolverType.GurobiIntegerProgramming => new GurobiNativeSolver().Solve(completedOptimizationModel,
solverParameter),
_ => throw new NotImplementedException(
"The specified type is not yet implemented. Use OR Tools for solving")
};

/// <summary>
/// Solves the given model by minimizing the objective function.
/// </summary>
Expand Down Expand Up @@ -155,7 +173,7 @@

solverWasSwitched = true;

if (FallbackSolver == IlpSolverType.CbcIntegerProgramming)

Check warning on line 176 in src/Anexia.MathematicalProgram/Solve/IlpSolver.cs

View workflow job for this annotation

GitHub Actions / test

'IlpSolverType.CbcIntegerProgramming' is obsolete: 'CBC Solver should not be used anymore, switch to SCIP instead.'
return (null, true);

configuredSolver = new Solver(FallbackSolver.ToEnumString());
Expand Down
Loading
Loading