diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2a84e79..154bad1 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -3,6 +3,7 @@ on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
+ - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+"
jobs:
deploy:
@@ -23,7 +24,7 @@ jobs:
run: |
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
git branch --remote --contains | grep origin/main
-
+
- name: Set VERSION variable from tag
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3ae50b8..3e23e7b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,10 +6,10 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Setup .NET SDKs
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: |
6.0.x
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a75cd2..e3d59ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,11 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
-## [2.8.0] - 2026-07-10
+## [2.9.0] - 2026-07-10
### Added
-- Update Version of Google OR Tools to 9.15.
+- 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-11-10
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/examples/Anexia.MathematicalProgram.Examples/Anexia.MathematicalProgram.Examples.csproj b/examples/Anexia.MathematicalProgram.Examples/Anexia.MathematicalProgram.Examples.csproj
index d5238f3..dfd29d8 100644
--- a/examples/Anexia.MathematicalProgram.Examples/Anexia.MathematicalProgram.Examples.csproj
+++ b/examples/Anexia.MathematicalProgram.Examples/Anexia.MathematicalProgram.Examples.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/examples/Anexia.MathematicalProgram.Examples/IlpExample.cs b/examples/Anexia.MathematicalProgram.Examples/IlpExample.cs
index 80db527..2bea47a 100644
--- a/examples/Anexia.MathematicalProgram.Examples/IlpExample.cs
+++ b/examples/Anexia.MathematicalProgram.Examples/IlpExample.cs
@@ -49,7 +49,7 @@ public static void Main()
// Create SCIP Solver and solve model. Different settings can be set via out parameters.
var result = SolverFactory.SolverFor(IlpSolverType.HiGhs).Solve(optimizationModel,
new SolverParameter(new EnableSolverOutput(false), RelativeGap.EMinus7,
- new TimeLimitInMilliseconds(10000), new NumberOfThreads(2), ExportModelFilePath: "model.txt"));
+ new TimeLimitInMilliseconds(10000), new NumberOfThreads(2), ExportModelFilePaths: "model.txt"));
Console.WriteLine(result);
// Output: ObjectiveValue: 2, IsFeasible: True, IsOptimal: True, OptimalityGap: 0
diff --git a/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj b/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj
index f7aca4f..733f992 100644
--- a/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj
+++ b/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj
@@ -22,7 +22,8 @@
-
+
+
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/ISolverResult.cs b/src/Anexia.MathematicalProgram/Result/ISolverResult.cs
index c5b1b3e..9a5374f 100644
--- a/src/Anexia.MathematicalProgram/Result/ISolverResult.cs
+++ b/src/Anexia.MathematicalProgram/Result/ISolverResult.cs
@@ -25,7 +25,7 @@ public interface ISolverResult
///
/// Indicates whether a solution to the optimization problem is feasible.
///
- IsFeasible IsFeasible { get; }
+ IsFeasible? IsFeasible { get; }
///
/// Indicates whether the solution to the optimization problem is optimal.
diff --git a/src/Anexia.MathematicalProgram/Result/ResultHandling.cs b/src/Anexia.MathematicalProgram/Result/ResultHandling.cs
index f51d495..7096eec 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;
@@ -35,7 +36,7 @@ internal static ISolverResult
: SolverResult(SolverResultStatus.Feasible, switchedToDefaultSolver, solutionValues, objectiveValue,
bestBound, true),
SolveStatus.INFEASIBLE => SolverResult(
- SolverResultStatus.Infeasible, switchedToDefaultSolver),
+ SolverResultStatus.Infeasible, switchedToDefaultSolver, isFeasible: false),
SolveStatus.UNBOUNDED => SolverResult(
SolverResultStatus.Unbounded, switchedToDefaultSolver),
SolveStatus.ABNORMAL => SolverResult(
@@ -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, isFeasible: false),
+ 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,
@@ -79,7 +108,7 @@ internal static ISolverResult
bestBound, true),
CpSolverStatus.Infeasible =>
SolverResult(SolverResultStatus.Infeasible,
- false),
+ false, isFeasible: false),
CpSolverStatus.Unknown => SolverResult(
SolverResultStatus.UnknownStatus,
false),
@@ -90,11 +119,50 @@ internal static ISolverResult
};
}
+ internal static ISolverResult HandleGurobi(int resultStatus,
+ ISolutionValues? solutionValues = null,
+ double? objectiveValue = null,
+ double? bestBound = null) where TVariableInterval : IAddableScalar
+ where TVariable : IVariable
+ {
+ if (resultStatus == GRB.Status.INFEASIBLE)
+ {
+ return SolverResult(
+ SolverResultStatus.Infeasible, false, isFeasible: false);
+ }
+
+ if (objectiveValue is null || bestBound is null)
+ {
+ throw new MathematicalProgramException("Mathematical program could not be solved.");
+ }
+
+ return resultStatus switch
+ {
+ GRB.Status.OPTIMAL => SolverResult(SolverResultStatus.Optimal, false, solutionValues, objectiveValue,
+ bestBound, true, true),
+ GRB.Status.SUBOPTIMAL => SolverResult(SolverResultStatus.Feasible, false, solutionValues, objectiveValue,
+ bestBound, true),
+ GRB.Status.TIME_LIMIT => SolverResult(SolverResultStatus.Timelimit, false, solutionValues, objectiveValue,
+ bestBound, true),
+ GRB.Status.INTERRUPTED => SolverResult(SolverResultStatus.CancelledByUser, false, solutionValues,
+ objectiveValue,
+ bestBound, true),
+ GRB.Status.MEM_LIMIT => SolverResult(SolverResultStatus.UnknownStatus, false, solutionValues,
+ objectiveValue,
+ bestBound, true),
+ GRB.Status.UNBOUNDED => SolverResult(
+ SolverResultStatus.Unbounded, false),
+
+ _ => throw new MathematicalProgramException($"Unknown result status in solver. {resultStatus}")
+ };
+ }
+
private static ISolverResult
SolverResult(SolverResultStatus resultStatus,
bool switchedToDefaultSolver,
ISolutionValues? solutionValues = null,
- double? objectiveValue = null, double? bestBound = null, bool isFeasible = false, bool isOptimal = false)
+ double? objectiveValue = null, double? bestBound = null, bool? isFeasible = null, bool isOptimal = false)
where TVariable : IVariable
where TVariableInterval : IAddableScalar
{
@@ -103,7 +171,7 @@ private static ISolverResult
new SolutionValues(ReadOnlyDictionary
.Empty),
objectiveValue is null ? null : new ObjectiveValue(objectiveValue.Value),
- new IsFeasible(isFeasible),
+ isFeasible is null ? null : new IsFeasible(isFeasible.Value),
new IsOptimal(isOptimal),
objectiveValue is null || bestBound is null ? null : CalculateGap(objectiveValue.Value, bestBound.Value),
resultStatus,
diff --git a/src/Anexia.MathematicalProgram/Result/SolverResult.cs b/src/Anexia.MathematicalProgram/Result/SolverResult.cs
index 298b224..16cb10f 100644
--- a/src/Anexia.MathematicalProgram/Result/SolverResult.cs
+++ b/src/Anexia.MathematicalProgram/Result/SolverResult.cs
@@ -26,7 +26,7 @@ namespace Anexia.MathematicalProgram.Result;
public readonly record struct SolverResult(
ISolutionValues SolutionValues,
ObjectiveValue? ObjectiveValue,
- IsFeasible IsFeasible,
+ IsFeasible? IsFeasible,
IsOptimal IsOptimal,
OptimalityGap? OptimalityGap,
SolverResultStatus SolverResultStatus,
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/ConstraintProgrammingSolver.cs b/src/Anexia.MathematicalProgram/Solve/ConstraintProgrammingSolver.cs
index 0858779..0e0432d 100644
--- a/src/Anexia.MathematicalProgram/Solve/ConstraintProgrammingSolver.cs
+++ b/src/Anexia.MathematicalProgram/Solve/ConstraintProgrammingSolver.cs
@@ -68,7 +68,14 @@ public ISolverResult, IntegerScalar, IIntegerSc
else model.Minimize(expr);
}
- if (solverParameter.ExportModelFilePath is not null) model.ExportToFile(solverParameter.ExportModelFilePath);
+ if (solverParameter.ExportModelFilePaths.Any())
+ {
+
+ foreach (var file in solverParameter.ExportModelFilePaths)
+ {
+ model.ExportToFile(file);
+ }
+ }
var solver = new CpSolver();
if (solverParameter.TimeLimitInMilliseconds is not null)
diff --git a/src/Anexia.MathematicalProgram/Solve/GurobiNativeSolver.cs b/src/Anexia.MathematicalProgram/Solve/GurobiNativeSolver.cs
new file mode 100644
index 0000000..05bd004
--- /dev/null
+++ b/src/Anexia.MathematicalProgram/Solve/GurobiNativeSolver.cs
@@ -0,0 +1,115 @@
+// ------------------------------------------------------------------------------------------
+//
+// 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
+ {
+ using 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();
+
+ using 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);
+
+
+ if (solverParameter.ExportModelFilePaths.Any())
+ {
+ logger?.LogInformation("Exporting model to {ExportModelFilePath}",
+ string.Join(", ", solverParameter.ExportModelFilePaths));
+
+ foreach (var file in solverParameter.ExportModelFilePaths)
+ {
+ gurobiModel.Write(file);
+ }
+ }
+
+ gurobiModel.Optimize();
+
+ 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/IlpCbcSolver.cs b/src/Anexia.MathematicalProgram/Solve/IlpCbcSolver.cs
index 0e669fa..97ebb8d 100644
--- a/src/Anexia.MathematicalProgram/Solve/IlpCbcSolver.cs
+++ b/src/Anexia.MathematicalProgram/Solve/IlpCbcSolver.cs
@@ -73,8 +73,17 @@ IntegerVariable or IntegerVariable or
if (solverParameter.TimeLimitInMilliseconds is not null)
Solver.SetTimeLimit(solverParameter.TimeLimitInMilliseconds.Value);
- if (solverParameter.ExportModelFilePath is not null)
- File.WriteAllText(solverParameter.ExportModelFilePath, Solver.ExportModelAsMpsFormat(true, false));
+ if (solverParameter.ExportModelFilePaths.SingleOrDefault(item => item.EndsWith(".mps")) is not null)
+ {
+ File.WriteAllText(solverParameter.ExportModelFilePaths.Single(item => item.EndsWith(".mps")),
+ Solver.ExportModelAsMpsFormat(true, false));
+ }
+
+ if (solverParameter.ExportModelFilePaths.SingleOrDefault(item => item.EndsWith(".lp")) is not null)
+ {
+ File.WriteAllText(solverParameter.ExportModelFilePaths.Single(item => item.EndsWith(".lp")),
+ Solver.ExportModelAsLpFormat(false));
+ }
if (solverParameter.EnableSolverOutput.Value) Solver.EnableOutput();
using var parameter = new MPSolverParameters();
diff --git a/src/Anexia.MathematicalProgram/Solve/IlpSolver.cs b/src/Anexia.MathematicalProgram/Solve/IlpSolver.cs
index f24eeda..9682e03 100644
--- a/src/Anexia.MathematicalProgram/Solve/IlpSolver.cs
+++ b/src/Anexia.MathematicalProgram/Solve/IlpSolver.cs
@@ -1,4 +1,3 @@
-using System.Diagnostics;
using Anexia.MathematicalProgram.Extensions;
using Anexia.MathematicalProgram.Model;
using Anexia.MathematicalProgram.Model.Interval;
@@ -7,6 +6,7 @@
using Anexia.MathematicalProgram.Result;
using Anexia.MathematicalProgram.SolverConfiguration;
using Google.OrTools.ModelBuilder;
+using Gurobi;
using Microsoft.Extensions.Logging;
namespace Anexia.MathematicalProgram.Solve;
@@ -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.
@@ -36,6 +36,26 @@ public ISolverResult, RealScalar, IRealScalar> Sol
completedOptimizationModel,
SolverParameter solverParameter)
{
+ if (SolverType == IlpSolverType.GurobiNativeIntegerProgramming)
+ {
+ try
+ {
+ return new GurobiNativeSolver().Solve(completedOptimizationModel,
+ solverParameter);
+ }
+ catch (MathematicalProgramException exception) when (exception.InnerException is GRBException)
+ {
+ if (exception.Message.Contains("No Gurobi license found"))
+ {
+ Logger.LogInformation("No Gurobi licence found. Original Exception {Exception}", exception);
+ return new IlpSolver(FallbackSolver, FallbackSolver, Logger).Solve(completedOptimizationModel,
+ solverParameter);
+ }
+
+ throw;
+ }
+ }
+
var (configuredSolver, solverWasSwitched) = InitializeSolver(solverParameter);
if (configuredSolver is null)
@@ -186,8 +206,19 @@ solverParameter.TimeLimitInMilliseconds is null
private void ExportModelIfRequested(SolverParameter solverParameter, Google.OrTools.ModelBuilder.Model model)
{
- if (solverParameter.ExportModelFilePath is null) return;
- Logger?.LogInformation("Exporting model to {ExportModelFilePath}", solverParameter.ExportModelFilePath);
- model.WriteToMpsFile(solverParameter.ExportModelFilePath, false);
+ if (!solverParameter.ExportModelFilePaths.Any()) return;
+ Logger?.LogInformation("Exporting model to {ExportModelFilePath}",
+ string.Join(", ", solverParameter.ExportModelFilePaths));
+
+ if (solverParameter.ExportModelFilePaths.SingleOrDefault(item => item.EndsWith(".mps")) is not null)
+ {
+ model.WriteToMpsFile(solverParameter.ExportModelFilePaths.SingleOrDefault(item => item.EndsWith(".mps")),
+ false);
+ }
+
+ foreach (var modelFilePath in solverParameter.ExportModelFilePaths.Where(item => !item.EndsWith(".mps")))
+ {
+ model.ExportToFile(modelFilePath);
+ }
}
}
\ No newline at end of file
diff --git a/src/Anexia.MathematicalProgram/Solve/LPSolver.cs b/src/Anexia.MathematicalProgram/Solve/LPSolver.cs
index f13faa3..a923646 100644
--- a/src/Anexia.MathematicalProgram/Solve/LPSolver.cs
+++ b/src/Anexia.MathematicalProgram/Solve/LPSolver.cs
@@ -139,10 +139,16 @@ public ISolverResult, RealScalar, IRealScalar> S
private void ExportModelIfRequested(SolverParameter solverParameter, Google.OrTools.ModelBuilder.Model model)
{
- if (solverParameter.ExportModelFilePath is not null)
+ if (solverParameter.ExportModelFilePaths.SingleOrDefault(item => item.EndsWith(".mps")) is not null)
{
- File.WriteAllText(solverParameter.ExportModelFilePath, model.ExportToMpsString(false));
- File.WriteAllText(solverParameter.ExportModelFilePath.Replace(".", "_lp."), model.ExportToLpString(false));
+ File.WriteAllText(solverParameter.ExportModelFilePaths.Single(item => item.EndsWith(".mps")),
+ model.ExportToMpsString(false));
+ }
+
+ if (solverParameter.ExportModelFilePaths.SingleOrDefault(item => item.EndsWith(".lp")) is not null)
+ {
+ File.WriteAllText(solverParameter.ExportModelFilePaths.Single(item => item.EndsWith(".lp")),
+ model.ExportToLpString(false));
}
}
}
\ No newline at end of file
diff --git a/src/Anexia.MathematicalProgram/Solve/SolverFactory.cs b/src/Anexia.MathematicalProgram/Solve/SolverFactory.cs
index f61bb9d..1d7ad65 100644
--- a/src/Anexia.MathematicalProgram/Solve/SolverFactory.cs
+++ b/src/Anexia.MathematicalProgram/Solve/SolverFactory.cs
@@ -26,7 +26,7 @@ public static
solverType switch
{
IlpSolverType.CbcIntegerProgramming => new IlpCbcSolver(),
- IlpSolverType.GurobiIntegerProgramming or
+ IlpSolverType.GurobiNativeIntegerProgramming or IlpSolverType.GurobiIntegerProgramming or
IlpSolverType.Scip or IlpSolverType.HiGhs => new IlpSolver(solverType, fallbackSolverType, logger),
_ => throw new ArgumentOutOfRangeException(nameof(solverType), solverType, null)
};
diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/ILPSolverType.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/ILPSolverType.cs
index e5448bd..45e9519 100644
--- a/src/Anexia.MathematicalProgram/SolverConfiguration/ILPSolverType.cs
+++ b/src/Anexia.MathematicalProgram/SolverConfiguration/ILPSolverType.cs
@@ -27,6 +27,12 @@ public enum IlpSolverType
///
[EnumMember(Value = "GUROBI_MIXED_INTEGER_PROGRAMMING")]
GurobiIntegerProgramming,
+
+ ///
+ /// Gurobi solver. A licence is needed for usage.
+ ///
+ [EnumMember(Value = "GUROBI_NATIVE_MIXED_INTEGER_PROGRAMMING")]
+ GurobiNativeIntegerProgramming,
///
/// SCIP solver.
diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs
index 7731586..7b29ed0 100644
--- a/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs
+++ b/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs
@@ -16,16 +16,16 @@ 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.
-/// The file path and name of a file where the model should be written to.
+/// 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,
- string? ExportModelFilePath = null)
+ IReadOnlyCollection<(string Key, string Value)>? AdditionalSolverSpecificParameters = null,
+ params string[] ExportModelFilePaths)
{
private const string RelativeGapKey = "RELATIVE_GAP";
private const string NumberOfThreadsKey = "NUMBER_OF_THREADS_KEY";
@@ -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);
}
@@ -138,5 +143,5 @@ internal string ToSolverSpecificParameters(LpSolverType solverType)
///
[ExcludeFromCodeCoverage]
public override string ToString() =>
- $"{nameof(EnableSolverOutput)}: {EnableSolverOutput}, {nameof(RelativeGap)}: {RelativeGap}, {nameof(TimeLimitInMilliseconds)}: {TimeLimitInMilliseconds}, {nameof(NumberOfThreads)}: {NumberOfThreads}, {nameof(ExportModelFilePath)}: {ExportModelFilePath}";
+ $"{nameof(EnableSolverOutput)}: {EnableSolverOutput}, {nameof(RelativeGap)}: {RelativeGap}, {nameof(TimeLimitInMilliseconds)}: {TimeLimitInMilliseconds}, {nameof(NumberOfThreads)}: {NumberOfThreads}, {nameof(ExportModelFilePaths)}: {ExportModelFilePaths}";
}
\ No newline at end of file
diff --git a/test/Anexia.MathematicalProgram.Tests/Anexia.MathematicalProgram.Tests.csproj b/test/Anexia.MathematicalProgram.Tests/Anexia.MathematicalProgram.Tests.csproj
index 727d890..cb975a5 100644
--- a/test/Anexia.MathematicalProgram.Tests/Anexia.MathematicalProgram.Tests.csproj
+++ b/test/Anexia.MathematicalProgram.Tests/Anexia.MathematicalProgram.Tests.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/test/Anexia.MathematicalProgram.Tests/Extensions/EnumExtensionTest.cs b/test/Anexia.MathematicalProgram.Tests/Extensions/EnumExtensionTest.cs
new file mode 100644
index 0000000..4bace64
--- /dev/null
+++ b/test/Anexia.MathematicalProgram.Tests/Extensions/EnumExtensionTest.cs
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------------------------------------
+//
+// Copyright (c) ANEXIA Internetdienstleistungs GmbH. All rights reserved.
+//
+// ------------------------------------------------------------------------------------------
+
+using Anexia.MathematicalProgram.Extensions;
+using Anexia.MathematicalProgram.Result;
+using Anexia.MathematicalProgram.SolverConfiguration;
+
+namespace Anexia.MathematicalProgram.Tests.Extensions;
+
+public sealed class EnumExtensionTest
+{
+ [Fact]
+ public void ToEnumStringReturnsEnumMemberValueForLpSolverType()
+ {
+ Assert.Equal("GLOP", LpSolverType.Glop.ToEnumString());
+ }
+
+ [Fact]
+ public void ToEnumStringReturnsEnumMemberValueForIlpSolverType()
+ {
+ Assert.Equal("HIGHS_MIXED_INTEGER_PROGRAMMING", IlpSolverType.HiGhs.ToEnumString());
+ }
+
+ [Fact]
+ public void ToEnumStringReturnsEmptyStringWhenEnumMemberAttributeIsMissing()
+ {
+ Assert.Equal(string.Empty, SolverResultStatus.Optimal.ToEnumString());
+ }
+
+ [Fact]
+ public void ToEnumStringReturnsEmptyStringForInvalidEnumValue()
+ {
+ Assert.Equal(string.Empty, ((LpSolverType)int.MaxValue).ToEnumString());
+ }
+}
diff --git a/test/Anexia.MathematicalProgram.Tests/Extensions/OptimizationSolverExtensionTest.cs b/test/Anexia.MathematicalProgram.Tests/Extensions/OptimizationSolverExtensionTest.cs
new file mode 100644
index 0000000..cc6018a
--- /dev/null
+++ b/test/Anexia.MathematicalProgram.Tests/Extensions/OptimizationSolverExtensionTest.cs
@@ -0,0 +1,62 @@
+// ------------------------------------------------------------------------------------------
+//
+// Copyright (c) ANEXIA Internetdienstleistungs GmbH. All rights reserved.
+//
+// ------------------------------------------------------------------------------------------
+
+using System.Collections.ObjectModel;
+using Anexia.MathematicalProgram.Extensions;
+using Anexia.MathematicalProgram.Model;
+using Anexia.MathematicalProgram.Model.Expression;
+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;
+
+namespace Anexia.MathematicalProgram.Tests.Extensions;
+
+public sealed class OptimizationSolverExtensionTest
+{
+ [Fact]
+ public void SolveWithoutSolverParameterUsesDefaultSolverParameter()
+ {
+ var model = new OptimizationModel, RealScalar, IRealScalar>();
+ var variable = model.NewVariable>(new RealInterval(0, 1), "v1");
+ var completedModel = model.SetObjective(
+ new ObjectiveFunction, RealScalar, IRealScalar>(
+ null,
+ new WeightedSum, RealScalar, IRealScalar>().Add(variable,
+ new RealScalar(1)),
+ maximize: true));
+ var solver = new FakeSolver();
+
+ var result = solver.Solve(completedModel);
+
+ Assert.Equal(solver.Result, result);
+ }
+
+ private sealed class FakeSolver :
+ IOptimizationSolver, RealScalar, IRealScalar, RealScalar>
+ {
+ internal readonly ISolverResult, RealScalar, IRealScalar> Result =
+ new SolverResult, RealScalar, IRealScalar>(
+ new SolutionValues, RealScalar, IRealScalar>(
+ ReadOnlyDictionary, RealScalar>.Empty),
+ null,
+ new IsFeasible(false),
+ new IsOptimal(false),
+ null,
+ SolverResultStatus.NotSolved,
+ false);
+
+
+ public ISolverResult, RealScalar, IRealScalar> Solve(
+ ICompletedOptimizationModel, RealScalar, IRealScalar> model,
+ SolverParameter solverParameter)
+ {
+ return Result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Anexia.MathematicalProgram.Tests/Factory/SolverResultFactory.cs b/test/Anexia.MathematicalProgram.Tests/Factory/SolverResultFactory.cs
index 6813793..bc484db 100644
--- a/test/Anexia.MathematicalProgram.Tests/Factory/SolverResultFactory.cs
+++ b/test/Anexia.MathematicalProgram.Tests/Factory/SolverResultFactory.cs
@@ -16,7 +16,7 @@ public static SolverResult
SolverResult(
ISolutionValues solutionValues,
ObjectiveValue? objectiveValue,
- IsFeasible isFeasible,
+ IsFeasible? isFeasible,
IsOptimal isOptimal,
OptimalityGap? optimalityGap,
SolverResultStatus solverResultStatus,
diff --git a/test/Anexia.MathematicalProgram.Tests/Model/BinaryScalarTest.cs b/test/Anexia.MathematicalProgram.Tests/Model/BinaryScalarTest.cs
index 449aa1f..e5c3fd6 100644
--- a/test/Anexia.MathematicalProgram.Tests/Model/BinaryScalarTest.cs
+++ b/test/Anexia.MathematicalProgram.Tests/Model/BinaryScalarTest.cs
@@ -32,8 +32,7 @@ public void AddWithBinaryScalarReturnsExpected(BinaryScalar left, BinaryScalar r
{
var result = left.Add(right);
- Assert.Equal(expected.IsOne, result.IsOne);
- Assert.Equal(expected.Value, result.Value);
+ Assert.Equal(expected, result);
}
[Theory]
@@ -42,8 +41,7 @@ public void SubtractWithBinaryScalarReturnsExpected(BinaryScalar left, BinarySca
{
var result = left.Subtract(right);
- Assert.Equal(expected.IsOne, result.IsOne);
- Assert.Equal(expected.Value, result.Value);
+ Assert.Equal(expected, result);
}
[Theory]
@@ -53,8 +51,7 @@ public void AddWithIBinaryScalarReturnsExpected(BinaryScalar left, BinaryScalar
IBinaryScalar r = right;
var result = left.Add(r);
- Assert.Equal(expected.IsOne, result.IsOne);
- Assert.Equal(expected.Value, result.Value);
+ Assert.Equal(expected, result);
}
[Theory]
@@ -73,8 +70,7 @@ public void SubtractWithIBinaryScalarReturnsExpected(BinaryScalar left, BinarySc
IBinaryScalar r = right;
var result = left.Subtract(r);
- Assert.Equal(expected.IsOne, result.IsOne);
- Assert.Equal(expected.Value, result.Value);
+ Assert.Equal(expected, result);
}
[Theory]
@@ -92,8 +88,7 @@ public void OperatorPlusReturnsExpected(BinaryScalar left, BinaryScalar right, B
{
var result = left + right;
- Assert.Equal(expected.IsOne, result.IsOne);
- Assert.Equal(expected.Value, result.Value);
+ Assert.Equal(expected, result);
}
[Theory]
@@ -102,7 +97,6 @@ public void OperatorMinusReturnsExpected(BinaryScalar left, BinaryScalar right,
{
var result = left - right;
- Assert.Equal(expected.IsOne, result.IsOne);
- Assert.Equal(expected.Value, result.Value);
+ Assert.Equal(expected, result);
}
-}
\ No newline at end of file
+}
diff --git a/test/Anexia.MathematicalProgram.Tests/Model/ConstraintBuilderTest.cs b/test/Anexia.MathematicalProgram.Tests/Model/ConstraintBuilderTest.cs
index b368cd0..c0e415d 100644
--- a/test/Anexia.MathematicalProgram.Tests/Model/ConstraintBuilderTest.cs
+++ b/test/Anexia.MathematicalProgram.Tests/Model/ConstraintBuilderTest.cs
@@ -26,7 +26,7 @@ public void ConstraintBuilderAddTermReturnsCorrectResult()
var v1 = model.NewVariable>(new RealInterval(0, 1), "v1");
var v2 = model.NewVariable>(new RealInterval(0, 2), "v2");
var v3 = model.NewVariable>(new RealInterval(0, 3), "v3");
-
+
var constraint = model.CreateConstraintBuilder()
.AddTermToSum(1, v1)
.AddTermToSum(2, v2)
@@ -58,6 +58,41 @@ public void ConstraintBuilderAddWeightedSumReturnsCorrectResult()
constraint);
}
+ [Fact]
+ public void ConstraintBuilderAddExistingWeightedSumReturnsCorrectResult()
+ {
+ var model = new OptimizationModel, RealScalar, IRealScalar>();
+
+ var v1 = model.NewVariable>(new RealInterval(0, 1), "v1");
+ var v2 = model.NewVariable>(new RealInterval(0, 2), "v2");
+ var weightedSum = model.CreateWeightedSumBuilder()
+ .AddTermToSum(1, v1)
+ .AddTermToSum(2, v2)
+ .Build();
+
+ var constraint = model.CreateConstraintBuilder()
+ .AddTermToSum(3, v1)
+ .AddWeightedSum(weightedSum)
+ .Build(new IntegralInterval(-10, 20));
+
+ Assert.Equal(
+ Constraint(WeightedSum((v1, 4), (v2, 2)), Interval(-10, 20)),
+ constraint);
+ }
+
+ [Fact]
+ public void ConstraintBuilderBuildsEmptyConstraintWhenNoTermsWereAdded()
+ {
+ var model = new OptimizationModel, RealScalar, IRealScalar>();
+
+ var constraint = model.CreateConstraintBuilder()
+ .Build(new IntegralInterval(0, 0));
+
+ Assert.Equal(new Constraint, RealScalar, IRealScalar>(
+ new WeightedSum, RealScalar, IRealScalar>(), new IntegralInterval(0, 0)),
+ constraint);
+ }
+
[Fact]
public void AddWeightedSumThrowsCorrectException()
{
diff --git a/test/Anexia.MathematicalProgram.Tests/Model/ConstraintTest.cs b/test/Anexia.MathematicalProgram.Tests/Model/ConstraintTest.cs
new file mode 100644
index 0000000..0ccbab2
--- /dev/null
+++ b/test/Anexia.MathematicalProgram.Tests/Model/ConstraintTest.cs
@@ -0,0 +1,84 @@
+// ------------------------------------------------------------------------------------------
+//
+// Copyright (c) ANEXIA Internetdienstleistungs GmbH. All rights reserved.
+//
+// ------------------------------------------------------------------------------------------
+
+using Anexia.MathematicalProgram.Model;
+using Anexia.MathematicalProgram.Model.Expression;
+using Anexia.MathematicalProgram.Model.Interval;
+using Anexia.MathematicalProgram.Model.Scalar;
+using Anexia.MathematicalProgram.Model.Variable;
+using static Anexia.MathematicalProgram.Tests.Factory.WeightedSumFactory;
+
+namespace Anexia.MathematicalProgram.Tests.Model;
+
+public sealed class ConstraintTest
+{
+ [Fact]
+ public void ConstraintKeepsWeightedSumIntervalAndName()
+ {
+ var (weightedSum, interval) = CreateWeightedSumAndInterval();
+
+ var constraint = new Constraint, RealScalar, IRealScalar>(
+ weightedSum,
+ interval,
+ "capacity");
+
+ Assert.Equal(
+ new Constraint, RealScalar, IRealScalar>(weightedSum, interval, "capacity"),
+ constraint);
+ }
+
+ [Fact]
+ public void ConstraintsConstructorMaterializesEnumerable()
+ {
+ var constraint = CreateConstraint("c1");
+ var source = new List, RealScalar, IRealScalar>> { constraint };
+ var constraints = new Constraints, RealScalar, IRealScalar>(
+ source.Where(_ => true));
+
+ source.Clear();
+
+ Assert.Equal(new[] { constraint as IConstraint, RealScalar, IRealScalar> },
+ [.. constraints]);
+ }
+
+ [Fact]
+ public void AddReturnsNewConstraintsCollectionWithoutChangingOriginal()
+ {
+ var first = CreateConstraint("c1");
+ var second = CreateConstraint("c2");
+ var constraints = new Constraints, RealScalar, IRealScalar>(first);
+
+ var updated = constraints.Add(second);
+
+ Assert.Equal(
+ new[]
+ {
+ first as IConstraint, RealScalar, IRealScalar>,
+ first as IConstraint, RealScalar, IRealScalar>,
+ second as IConstraint, RealScalar, IRealScalar>
+ },
+ [.. constraints, ..updated]);
+ }
+
+ private static Constraint, RealScalar, IRealScalar> CreateConstraint(string name)
+ {
+ var (weightedSum, interval) = CreateWeightedSumAndInterval();
+
+ return new Constraint, RealScalar, IRealScalar>(
+ weightedSum,
+ interval,
+ name);
+ }
+
+ private static (IWeightedSum, RealScalar, IRealScalar> WeightedSum,
+ IInterval Interval) CreateWeightedSumAndInterval()
+ {
+ var model = new OptimizationModel, RealScalar, IRealScalar>();
+ var variable = model.NewVariable>(new RealInterval(0, 10), "x");
+
+ return (WeightedSum((variable, 2)), new RealInterval(-4, 8));
+ }
+}
\ No newline at end of file
diff --git a/test/Anexia.MathematicalProgram.Tests/Model/IntervalTest.cs b/test/Anexia.MathematicalProgram.Tests/Model/IntervalTest.cs
index 7f4f849..de9db14 100644
--- a/test/Anexia.MathematicalProgram.Tests/Model/IntervalTest.cs
+++ b/test/Anexia.MathematicalProgram.Tests/Model/IntervalTest.cs
@@ -1,4 +1,4 @@
-// ------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------
//
// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved.
//
@@ -13,6 +13,22 @@ namespace Anexia.MathematicalProgram.Tests.Model;
public sealed class IntervalTest
{
+ [Fact]
+ public void RealIntervalKeepsInclusiveLowerBound()
+ {
+ var interval = new RealInterval(new RealScalar(-1.5), new RealScalar(3.25));
+
+ Assert.Equal(new RealScalar(-1.5), interval.LowerBound);
+ }
+
+ [Fact]
+ public void RealIntervalKeepsInclusiveUpperBound()
+ {
+ var interval = new RealInterval(new RealScalar(-1.5), new RealScalar(3.25));
+
+ Assert.Equal(new RealScalar(3.25), interval.UpperBound);
+ }
+
[Theory]
[InlineData(6, 5)]
[InlineData(0.1, 0)]
@@ -21,7 +37,23 @@ public sealed class IntervalTest
public void IntervalInitializingThrowsExpectedException(double left, double right) =>
Assert.Throws>(() =>
new RealInterval(new RealScalar(left), new RealScalar(right)));
-
+
+ [Fact]
+ public void IntegralIntervalKeepsInclusiveLowerBound()
+ {
+ var interval = new IntegralInterval(new IntegerScalar(-3), new IntegerScalar(7));
+
+ Assert.Equal(new IntegerScalar(-3), interval.LowerBound);
+ }
+
+ [Fact]
+ public void IntegralIntervalKeepsInclusiveUpperBound()
+ {
+ var interval = new IntegralInterval(new IntegerScalar(-3), new IntegerScalar(7));
+
+ Assert.Equal(new IntegerScalar(7), interval.UpperBound);
+ }
+
[Theory]
[InlineData(6, 5)]
[InlineData(-1, -21)]
@@ -29,4 +61,64 @@ public void IntervalInitializingThrowsExpectedException(double left, double righ
public void IntegralIntervalInitializingThrowsExpectedException(int left, int right) =>
Assert.Throws>(() =>
new IntegralInterval(new IntegerScalar(left), new IntegerScalar(right)));
-}
\ No newline at end of file
+
+ [Fact]
+ public void BinaryIntervalUsesZeroAsLowerBound()
+ {
+ var interval = new BinaryInterval();
+
+ Assert.Equal(BinaryScalar.Zero, interval.LowerBound);
+ }
+
+ [Fact]
+ public void BinaryIntervalUsesOneAsUpperBound()
+ {
+ var interval = new BinaryInterval();
+
+ Assert.Equal(BinaryScalar.One, interval.UpperBound);
+ }
+
+ [Theory]
+ [InlineData(-2.5)]
+ [InlineData(0)]
+ [InlineData(4.75)]
+ public void PointUsesValueAsLowerBound(double value)
+ {
+ var point = new Point(value);
+
+ Assert.Equal(new RealScalar(value), point.LowerBound);
+ }
+
+ [Theory]
+ [InlineData(-2.5)]
+ [InlineData(0)]
+ [InlineData(4.75)]
+ public void PointUsesValueAsUpperBound(double value)
+ {
+ var point = new Point(value);
+
+ Assert.Equal(new RealScalar(value), point.UpperBound);
+ }
+
+ [Theory]
+ [InlineData(-2)]
+ [InlineData(0)]
+ [InlineData(4)]
+ public void IntegralPointUsesValueAsLowerBound(int value)
+ {
+ var point = new IntegralPoint(value);
+
+ Assert.Equal(new IntegerScalar(value), point.LowerBound);
+ }
+
+ [Theory]
+ [InlineData(-2)]
+ [InlineData(0)]
+ [InlineData(4)]
+ public void IntegralPointUsesValueAsUpperBound(int value)
+ {
+ var point = new IntegralPoint(value);
+
+ Assert.Equal(new IntegerScalar(value), point.UpperBound);
+ }
+}
diff --git a/test/Anexia.MathematicalProgram.Tests/Model/ScalarTest.cs b/test/Anexia.MathematicalProgram.Tests/Model/ScalarTest.cs
new file mode 100644
index 0000000..8630a33
--- /dev/null
+++ b/test/Anexia.MathematicalProgram.Tests/Model/ScalarTest.cs
@@ -0,0 +1,140 @@
+// ------------------------------------------------------------------------------------------
+//
+// Copyright (c) ANEXIA Internetdienstleistungs GmbH. All rights reserved.
+//
+// ------------------------------------------------------------------------------------------
+
+using Anexia.MathematicalProgram.Model.Scalar;
+
+namespace Anexia.MathematicalProgram.Tests.Model;
+
+public sealed class ScalarTest
+{
+ [Fact]
+ public void RealScalarAddReturnsSum()
+ {
+ Assert.Equal(new RealScalar(9.5), new RealScalar(7.5).Add(new RealScalar(2)));
+ }
+
+ [Fact]
+ public void RealScalarSubtractReturnsDifference()
+ {
+ Assert.Equal(new RealScalar(5.5), new RealScalar(7.5).Subtract(new RealScalar(2)));
+ }
+
+ [Fact]
+ public void RealScalarAddViaInterfaceReturnsSum()
+ {
+ IRealScalar right = new RealScalar(2);
+
+ Assert.Equal(9.5, new RealScalar(7.5).Add(right).Value);
+ }
+
+ [Fact]
+ public void RealScalarSubtractViaInterfaceReturnsDifference()
+ {
+ IRealScalar right = new RealScalar(2);
+
+ Assert.Equal(5.5, new RealScalar(7.5).Subtract(right).Value);
+ }
+
+ [Fact]
+ public void RealScalarPlusOperatorReturnsSum()
+ {
+ Assert.Equal(new RealScalar(9.5), new RealScalar(7.5) + new RealScalar(2));
+ }
+
+ [Fact]
+ public void RealScalarMinusOperatorReturnsDifference()
+ {
+ Assert.Equal(new RealScalar(5.5), new RealScalar(7.5) - new RealScalar(2));
+ }
+
+ [Fact]
+ public void IntegerScalarAddReturnsSum()
+ {
+ Assert.Equal(new IntegerScalar(9), new IntegerScalar(7).Add(new IntegerScalar(2)));
+ }
+
+ [Fact]
+ public void IntegerScalarSubtractReturnsDifference()
+ {
+ Assert.Equal(new IntegerScalar(5), new IntegerScalar(7).Subtract(new IntegerScalar(2)));
+ }
+
+ [Fact]
+ public void IntegerScalarAddViaIntegerInterfaceReturnsSum()
+ {
+ IIntegerScalar right = new IntegerScalar(2);
+
+ Assert.Equal(9, new IntegerScalar(7).Add(right).Value);
+ }
+
+ [Fact]
+ public void IntegerScalarSubtractViaIntegerInterfaceReturnsDifference()
+ {
+ IIntegerScalar right = new IntegerScalar(2);
+
+ Assert.Equal(5, new IntegerScalar(7).Subtract(right).Value);
+ }
+
+ [Fact]
+ public void IntegerScalarAddRealScalarReturnsRealSum()
+ {
+ IRealScalar right = new RealScalar(2.5);
+
+ Assert.Equal(9.5, new IntegerScalar(7).Add(right).Value);
+ }
+
+ [Fact]
+ public void IntegerScalarSubtractRealScalarReturnsRealDifference()
+ {
+ IRealScalar right = new RealScalar(2.5);
+
+ Assert.Equal(4.5, new IntegerScalar(7).Subtract(right).Value);
+ }
+
+ [Fact]
+ public void IntegerScalarPlusOperatorReturnsSum()
+ {
+ Assert.Equal(new IntegerScalar(9), new IntegerScalar(7) + new IntegerScalar(2));
+ }
+
+ [Fact]
+ public void IntegerScalarMinusOperatorReturnsDifference()
+ {
+ Assert.Equal(new IntegerScalar(5), new IntegerScalar(7) - new IntegerScalar(2));
+ }
+
+ [Fact]
+ public void BinaryScalarAddEvenIntegerScalarKeepsValue()
+ {
+ IIntegerScalar even = new IntegerScalar(4);
+
+ Assert.Equal(1, BinaryScalar.One.Add(even).Value);
+ }
+
+ [Fact]
+ public void BinaryScalarAddOddIntegerScalarFlipsValue()
+ {
+ IIntegerScalar odd = new IntegerScalar(3);
+
+ Assert.Equal(0, BinaryScalar.One.Add(odd).Value);
+ }
+
+ [Fact]
+ public void BinaryScalarSubtractEvenIntegerScalarKeepsValue()
+ {
+ IIntegerScalar even = new IntegerScalar(4);
+
+ Assert.Equal(1, BinaryScalar.One.Subtract(even).Value);
+ }
+
+ [Fact]
+ public void BinaryScalarSubtractOddIntegerScalarFlipsValue()
+ {
+ IIntegerScalar odd = new IntegerScalar(3);
+
+ Assert.Equal(1, BinaryScalar.Zero.Subtract(odd).Value);
+ }
+}
diff --git a/test/Anexia.MathematicalProgram.Tests/Result/ResultHandlingTest.cs b/test/Anexia.MathematicalProgram.Tests/Result/ResultHandlingTest.cs
new file mode 100644
index 0000000..d74b9a2
--- /dev/null
+++ b/test/Anexia.MathematicalProgram.Tests/Result/ResultHandlingTest.cs
@@ -0,0 +1,289 @@
+// ------------------------------------------------------------------------------------------
+//
+// Copyright (c) ANEXIA Internetdienstleistungs GmbH. All rights reserved.
+//
+// ------------------------------------------------------------------------------------------
+
+using System.Collections.ObjectModel;
+using Anexia.MathematicalProgram.Model.Scalar;
+using Anexia.MathematicalProgram.Model.Variable;
+using Anexia.MathematicalProgram.Result;
+using Anexia.MathematicalProgram.Solve;
+using Google.OrTools.ModelBuilder;
+using Google.OrTools.Sat;
+using Gurobi;
+
+namespace Anexia.MathematicalProgram.Tests.Result;
+
+public sealed class ResultHandlingTest
+{
+ public static IEnumerable