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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

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

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

## Solver parameters (SolverParameter)

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

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

Examples:

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

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

## Contributing

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

## Licensing

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



Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Update="Google.OrTools" Version="9.15.6755" />
<PackageReference Update="Google.OrTools" Version="9.14.6206" />

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OR Tools 9.15.x has problems with Gurobi 13.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waiting for google/or-tools#5272

<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Serilog" Version="4.3.0" />
Expand Down
2 changes: 1 addition & 1 deletion examples/Anexia.MathematicalProgram.Examples/IlpExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
</ItemGroup>

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

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

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

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

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

/// <summary>
/// The constraint's name.
/// </summary>
public string? Name { get; }
}
2 changes: 1 addition & 1 deletion src/Anexia.MathematicalProgram/Result/ISolverResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public interface ISolverResult<in TVariable, TCoefficient, in TVariableInterval>
/// <summary>
/// Indicates whether a solution to the optimization problem is feasible.
/// </summary>
IsFeasible IsFeasible { get; }
IsFeasible? IsFeasible { get; }

/// <summary>
/// Indicates whether the solution to the optimization problem is optimal.
Expand Down
76 changes: 72 additions & 4 deletions src/Anexia.MathematicalProgram/Result/ResultHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Anexia.MathematicalProgram.Solve;
using Google.OrTools.ModelBuilder;
using Google.OrTools.Sat;
using Gurobi;

namespace Anexia.MathematicalProgram.Result;

Expand All @@ -35,7 +36,7 @@ internal static ISolverResult<TVariable, TCoefficient, TVariableInterval>
: SolverResult(SolverResultStatus.Feasible, switchedToDefaultSolver, solutionValues, objectiveValue,
bestBound, true),
SolveStatus.INFEASIBLE => SolverResult<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.Infeasible, switchedToDefaultSolver),
SolverResultStatus.Infeasible, switchedToDefaultSolver, isFeasible: false),
SolveStatus.UNBOUNDED => SolverResult<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.Unbounded, switchedToDefaultSolver),
SolveStatus.ABNORMAL => SolverResult<TVariable, TCoefficient, TVariableInterval>(
Expand All @@ -60,6 +61,34 @@ internal static ISolverResult<TVariable, TCoefficient, TVariableInterval>
};
}

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

internal static ISolverResult<TVariable, TCoefficient, TVariableInterval> Handle<TVariable, TCoefficient,
TVariableInterval>(CpSolverStatus resultStatus,
ISolutionValues<TVariable, TCoefficient, TVariableInterval>? solutionValues = null,
Expand All @@ -79,7 +108,7 @@ internal static ISolverResult<TVariable, TCoefficient, TVariableInterval>
bestBound, true),
CpSolverStatus.Infeasible =>
SolverResult<TVariable, TCoefficient, TVariableInterval>(SolverResultStatus.Infeasible,
false),
false, isFeasible: false),
CpSolverStatus.Unknown => SolverResult<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.UnknownStatus,
false),
Expand All @@ -90,11 +119,50 @@ internal static ISolverResult<TVariable, TCoefficient, TVariableInterval>
};
}

internal static ISolverResult<TVariable, TCoefficient, TVariableInterval> HandleGurobi<TVariable, TCoefficient,
TVariableInterval>(int resultStatus,
ISolutionValues<TVariable, TCoefficient, TVariableInterval>? solutionValues = null,
double? objectiveValue = null,
double? bestBound = null) where TVariableInterval : IAddableScalar<TVariableInterval, TVariableInterval>
where TVariable : IVariable<TVariableInterval>
{
if (resultStatus == GRB.Status.INFEASIBLE)
{
return SolverResult<TVariable, TCoefficient, TVariableInterval>(
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<TVariable, TCoefficient, TVariableInterval>(
SolverResultStatus.Unbounded, false),

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

private static ISolverResult<TVariable, TCoefficient, TVariableInterval>
SolverResult<TVariable, TCoefficient, TVariableInterval>(SolverResultStatus resultStatus,
bool switchedToDefaultSolver,
ISolutionValues<TVariable, TCoefficient, TVariableInterval>? 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<TVariableInterval>
where TVariableInterval : IAddableScalar<TVariableInterval, TVariableInterval>
{
Expand All @@ -103,7 +171,7 @@ private static ISolverResult<TVariable, TCoefficient, TVariableInterval>
new SolutionValues<TVariable, TCoefficient, TVariableInterval>(ReadOnlyDictionary<TVariable, TCoefficient>
.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,
Expand Down
2 changes: 1 addition & 1 deletion src/Anexia.MathematicalProgram/Result/SolverResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace Anexia.MathematicalProgram.Result;
public readonly record struct SolverResult<TVariable, TCoefficient, TVariableInterval>(
ISolutionValues<TVariable, TCoefficient, TVariableInterval> SolutionValues,
ObjectiveValue? ObjectiveValue,
IsFeasible IsFeasible,
IsFeasible? IsFeasible,
IsOptimal IsOptimal,
OptimalityGap? OptimalityGap,
SolverResultStatus SolverResultStatus,
Expand Down
4 changes: 3 additions & 1 deletion src/Anexia.MathematicalProgram/Result/SolverResultStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ public enum SolverResultStatus
ModelInvalid,
InvalidSolverParameters,
SolverTypeUnavailable,
IncompatibleOptions
IncompatibleOptions,
InfOrUnbound,
Timelimit
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ public ISolverResult<IIntegerVariable<IIntegerScalar>, 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)
Expand Down
Loading
Loading