From a9068aee695b340ada0608d40018a078ecdbdb5c Mon Sep 17 00:00:00 2001 From: Manuel57 Date: Fri, 17 Oct 2025 11:35:26 +0200 Subject: [PATCH] SIANXKE-473: implement solver that directly uses the Gurobi API --- CHANGELOG.md | 7 + README.md | 41 +++- .../Anexia.MathematicalProgram.csproj | 1 + .../Model/Expression/Constraint.cs | 13 +- .../Model/Expression/IConstraint.cs | 5 + .../Result/ResultHandling.cs | 66 +++++++ .../Result/SolverResultStatus.cs | 4 +- .../Solve/GurobiNativeSolver.cs | 109 +++++++++++ .../Solve/IlpSolver.cs | 20 +- .../SolverConfiguration/SolverParameter.cs | 27 +-- .../Solve/GurobiNativeSolverTest.cs | 180 ++++++++++++++++++ 11 files changed, 453 insertions(+), 20 deletions(-) create mode 100644 src/Anexia.MathematicalProgram/Solve/GurobiNativeSolver.cs create mode 100644 test/Anexia.MathematicalProgram.Tests/Solve/GurobiNativeSolverTest.cs 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://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 @@ -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