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

Enable call-back support for Java & Python, and add tests on XPRESS #108

Open
wants to merge 29 commits into
base: feature/xpress_only_RTE
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a7e7aee
Add callback support to Java & Python, with XPRESS tests
pet-mit Oct 31, 2023
20af016
revert change
pet-mit Oct 31, 2023
f69156d
Merge branch 'feature/xpress_only_RTE' into feature/xpress_only_pj_cb…
pet-mit Oct 31, 2023
976c18b
add python test
pet-mit Oct 31, 2023
e8b6290
revert change
pet-mit Oct 31, 2023
bbaf1ed
correct CMakeLists.txt for xpress tests
sgatto Oct 31, 2023
4420059
Merge branch 'feature/xpress_only_RTE' into feature/xpress_only_pj_cb…
sgatto Nov 7, 2023
668bcfe
clean CMakeLists.txt
sgatto Nov 7, 2023
e4e91a0
clean xpress test cmakelist
sgatto Nov 7, 2023
a5d7d4e
refactor
sgatto Nov 7, 2023
1234735
ignore a test if using Windows
sgatto Nov 7, 2023
a12b9b2
refactor
sgatto Nov 8, 2023
8667b6a
enable python tests to show cpp logs
sgatto Nov 8, 2023
82f3061
allow python exception to travel through cpp back to python
sgatto Nov 8, 2023
d35a0eb
fix test
sgatto Nov 8, 2023
8808931
wrap callback call with python GIL_state lock
sgatto Nov 9, 2023
32a8063
add debug CI for ubuntu.yml
sgatto Nov 9, 2023
bfca590
Merge branch 'feature/xpress_only_RTE' into feature/xpress_only_pj_cb…
sgatto Nov 9, 2023
4b06f48
Merge branch 'feature/xpress_only_RTE' into feature/xpress_only_pj_cb…
sgatto Nov 10, 2023
8ebcd97
change job name for ubuntu.yml
sgatto Nov 13, 2023
b75cdea
java: test user exception in callback
pet-mit Nov 14, 2023
6ef17e6
Merge remote-tracking branch 'origin/feature/xpress_only_pj_cb_tests'…
pet-mit Nov 14, 2023
883cecc
Merge branch 'feature/xpress_only_RTE' into feature/xpress_only_pj_cb…
sgatto Nov 15, 2023
2a1fc2e
do not re-throw caught exceptions in call-back
pet-mit Nov 15, 2023
4c64863
Merge remote-tracking branch 'origin/feature/xpress_only_pj_cb_tests'…
sgatto Nov 15, 2023
8bc9582
clean and move test files
sgatto Nov 15, 2023
44f10de
rename
sgatto Nov 15, 2023
9669479
attempt safe python call-back
pet-mit Nov 24, 2023
c3cbd46
remove RunSafeCallback
pet-mit Nov 24, 2023
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
27 changes: 15 additions & 12 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ env:

