google / or-tools

Google's Operations Research tools:
https://developers.google.com/optimization/
Apache License 2.0
11.23k stars 2.13k forks source link

Expose linear solver callback to .Net #2603

Closed CADBIMDeveloper closed 1 year ago

CADBIMDeveloper commented 3 years ago

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!

lperron commented 1 year ago

Here is the situation:

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.