diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3e4ae9..1fdb78e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index a238174..52d2750 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://www.nuget.org/packages/Anexia.MathematicalProgram)
[](https://github.com/anexia/dotnetcore-mathematical-program/actions/workflows/test.yml)
-[](https://codecov.io/github/Anexia/dotnetcore-mathematical-program/coverage.svg?branch=main)
+[](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
@@ -66,6 +66,42 @@ 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.
@@ -73,6 +109,3 @@ Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for m
## Licensing
This project is licensed under MIT License. See [LICENSE](LICENSE) for more information.
-
-
-
diff --git a/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj b/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj
index f75fa29..545f280 100644
--- a/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj
+++ b/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj
@@ -23,6 +23,7 @@
+
diff --git a/src/Anexia.MathematicalProgram/Model/Expression/Constraint.cs b/src/Anexia.MathematicalProgram/Model/Expression/Constraint.cs
index 918479b..18ddb85 100644
--- a/src/Anexia.MathematicalProgram/Model/Expression/Constraint.cs
+++ b/src/Anexia.MathematicalProgram/Model/Expression/Constraint.cs
@@ -22,13 +22,14 @@ public readonly record struct
IConstraint
where TVariable : IVariable
where TInterval : IAddableScalar
- where TVariableCoefficient : IAddableScalar
+ where TVariableCoefficient : IAddableScalar
{
internal Constraint(IWeightedSum weightedSum,
- IInterval interval)
+ IInterval interval, string? name = null)
{
WeightedSum = weightedSum;
Interval = interval;
+ Name = name;
}
///
@@ -41,7 +42,13 @@ internal Constraint(IWeightedSum wei
///
public IInterval Interval { get; }
+ ///
+ /// The constraint's name.
+ ///
+ public string? Name { get; }
+
///
[ExcludeFromCodeCoverage]
- public override string ToString() => $"{Interval.LowerBound} <= {WeightedSum} <= {Interval.UpperBound}";
+ public override string ToString() =>
+ $"{Name ?? ""}: {Interval.LowerBound} <= {WeightedSum} <= {Interval.UpperBound}";
}
\ No newline at end of file
diff --git a/src/Anexia.MathematicalProgram/Model/Expression/IConstraint.cs b/src/Anexia.MathematicalProgram/Model/Expression/IConstraint.cs
index 018a97a..0bb0c75 100644
--- a/src/Anexia.MathematicalProgram/Model/Expression/IConstraint.cs
+++ b/src/Anexia.MathematicalProgram/Model/Expression/IConstraint.cs
@@ -23,4 +23,9 @@ public interface IConstraint whe
/// The constraint's interval.
///
public IInterval Interval { get; }
+
+ ///
+ /// The constraint's name.
+ ///
+ public string? Name { get; }
}
\ No newline at end of file
diff --git a/src/Anexia.MathematicalProgram/Result/ResultHandling.cs b/src/Anexia.MathematicalProgram/Result/ResultHandling.cs
index f51d495..57b796f 100644
--- a/src/Anexia.MathematicalProgram/Result/ResultHandling.cs
+++ b/src/Anexia.MathematicalProgram/Result/ResultHandling.cs
@@ -11,6 +11,7 @@
using Anexia.MathematicalProgram.Solve;
using Google.OrTools.ModelBuilder;
using Google.OrTools.Sat;
+using Gurobi;
namespace Anexia.MathematicalProgram.Result;
@@ -60,6 +61,34 @@ internal static ISolverResult
};
}
+ internal static ISolverResult
+ Handle(int resultStatus,
+ bool switchedToDefaultSolver,
+ ISolutionValues? solutionValues = null,
+ double? objectiveValue = null,
+ double? bestBound = null) where TVariable : IVariable
+ where TVariableInterval : IAddableScalar
+ {
+ 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(
+ SolverResultStatus.Infeasible, switchedToDefaultSolver),
+ GRB.Status.UNBOUNDED => SolverResult(
+ SolverResultStatus.Unbounded, switchedToDefaultSolver),
+ GRB.Status.INTERRUPTED => SolverResult(
+ SolverResultStatus.CancelledByUser, switchedToDefaultSolver),
+ GRB.Status.INF_OR_UNBD => SolverResult(
+ SolverResultStatus.InfOrUnbound, switchedToDefaultSolver),
+ GRB.Status.TIME_LIMIT => SolverResult(
+ SolverResultStatus.Timelimit, switchedToDefaultSolver),
+ _ => throw new MathematicalProgramException($"Unknown result status in solver. {resultStatus}")
+ };
+ }
+
internal static ISolverResult Handle(CpSolverStatus resultStatus,
ISolutionValues? solutionValues = null,
@@ -90,6 +119,43 @@ internal static ISolverResult
};
}
+ internal static ISolverResult HandleGurobi(int resultStatus,
+ ISolutionValues? solutionValues = null,
+ double? objectiveValue = null,
+ double? bestBound = null) where TVariableInterval : IAddableScalar
+ where TVariable : IVariable
+ {
+ 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(SolverResultStatus.Unbounded, false),
+
+ _ => throw new MathematicalProgramException($"Unknown result status in solver. {resultStatus}")
+ };
+ }
+
private static ISolverResult
SolverResult(SolverResultStatus resultStatus,
bool switchedToDefaultSolver,
diff --git a/src/Anexia.MathematicalProgram/Result/SolverResultStatus.cs b/src/Anexia.MathematicalProgram/Result/SolverResultStatus.cs
index fc7b57a..d0e765c 100644
--- a/src/Anexia.MathematicalProgram/Result/SolverResultStatus.cs
+++ b/src/Anexia.MathematicalProgram/Result/SolverResultStatus.cs
@@ -24,5 +24,7 @@ public enum SolverResultStatus
ModelInvalid,
InvalidSolverParameters,
SolverTypeUnavailable,
- IncompatibleOptions
+ IncompatibleOptions,
+ InfOrUnbound,
+ Timelimit
}
\ No newline at end of file
diff --git a/src/Anexia.MathematicalProgram/Solve/GurobiNativeSolver.cs b/src/Anexia.MathematicalProgram/Solve/GurobiNativeSolver.cs
new file mode 100644
index 0000000..6f6af50
--- /dev/null
+++ b/src/Anexia.MathematicalProgram/Solve/GurobiNativeSolver.cs
@@ -0,0 +1,109 @@
+// ------------------------------------------------------------------------------------------
+//
+// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved.
+//
+// ------------------------------------------------------------------------------------------
+
+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? logger = null)
+ : MemberwiseEquatable,
+ IOptimizationSolver, IRealScalar, IRealScalar, RealScalar>
+{
+ public ISolverResult, RealScalar, IRealScalar> Solve(
+ ICompletedOptimizationModel, 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 or IntegerVariable or
+ IntegerVariable or IntegerVariable => gurobiModel.AddVar(
+ item.Interval.LowerBound.Value,
+ item.Interval.UpperBound.Value, 0, GRB.INTEGER, item.Name),
+ BinaryVariable or IntegerVariable => 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, RealScalar, IRealScalar>(gurobiModel.Status,
+ false);
+
+ var solutionValues = new SolutionValues, 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Anexia.MathematicalProgram/Solve/IlpSolver.cs b/src/Anexia.MathematicalProgram/Solve/IlpSolver.cs
index f24eeda..5edf7b3 100644
--- a/src/Anexia.MathematicalProgram/Solve/IlpSolver.cs
+++ b/src/Anexia.MathematicalProgram/Solve/IlpSolver.cs
@@ -26,7 +26,7 @@ public sealed class IlpSolver(
private ILogger? Logger { get; } = logger;
///
- /// 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.
///
/// The model to be solved.
/// Parameters to be passed to the underlying solver.
@@ -88,6 +88,24 @@ IntegerVariable or IntegerVariable or
configuredSolver.BestObjectiveBound);
}
+ ///
+ /// Solves the given optimization model directly using the specified solver API. Switches solver to specified default solver, when the given type is not available.
+ ///
+ /// The model to be solved.
+ /// Parameters to be passed to the underlying solver.
+ /// Solver result containing solution information.
+ public ISolverResult, RealScalar, IRealScalar> SolveWithoutORTools(
+ ICompletedOptimizationModel, 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")
+ };
+
///
/// Solves the given model by minimizing the objective function.
///
diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs
index 7731586..c834ed4 100644
--- a/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs
+++ b/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs
@@ -16,15 +16,15 @@ namespace Anexia.MathematicalProgram.SolverConfiguration;
/// Time limit of the solving process.
/// The number of threads that should be used by the solver.
/// The relative gap when the solver should terminate.
-/// Additional solver specific parameters (key-value string, use key:value for GLOP, key=value for other supported solvers) to pass to the solver. The correct format for the desired solver
-/// must be used. Check corresponding solver documentations to be sure.
+/// Additional solver specific parameters (key-value string pairs) to pass to the solver. The correct format for the desired solver
+/// must be used. Check corresponding solver documentations to be sure. E.g., Gurobi parameters can be found here: https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#secparameterreference
/// The file path and name of a file where the model should be written to.
public record SolverParameter(
EnableSolverOutput EnableSolverOutput,
RelativeGap? RelativeGap = null,
TimeLimitInMilliseconds? TimeLimitInMilliseconds = null,
NumberOfThreads? NumberOfThreads = null,
- IReadOnlyCollection? AdditionalSolverSpecificParameters = null,
+ IReadOnlyCollection<(string Key, string Value)>? AdditionalSolverSpecificParameters = null,
string? ExportModelFilePath = null)
{
private const string RelativeGapKey = "RELATIVE_GAP";
@@ -109,20 +109,23 @@ public SolverParameter()
{
}
- internal string ToSolverSpecificParameters(IlpSolverType solverType)
+ internal List<(string Key, string Value)> ToSolverSpecificParametersList(IlpSolverType solverType)
{
- var parameters = new List();
+ var parameters = new List<(string Key, string Value)>();
if (NumberOfThreads is not null)
- parameters.Add(
- $"{IlpParameterKeyMapping[(solverType, NumberOfThreadsKey)]}={NumberOfThreads.Value}");
+ parameters.Add((IlpParameterKeyMapping[(solverType, NumberOfThreadsKey)],
+ NumberOfThreads.Value.ToString(CultureInfo.InvariantCulture)));
if (RelativeGap is not null)
- parameters.Add(
- $"{IlpParameterKeyMapping[(solverType, RelativeGapKey)]}={RelativeGap.Value.ToString(CultureInfo.InvariantCulture)}");
+ parameters.Add((IlpParameterKeyMapping[(solverType, RelativeGapKey)],
+ RelativeGap.Value.ToString(CultureInfo.InvariantCulture)));
if (AdditionalSolverSpecificParameters is not null) parameters.AddRange(AdditionalSolverSpecificParameters);
- return string.Join(',', parameters);
+ return parameters;
}
+ internal string ToSolverSpecificParameters(IlpSolverType solverType) => string.Join(',',
+ ToSolverSpecificParametersList(solverType).Select(parameter => $"{parameter.Key}={parameter.Value}"));
+
internal string ToSolverSpecificParameters(LpSolverType solverType)
{
var parameters = new List();
@@ -130,7 +133,9 @@ internal string ToSolverSpecificParameters(LpSolverType solverType)
parameters.Add(
$"{LpParameterKeyMapping[(solverType, NumberOfThreadsKey)]}{LpKeyValueSeparators[solverType]}{NumberOfThreads.Value}");
- if (AdditionalSolverSpecificParameters is not null) parameters.AddRange(AdditionalSolverSpecificParameters);
+ if (AdditionalSolverSpecificParameters is not null)
+ parameters.AddRange(AdditionalSolverSpecificParameters.Select(parameter =>
+ $"{parameter.Key}{LpKeyValueSeparators[solverType]}{parameter.Value}"));
return string.Join(',', parameters);
}
diff --git a/test/Anexia.MathematicalProgram.Tests/Solve/GurobiNativeSolverTest.cs b/test/Anexia.MathematicalProgram.Tests/Solve/GurobiNativeSolverTest.cs
new file mode 100644
index 0000000..4ac7383
--- /dev/null
+++ b/test/Anexia.MathematicalProgram.Tests/Solve/GurobiNativeSolverTest.cs
@@ -0,0 +1,180 @@
+// ------------------------------------------------------------------------------------------
+//
+// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved.
+//
+// ------------------------------------------------------------------------------------------
+
+
+using System.Collections.ObjectModel;
+using Anexia.MathematicalProgram.Model;
+using Anexia.MathematicalProgram.Model.Interval;
+using Anexia.MathematicalProgram.Model.Scalar;
+using Anexia.MathematicalProgram.Model.Variable;
+using Anexia.MathematicalProgram.Result;
+using Anexia.MathematicalProgram.Solve;
+using Anexia.MathematicalProgram.SolverConfiguration;
+using static Anexia.MathematicalProgram.Tests.Factory.IntervalFactory;
+using static Anexia.MathematicalProgram.Tests.Factory.SolutionValuesFactory;
+using static Anexia.MathematicalProgram.Tests.Factory.SolverResultFactory;
+
+
+namespace Anexia.MathematicalProgram.Tests.Solve;
+
+public sealed class GurobiNativeSolverTest
+{
+ [Fact(Skip = "Licence needed")]
+ public void SolverWithSimpleFeasibleIlpModelReturnsCorrectResult()
+ {
+ /*
+ * min 2x, s.t. x=1, x binary
+ */
+
+ var model =
+ new OptimizationModel, IRealScalar, IRealScalar>();
+ var v1 = model.NewVariable>(Interval(1, 1), "TestVariable");
+
+
+ var optimizationModel =
+ model.SetObjective(
+ model.CreateObjectiveFunctionBuilder().AddTermToSum(new IntegerScalar(2), v1).Build(false));
+
+ var result = new IlpSolver(IlpSolverType.GurobiIntegerProgramming).SolveWithoutORTools(optimizationModel,
+ new SolverParameter(new EnableSolverOutput(true)));
+
+ Assert.Equal(
+ SolverResult(
+ SolutionValues, RealScalar, IRealScalar>(
+ (v1, new RealScalar(1))), new ObjectiveValue(2), new IsFeasible(true),
+ new IsOptimal(true), new OptimalityGap(0),
+ SolverResultStatus.Optimal, false), result);
+ }
+
+ [Fact(Skip = "Licence needed")]
+ public void SolverWithSimpleFeasibleBinaryIlpModelReturnsCorrectResult()
+ {
+ /*
+ * max 2x, x binary
+ */
+
+ var model =
+ new OptimizationModel, IRealScalar, IRealScalar>();
+ var v1 = model.NewBinaryVariable("TestVariable");
+
+ var optimizationModel =
+ model.SetObjective(
+ model.CreateObjectiveFunctionBuilder().AddTermToSum(new IntegerScalar(2), v1).Build());
+
+ var result = new IlpSolver(IlpSolverType.GurobiIntegerProgramming).SolveWithoutORTools(optimizationModel,
+ new SolverParameter(new EnableSolverOutput(true)));
+
+ Assert.Equal(
+ SolverResult(
+ SolutionValues, RealScalar, IRealScalar>(
+ (v1, new RealScalar(1))), new ObjectiveValue(2), new IsFeasible(true),
+ new IsOptimal(true), new OptimalityGap(0),
+ SolverResultStatus.Optimal, false), result);
+ }
+
+ [Fact(Skip = "Licence needed")]
+ public void SolverWithInfeasibleIlModelReturnsCorrectResult()
+ {
+ /*
+ * max 2x, s.t. x=3, x binary
+ */
+
+ var model =
+ new OptimizationModel, IRealScalar, IRealScalar>();
+ var x = model.NewVariable>(Interval(0, 1), "c");
+
+
+ model.AddConstraint(model.CreateConstraintBuilder()
+ .AddTermToSum(new IntegerScalar(1), x).Build(Point(3)));
+
+
+ var optimizationModel =
+ model.SetObjective(model.CreateObjectiveFunctionBuilder().AddTermToSum(new IntegerScalar(2), x)
+ .Build());
+
+
+ var result = new IlpSolver(IlpSolverType.GurobiIntegerProgramming).SolveWithoutORTools(optimizationModel,
+ new SolverParameter(
+ EnableSolverOutput.True,
+ RelativeGap.EMinus7,
+ null,
+ new NumberOfThreads(2)));
+
+
+ Assert.Equal(
+ SolverResult(
+ new SolutionValues, RealScalar, IRealScalar>(
+ ReadOnlyDictionary, RealScalar>.Empty), null, new IsFeasible(false),
+ new IsOptimal(false), null,
+ SolverResultStatus.Infeasible, false), result);
+ }
+
+ [Fact(Skip = "Licence needed")]
+ public void SolverWithUnboundedIlModelReturnsCorrectResult()
+ {
+ /*
+ * max 2x, x positive
+ */
+
+ var model = new OptimizationModel, IRealScalar, IRealScalar>();
+ var x = model.NewVariable>(Interval(0, double.PositiveInfinity), "x");
+
+ var optimizationModel =
+ model.SetObjective(model.CreateObjectiveFunctionBuilder().AddTermToSum(new IntegerScalar(2), x)
+ .Build());
+
+ var result = new IlpSolver(IlpSolverType.GurobiIntegerProgramming).SolveWithoutORTools(optimizationModel,
+ new SolverParameter());
+
+ Assert.Equal(
+ SolverResult(
+ new SolutionValues, RealScalar, IRealScalar>(
+ ReadOnlyDictionary, RealScalar>.Empty), null, new IsFeasible(false),
+ new IsOptimal(false), null,
+ SolverResultStatus.Unbounded, false), result);
+ }
+
+ [Fact(Skip = "Licence needed")]
+ public void GurobiWithoutORToolsGivesSameResultAsWithORTools()
+ {
+ var model =
+ new OptimizationModel, RealScalar, IRealScalar>();
+
+ var x = model.NewVariable>(
+ new IntegralInterval(new IntegerScalar(1), new IntegerScalar(3)), "x");
+ var y = model.NewVariable>(
+ new IntegralInterval(0, 1), "y");
+ var xMinusY = model.CreateWeightedSumBuilder()
+ .AddWeightedSum([x, y], [1, -1]).Build();
+
+ var constraint = model.CreateConstraintBuilder()
+ .AddWeightedSum(xMinusY)
+ .Build(new RealInterval(0, double.PositiveInfinity));
+
+ model.AddConstraint(constraint);
+
+ var objFunction = model.CreateObjectiveFunctionBuilder().AddTermToSum(2, x)
+ .AddTermToSum(2, y).Build(false);
+
+ var optimizationModel = model.SetObjective(objFunction);
+
+ var resultORTools = SolverFactory.SolverFor(IlpSolverType.GurobiIntegerProgramming).Solve(optimizationModel,
+ new SolverParameter(new EnableSolverOutput(false), RelativeGap.EMinus7,
+ new TimeLimitInMilliseconds(10000), new NumberOfThreads(2), AdditionalSolverSpecificParameters:
+ [
+ ("ResultFile", "resultOR.sol")
+ ]));
+
+
+ var resultGurobiAPI = new IlpSolver(IlpSolverType.GurobiIntegerProgramming).SolveWithoutORTools(
+ optimizationModel,
+ new SolverParameter(new EnableSolverOutput(false), RelativeGap.EMinus7,
+ new TimeLimitInMilliseconds(10000), new NumberOfThreads(2),
+ AdditionalSolverSpecificParameters: [("ResultFile", "resultGRB.sol")]));
+
+ Assert.Equal(resultORTools, resultGurobiAPI);
+ }
+}
\ No newline at end of file