jobs:
build:
name: ${{ matrix.os }} shrd=${{ matrix.cmake.shared }} java=${{ matrix.cmake.java }} dotnet=${{ matrix.cmake.dotnet }} python=${{ matrix.cmake.python }}-${{ matrix.cmake.python-version }}
name: ${{ matrix.cmake.config }} shrd=${{ matrix.cmake.shared }} java=${{ matrix.cmake.java }} dotnet=${{ matrix.cmake.dotnet }} python=${{ matrix.cmake.python }}-${{ matrix.cmake.python-version }}
runs-on: ${{ matrix.os }}
env:
XPRESSDIR: ${{ github.workspace }}/xpressmp
Expand All @@ -25,11 +25,12 @@ jobs:
matrix:
os: ["ubuntu-22.04"]
cmake: [
{shared: OFF, java: OFF, dotnet: OFF, python: OFF, python-version: "3.8", publish-cxx-or: ON},
{shared: ON, java: ON, dotnet: ON, python: OFF, python-version: "3.8", publish-cxx-or: ON},
{shared: ON, java: OFF, dotnet: OFF, python: ON, python-version: "3.8", publish-cxx-or: OFF},
{shared: ON, java: OFF, dotnet: OFF, python: ON, python-version: "3.9", publish-cxx-or: OFF},
{shared: ON, java: OFF, dotnet: OFF, python: ON, python-version: "3.10", publish-cxx-or: OFF},
{config: "Release", shared: OFF, java: OFF, dotnet: OFF, python: OFF, python-version: "3.8", publish-cxx-or: ON},
{config: "Release", shared: ON, java: ON, dotnet: ON, python: OFF, python-version: "3.8", publish-cxx-or: ON},
{config: "Release", shared: ON, java: OFF, dotnet: OFF, python: ON, python-version: "3.8", publish-cxx-or: OFF},
{config: "Debug", shared: ON, java: OFF, dotnet: OFF, python: ON, python-version: "3.8", publish-cxx-or: OFF},
{config: "Release", shared: ON, java: OFF, dotnet: OFF, python: ON, python-version: "3.9", publish-cxx-or: OFF},
{config: "Release", shared: ON, java: OFF, dotnet: OFF, python: ON, python-version: "3.10", publish-cxx-or: OFF},
]

steps:
Expand Down Expand Up @@ -72,14 +73,14 @@ jobs:
if: ${{ startsWith(matrix.os, 'ubuntu') }}
uses: hendrikmuhs/[email protected]
with:
key: ${{ matrix.os }}-${{ matrix.cmake.shared }}-${{ matrix.cmake.dotnet }}-${{ matrix.cmake.java }}-${{ matrix.cmake.python }}-${{ matrix.cmake.python-version }}
key: ${{ matrix.cmake.config }}-${{ matrix.cmake.shared }}-${{ matrix.cmake.dotnet }}-${{ matrix.cmake.java }}-${{ matrix.cmake.python }}-${{ matrix.cmake.python-version }}

- name: Check cmake
run: cmake --version
- name: Configure OR-Tools
run: >
cmake -S . -B build
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_BUILD_TYPE=${{ matrix.cmake.config }}
-DBUILD_SHARED_LIBS=${{ matrix.cmake.shared }}
-DBUILD_PYTHON=${{ matrix.cmake.python }}
-DBUILD_JAVA=${{ matrix.cmake.java }}
Expand All @@ -93,14 +94,14 @@ jobs:
run: >
cmake
--build build
--config Release
--config ${{ matrix.cmake.config }}
--target all install -j4

- name: run tests not xpress
working-directory: ./build/
run: >
ctest
-C Release
-C ${{ matrix.cmake.config }}
--output-on-failure
-E "xpress"

Expand All @@ -109,7 +110,7 @@ jobs:
run: >
ctest
-V
-C Release
-C ${{ matrix.cmake.config }}
--output-on-failure
-R "xpress"

Expand All @@ -119,8 +120,10 @@ jobs:
run: |
SHARED=${{ matrix.cmake.shared }}
[ $SHARED == "ON" ] && WITH_SHARED="_shared" || WITH_SHARED="_static"
CONFIG=${{ matrix.cmake.config }}
[ $CONFIG == "Debug" ] && WITH_DEBUG="_debug" || WITH_DEBUG=""
OS="_${{ matrix.os }}"
APPENDIX="${OS}"
APPENDIX="${OS}${WITH_DEBUG}"
echo "::set-output name=appendix::$APPENDIX"
APPENDIX_WITH_SHARED="${OS}${WITH_SHARED}"
echo "::set-output name=appendix_with_shared::$APPENDIX_WITH_SHARED"
Expand Down
2 changes: 1 addition & 1 deletion cmake/python.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ if(BUILD_TESTING)

add_test(
NAME python_${COMPONENT_NAME}_${TEST_NAME}
COMMAND ${VENV_Python3_EXECUTABLE} -m pytest ${FILE_NAME}
COMMAND ${VENV_Python3_EXECUTABLE} -m pytest ${FILE_NAME} -s
WORKING_DIRECTORY ${VENV_DIR})
message(STATUS "Configuring test ${FILE_NAME} done")
endfunction()
Expand Down
135 changes: 135 additions & 0 deletions ortools/linear_solver/java/XpressCallbackTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.google.ortools.java;

