Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose linear solver callback to .Net #2603

Closed
CADBIMDeveloper opened this issue Jun 20, 2021 · 1 comment
Closed

Expose linear solver callback to .Net #2603

CADBIMDeveloper opened this issue Jun 20, 2021 · 1 comment
Assignees
Labels
Feature Request Missing Feature/Wrapper Lang: .NET .Net wrapper issue Solver: Linear Solver Related to all Linear Solver (GLOP, BOP, CBC etc...)
Milestone

Comments

@CADBIMDeveloper
Copy link
Contributor

CADBIMDeveloper commented Jun 20, 2021

What language and solver does this apply to?
Solver: Linear Solvers, which support callbacks (SCIP for example)
Languages: C#, C++
Version: building from stable branch

Describe the problem you are trying to solve.

  1. Track feasible solutions from solver to be able to use them in case when user doesn't want to wait for the optimal one
  2. Use (1 - current MIP GAP) as a "percentage", just for the UI

Describe the solution you'd like
Expose linear solvers callback functionality to .Net

Additional context
I tried to implement it myself, but unfortunatelly I don't have an appropriate experience with C++ and SWIG.

What I've done:
I added a new C++ class:

class LinearSolutionCallback : public MPCallback {
 public:
  LinearSolutionCallback() : MPCallback(false, false) {
      context_ = NULL;
  }

  virtual ~LinearSolutionCallback() {}

  virtual void OnSolutionCallback() {}
  
  double VariableValue(const MPVariable* variable) { return context_->VariableValue(variable); }
  
  bool CanQueryVariableValues(){ return context_->CanQueryVariableValues(); }
  
  MPCallbackEvent Event() { return context_->Event(); }
  
  int64_t NumExploredNodes() { return context_->NumExploredNodes(); }
  
  void RunCallback(MPCallbackContext* callback_context) override{
      context_ = callback_context;

      OnSolutionCallback();
  }
  
 private:
  MPCallbackContext* context_;
};

I added the following pieces of the code to linear_solver.i file:

...
%module(directors="1") operations_research_linear
...
%rename (SolverCallbackContext) operations_research::MPCallbackContext;
%rename (SolverSolutionCallbackEvent) operations_research::MPCallbackEvent;
...
%unignore operations_research::MPSolver::SupportsCallbacks;
%unignore operations_research::MPSolver::SetCallback;
%extend operations_research::MPSolver {
...
  void SetCallback(operations_research::LinearSolutionCallback* callback) {
	$self->SetCallback(callback);
  }
}
// Callback API

// Expose callback event type enum
%unignore operations_research::MPCallbackEvent::kUnknown;
%unignore operations_research::MPCallbackEvent::kPolling;
%unignore operations_research::MPCallbackEvent::kPresolve;
%unignore operations_research::MPCallbackEvent::kSimplex;
%unignore operations_research::MPCallbackEvent::kMip;
%unignore operations_research::MPCallbackEvent::kMipSolution;
%unignore operations_research::MPCallbackEvent::kMipNode;
%unignore operations_research::MPCallbackEvent::kBarrier;
%unignore operations_research::MPCallbackEvent::kMessage;
%unignore operations_research::MPCallbackEvent::kMultiObj;

// MPCallbackContext
%unignore operations_research::MPCallbackContext::MPCallbackContext;
%unignore operations_research::MPCallbackContext::~MPCallbackContext;
%rename (GetEventType) operations_research::MPCallbackContext::Event;
%unignore operations_research::MPCallbackContext::CanQueryVariableValues;
%unignore operations_research::MPCallbackContext::VariableValue;
%unignore operations_research::MPCallbackContext::NumExploredNodes;

%feature("director") operations_research::LinearSolutionCallback;
%unignore operations_research::LinearSolutionCallback;
%unignore operations_research::LinearSolutionCallback::LinearSolutionCallback;
%unignore operations_research::LinearSolutionCallback::~LinearSolutionCallback;
%unignore operations_research::LinearSolutionCallback::VariableValue;
%feature("nodirector") operations_research::LinearSolutionCallback::VariableValue;
%unignore operations_research::LinearSolutionCallback::CanQueryVariableValues;
%feature("nodirector") operations_research::LinearSolutionCallback::CanQueryVariableValues;
%unignore operations_research::LinearSolutionCallback::Event;
%feature("nodirector") operations_research::LinearSolutionCallback::Event;
%unignore operations_research::LinearSolutionCallback::NumExploredNodes;
%feature("nodirector") operations_research::LinearSolutionCallback::NumExploredNodes;
%unignore operations_research::LinearSolutionCallback::OnSolutionCallback;

My C# code:

var solver = new Solver("Optimization", Solver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);

var logBuilder = new StringBuilder();
var callBack = new CallBack(logBuilder);

if (solver.SupportsCallbacks())
{
	logBuilder.AppendLine("Callbacks are supported. Subscribe to callback event");

	solver.SetCallback(callBack);
}
...
var result = solver.Solve(); // <-SEHException here
...

public class CallBack : LinearSolutionCallback
{
	private readonly StringBuilder stringBuilder;

	public CallBack(StringBuilder stringBuilder)
	{
		this.stringBuilder = stringBuilder;
	}
	
	public override void OnSolutionCallback()
	{
		stringBuilder.AppendLine($"[{DateTime.Now}] Solution call back!");
		stringBuilder.AppendLine($"Event: {Event()}");
		stringBuilder.AppendLine($"Can query values: {CanQueryVariableValues()}");
		stringBuilder.AppendLine($"Explored {NumExploredNodes()} nodes");
	}
}

I also tried to use LinearSolutionCallback in C++ program. It works!

...
class SolverLog : public LinearSolutionCallback {
 public:
    SolverLog() : LinearSolutionCallback() {};
    ~SolverLog() {};

    void OnSolutionCallback() override {
        LOG(INFO) << "Callback invoked";
    }
};
...
std::unique_ptr<MPSolver> solver(MPSolver::CreateSolver("SCIP"));
SolverLog solverlog = SolverLog();
solver->SetCallback(&solverlog);
...
const MPSolver::ResultStatus result_status = solver->Solve();
...

I'll be happy to create a pull request if you help me to find out what is going wrong. I tried to compare Google.OrTools.Sat.SolutionCallback and Google.OrTools.LinearSolver.LinearSolutionCallback wrapper classes and did't find any meaningfull difference.

Thank you!

@Mizux Mizux self-assigned this Jun 21, 2021
@Mizux Mizux added Feature Request Missing Feature/Wrapper Lang: .NET .Net wrapper issue Solver: Linear Solver Related to all Linear Solver (GLOP, BOP, CBC etc...) labels Jun 21, 2021
@Mizux Mizux added this to the Backlog milestone Jun 21, 2021
@lperron
Copy link
Collaborator

lperron commented Sep 28, 2023

Here is the situation:

  • MPsolver is in maintenance mode, so we will not implement this feature.
  • MathOpt is comming with full support for callbacks.
  • But it will most likely never support C#

So the only workaround is to use CP-SAT which supports callbacks in all languages.
The only limitation is that it does not support continuous variables.

I hope this workaround is sufficient.

@lperron lperron closed this as completed Sep 28, 2023
@Mizux Mizux modified the milestones: Backlog, v9.8 Oct 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Request Missing Feature/Wrapper Lang: .NET .Net wrapper issue Solver: Linear Solver Related to all Linear Solver (GLOP, BOP, CBC etc...)
Projects
None yet
Development

No branches or pull requests

3 participants