Skip to content

Commit

Permalink
Support loading CasADi problems directly (without depending on the Ca…
Browse files Browse the repository at this point in the history
…sADi C++ library)
  • Loading branch information
tttapa committed Mar 26, 2024
1 parent 9641fee commit 4ac347f
Show file tree
Hide file tree
Showing 30 changed files with 554 additions and 137 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ jobs:
-D ALPAQA_WITH_LONG_DOUBLE=On \
-D ALPAQA_WITH_QUAD_PRECISION=$quadmath \
-D ALPAQA_WITH_CASADI=On \
-D ALPAQA_WITH_EXTERNAL_CASADI=Off \
-D ALPAQA_WITH_IPOPT=On \
-D ALPAQA_WITH_CUTEST=On \
-D ALPAQA_WITH_CUTEST_EXAMPLES=Off \
-D ALPAQA_WITH_DRIVERS=On \
-D ALPAQA_WITH_GRADIENT_CHECKER=On \
-D CMAKE_TOOLCHAIN_FILE="$staging/$host.toolchain.cmake" \
-D CMAKE_PREFIX_PATH="$staging/mumps/usr/local;$staging/ipopt/usr/local" \
-D CMAKE_FIND_ROOT_PATH="$staging/eigen;$staging/casadi;$staging/openblas;$staging/mumps;$staging/ipopt"
-D CMAKE_FIND_ROOT_PATH="$staging/eigen;$staging/openblas;$staging/mumps;$staging/ipopt"
env:
CXXFLAGS: '-static-libstdc++'
# Build
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/matlab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
--build=missing
-s build_type=Release
-of build-matlab
-o with_matlab=True -o with_json=True -o with_casadi=True
-o with_matlab=True -o with_json=True -o with_casadi=True -o with_external_casadi=True
- name: Patch Conan toolchain file
run: sed -i 's/CMAKE_PREFIX_PATH/CMAKE_FIND_ROOT_PATH/g' build-matlab/build/Release/generators/conan_toolchain.cmake
# Build
Expand Down Expand Up @@ -231,7 +231,7 @@ jobs:
--build=missing
-s build_type=Release
-of build-matlab
-o with_matlab=True -o with_json=True -o with_casadi=True
-o with_matlab=True -o with_json=True -o with_casadi=True -o with_external_casadi=True
# Build
- name: Configure
run: cmake --preset conan-release
Expand Down Expand Up @@ -328,7 +328,7 @@ jobs:
--build=missing
-s build_type=Release
-of build-matlab
-o with_matlab=True -o with_json=True -o with_casadi=True
-o with_matlab=True -o with_json=True -o with_casadi=True -o with_external_casadi=True
# Build
- name: Configure
run: cmake --preset conan-default
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ option(ALPAQA_WITH_CXX_23
# Enable/disable optional dependencies
option(ALPAQA_WITH_CASADI
"Build the CasADi loader" On)
cmake_dependent_option(ALPAQA_WITH_EXTERNAL_CASADI
"Link to the CasADi C++ library. When enabled, CasADi functions can be used to initialize alpaqa problems, otherwise, only pre-compiled CasADi functions can be loaded" Off "ALPAQA_WITH_CASADI" Off)
cmake_dependent_option(ALPAQA_WITH_CUTEST
"Build the CUTEst loader" On "ALPAQA_WITH_CXX_23" Off)
option(ALPAQA_WITH_QPALM
Expand Down
9 changes: 5 additions & 4 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class AlpaqaRecipe(ConanFile):
"with_matlab": False,
"with_drivers": True,
"with_gradient_checker": False,
"with_casadi": False,
"with_casadi": True,
"with_external_casadi": False,
"with_cutest": False,
"with_qpalm": False,
"with_json": True,
Expand Down Expand Up @@ -68,7 +69,7 @@ class AlpaqaRecipe(ConanFile):
def requirements(self):
self.requires("eigen/3.4.0", transitive_headers=True)
self.test_requires("gtest/1.11.0")
if self.options.with_casadi:
if self.options.with_external_casadi:
self.requires("casadi/3.6.4@alpaqa", transitive_headers=True)
if self.options.with_json:
self.requires("nlohmann_json/3.11.2", transitive_headers=True)
Expand All @@ -87,8 +88,8 @@ def validate(self):
if self.options.with_matlab and not self.options.with_json:
msg = "MATLAB MEX interface requires JSON. Set 'with_json=True'."
raise ConanInvalidConfiguration(msg)
if self.options.with_matlab and not self.options.with_casadi:
msg = "MATLAB MEX interface requires CasADi. Set 'with_casadi=True'."
if self.options.with_matlab and not self.options.with_external_casadi:
msg = "MATLAB MEX interface requires CasADi. Set 'with_external_casadi=True'."
raise ConanInvalidConfiguration(msg)

def layout(self):
Expand Down
8 changes: 4 additions & 4 deletions doxygen/pages/Installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,12 @@ use [Conan](https://conan.io/) to manage and build the necessary dependencies.
```sh
python3 -m pip install -U conan cmake ninja
conan profile detect --force
conan create scripts/recipes/casadi --build=missing
conan export scripts/recipes/casadi
conan install . \
--build=missing \
-c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" \
-of build-matlab \
-o with_matlab=True -o with_json=True -o with_casadi=True
-o with_matlab=True -o with_external_casadi=True
cmake --preset conan-default
cmake --build --preset conan-release -j -t alpaqa_mex
cmake --install build-matlab/build \
Expand All @@ -224,11 +224,11 @@ cmake --install build-matlab/build \
```sh
python -m pip install -U conan cmake ninja
conan profile detect --force
conan create scripts/recipes/casadi --build=missing
conan export scripts/recipes/casadi
conan install . \
--build=missing \
-of build-matlab \
-o with_matlab=True -o with_json=True -o with_casadi=True
-o with_matlab=True -o with_external_casadi=True
cmake --preset conan-default
cmake --build --preset conan-release -j -t alpaqa_mex
cmake --install build-matlab/build \
Expand Down
4 changes: 2 additions & 2 deletions examples/CMake/Solver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ For example, to enable CasADi, first export CasADi itself, and then rebuild
alpaqa with CasADi support enabled:
```sh
conan export ../../../scripts/recipes/casadi
conan install . --build=missing -o 'alpaqa/*:with_casadi=True'
conan install . --build=missing -o 'alpaqa/*:with_external_casadi=True'
```

If your project always requires CasADi, add the following options at the bottom
of your project's `conanfile.txt`:
```conanfile
[options]
alpaqa/*:with_casadi=True
alpaqa/*:with_external_casadi=True
```

In your project's `CMakeLists.txt`, enable the optional CasADi component:
Expand Down
5 changes: 5 additions & 0 deletions interfaces/python/src/alpaqa.py.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ PYBIND11_MODULE(MODULE_NAME, m) {
#else
m.attr("with_casadi") = false;
#endif
#if ALPAQA_WITH_EXTERNAL_CASADI
m.attr("with_external_casadi") = true;
#else
m.attr("with_external_casadi") = false;
#endif
#if ALPAQA_WITH_CASADI_OCP
m.attr("with_casadi_ocp") = true;
#else
Expand Down
2 changes: 1 addition & 1 deletion python/test/test_alm.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_alm_pyapi_compile():
assert np.linalg.norm(y - [0, -2/3]) < 1e-5


@pytest.mark.skipif(not pa.with_casadi, reason="requires CasADi")
@pytest.mark.skipif(not pa.with_external_casadi, reason="requires CasADi")
def test_alm_pyapi_build():
import casadi as cs

Expand Down
44 changes: 35 additions & 9 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ add_library(alpaqa
"alpaqa/src/inner/directions/panoc/convex-newton.cpp"
"alpaqa/src/inner/internal/panoc-helpers.cpp"
)
if (ALPAQA_WITH_DL OR ALPAQA_WITH_CASADI)
target_sources(alpaqa PRIVATE
"alpaqa/src/util/dl.cpp")
target_link_libraries(alpaqa PRIVATE ${CMAKE_DL_LIBS})
endif()
if (ALPAQA_WITH_OCP)
target_sources(alpaqa PRIVATE
"alpaqa/src/inner/panoc-ocp.cpp"
Expand Down Expand Up @@ -133,22 +138,34 @@ endif()

# CasADi
if (ALPAQA_WITH_CASADI)
find_package(casadi REQUIRED)
find_package(Threads REQUIRED)
if (ALPAQA_WITH_EXTERNAL_CASADI)
find_package(casadi REQUIRED)
find_package(Threads REQUIRED)
endif()
# Normal NLPs
add_library(casadi-loader
"interop/casadi/src/CasADiProblem.cpp"
"interop/casadi/src/CompleteCasADiProblem.cpp"
"interop/casadi/include/alpaqa/casadi/CasADiFunctionWrapper.hpp")
target_include_directories(casadi-loader PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/interop/casadi/include>
$<INSTALL_INTERFACE:${ALPAQA_INSTALL_INCLUDEDIR}>)
target_compile_definitions(casadi-loader PUBLIC ALPAQA_WITH_CASADI)
target_compile_definitions(casadi-loader PUBLIC
$<$<BOOL:${ALPAQA_WITH_CASADI}>:ALPAQA_WITH_CASADI>
$<$<BOOL:${ALPAQA_WITH_EXTERNAL_CASADI}>:ALPAQA_WITH_EXTERNAL_CASADI>)
set_property(TARGET casadi-loader
PROPERTY OUTPUT_NAME alpaqa-casadi-loader)
target_link_libraries(casadi-loader
PUBLIC alpaqa::alpaqa
PRIVATE casadi Threads::Threads alpaqa::warnings)
PRIVATE alpaqa::warnings)
if (ALPAQA_WITH_EXTERNAL_CASADI)
target_sources(casadi-loader PRIVATE
"interop/casadi/src/CompleteCasADiProblem.cpp")
target_link_libraries(casadi-loader PRIVATE casadi Threads::Threads)
else()
target_sources(casadi-loader PRIVATE
"interop/casadi/src/casadi-external-function.cpp"
"interop/casadi/src/casadi-external-function.cpp")
endif()
alpaqa_configure_visibility(casadi-loader)
set_target_properties(casadi-loader PROPERTIES SOVERSION ${PROJECT_VERSION})
add_library(alpaqa::casadi-loader ALIAS casadi-loader)
Expand All @@ -174,7 +191,10 @@ if (ALPAQA_WITH_OCP AND ALPAQA_WITH_CASADI_OCP)
PROPERTY OUTPUT_NAME alpaqa-casadi-ocp-loader)
target_link_libraries(casadi-ocp-loader
PUBLIC alpaqa::alpaqa
PRIVATE casadi Threads::Threads alpaqa::warnings)
PRIVATE alpaqa::warnings)
if (ALPAQA_WITH_EXTERNAL_CASADI)
target_link_libraries(casadi-ocp-loader PRIVATE casadi Threads::Threads)
endif()
alpaqa_configure_visibility(casadi-ocp-loader)
set_target_properties(casadi-ocp-loader PROPERTIES SOVERSION ${PROJECT_VERSION})
add_library(alpaqa::casadi-ocp-loader ALIAS casadi-ocp-loader)
Expand Down Expand Up @@ -206,7 +226,7 @@ if (ALPAQA_WITH_DL)
PROPERTY OUTPUT_NAME alpaqa-dl-loader)
target_link_libraries(dl-loader
PUBLIC alpaqa::alpaqa alpaqa::dl-api
PRIVATE ${CMAKE_DL_LIBS} alpaqa::warnings)
PRIVATE alpaqa::warnings)
alpaqa_configure_visibility(dl-loader)
set_target_properties(dl-loader PROPERTIES SOVERSION ${PROJECT_VERSION})
add_library(alpaqa::dl-loader ALIAS dl-loader)
Expand Down Expand Up @@ -398,7 +418,10 @@ if (ALPAQA_WITH_DRIVERS)
target_link_libraries(driver PRIVATE alpaqa::dl-loader)
endif()
if (TARGET alpaqa::casadi-loader)
target_link_libraries(driver PRIVATE alpaqa::casadi-loader casadi)
target_link_libraries(driver PRIVATE alpaqa::casadi-loader)
endif()
if (ALPAQA_WITH_EXTERNAL_CASADI)
target_link_libraries(driver PRIVATE casadi)
endif()
if (TARGET alpaqa::cutest-interface)
target_link_libraries(driver PRIVATE alpaqa::cutest-interface)
Expand Down Expand Up @@ -438,7 +461,10 @@ if (ALPAQA_WITH_GRADIENT_CHECKER)
target_link_libraries(gradient-checker PRIVATE alpaqa::dl-loader)
endif()
if (TARGET alpaqa::casadi-loader)
target_link_libraries(gradient-checker PRIVATE alpaqa::casadi-loader casadi)
target_link_libraries(gradient-checker PRIVATE alpaqa::casadi-loader)
endif()
if (ALPAQA_WITH_EXTERNAL_CASADI)
target_link_libraries(gradient-checker PRIVATE casadi)
endif()
if (TARGET alpaqa::cutest-interface)
target_link_libraries(gradient-checker PRIVATE alpaqa::cutest-interface)
Expand Down
21 changes: 21 additions & 0 deletions src/alpaqa/include/alpaqa/util/dl.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <alpaqa/export.h>
#include <filesystem>
#include <memory>
#include <stdexcept>