import com.google.ortools.Loader;
import com.google.ortools.linearsolver.*;
import java.lang.RuntimeException;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class XpressCallbackTest {
// Numerical tolerance for checking primal, dual, objective values
// and other values.
private static final double NUM_TOLERANCE = 1e-5;

static class CustomException extends RuntimeException {
public CustomException() {
}

public CustomException(String msg) {
super(msg);
}

public CustomException(Throwable throwable) {
super(throwable);
}

public CustomException(String message, Throwable cause) {
super(message, cause);
}
}

@BeforeEach
public void setUp() {
Loader.loadNativeLibraries();
}

static class MyMpCallback extends MPCallback {
private final MPSolver mpSolver;
private final boolean throwRuntimeException;
private int nSolutions = 0;
private List<Double> lastVarValues = null;

public MyMpCallback(MPSolver mpSolver, boolean throwRuntimeException) {
super(false, false);
this.mpSolver = mpSolver;
this.throwRuntimeException = throwRuntimeException;
}

@Override
public void runCallback(MPCallbackContext callback_context) {
if (throwRuntimeException) {
throw new CustomException("this is a test exception in a callback");
}
if (!callback_context.event().equals(MPCallbackEvent.MIP_SOLUTION)) {
return;
}
nSolutions++;
if (!callback_context.canQueryVariableValues()) {
return;
}
lastVarValues = new ArrayList<>();
for (MPVariable variable : mpSolver.variables()) {
lastVarValues.add(callback_context.variableValue(variable));
}
}
}

private MPSolver initSolver() {
MPSolver solver = MPSolver.createSolver("XPRESS_MIXED_INTEGER_PROGRAMMING");

int nVars = 30;
int maxTime = 30;
solver.objective().setMaximization();

Random rand = new Random(123);
for (int i = 0; i < nVars; i++) {
MPVariable x = solver.makeIntVar(-rand.nextDouble() * 200, rand.nextDouble() * 200, "x_" + i);
solver.objective().setCoefficient(x, rand.nextDouble() * 200 - 100);
if (i==0) {
continue;
}
double rand1 = -rand.nextDouble() * 2000;
double rand2 = rand.nextDouble() * 2000;
MPConstraint constraint = solver.makeConstraint(Math.min(rand1, rand2), Math.max(rand1, rand2));
constraint.setCoefficient(x, rand.nextDouble() * 200 - 100);
for (int j = 0; j < i ; j++) {
constraint.setCoefficient(solver.variable(j), rand.nextDouble() * 200 - 100);
}
}

solver.setSolverSpecificParametersAsString("PRESOLVE 0 MAXTIME " + maxTime);
solver.enableOutput();
return solver;
}

@Test
public void testXpressNewMipSolCallback() {
MPSolver solver = initSolver();
if (solver == null) {
return;
}

MyMpCallback cb = new MyMpCallback(solver, false);
solver.setCallback(cb);

solver.solve();


// This is a tough MIP, in 30 seconds XPRESS should have found at least 5
// solutions (tested with XPRESS v9.0, may change in later versions)
assertTrue(cb.nSolutions > 5);
// Test that the last solution intercepted by callback is the same as the optimal one retained
for (int i = 0; i < solver.numVariables(); i++) {
assertEquals(solver.variable(i).solutionValue(), cb.lastVarValues.get(i), NUM_TOLERANCE);
}
}

@Test
public void testCallbackThrowsException() {
// Test that when the callback throws an exception, it is caught by or-tools
MPSolver solver = initSolver();
if (solver == null) {
return;
}

MyMpCallback cb = new MyMpCallback(solver, true);
solver.setCallback(cb);

assertDoesNotThrow(() -> solver.solve());
}
}
36 changes: 36 additions & 0 deletions ortools/linear_solver/java/linear_solver.i
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class MPModelRequest;
class MPSolutionResponse;
} // namespace operations_research

