diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3039704 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + + - package-ecosystem: "nuget" + directory: "/src" + schedule: + interval: "weekly" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7b3f033 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,42 @@ +name: "Publish package" +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 8.0.x + + - name: Verify commit exists in origin/main + 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 + + - name: Build + run: dotnet build --configuration Release /p:Version=${VERSION} + + - name: Test + run: dotnet test --configuration Release /p:Version=${VERSION} --no-build + + - name: Pack + run: dotnet pack --configuration Release /p:Version=${VERSION} --no-build --output . + + - name: Push + run: dotnet nuget push Anexia.MathematicalProgram.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_TOKEN} + env: + NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3ae50b8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: "Run tests" + +on: [ push, pull_request ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 8.0.x + + - name: Dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release + + - name: Test + run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + + - uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true # optional (default = false) + verbose: true # optional (default = false) + token: ${{ secrets.CODECOV_TOKEN }} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59a96a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Common IntelliJ Platform excludes + +# User specific +**/.idea/**/workspace.xml +**/.idea/**/tasks.xml +**/.idea/shelf/* +**/.idea/dictionaries +target/ + +# Sensitive or high-churn files +**/.idea/**/dataSources/ +**/.idea/**/dataSources.ids +**/.idea/**/dataSources.xml +**/.idea/**/dataSources.local.xml +**/.idea/**/sqlDataSources.xml +**/.idea/**/dynamic.xml + +# Rider +# Rider auto-generates .iml files, and contentModel.xml +**/.idea/**/*.iml +**/.idea/**/contentModel.xml +**/.idea/**/modules.xml + +**/.idea/ +.tmp/ +.teamcity/ + +# Ignore http client files from rider +**/.http +*.http + +*.suo +*.user +.vs/ +[Bb]in/ +[Oo]bj/ +_UpgradeReport_Files/ +[Pp]ackages/ + +Thumbs.db +Desktop.ini +.DS_Store +**/logs/ +/**/**/obj/ +/**/**/bin/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f24c243 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + +- repo: https://github.com/dotnet/format + rev: v8.0.453106 + hooks: + - id: dotnet-format diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2d22982 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Guidance on how to contribute + +> By submitting a pull request or filing a bug, issue, or feature request, +> you are agreeing to comply with this waiver of copyright interest. +> Details can be found in our [LICENSE](LICENSE). + + +There are two primary ways to help: +- Using the issue tracker, and +- Changing the code-base. + + +## Using the issue tracker + +Use the issue tracker to suggest feature requests, report bugs, and ask questions. +This is also a great way to connect with the developers of the project as well +as others who are interested in this solution. + +Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in +the issue that you will take on that effort, then follow the _Changing the code-base_ +guidance below. + + +## Changing the code-base + +Generally speaking, you should fork this repository, make changes in your +own fork, and then submit a pull request. All new code should have associated +unit tests that validate implemented features and the presence or lack of defects. +Additionally, the code should follow any stylistic and architectural guidelines +prescribed by the project. In the absence of such guidelines, mimic the styles +and patterns in the existing code-base. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..faf586e --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,46 @@ + + + net8.0 + enable + 12 + true + true + latest + false + false + true + CA1716 + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fb4b56a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 ANEXIA Internetdienstleisungs GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd99bda --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# dotnet-mathematical-program + +[![](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) +This library allows you to build and solve linear programs and integer linear programs in a very handy way. +The implementation uses Google´s GLOP linear solver for linear programs and optionally, the Coin-OR CBC branch and cut +solver +or the Gurobi solver for integer linear programs via the [Google OR-Tools](https://developers.google.com/optimization) +API. + +## Installation + +- Install the latest version of `Anexia.MathematicalProgram` package via nuget + +## Description + +This library works for any linear program (LP) or integer linear program (ILP). + +### Anexia.MathematicalProgram.Model + +- To build the objective function of your LP/ILP you can use the class `Terms`. + Each `Term` is defined by a `Coefficient` and a variable of type `Google.OrTools.LinearSolver.Variable`. + Moreover, there is the possibility to have an additional `Constant`. + +- To build your constraints you can use the class `Constraints`. + Each `Constraint` is defined by `Terms` and an `Interval` that has a lower and an upper bound of + type `double`. + For binary intervals simply use `Interval.BinaryInterval`. + Another implementation is the class `Point` for the case of lower bound equals upper bound. + +### Anexia.MathematicalProgram.Solve + +#### Linear Programming + +For solving an LP you may initialize the `LinearProgramSolver` which uses the GLOP solver in the background. + +- **Configuration:** Via `LinearProgramSolver.SetSolverConfigurations()` you can set `SolverParameter` containing a `TimeLimitInMilliseconds`, + the `NumberOfThreads` that should be maximally used, a `EnableSolverOutput` flag to determine if the solver output should be + printed on the console and the `RelativeGap` to specify the gap where the solver terminates. +- **Variables:** Via `LinearProgramSolver.AddContinuousVariable()` your continuous LP variables can be added + using an `IInterval` and a variable name of type `string`. + The `out`parameter of this method is of type `Google.OrTools.LinearSolver.Variable`. +- **Constraints:** Via `LinearProgramSolver.AddConstraints()` you can simply add your beforehand initialized + contraints to the solver. +- **Objective:** Via `LinearProgramSolver.AddObjective()` you can add your objective in form of the `Terms` + and a `Constant` to the solver. + With a `bool` you can choose if the LP should be `minimized` or `maximized`. +- **Solve:** Via `LinearProgramSolver.Solve()` you either + - obtain a `SolverResult` (explained below) or + - a `MathematicalProgramException` with a detailed message on the occured problem is thrown. + +#### Integer Linear Programming + +For solving an ILP you may initialize the `IntegerLinearProgramSolver` which uses per default the CBC solver in the +background. +Using the highly performant Gurobi solver requires a valid license. +Creating a solver using Gurobi can be done in two ways. Either by passing the argument +`IntegerLinearProgramSolver(ILPSolverType.GurobiMixedIntegerProgramming)`, +or using the static +method `IntegerLinearProgramSolver.Create(ILPSolverType.GurobiMixedIntegerProgramming, our var message)`. +In both ways, the solver checks if the given type is supported, e.g., a valid licence is present, or otherwise, creates +the solver of type CBC. The `Create` method additionally returns a warning message via out parameter. +If the solver has been created as expected, this message is null, otherwise, it contains information that the +solver type switched to CBC. + +The main difference to linear program solving is that in this case just integer variables can be added. +The rest of the methods work similar to the LinearProgramSolver. + +- **Variables:** Via `IntegerLinearProgramSolver.AddIntegerVariable()` your integer ILP variables can be added using + an `IInterval` and a variable name of type `string`. The `out`parameter of this method is of type + `Google.OrTools.LinearSolver.Variable` which are strictly integer. +- **Configuration**, **Constraints**, **Objective**, and **Solve** as above. + +### Anexia.MathematicalProgram.Result + +After solving the LP/ILP you get a `SolverResult` according to the `Google.OrTools.LinearSolver.Solver.ResultStatus`. +The `SolverResult` containts following information: + +- **Solver:** This is the already solved `Google.OrTools.LinearSolver.Solver`. + - You have the opportunity to log the LP/ILP model in a human readable format by `Solver.ExportModelAsLpFormat()`. + - You can read out the actual values of the variables via `Google.OrTools.LinearSolver.Variable.SolutionValue()` + to transform the result correctly. + - As soon as the solved solver is not needed any more, it should be removed via `Solver.Dispose()`. +- **ObjectiveValue:** Actual objective value. This value can be either the optimum, a deviation of the optimum + if the LP/ILP was not entirely solved, or `double.NaN` if the LP/ILP is infeasible. +- **IsFeasible:** Information whether the LP/ILP is generally feasible. +- **IsOptimal:** Information wheter the LP/ILP was solved to optimality. +- **OptimalityGap:** The deviation to the optimum calculated by + `Math.Abs(objective.BestBound() - objectiveValue) / objectiveValue)`. This value is `0` if the optimum was reached, + and `double.NaN` if the model is infeasible. + +*** + +## Examples for using this library + +### Example 1 (Build and solve LP) + +- Feasible model: max x, s.t. x <= 2, x >= 1, continuous variable x <= 5 +- Result: x = 2, objective value = 2 + +``` +var solver = new LinearProgramSolver().SetSolverConfigurations(TimeLimitInMilliseconds.Unbounded); + +solver = solver.AddContinuousVariable(new Interval(double.NegativeInfinity, 5), "TestVariable", out var testVariable); + +solver = solver.AddObjective(new Terms(new Term(new Coefficient(1), testVariable)), false); + +var constraints = new Constraints( + new Constraint(new Terms(new Term(new Coefficient(1), testVariable)), + new Interval(double.NegativeInfinity, 2)), + new Constraint(new Terms(new Term(new Coefficient(1), testVariable)), + new Interval(1, double.PositiveInfinity))); + +solver = solver.AddConstraints(constraints); + +var result = solver.Solve(); + +Logger.Information(result.SolvedSolver.ExportModelAsLpFormat(false)); +``` + +### Example 2 (Build and solve ILP) + +- Feasible model: min 2x + y, s.t. x >= y, integer variables x in [1,3], y binary +- Result: x = 1, y = 0, objective value = 2 + +``` +var solver = new IntegerLinearProgramSolver() + .SetSolverConfigurations(new TimeLimitInMilliseconds(10), 2, true) + .AddIntegerVariable(new Interval(1, 3), "VariableX", out var variableX) + .AddIntegerVariable(new Interval(0, 1), "VariableY", out var variableY) + .AddObjective( + new Terms(new Term(new Coefficient(2), variableX), new Term(new Coefficient(1), variableY)), true) + .AddConstraints(new Constraints(new Constraint( + new Terms(new Term(new Coefficient(1), variableX), new Term(new Coefficient(-1), variableY)), + new Interval(0, double.PositiveInfinity)))); + + var result = solver.Solve(); +``` + +### Example 3 (Build and solve ILP) + +- Infeasible model: max 2x, s.t. x = 3, variable x binary + +``` +var solver = new IntegerLinearProgramSolver() + .SetSolverConfigurations(TimeLimitInMilliseconds.Unbounded, 2, true) + .AddIntegerVariable(Interval.BinaryInterval, "TestVariable", out var testVariable) + .AddObjective(new Terms(new Term(new Coefficient(2), testVariable)), false) + .AddConstraints( + new Constraints(new Constraint(new Terms(new Term(new Coefficient(1), testVariable)), new Point(3)))); + + var result = solver.Solve(); + + Logger.Information(result.SolvedSolver.ExportModelAsLpFormat(false)); +``` + + +## Contributing + +Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information. + +## Licensing + +This project is licensed under MIT License. See [LICENSE](LICENSE) for more information. + + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5949fc2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Reporting Security Issues + +Please report any security issues you discovered to opensource[at]anexia-it[dot]com + +We will assess the risk, plus make a fix available before we create a GitHub issue. + +Thank you for your contribution. diff --git a/dotnetcore-mathematical-program.sln b/dotnetcore-mathematical-program.sln new file mode 100644 index 0000000..920c898 --- /dev/null +++ b/dotnetcore-mathematical-program.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Anexia.MathematicalProgram", "src\Anexia.MathematicalProgram\Anexia.MathematicalProgram.csproj", "{B573BB67-7970-409D-BC59-309D6CC51853}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Anexia.MathematicalProgram.Tests", "test\Anexia.MathematicalProgram.Tests\Anexia.MathematicalProgram.Tests.csproj", "{EB1C030D-619B-41A2-801F-5F34C8465526}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{F531319E-2E84-40DB-95FD-F767AC5AF859}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B573BB67-7970-409D-BC59-309D6CC51853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B573BB67-7970-409D-BC59-309D6CC51853}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B573BB67-7970-409D-BC59-309D6CC51853}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B573BB67-7970-409D-BC59-309D6CC51853}.Release|Any CPU.Build.0 = Release|Any CPU + {EB1C030D-619B-41A2-801F-5F34C8465526}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB1C030D-619B-41A2-801F-5F34C8465526}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB1C030D-619B-41A2-801F-5F34C8465526}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB1C030D-619B-41A2-801F-5F34C8465526}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj b/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj new file mode 100644 index 0000000..6d08c5c --- /dev/null +++ b/src/Anexia.MathematicalProgram/Anexia.MathematicalProgram.csproj @@ -0,0 +1,19 @@ + + + net8.0 + Anexia.MathematicalProgram + Anexia.MathematicalProgram + enable + enable + true + + + + Anexia.MathematicalProgram + anexia;MLackenbucher + ANEXIA Internetdienstleistungs GmbH + MIT + https://github.com/anexia/dotnetcore-mathematical-program/ + README.md + + diff --git a/src/Anexia.MathematicalProgram/Extensions/EnumExtension.cs b/src/Anexia.MathematicalProgram/Extensions/EnumExtension.cs new file mode 100644 index 0000000..0cd0d06 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Extensions/EnumExtension.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using System.Runtime.Serialization; + +#endregion + +namespace Anexia.MathematicalProgram.Extensions; + +internal static class EnumExtension +{ + internal static string ToEnumString(this T type) where T : Enum + { + var enumType = typeof(T); + var name = Enum.GetName(enumType, type); + + if (name is null) return string.Empty; + + var fieldInfo = enumType.GetField(name); + + if (fieldInfo is null) return string.Empty; + + var enumMemberAttribute = + ((EnumMemberAttribute[])fieldInfo.GetCustomAttributes(typeof(EnumMemberAttribute), true)).FirstOrDefault(); + + return enumMemberAttribute?.Value ?? string.Empty; + } +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/Coefficient.cs b/src/Anexia.MathematicalProgram/Model/Coefficient.cs new file mode 100644 index 0000000..8a9b0af --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/Coefficient.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents a coefficient. +/// +/// The coefficient's value. +public sealed class Coefficient(double value) : MemberwiseEquatable +{ + public double Value { get; } = value; + + /// + public override string ToString() => $"{Value:F1}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/Constant.cs b/src/Anexia.MathematicalProgram/Model/Constant.cs new file mode 100644 index 0000000..6242403 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/Constant.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents a constant. +/// +/// The constant's value. +public sealed class Constant(double value) : MemberwiseEquatable +{ + public static readonly Constant Zero = new(0); + public double Value { get; } = value; + public static Constant operator +(Constant left, double right) => new(left.Value + right); + /// + public override string ToString() => $"{Value:F1}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/Constraint.cs b/src/Anexia.MathematicalProgram/Model/Constraint.cs new file mode 100644 index 0000000..585a7a2 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/Constraint.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents a constraint. +/// +/// A list of terms. +/// The lower and upper bound for the sum of the terms. +public sealed class Constraint(Terms terms, IInterval interval) : MemberwiseEquatable +{ + public Terms Terms { get; } = terms; + public IInterval Interval { get; } = interval; + + /// + public override string ToString() => $"{nameof(Interval)}: {Interval}, {nameof(Terms)}: {Terms}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/Constraints.cs b/src/Anexia.MathematicalProgram/Model/Constraints.cs new file mode 100644 index 0000000..05aa602 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/Constraints.cs @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using System.Collections; +using System.Collections.Immutable; + +#endregion + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents an immutable list of constraints. +/// +/// The constraints. +public sealed class Constraints(ImmutableList elements) : IEnumerable, IEquatable +{ + private ImmutableList Elements { get; } = elements; + + public Constraints(IEnumerable elements) + : this(elements.ToImmutableList()) + { + } + + public Constraints(params Constraint[] values) + : this(values.ToImmutableList()) + { + } + + public Constraints() + : this(ImmutableList.Empty) + { + } + + + /// + public bool Equals(Constraints? other) + { + if (other is null) return false; + + return ReferenceEquals(this, other) || Elements.SequenceEqual(other.Elements); + } + + /// + public override bool Equals(object? obj) => ReferenceEquals(this, obj) || obj is Constraints other && Equals(other); + + /// + public override int GetHashCode() => Elements.GetHashCode(); + + /// + /// Adds the given constraint. + /// + /// The constraint to be added. + /// A new object with the constraint added. + public Constraints Add(Constraint constraint) => new(Elements.Add(constraint)); + + /// + public IEnumerator GetEnumerator() => Elements.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + public override string ToString() => string.Join(",", Elements); +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/IInterval.cs b/src/Anexia.MathematicalProgram/Model/IInterval.cs new file mode 100644 index 0000000..1047471 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/IInterval.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents an Interval. +/// +public interface IInterval +{ + /// + /// The interval's lower bound. + /// + public LowerBound LowerBound { get; } + + /// + /// The interval's upper bound. + /// + public UpperBound UpperBound { get; } +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/InadmissibleBoundsException.cs b/src/Anexia.MathematicalProgram/Model/InadmissibleBoundsException.cs new file mode 100644 index 0000000..c2d8165 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/InadmissibleBoundsException.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// An exception where the lower bound is greater than the upper bound. +/// +public sealed class InadmissibleBoundsException : Exception +{ + /// + /// Initializes a new instance of with given bounds. + /// + /// The lower bound. + /// The upper bound. + internal InadmissibleBoundsException(LowerBound lowerBound, UpperBound upperBound) + : base($"Lower bound {lowerBound} is larger than upper bound {upperBound}") + { + } +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/Interval.cs b/src/Anexia.MathematicalProgram/Model/Interval.cs new file mode 100644 index 0000000..74b3046 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/Interval.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents a simple bounded interval with inclusive upper and lower bounds. +/// +public sealed class Interval : MemberwiseEquatable, IInterval +{ + /// + /// An interval with bounds [0,1]. + /// + public static readonly Interval BinaryInterval = new(0, 1); + + /// + /// Initializes an instance of type with the given bounds. + /// + /// The lower bound. + /// The upper bound. + /// Throws an when the lower bound is grater than the upper bound. + public Interval(LowerBound lowerBound, UpperBound upperBound) + { + if (lowerBound > upperBound) throw new InadmissibleBoundsException(lowerBound, upperBound); + LowerBound = lowerBound; + UpperBound = upperBound; + } + + internal Interval(double lowerBound, double upperBound) + : this(new LowerBound(lowerBound), new UpperBound(upperBound)) + { + } + + /// + /// The lower bound. + /// + public LowerBound LowerBound { get; } + + /// + /// The upper bound. + /// + public UpperBound UpperBound { get; } + + /// + public override string ToString() => $"Interval [{LowerBound},{UpperBound}]"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/LowerBound.cs b/src/Anexia.MathematicalProgram/Model/LowerBound.cs new file mode 100644 index 0000000..acfa75e --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/LowerBound.cs @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents a lower bound. +/// +/// The lower bound's value. +public sealed class LowerBound(double value) : MemberwiseEquatable +{ + public double Value { get; } = value; + + /// + /// Checks if a given lower bound is less than or equal to an upper bound. + /// + /// The lower bound. + /// The upper bound. + /// True, when the given lower bound is less than or equal to the given upper bound. False, otherwise. + public static bool operator <=(LowerBound lowerBound, UpperBound upperBound) => + lowerBound.Value <= upperBound.Value; + + /// + /// Checks if a given lower bound is greater than or equal to an upper bound. + /// + /// The lower bound. + /// The upper bound. + /// True, when the given lower bound is grater than or equal to the given upper bound. False, otherwise. + public static bool operator >=(LowerBound lowerBound, UpperBound upperBound) => + lowerBound.Value >= upperBound.Value; + + /// + /// Checks if a given lower bound is less than an upper bound. + /// + /// The lower bound. + /// The upper bound. + /// True, when the given lower bound is less than to the given upper bound. False, otherwise. + public static bool operator <(LowerBound lowerBound, UpperBound upperBound) => lowerBound.Value < upperBound.Value; + + /// + /// Checks if a given lower bound is greater than an upper bound. + /// + /// The lower bound. + /// The upper bound. + /// True, when the given lower bound is grater than the given upper bound. False, otherwise. + public static bool operator >(LowerBound lowerBound, UpperBound upperBound) => lowerBound.Value > upperBound.Value; + + /// + public override string ToString() => $"{Value:F1}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/Point.cs b/src/Anexia.MathematicalProgram/Model/Point.cs new file mode 100644 index 0000000..5312342 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/Point.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents an inclusive interval where the lower bound equals the upper bound. +/// +public sealed class Point : MemberwiseEquatable, IInterval +{ + /// + /// Represents an interval [1,1] + /// + public static readonly Point One = new(1); + + public Point(double value) + : this(new LowerBound(value), new UpperBound(value)) + { + } + + private Point(LowerBound lowerBound, UpperBound upperBound) + { + LowerBound = lowerBound; + UpperBound = upperBound; + } + + /// + /// The lower bound. + /// + public LowerBound LowerBound { get; } + + /// + /// The upper bound. + /// + public UpperBound UpperBound { get; } + + /// + public override string ToString() => $"Point [{LowerBound},{UpperBound}]"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/Term.cs b/src/Anexia.MathematicalProgram/Model/Term.cs new file mode 100644 index 0000000..f1d9dd5 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/Term.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Google.OrTools.LinearSolver; + +#endregion + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents a term consisting of a coefficient and a variable. The value of the Term is given by +/// * . +/// +/// The coefficient. +/// The variable. +public sealed class Term(Coefficient coefficient, Variable variable) : MemberwiseEquatable +{ + /// + /// The coefficient. + /// + public Coefficient Coefficient { get; } = coefficient; + + /// + /// The variable. + /// + public Variable Variable { get; } = variable; + + /// + public override string ToString() => + $"{nameof(Coefficient)} * ({nameof(Variable)} Name, {nameof(Variable)} HashCode): {Coefficient} * ({Variable.Name()}, {Variable.GetHashCode()})"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/Terms.cs b/src/Anexia.MathematicalProgram/Model/Terms.cs new file mode 100644 index 0000000..e20c6b6 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/Terms.cs @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using System.Collections; +using System.Collections.Immutable; + +#endregion + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents a set of terms. +/// +/// A set of terms. +public sealed class Terms(ImmutableHashSet elements) : IEquatable, IEnumerable +{ + /// + /// Initializes a new instance of with given terms. + /// + /// An enumerable of terms. + public Terms(IEnumerable elements) + : this(elements.ToImmutableHashSet()) + { + } + + /// + /// Initializes a new instance of with given terms. + /// + /// A params list of terms. + public Terms(params Term[] values) + : this(values.ToImmutableList()) + { + } + + /// + /// Initializes a new instance of with empty terms. + /// + public Terms() + : this(ImmutableHashSet.Empty) + { + } + + /// + public bool Equals(Terms? other) + { + if (other is null) return false; + + return ReferenceEquals(this, other) || Elements.SequenceEqual(other.Elements); + } + + /// + public override bool Equals(object? obj) => ReferenceEquals(this, obj) || obj is Terms other && Equals(other); + + /// + public override int GetHashCode() => Elements.GetHashCode(); + + private ImmutableHashSet Elements { get; } = elements; + + /// + /// Adds the given term to the end of the immutable list. + /// + /// The term to be added. + /// A new object with the term added. + public Terms Add(Term term) => new(Elements.Add(term)); + + /// + public IEnumerator GetEnumerator() => Elements.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + public override string ToString() => string.Join(",", Elements); +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/UpperBound.cs b/src/Anexia.MathematicalProgram/Model/UpperBound.cs new file mode 100644 index 0000000..d2d74ea --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/UpperBound.cs @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents an upper bound. +/// +/// The upper bound's value. +public sealed class UpperBound(double value) : MemberwiseEquatable +{ + public double Value { get; } = value; + + /// + /// Checks if a given upper bound is less than or equal to a lower bound. + /// + /// The upper bound. + /// The lower bound. + /// True, when the given upper bound is less than or equal to the given lower bound. False, otherwise. + public static bool operator <=(UpperBound upperBound, LowerBound lowerBound) => + upperBound.Value <= lowerBound.Value; + + /// + /// Checks if a given upper bound is greater than or equal to an lower bound. + /// + /// The upper bound. + /// The lower bound. + /// True, when the given upper bound is grater than or equal to the given lower bound. False, otherwise. + public static bool operator >=(UpperBound upperBound, LowerBound lowerBound) => + upperBound.Value >= lowerBound.Value; + + /// + /// Checks if a given upper bound is less than an lower bound. + /// + /// The upper bound. + /// The lower bound. + /// True, when the given upper bound is less than to the given lower bound. False, otherwise. + public static bool operator <(UpperBound upperBound, LowerBound lowerBound) => upperBound.Value < lowerBound.Value; + + /// + /// Checks if a given upper bound is greater than an lower bound. + /// + /// The upper bound. + /// The lower bound. + /// True, when the given upper bound is grater than the given lower bound. False, otherwise. + public static bool operator >(UpperBound upperBound, LowerBound lowerBound) => upperBound.Value > lowerBound.Value; + + /// + public override string ToString() => $"{Value:F1}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Model/WarningMessage.cs b/src/Anexia.MathematicalProgram/Model/WarningMessage.cs new file mode 100644 index 0000000..7d57b63 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Model/WarningMessage.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Model; + +/// +/// Represents a warning message. +/// +/// A message. +public sealed class WarningMessage(string message) : MemberwiseEquatable +{ + public string Message { get; } = message; + + /// + public override string ToString() => $"{nameof(Message)}: {Message}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Result/IsFeasible.cs b/src/Anexia.MathematicalProgram/Result/IsFeasible.cs new file mode 100644 index 0000000..f460717 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Result/IsFeasible.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Result; + +/// +/// Represents an object containing feasibility information. +/// +/// Boolean representing feasible when true, or unfeasible when false. +public sealed class IsFeasible(bool value) : MemberwiseEquatable +{ + /// + /// True for feasible, false when not. + /// + public bool Value { get; } = value; + + /// + public override string ToString() => $"{Value}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Result/IsOptimal.cs b/src/Anexia.MathematicalProgram/Result/IsOptimal.cs new file mode 100644 index 0000000..1fcf5bf --- /dev/null +++ b/src/Anexia.MathematicalProgram/Result/IsOptimal.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Result; + +/// +/// Represents an object containing optimality information. +/// +/// Boolean representing optimal when true, or not optimal when false. +public sealed class IsOptimal(bool value) : MemberwiseEquatable +{ + /// + /// True for optimal, false when not. + /// + public bool Value { get; } = value; + + /// + public override string ToString() => $"{Value}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Result/ObjectiveValue.cs b/src/Anexia.MathematicalProgram/Result/ObjectiveValue.cs new file mode 100644 index 0000000..6ff7628 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Result/ObjectiveValue.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Result; + +/// +/// Represents an objective value. +/// +/// The objective's value. +public sealed class ObjectiveValue(double value) : MemberwiseEquatable +{ + /// + /// The objective value. + /// + public double Value { get; } = value; + + /// + public override string ToString() => $"{Value}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Result/OptimalityGap.cs b/src/Anexia.MathematicalProgram/Result/OptimalityGap.cs new file mode 100644 index 0000000..a11f7f4 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Result/OptimalityGap.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Result; + +/// +/// Represents an optimality gap. +/// +/// The gap's value. +public sealed class OptimalityGap(double value) : MemberwiseEquatable +{ + /// + /// The optimality gap. + /// + public double Value { get; } = value; + + /// + public override string ToString() => $"{Value}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Result/ResultHandling.cs b/src/Anexia.MathematicalProgram/Result/ResultHandling.cs new file mode 100644 index 0000000..bb5efad --- /dev/null +++ b/src/Anexia.MathematicalProgram/Result/ResultHandling.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Solve; +using Google.OrTools.LinearSolver; + +#endregion + +namespace Anexia.MathematicalProgram.Result; + +internal sealed class ResultHandling(Solver solver) : MemberwiseEquatable +{ + private Solver Solver { get; } = solver; + + internal SolverResult Handle(Solver.ResultStatus resultStatus) + { + switch (resultStatus) + { + case Solver.ResultStatus.OPTIMAL: + return new SolverResult(Solver, + new ObjectiveValue(Solver.Objective().Value()), + new IsFeasible(true), + new IsOptimal(true), + new OptimalityGap(0)); + case Solver.ResultStatus.FEASIBLE: + var objective = Solver.Objective(); + var objectiveValue = objective.Value(); + + return new SolverResult(Solver, + new ObjectiveValue(objectiveValue), + new IsFeasible(true), + new IsOptimal(false), + new OptimalityGap(Math.Abs(objective.BestBound() - objectiveValue) / objectiveValue)); + case Solver.ResultStatus.INFEASIBLE: + return new SolverResult(Solver, + new ObjectiveValue(double.NaN), + new IsFeasible(false), + new IsOptimal(false), + new OptimalityGap(double.NaN)); + case Solver.ResultStatus.UNBOUNDED: + throw new MathematicalProgramException("Mathematical program is unbounded."); + case Solver.ResultStatus.ABNORMAL: + throw new MathematicalProgramException("Mathematical program is abnormal (probably numerical error)."); + case Solver.ResultStatus.NOT_SOLVED: + throw new MathematicalProgramException("Mathematical program could not be diagnosed and solved."); + case Solver.ResultStatus.MODEL_INVALID: + throw new MathematicalProgramException("Mathematical model is not valid."); + default: + throw new MathematicalProgramException("Unknown result status in linear solver."); + } + } + + /// + public override string ToString() => $"{nameof(Solver)}: {Solver}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Result/SolverResult.cs b/src/Anexia.MathematicalProgram/Result/SolverResult.cs new file mode 100644 index 0000000..2c813b4 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Result/SolverResult.cs @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Google.OrTools.LinearSolver; + +#endregion + +namespace Anexia.MathematicalProgram.Result; + +/// +/// Represents the solver's result. +/// +/// The original solver used. +/// The objective value. +/// Information, whether the model was feasible, or not. +/// Information, whether the solution is optimal, or not. +/// The optimality gap. +public sealed class SolverResult( + Solver solvedSolver, + ObjectiveValue objectiveValue, + IsFeasible isFeasible, + IsOptimal isOptimal, + OptimalityGap optimalityGap) : MemberwiseEquatable +{ + /// + /// The original solver used. + /// + public Solver SolvedSolver { get; } = solvedSolver; + + /// + /// The objective value. + /// + public ObjectiveValue ObjectiveValue { get; } = objectiveValue; + + /// + /// Information, whether the model was feasible, or not. + /// + public IsFeasible IsFeasible { get; } = isFeasible; + + /// + /// Information, whether the solution is optimal, or not. + /// + public IsOptimal IsOptimal { get; } = isOptimal; + + /// + /// The optimality gap. + /// + public OptimalityGap OptimalityGap { get; } = optimalityGap; + + /// + public override string ToString() => + $"{nameof(ObjectiveValue)}: {ObjectiveValue}, {nameof(IsFeasible)}: {IsFeasible}, {nameof(IsOptimal)}: {IsOptimal}, {nameof(OptimalityGap)}: {OptimalityGap}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Solve/IntegerLinearProgramSolver.cs b/src/Anexia.MathematicalProgram/Solve/IntegerLinearProgramSolver.cs new file mode 100644 index 0000000..04f3919 --- /dev/null +++ b/src/Anexia.MathematicalProgram/Solve/IntegerLinearProgramSolver.cs @@ -0,0 +1,211 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Extensions; +using Anexia.MathematicalProgram.Model; +using Anexia.MathematicalProgram.Result; +using Anexia.MathematicalProgram.SolverConfiguration; +using Google.OrTools.LinearSolver; + +#endregion + +namespace Anexia.MathematicalProgram.Solve; + +/// +/// Represents a solver for solving Integer Linear Programming models. +/// +public sealed class IntegerLinearProgramSolver : MemberwiseEquatable, IDisposable +{ + private IntegerLinearProgramSolver(Solver solver, IlpSolverType solverType) + { + Solver = solver; + SolverType = solverType; + } + + /// + /// Initializes a new instance of with an optional + /// . When the is not specified, the default solver is + /// CBC. + /// If the given solver type is either not supported, or no licence is available, it is switched to + /// CBC. + /// + /// The desired solver type. + public IntegerLinearProgramSolver(IlpSolverType solverType = IlpSolverType.CbcMixedIntegerProgramming) + : this(Solver.CreateSolver(CheckTypeSupportedOrSwitchToCbc(solverType).ToEnumString()), + CheckTypeSupportedOrSwitchToCbc(solverType)) + { + } + + /// + /// Creates an instance of with a given solver type. + /// If the given solver type is either not supported, or no licence is available, it is switched to + /// CBC. Furthermore, a warning message is set via + /// . + /// + /// The desired solver type. + /// A warning message when the solver tye gets switched automatically. + /// A new instance of . + public static IntegerLinearProgramSolver Create(IlpSolverType solverType, out WarningMessage? message) + { + message = null; + + if (Solver.SupportsProblemType(Enum.Parse(solverType.ToEnumString()))) + return new IntegerLinearProgramSolver(solverType); + + message = new( + $"Solver type {solverType} is not supported -> Switched to CBC. There might be no valid licence or an unsupported Gurobi version."); + + return new IntegerLinearProgramSolver(); + } + + private static IlpSolverType CheckTypeSupportedOrSwitchToCbc(IlpSolverType solverType) => + Solver.SupportsProblemType(Enum.Parse(solverType.ToEnumString())) + ? solverType + : IlpSolverType.CbcMixedIntegerProgramming; + + private Solver Solver { get; } + private IlpSolverType SolverType { get; } + + /// + /// Returns the model in LP format. + /// + /// Specifies whether the model should be obfuscated, or not. True by default. + /// The model. + public string ModelAsLpFormat(bool obfuscated = true) => Solver.ExportModelAsLpFormat(obfuscated); + + /// + /// Returns the number of constraints. + /// + /// The number of constraints. + public int NumberOfConstraints() => Solver.NumConstraints(); + + /// + /// Returns the number of variables. + /// + /// The number of variables. + public int NumberOfVariables() => Solver.NumVariables(); + + /// + /// Adds a new integer variable to the solver and returns it as an out parameter. + /// + /// The desired variable's interval. + /// The desired variable's name. + /// The newly created variable. + /// The updated solver. + public IntegerLinearProgramSolver AddIntegerVariable(IInterval interval, string variableName, out Variable variable) + { + variable = Solver.MakeIntVar(interval.LowerBound.Value, interval.UpperBound.Value, variableName); + + return this; + } + + /// + /// Adds the given constraints to the solver. + /// + /// The constraints to be added. + /// The updated solver. + public IntegerLinearProgramSolver AddConstraints(Constraints constraints) + { + foreach (var constraint in constraints) + { + var interval = constraint.Interval; + var solverConstraint = Solver.MakeConstraint(interval.LowerBound.Value, interval.UpperBound.Value); + + foreach (var term in constraint.Terms) + solverConstraint.SetCoefficient(term.Variable, term.Coefficient.Value); + } + + return this; + } + + /// + /// Sets the objective function of the solver. + /// + /// The terms of the objective function. + /// Boolean whether to minimize or maximize. + /// The updated solver. + public IntegerLinearProgramSolver SetObjective(Terms terms, bool minimize) => + SetObjective(terms, Constant.Zero, minimize); + + /// + /// Sets the objective function of the solver. + /// + /// The terms of the objective function. + /// An additional constant offset. + /// Boolean whether to minimize or maximize. + /// The updated solver. + public IntegerLinearProgramSolver SetObjective(Terms terms, Constant constant, bool minimize) + { + var objective = Solver.Objective(); + objective.Clear(); + + foreach (var term in terms) objective.SetCoefficient(term.Variable, term.Coefficient.Value); + + objective.SetOffset(constant.Value); + + if (minimize) + objective.SetMinimization(); + else + objective.SetMaximization(); + + return this; + } + + /// + /// Starts the solving process. + /// + /// The result after solving. + /// Throws a if an error occured while solving. + /// Furthermore, when the result status is anything other than feasible, infeasible or optimal. + /// + public SolverResult Solve() => Solve(new SolverParameter()); + + /// + /// Starts the solving process with additional parameters. + /// + /// The parameters to be used by the solver. + /// The result. + /// Throws a if an error occured while solving. + /// Furthermore, when the result status is anything other than feasible, infeasible or optimal. + /// + public SolverResult Solve(SolverParameter solverParameter) + { + try + { + _ = Solver.SetNumThreads((int)(solverParameter.NumberOfThreads?.Value ?? 0)); + + if (solverParameter.TimeLimitInMilliseconds is not null) + Solver.SetTimeLimit(solverParameter.TimeLimitInMilliseconds.Value); + + if (solverParameter.EnableSolverOutput.Value) Solver.EnableOutput(); + using var parameter = new MPSolverParameters(); + + parameter.SetDoubleParam(MPSolverParameters.DoubleParam.RELATIVE_MIP_GAP, + solverParameter.RelativeGap.Value); + + var resultStatus = Solver.Solve(parameter); + + return new ResultHandling(Solver).Handle(resultStatus); + } + catch (Exception exception) + { + throw new MathematicalProgramException(exception); + } + } + + + /// + public void Dispose() + { + Solver.Clear(); + Solver.Dispose(); + } + + /// + public override string ToString() => $"IntegerLinearProgrammingSolver {SolverType.ToEnumString()}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Solve/LinearProgramSolver.cs b/src/Anexia.MathematicalProgram/Solve/LinearProgramSolver.cs new file mode 100644 index 0000000..32d539d --- /dev/null +++ b/src/Anexia.MathematicalProgram/Solve/LinearProgramSolver.cs @@ -0,0 +1,158 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; +using Anexia.MathematicalProgram.Result; +using Anexia.MathematicalProgram.SolverConfiguration; +using Google.OrTools.LinearSolver; + +#endregion + +namespace Anexia.MathematicalProgram.Solve; + +/// +/// Represents a solver for solving Linear Programming models. +/// +public sealed class LinearProgramSolver : MemberwiseEquatable, IDisposable +{ + private LinearProgramSolver(Solver solver) + { + Solver = solver; + } + + /// + /// Initializes a new instance of using + /// GLOP. + /// + public LinearProgramSolver() + : this(Solver.CreateSolver(LpSolverType.Glop.ToString())) + { + } + + private Solver Solver { get; } + + /// + /// Adds a new continuous variable to the solver and returns it as an out parameter. + /// + /// The desired variable's interval. + /// The desired variable's name. + /// The newly created variable. + /// The updated solver. + public LinearProgramSolver AddContinuousVariable(IInterval interval, string variableName, out Variable variable) + { + variable = Solver.MakeNumVar(interval.LowerBound.Value, interval.UpperBound.Value, variableName); + + return this; + } + + /// + /// Adds the given constraints to the solver. + /// + /// The constraints to be added. + /// The updated solver. + public LinearProgramSolver AddConstraints(Constraints constraints) + { + foreach (var constraint in constraints) + { + var interval = constraint.Interval; + var solverConstraint = Solver.MakeConstraint(interval.LowerBound.Value, interval.UpperBound.Value); + + foreach (var term in constraint.Terms) + solverConstraint.SetCoefficient(term.Variable, term.Coefficient.Value); + } + + return this; + } + + /// + /// Sets the objective function of the solver. + /// + /// The terms of the objective function. + /// Boolean whether to minimize or maximize. + /// The updated solver. + public LinearProgramSolver SetObjective(Terms terms, bool minimize) => SetObjective(terms, Constant.Zero, minimize); + + /// + /// Sets the objective function of the solver. + /// + /// The terms of the objective function. + /// An additional constant offset. + /// Boolean whether to minimize or maximize. + /// The updated solver. + public LinearProgramSolver SetObjective(Terms terms, Constant constant, bool minimize) + { + var objective = Solver.Objective(); + objective.Clear(); + + foreach (var term in terms) objective.SetCoefficient(term.Variable, term.Coefficient.Value); + + objective.SetOffset(constant.Value); + + if (minimize) + objective.SetMinimization(); + else + objective.SetMaximization(); + + return this; + } + + /// + /// Starts the solving process. + /// + /// The result after solving. + /// Throws a if an error occured while solving. + /// Furthermore, when the result status is anything other than feasible, infeasible or optimal. + /// + public SolverResult Solve() => Solve(new SolverParameter()); + + /// + /// Starts the solving process with additional parameters. + /// + /// The parameters to be used by the solver. + /// The result after solving. + /// Throws a if an error occured while solving. + /// Furthermore, when the result status is anything other than feasible, infeasible or optimal. + /// + public SolverResult Solve(SolverParameter solverParameter) + { + try + { + _ = Solver.SetNumThreads((int)(solverParameter.NumberOfThreads?.Value ?? 0)); + + if (solverParameter.TimeLimitInMilliseconds is not null) + Solver.SetTimeLimit(solverParameter.TimeLimitInMilliseconds.Value); + + if (solverParameter.EnableSolverOutput.Value) Solver.EnableOutput(); + + var parameter = new MPSolverParameters(); + + parameter.SetDoubleParam(MPSolverParameters.DoubleParam.RELATIVE_MIP_GAP, + solverParameter.RelativeGap.Value); + + var resultStatus = Solver.Solve(parameter); + + return new ResultHandling(Solver).Handle(resultStatus); + } + catch (Exception exception) + { + Console.WriteLine(exception); + + throw new MathematicalProgramException(exception); + } + } + + /// + public void Dispose() + { + Solver.Clear(); + Solver.Dispose(); + } + + /// + public override string ToString() => "Google.OrTools.LinearSolver Glop"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/Solve/MathematicalProgramException.cs b/src/Anexia.MathematicalProgram/Solve/MathematicalProgramException.cs new file mode 100644 index 0000000..d04e06c --- /dev/null +++ b/src/Anexia.MathematicalProgram/Solve/MathematicalProgramException.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.Solve; + +/// +/// An exception occured while solving a model. +/// +public sealed class MathematicalProgramException : Exception +{ + internal MathematicalProgramException(Exception exception) + : base($"Error in solver: {exception.Message}, {exception.InnerException}", exception) + { + } + + internal MathematicalProgramException(string message) + : base(message) + { + } +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/EnableSolverOutput.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/EnableSolverOutput.cs new file mode 100644 index 0000000..1a7f528 --- /dev/null +++ b/src/Anexia.MathematicalProgram/SolverConfiguration/EnableSolverOutput.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.SolverConfiguration; + +/// +/// Represents a setting whether to enable solver log output on the console, or not. +/// +/// True to enable solver console log output, false to disable. +public sealed class EnableSolverOutput(bool value) : MemberwiseEquatable +{ + public static readonly EnableSolverOutput True = new(true); + public static readonly EnableSolverOutput False = new(false); + + public bool Value { get; } = value; + + /// + public override string ToString() => $"{nameof(Value)}: {Value}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/ILPSolverType.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/ILPSolverType.cs new file mode 100644 index 0000000..c9ec0b1 --- /dev/null +++ b/src/Anexia.MathematicalProgram/SolverConfiguration/ILPSolverType.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using System.Runtime.Serialization; + +#endregion + +namespace Anexia.MathematicalProgram.SolverConfiguration; + +/// +/// The supported ILP solver types. +/// +public enum IlpSolverType +{ + /// + /// CBC solver. + /// + [EnumMember(Value = "CBC_MIXED_INTEGER_PROGRAMMING")] + CbcMixedIntegerProgramming, + + /// + /// Gurobi solver. A licence is needed for usage. + /// + [EnumMember(Value = "GUROBI_MIXED_INTEGER_PROGRAMMING")] + GurobiMixedIntegerProgramming +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/LPSolverType.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/LPSolverType.cs new file mode 100644 index 0000000..387c052 --- /dev/null +++ b/src/Anexia.MathematicalProgram/SolverConfiguration/LPSolverType.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.SolverConfiguration; + +/// +/// The supported LP solvers. +/// +public enum LpSolverType +{ + /// + /// GLOP solver. + /// + Glop +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/NumberOfThreads.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/NumberOfThreads.cs new file mode 100644 index 0000000..a0c100f --- /dev/null +++ b/src/Anexia.MathematicalProgram/SolverConfiguration/NumberOfThreads.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.SolverConfiguration; + +/// +/// Represents the number of threads to be used by the solver. +/// +/// The value. +public sealed class NumberOfThreads(uint value) : MemberwiseEquatable +{ + public uint Value { get; } = value; + + /// + public override string ToString() => $"{nameof(Value)}: {Value}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/RelativeGap.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/RelativeGap.cs new file mode 100644 index 0000000..b00927f --- /dev/null +++ b/src/Anexia.MathematicalProgram/SolverConfiguration/RelativeGap.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.SolverConfiguration; + +/// +/// Represents the relative gap when the solver terminates. It is an upper bound on the actual MIP gap given by (|ObjBound - ObjValue|) / |ObjValue|. +/// +/// The gap. +public sealed class RelativeGap(double relativeGap) : MemberwiseEquatable +{ + public static readonly RelativeGap EMinus7 = new(0.0000001); + + public double Value { get; } = relativeGap; + + /// + /// Calculates the given negative power of 10, i.e., 10^-negativeExponent + /// + /// The exponent of 10^-1 + /// The calculated value. + public static RelativeGap FromEMinus(uint negativeExponent) => new(Math.Pow(10, -negativeExponent)); + + /// + public override string ToString() => $"{nameof(Value)}: {Value}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs new file mode 100644 index 0000000..d700795 --- /dev/null +++ b/src/Anexia.MathematicalProgram/SolverConfiguration/SolverParameter.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.SolverConfiguration; + +/// +/// Represents the parameters that can be set for a solver. +/// +/// Whether to enable the solver's underlying log output. +/// 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. +public sealed class SolverParameter( + EnableSolverOutput enableSolverOutput, + RelativeGap relativeGap, + TimeLimitInMilliseconds? timeLimitInMilliseconds = null, + NumberOfThreads? numberOfThreads = null) : MemberwiseEquatable +{ + /// + /// Create default solver parameters with given time limit. + /// + /// The time limit in milliseconds. + /// + /// ParameterDescription + /// NumberOfThreadsDefault value: null + /// EnableSolverOutputDefault value: false + /// RelativeGapDefault value: E-7 + /// + public SolverParameter(TimeLimitInMilliseconds timeLimitInMilliseconds) + : this(EnableSolverOutput.False, RelativeGap.EMinus7, timeLimitInMilliseconds, new NumberOfThreads(0)) + { + } + + /// + /// Create solver parameters without a time limit. + /// + /// + /// ParameterDescription + /// TimeLimitInMillisecondsDefault value: null + /// NumberOfThreadsDefault value: null + /// EnableSolverOutputDefault value: false + /// RelativeGapDefault value: E-7 + /// + public SolverParameter(EnableSolverOutput enableSolverOutput, + NumberOfThreads numberOfThreads, + RelativeGap relativeGap) + : this(enableSolverOutput, relativeGap, null, numberOfThreads) + { + } + + /// + /// Create default solver parameters + /// + /// + /// ParameterDescription + /// TimeLimitInMillisecondsDefault value: null + /// NumberOfThreadsDefault value: null + /// EnableSolverOutputDefault value: false + /// RelativeGapDefault value: E-7 + /// + public SolverParameter() + : this(EnableSolverOutput.False, new NumberOfThreads(0), RelativeGap.EMinus7) + { + } + + public RelativeGap RelativeGap { get; } = relativeGap; + public TimeLimitInMilliseconds? TimeLimitInMilliseconds { get; } = timeLimitInMilliseconds; + public EnableSolverOutput EnableSolverOutput { get; } = enableSolverOutput; + public NumberOfThreads? NumberOfThreads { get; } = numberOfThreads; + + /// + public override string ToString() => + $"{nameof(RelativeGap)}: {RelativeGap}, {nameof(TimeLimitInMilliseconds)}: {TimeLimitInMilliseconds}, {nameof(EnableSolverOutput)}: {EnableSolverOutput}, {nameof(NumberOfThreads)}: {NumberOfThreads}"; +} \ No newline at end of file diff --git a/src/Anexia.MathematicalProgram/SolverConfiguration/TimeLimitInMilliseconds.cs b/src/Anexia.MathematicalProgram/SolverConfiguration/TimeLimitInMilliseconds.cs new file mode 100644 index 0000000..13a23cb --- /dev/null +++ b/src/Anexia.MathematicalProgram/SolverConfiguration/TimeLimitInMilliseconds.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.MathematicalProgram.SolverConfiguration; + +/// +/// The time limit. +/// +/// +public sealed class TimeLimitInMilliseconds(uint value) : MemberwiseEquatable +{ + public uint Value { get; } = value; + + /// + public override string ToString() => $"{nameof(Value)}: {Value}"; +} \ 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 new file mode 100644 index 0000000..da7b273 --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Anexia.MathematicalProgram.Tests.csproj @@ -0,0 +1,17 @@ + + + + enable + + false + + Anexia.MathematicalProgram.Tests + + Anexia.MathematicalProgram.Tests + + + + + + + diff --git a/test/Anexia.MathematicalProgram.Tests/Factory/ConstraintFactory.cs b/test/Anexia.MathematicalProgram.Tests/Factory/ConstraintFactory.cs new file mode 100644 index 0000000..0fe7360 --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Factory/ConstraintFactory.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; + +#endregion + +namespace Anexia.MathematicalProgram.Tests.Factory; + +internal static class ConstraintFactory +{ + public static Constraint Constraint(Term[] terms, IInterval interval) => new(new Terms(terms), interval); + + public static Constraints Constraints(params Constraint[] constraints) => new(constraints); +} \ No newline at end of file diff --git a/test/Anexia.MathematicalProgram.Tests/Factory/IntervalFactory.cs b/test/Anexia.MathematicalProgram.Tests/Factory/IntervalFactory.cs new file mode 100644 index 0000000..f633689 --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Factory/IntervalFactory.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; + +#endregion + +namespace Anexia.MathematicalProgram.Tests.Factory; + +internal static class IntervalFactory +{ + public static Interval Interval(double left, double right) => new(new LowerBound(left), new UpperBound(right)); + + public static Point Point(double point) => new(point); +} \ No newline at end of file diff --git a/test/Anexia.MathematicalProgram.Tests/Factory/TermFactory.cs b/test/Anexia.MathematicalProgram.Tests/Factory/TermFactory.cs new file mode 100644 index 0000000..86d3569 --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Factory/TermFactory.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; +using Google.OrTools.LinearSolver; + +#endregion + +namespace Anexia.MathematicalProgram.Tests.Factory; + +internal static class TermFactory +{ + public static Term Term(double coefficient, Variable variable) => new(new Coefficient(coefficient), variable); +} \ No newline at end of file diff --git a/test/Anexia.MathematicalProgram.Tests/Model/ConstantTest.cs b/test/Anexia.MathematicalProgram.Tests/Model/ConstantTest.cs new file mode 100644 index 0000000..1f1416b --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Model/ConstantTest.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; + +#endregion + +namespace Anexia.MathematicalProgram.Tests.Model; + +public sealed class ConstantTest +{ + [Theory] + [InlineData(5, 5, 10)] + [InlineData(17, -7, 10)] + [InlineData(-7, -7, -14)] + [InlineData(double.MaxValue, -5, double.MaxValue)] + [InlineData(double.MaxValue, -double.MaxValue, 0)] + [InlineData(double.MaxValue, 5, double.MaxValue)] + [InlineData(double.MinValue, -7, double.MinValue)] + public void ConstantAdditionReturnsCorrectResult(double constant, double toAdd, double result) => + Assert.Equal(new Constant(constant) + toAdd, new Constant(result)); +} \ No newline at end of file diff --git a/test/Anexia.MathematicalProgram.Tests/Model/ConstraintsComparisonTest.cs b/test/Anexia.MathematicalProgram.Tests/Model/ConstraintsComparisonTest.cs new file mode 100644 index 0000000..8f90073 --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Model/ConstraintsComparisonTest.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; +using Anexia.MathematicalProgram.Solve; +using Anexia.MathematicalProgram.SolverConfiguration; +using Anexia.MathematicalProgram.Tests.Factory; +using static Anexia.MathematicalProgram.Tests.Factory.ConstraintFactory; +using static Anexia.MathematicalProgram.Tests.Factory.TermFactory; +#endregion + +namespace Anexia.MathematicalProgram.Tests.Model; + +public sealed class ConstraintsComparisonTest +{ + [Fact] + public void ConstraintsWithDifferentTermsOrderMatch() + { + var solver = IntegerLinearProgramSolver.Create(IlpSolverType.CbcMixedIntegerProgramming, out _); + + _ = solver.AddIntegerVariable(IntervalFactory.Interval(0, 1), "1", out var variable1); + _ = solver.AddIntegerVariable(IntervalFactory.Interval(0, 1), "2", out var variable2); + _ = solver.AddIntegerVariable(IntervalFactory.Interval(0, 1), "3", out var variable3); + + var constraints = Constraints(Constraint([Term(1, variable1)], Point.One), + Constraint([Term(1, variable2), Term(1, variable3)], Point.One), + Constraint([Term(1, variable1), Term(1, variable3)], Point.One)); + + var constraintDifferentTermsOrder = Constraints(Constraint([Term(1, variable1)], Point.One), + Constraint([Term(1, variable3), Term(1, variable2)], Point.One), + Constraint([Term(1, variable3), Term(1, variable1)], Point.One)); + + Assert.Equal(constraintDifferentTermsOrder, constraints); + } + + [Fact] + public void ConstraintsWithDifferentConstraintOrderDoNotOrderMatch() + { + var solver = IntegerLinearProgramSolver.Create(IlpSolverType.CbcMixedIntegerProgramming, out _); + + _ = solver.AddIntegerVariable(IntervalFactory.Interval(0, 1), "1", out var variable1); + _ = solver.AddIntegerVariable(IntervalFactory.Interval(0, 1), "2", out var variable2); + _ = solver.AddIntegerVariable(IntervalFactory.Interval(0, 1), "3", out var variable3); + + var constraints = Constraints(Constraint([Term(1, variable1)], Point.One), + Constraint([Term(1, variable2), Term(1, variable3)], Point.One), + Constraint([Term(1, variable1), Term(1, variable3)], Point.One)); + + var constraintDifferentTermsOrder = Constraints(Constraint([Term(1, variable1)], Point.One), + Constraint([Term(1, variable3), Term(1, variable1)], Point.One), + Constraint([Term(1, variable3), Term(1, variable2)], Point.One)); + + Assert.NotEqual(constraintDifferentTermsOrder, constraints); + } +} \ No newline at end of file diff --git a/test/Anexia.MathematicalProgram.Tests/Model/IntervalTest.cs b/test/Anexia.MathematicalProgram.Tests/Model/IntervalTest.cs new file mode 100644 index 0000000..d02b058 --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Model/IntervalTest.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; + +#endregion + +namespace Anexia.MathematicalProgram.Tests.Model; + +public sealed class IntervalTest +{ + [Theory] + [InlineData(6, 5)] + [InlineData(0.1, 0)] + [InlineData(-0.1, -0.11)] + [InlineData(double.MaxValue, double.Epsilon)] + public void IntervalInitializingThrowsExpectedException(double left, double right) => + Assert.Throws(() => new Interval(new LowerBound(left), new UpperBound(right))); + + [Fact] + public void SuccessfulIntervalInitializing() => Assert.NotNull(new Interval(new LowerBound(5), new UpperBound(6))); +} \ No newline at end of file diff --git a/test/Anexia.MathematicalProgram.Tests/Solve/IntegerLinearSolverTest.cs b/test/Anexia.MathematicalProgram.Tests/Solve/IntegerLinearSolverTest.cs new file mode 100644 index 0000000..075bfc1 --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Solve/IntegerLinearSolverTest.cs @@ -0,0 +1,84 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; +using Anexia.MathematicalProgram.Result; +using Anexia.MathematicalProgram.Solve; +using Anexia.MathematicalProgram.SolverConfiguration; +using static Anexia.MathematicalProgram.Tests.Factory.ConstraintFactory; +using static Anexia.MathematicalProgram.Tests.Factory.IntervalFactory; +using static Anexia.MathematicalProgram.Tests.Factory.TermFactory; +using Interval = Anexia.MathematicalProgram.Model.Interval; +using Point = Anexia.MathematicalProgram.Model.Point; + +#endregion + +namespace Anexia.MathematicalProgram.Tests.Solve; + +public sealed class IntegerLinearSolverTest +{ + [Fact] + public void SolverWithoutObjectiveAndConstraintsReturnsCorrectResult() + { + var result = new IntegerLinearProgramSolver().Solve(); + + Assert.Equal( + new SolverResult(result.SolvedSolver, new ObjectiveValue(0), new IsFeasible(true), new IsOptimal(true), + new OptimalityGap(0)), result); + } + + [Fact] + public void SolverWithSimpleFeasibleIlpModelReturnsCorrectResult() + { + /* + * min 2x, s.t. x=1, x binary + */ + var result = new IntegerLinearProgramSolver() + .AddIntegerVariable(Interval.BinaryInterval, "TestVariable", out var testVariable) + .SetObjective(new Terms(Term(2, testVariable)), true) + .AddConstraints(Constraints(Constraint([Term(1, testVariable)], Point.One))).Solve(); + + Assert.Equal(new SolverResult( + result.SolvedSolver, new ObjectiveValue(2), new IsFeasible(true), new IsOptimal(true), + new OptimalityGap(0)), result); + } + + [Fact] + public void SolverWithInfeasibleIlModelReturnsCorrectResult() + { + /* + * max 2x, s.t. x=3, x binary + */ + var result = new IntegerLinearProgramSolver() + .AddIntegerVariable(Interval.BinaryInterval, "TestVariable", out var testVariable) + .SetObjective(new Terms(Term(2, testVariable)), false) + .AddConstraints(Constraints(Constraint([Term(1, testVariable)], Point(3)))).Solve(new SolverParameter( + EnableSolverOutput.True, + RelativeGap.EMinus7, + new TimeLimitInMilliseconds(10), + new NumberOfThreads(2))); + + Assert.Equal(new SolverResult( + result.SolvedSolver, new ObjectiveValue(double.NaN), new IsFeasible(false), new IsOptimal(false), + new OptimalityGap(double.NaN)), result); + } + + [Fact] + public void SolverWithUnboundedIlModelThrowsExpectedException() + { + /* + * max 2x, x positive + */ + var solver = new IntegerLinearProgramSolver() + .AddIntegerVariable(Interval(0, double.PositiveInfinity), "TestVariable", out var testVariable) + .SetObjective(new Terms(Term(2, testVariable)), false); + + Assert.Throws(() => + solver.Solve(new SolverParameter(new TimeLimitInMilliseconds(10)))); + } +} \ No newline at end of file diff --git a/test/Anexia.MathematicalProgram.Tests/Solve/LinearSolverTest.cs b/test/Anexia.MathematicalProgram.Tests/Solve/LinearSolverTest.cs new file mode 100644 index 0000000..b284243 --- /dev/null +++ b/test/Anexia.MathematicalProgram.Tests/Solve/LinearSolverTest.cs @@ -0,0 +1,98 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH.All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +#region + +using Anexia.MathematicalProgram.Model; +using Anexia.MathematicalProgram.Result; +using Anexia.MathematicalProgram.Solve; +using Anexia.MathematicalProgram.SolverConfiguration; +using static System.Double; +using static Anexia.MathematicalProgram.Tests.Factory.ConstraintFactory; +using static Anexia.MathematicalProgram.Tests.Factory.IntervalFactory; +using static Anexia.MathematicalProgram.Tests.Factory.TermFactory; + +#endregion + +namespace Anexia.MathematicalProgram.Tests.Solve; + +public sealed class LinearSolverTest +{ + [Fact] + public void SolverWithoutObjectiveAndConstraintsReturnsCorrectResult() + { + var result = new LinearProgramSolver().Solve(); + + Assert.Equal(new SolverResult( + result.SolvedSolver, new ObjectiveValue(0), new IsFeasible(true), new IsOptimal(true), + new OptimalityGap(0)), result); + } + + [Fact] + public void SolverWithSimpleFeasibleMinimizationLpModelReturnsCorrectResult() + { + /* + * min 2x, s.t. x=2, x in (0,3) + */ + var result = new LinearProgramSolver() + .AddContinuousVariable(Interval(0, 3), "TestVariable", out var testVariable) + .SetObjective(new Terms(Term(2, testVariable)), true) + .AddConstraints(Constraints(Constraint([Term(1, testVariable)], Point(2)))).Solve(); + + Assert.Equal(new SolverResult( + result.SolvedSolver, new ObjectiveValue(4), new IsFeasible(true), new IsOptimal(true), + new OptimalityGap(0)), result); + } + + [Fact] + public void SolverWithSimpleFeasibleMaximizationLpModelReturnsCorrectResult() + { + /* + * max 2x, s.t. x<=2, x in (0,3) + */ + var result = new LinearProgramSolver() + .AddContinuousVariable(Interval(0, 3), "TestVariable", out var testVariable) + .SetObjective(new Terms(Term(2, testVariable)), false) + .AddConstraints(Constraints(Constraint([Term(1, testVariable)], Interval(NegativeInfinity, 2)))).Solve(); + + Assert.Equal(new SolverResult( + result.SolvedSolver, new ObjectiveValue(4), new IsFeasible(true), new IsOptimal(true), + new OptimalityGap(0)), result); + } + + [Fact] + public void SolverWithInfeasibleLpModelReturnsCorrectResult() + { + /* + * max 2x, s.t. x=3, x in (0,1) + */ + var result = new LinearProgramSolver() + .AddContinuousVariable(Interval(0, 1), "TestVariable", out var testVariable) + .SetObjective(new Terms(Term(2, testVariable)), false) + .AddConstraints(Constraints(Constraint([Term(1, testVariable)], Point(3)))).Solve(new SolverParameter( + EnableSolverOutput.True, + RelativeGap.EMinus7, new TimeLimitInMilliseconds(10), + new NumberOfThreads(2))); + + Assert.Equal(new SolverResult( + result.SolvedSolver, new ObjectiveValue(double.NaN), new IsFeasible(false), new IsOptimal(false), + new OptimalityGap(double.NaN)), result); + } + + [Fact] + public void SolverWithAbnormalLpModelThrowsExpectedException() + { + /* + * min infinity x, x in R + */ + var solver = new LinearProgramSolver() + .AddContinuousVariable(Interval(NegativeInfinity, PositiveInfinity), "TestVariable", out var testVariable) + .SetObjective(new Terms(Term(NegativeInfinity, testVariable)), true); + + Assert.Throws(() => + solver.Solve(new SolverParameter(new TimeLimitInMilliseconds(10)))); + } +} \ No newline at end of file