namespace alpaqa ::util {

/// Failed to load a DLL or SO file, or failed to access a function in it.
struct ALPAQA_EXPORT dynamic_load_error : std::runtime_error {
using std::runtime_error::runtime_error;
};

/// Load a DLL or SO file.
ALPAQA_EXPORT std::shared_ptr<void>
load_lib(const std::filesystem::path &so_filename);
/// Get a pointer to a function inside of a loaded DLL or SO file.
ALPAQA_EXPORT void *load_func(void *lib_handle, const std::string &name);

} // namespace alpaqa::util
4 changes: 2 additions & 2 deletions src/alpaqa/src/driver/alpaqa-driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
#include "results.hpp"
#include "solver-driver.hpp"

#ifdef ALPAQA_WITH_CASADI
#ifdef ALPAQA_WITH_EXTERNAL_CASADI
#include <casadi/config.h>
#endif
#ifdef ALPAQA_WITH_JSON
Expand Down Expand Up @@ -144,7 +144,7 @@ void print_usage(const char *a0) {
<< " * Eigen " << EIGEN_WORLD_VERSION << '.'
<< EIGEN_MAJOR_VERSION << '.' << EIGEN_MINOR_VERSION
<< " (https://gitlab.com/libeigen/eigen) - MPL-2.0\n"
#ifdef ALPAQA_WITH_CASADI
#ifdef ALPAQA_WITH_EXTERNAL_CASADI
<< " * CasADi " CASADI_VERSION_STRING
" (https://github.com/casadi/casadi) - LGPL-3.0-or-later\n"
#endif
Expand Down
4 changes: 2 additions & 2 deletions src/alpaqa/src/driver/gradient-checker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

#include <Eigen/Sparse>

#ifdef ALPAQA_WITH_CASADI
#ifdef ALPAQA_WITH_EXTERNAL_CASADI
#include <casadi/config.h>
#endif
#ifdef ALPAQA_WITH_IPOPT
Expand Down Expand Up @@ -72,7 +72,7 @@ void print_usage(const char *a0) {
<< " * Eigen " << EIGEN_WORLD_VERSION << '.'
<< EIGEN_MAJOR_VERSION << '.' << EIGEN_MINOR_VERSION
<< " (https://gitlab.com/libeigen/eigen) - MPL-2.0\n"
#ifdef ALPAQA_WITH_CASADI
#ifdef ALPAQA_WITH_EXTERNAL_CASADI
<< " * CasADi " CASADI_VERSION_STRING
" (https://github.com/casadi/casadi) - LGPL-3.0-or-later\n"
#endif
Expand Down
75 changes: 75 additions & 0 deletions src/alpaqa/src/util/dl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include <alpaqa/util/dl.hpp>

#if _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif

#include <cassert>

namespace alpaqa::util {

#if _WIN32
std::shared_ptr<char> get_last_error_msg() {
char *err = nullptr;
auto opt = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_IGNORE_INSERTS;
auto lang = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT);
if (FormatMessage(opt, NULL, GetLastError(), lang,
reinterpret_cast<char *>(&err), 0, NULL) != 0) {
return std::shared_ptr<char>{err, [](char *e) { LocalFree(e); }};
} else {
static char msg[] = "(failed to get error message)";
return std::shared_ptr<char>{msg, [](char *) {}};
}
}

std::shared_ptr<void> load_lib(const std::filesystem::path &so_filename) {
assert(!so_filename.empty());
void *h = LoadLibraryW(so_filename.c_str());
if (!h)
throw dynamic_load_error("Unable to load \"" + so_filename.string() +
"\": " + get_last_error_msg().get());
#if ALPAQA_NO_DLCLOSE
return std::shared_ptr<void>{h, +[](void *) {}};
#else
return std::shared_ptr<void>{
h, +[](void *h) { FreeLibrary(static_cast<HMODULE>(h)); }};
#endif
}

void *load_func(void *handle, const std::string &name) {
assert(handle);
auto *h = GetProcAddress(static_cast<HMODULE>(handle), name.c_str());
if (!h)
throw dynamic_load_error("Unable to load function '" + name +
"': " + get_last_error_msg().get());
return reinterpret_cast<void *>(h);
}
#else
std::shared_ptr<void> load_lib(const std::filesystem::path &so_filename) {
assert(!so_filename.empty());
::dlerror();
void *h = ::dlopen(so_filename.c_str(), RTLD_LOCAL | RTLD_NOW);
if (auto *err = ::dlerror())
throw dynamic_load_error(err);
#if ALPAQA_NO_DLCLOSE
return std::shared_ptr<void>{h, +[](void *) {}};
#else
return std::shared_ptr<void>{h, &::dlclose};
#endif
}

void *load_func(void *handle, const std::string &name) {
assert(handle);
::dlerror();
auto *h = ::dlsym(handle, name.c_str());
if (auto *err = ::dlerror())
throw dynamic_load_error("Unable to load function '" + name +
"': " + err);
return h;
}
#endif

} // namespace alpaqa::util
Loading

0 comments on commit 4ac347f

Please sign in to comment.