// cross-language polymorphism should be enabled to support MPCallback feature
%module(directors="1") operations_research;

%{
#include "ortools/linear_solver/linear_solver.h"
#include "ortools/linear_solver/model_exporter.h"
Expand Down Expand Up @@ -512,6 +515,39 @@ PROTO2_RETURN(
%rename (ShowUnusedVariables) operations_research::MPModelExportOptions::show_unused_variables;
%rename (MaxLineLength) operations_research::MPModelExportOptions::max_line_length;

// Expose the MPCallback & MPCallbackContext APIs
// Enable cross-language polymorphism for MPCallback virtual class
%feature("director") operations_research::MPCallback;
%unignore operations_research::MPCallback;
%unignore operations_research::MPCallback::MPCallback;
%unignore operations_research::MPCallback::~MPCallback;
%rename (runCallback) operations_research::MPCallback::RunCallback;
%rename (mightAddCuts) operations_research::MPCallback::might_add_cuts;
%rename (mightAddLazyConstraints) operations_research::MPCallback::might_add_lazy_constraints;
%unignore operations_research::MPCallbackContext;
%unignore operations_research::MPCallbackContext::MPCallbackContext;
%unignore operations_research::MPCallbackContext::~MPCallbackContext;
%unignore operations_research::MPCallbackEvent;
%rename (UNKNOWN) operations_research::MPCallbackEvent::kUnknown;
%rename (POLLING) operations_research::MPCallbackEvent::kPolling;
%rename (PRESOLVE) operations_research::MPCallbackEvent::kPresolve;
%rename (SIMPLEX) operations_research::MPCallbackEvent::kSimplex;
%rename (MIP) operations_research::MPCallbackEvent::kMip;
%rename (MIP_SOLUTION) operations_research::MPCallbackEvent::kMipSolution;
%rename (MIP_NODE) operations_research::MPCallbackEvent::kMipNode;
%rename (BARRIER) operations_research::MPCallbackEvent::kBarrier;
%rename (MESSAGE) operations_research::MPCallbackEvent::kMessage;
%rename (MULTI_OBJ) operations_research::MPCallbackContext::MPCallbackEvent::kMultiObj;
%rename (event) operations_research::MPCallbackContext::Event;
%rename (canQueryVariableValues) operations_research::MPCallbackContext::CanQueryVariableValues;
%rename (variableValue) operations_research::MPCallbackContext::VariableValue;
%rename (addCut) operations_research::MPCallbackContext::AddCut;
%rename (addLazyConstraint) operations_research::MPCallbackContext::AddLazyConstraint;
%rename (suggestSolution) operations_research::MPCallbackContext::SuggestSolution;
%rename (numExploredNodes) operations_research::MPCallbackContext::NumExploredNodes;
%rename (setCallback) operations_research::MPSolver::SetCallback;

%include "ortools/linear_solver/linear_solver_callback.h"
%include "ortools/linear_solver/linear_solver.h"
%include "ortools/linear_solver/model_exporter.h"

Expand Down
3 changes: 2 additions & 1 deletion ortools/linear_solver/linear_solver_callback.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class MPCallbackContext {
//
class MPCallback {
public:
// If you intend to call call MPCallbackContext::AddCut(), you must set
// If you intend to call MPCallbackContext::AddCut(), you must set
// might_add_cuts below to be true. Likewise for
// MPCallbackContext::AddLazyConstraint() and might_add_lazy_constraints.
MPCallback(bool might_add_cuts, bool might_add_lazy_constraints)
Expand All @@ -153,6 +153,7 @@ class MPCallback {
bool might_add_cuts() const { return might_add_cuts_; }
bool might_add_lazy_constraints() const {
return might_add_lazy_constraints_;

}

private:
Expand Down
54 changes: 54 additions & 0 deletions ortools/linear_solver/python/linear_solver.i
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,25 @@ class MPSolutionResponse;
class IISResponse;
} // namespace operations_research

// cross-language polymorphism should be enabled to support MPCallback feature
%module(directors="1") operations_research;
%feature("director:except") {
if ($error != NULL) {
throw Swig::DirectorMethodException();
}
}

%exception {
try { $action }
catch (Swig::DirectorException &e) { SWIG_fail; }
}

%{
#include "ortools/linear_solver/linear_solver.h"
#include "ortools/linear_solver/model_exporter.h"
#include "ortools/linear_solver/model_exporter_swig_helper.h"
#include "ortools/linear_solver/model_validator.h"
#include "ortools/linear_solver/python/python_mp_callback.h"
%}

%pythoncode %{
Expand Down Expand Up @@ -275,6 +289,12 @@ PY_CONVERT(MPConstraint);
PY_CONVERT_HELPER_PTR(MPVariable);
PY_CONVERT(MPVariable);

PY_CONVERT_HELPER_PTR(PythonMPCallback);
PY_CONVERT(PythonMPCallback);

PY_CONVERT_HELPER_PTR(MPCallbackContext);
PY_CONVERT(MPCallbackContext);

%ignoreall

%unignore operations_research;
Expand Down Expand Up @@ -494,7 +514,41 @@ PY_CONVERT(MPVariable);
// Expose the model validator.
%rename (FindErrorInModelProto) operations_research::FindErrorInMPModelProto;

// Expose the MPCallback & MPCallbackContext APIs
// Enable cross-language polymorphism for MPCallback virtual class
%feature("director") operations_research::PythonMPCallback;
%rename (MPCallback) operations_research::PythonMPCallback;
%unignore operations_research::PythonMPCallback::PythonMPCallback;
%unignore operations_research::PythonMPCallback::~PythonMPCallback;
%unignore operations_research::PythonMPCallback::Run;
%unignore operations_research::PythonMPCallback::might_add_cuts;
%unignore operations_research::PythonMPCallback::might_add_lazy_constraints;
%unignore operations_research::MPCallbackContext;
%unignore operations_research::MPCallbackContext::MPCallbackContext;
%unignore operations_research::MPCallbackContext::~MPCallbackContext;
%unignore operations_research::MPCallbackEvent;
%rename (UNKNOWN) operations_research::MPCallbackEvent::kUnknown;
%rename (POLLING) operations_research::MPCallbackEvent::kPolling;
%rename (PRESOLVE) operations_research::MPCallbackEvent::kPresolve;
%rename (SIMPLEX) operations_research::MPCallbackEvent::kSimplex;
%rename (MIP) operations_research::MPCallbackEvent::kMip;
%rename (MIP_SOLUTION) operations_research::MPCallbackEvent::kMipSolution;
%rename (MIP_NODE) operations_research::MPCallbackEvent::kMipNode;
%rename (BARRIER) operations_research::MPCallbackEvent::kBarrier;
%rename (MESSAGE) operations_research::MPCallbackEvent::kMessage;
%rename (MULTI_OBJ) operations_research::MPCallbackContext::MPCallbackEvent::kMultiObj;
%unignore operations_research::MPCallbackContext::Event;
%unignore operations_research::MPCallbackContext::CanQueryVariableValues;
%unignore operations_research::MPCallbackContext::VariableValue;
%unignore operations_research::MPCallbackContext::AddCut;
%unignore operations_research::MPCallbackContext::AddLazyConstraint;
%unignore operations_research::MPCallbackContext::SuggestSolution;
%unignore operations_research::MPCallbackContext::NumExploredNodes;
%unignore operations_research::MPSolver::SetCallback;

%include "ortools/linear_solver/linear_solver_callback.h"
%include "ortools/linear_solver/linear_solver.h"
%include "ortools/linear_solver/python/python_mp_callback.h"
%include "ortools/linear_solver/model_exporter.h"
%include "ortools/linear_solver/model_exporter_swig_helper.h"

Expand Down
Loading
Loading