diff --git a/bioptim/interfaces/acados_interface.py b/bioptim/interfaces/acados_interface.py index a7517996b..a56f0400c 100644 --- a/bioptim/interfaces/acados_interface.py +++ b/bioptim/interfaces/acados_interface.py @@ -1,5 +1,6 @@ from time import perf_counter from datetime import datetime +from pathlib import Path import numpy as np from scipy import linalg @@ -21,6 +22,25 @@ ) +def _configure_acados_codegen(acados_ocp: AcadosOcp, solver_options: Solver.ACADOS) -> None: + """Configure code generation with the Acados v0.5.5 API.""" + + if not hasattr(acados_ocp, "code_gen_options"): + raise RuntimeError( + "This version of bioptim requires Acados v0.5.5 or newer. " + "Please reinstall Acados using the scripts from bioptim/external." + ) + + code_gen_options = acados_ocp.code_gen_options + if solver_options.acados_dir: + acados_dir = Path(solver_options.acados_dir).expanduser().resolve() + code_gen_options.acados_include_path = str(acados_dir / "include") + code_gen_options.acados_lib_path = str(acados_dir / "lib") + + code_gen_options.code_export_directory = solver_options.c_generated_code_path + code_gen_options.json_file = "acados_ocp.json" + + class AcadosInterface(SolverInterface): """ The ACADOS solver interface @@ -115,8 +135,8 @@ def __init__(self, ocp, solver_options: Solver.ACADOS = None): solver_options = Solver.ACADOS() self.opts = solver_options - self.acados_ocp = AcadosOcp(acados_path=solver_options.acados_dir) - self.acados_ocp.code_export_directory = solver_options.c_generated_code_path + self.acados_ocp = AcadosOcp() + _configure_acados_codegen(self.acados_ocp, solver_options) self.acados_model = AcadosModel() self.__set_cost_type(solver_options.cost_type) @@ -916,7 +936,12 @@ def solve(self, expand_during_shake_tree: Bool = False) -> AnyListorDict: if self.ocp_solver is None: for key in options: setattr(self.acados_ocp.solver_options, key, options[key]) - self.ocp_solver = AcadosOcpSolver(self.acados_ocp, json_file="acados_ocp.json", build=self.opts.c_compile) + self.ocp_solver = AcadosOcpSolver( + self.acados_ocp, + json_file=self.acados_ocp.code_gen_options.json_file, + build=self.opts.c_compile, + check_reuse_possible=False, + ) self.opts.set_only_first_options_has_changed(False) self.opts.set_has_tolerance_changed(False) diff --git a/external/acados b/external/acados index 48e223e85..59d93e17d 160000 --- a/external/acados +++ b/external/acados @@ -1 +1 @@ -Subproject commit 48e223e85f0408ebfd1d8c6d6fb0589e9c41b3aa +Subproject commit 59d93e17d2985fdd73fc58b8a83ed8f83a024171 diff --git a/external/acados_install_linux.sh b/external/acados_install_linux.sh index a30489718..5ac8064ab 100755 --- a/external/acados_install_linux.sh +++ b/external/acados_install_linux.sh @@ -19,7 +19,7 @@ ARG1=${1:-$NB_CPU_MAX} if [ -z "$1" ]; then echo " Argument 1 (NB_CPU) not provided, falling back on maximum number of CPUs ($NB_CPU_MAX)." fi -echo " Number of threads for acados with openMP: NB_CPU=$ARG1" +echo " Number of build threads: NB_CPU=$ARG1" echo "" ARG2=${2:-$CONDA_PREFIX} @@ -34,9 +34,18 @@ fi echo " set CMAKE_INSTALL_PREFIX=$ARG2" echo "" -ARG3=${3:-X64_AUTOMATIC} +case "$(uname -m)" in + aarch64|arm64) + DEFAULT_BLASFEO_TARGET=ARMV8A_ARM_CORTEX_A57 + ;; + *) + DEFAULT_BLASFEO_TARGET=X64_AUTOMATIC + ;; +esac + +ARG3=${3:-$DEFAULT_BLASFEO_TARGET} if [ -z "$3" ]; then - echo " Argument 3 (BLASFEO_TARGET) not provided, falling back on X64_AUTOMATIC" + echo " Argument 3 (BLASFEO_TARGET) not provided, falling back on $DEFAULT_BLASFEO_TARGET" fi echo " set BLASFEO_TARGET=$ARG3" echo "" @@ -54,8 +63,7 @@ mkdir acados/build cd acados/build # We must manually change the minimum required cmake version in some of acados' dependencies -sed -i "s/cmake_minimum_required(VERSION 3.5)/cmake_minimum_required(VERSION 3.14)/" ../external/blasfeo/CMakeLists.txt -sed -i "s/cmake_minimum_required(VERSION 2.6)/cmake_minimum_required(VERSION 3.14)/" ../external/qpoases/CMakeLists.txt +sed -i "s/cmake_minimum_required(VERSION 3.5)/cmake_minimum_required(VERSION 3.14)/" ../external/qpoases/CMakeLists.txt sed -i "s/CMAKE_MINIMUM_REQUIRED( VERSION 2.8 )/cmake_minimum_required(VERSION 3.14)/" ../external/qpdunes/CMakeLists.txt sed -i "s/cmake_minimum_required (VERSION 3.2)/cmake_minimum_required (VERSION 3.14)/" ../external/osqp/CMakeLists.txt sed -i "s/cmake_minimum_required (VERSION 3.2)/cmake_minimum_required (VERSION 3.14)/" ../external/osqp/lin_sys/direct/qdldl/qdldl_sources/CMakeLists.txt @@ -64,14 +72,16 @@ sed -i "s/cmake_minimum_required (VERSION 3.2)/cmake_minimum_required (VERSION 3 cmake .. \ -DCMAKE_INSTALL_PREFIX="$ARG2" \ -DACADOS_INSTALL_DIR="$ARG2" \ - -DACADOS_PYTHON=ON \ -DACADOS_WITH_QPOASES=ON \ -DACADOS_WITH_OSQP=ON \ -DACADOS_WITH_QPDUNES=ON \ -DBLASFEO_TARGET="$ARG3" \ - -DACADOS_WITH_OPENMP=ON \ - -DACADOS_NUM_THREADS=$ARG1 -make install -j$NB_CPU_MAX + -DACADOS_WITH_OPENMP=ON +make install -j$ARG1 + +# Acados v0.5.5 requires this generated metadata at code-generation time, +# but its CMake install target does not copy it to the installation prefix. +install -m 644 ../lib/link_libs.json ../lib/git_commit_hash "$ARG2/lib/" # Prepare the Python interface cd ../interfaces/acados_template diff --git a/external/acados_install_mac.sh b/external/acados_install_mac.sh index 4f3f89d04..63fafd3ff 100755 --- a/external/acados_install_mac.sh +++ b/external/acados_install_mac.sh @@ -19,7 +19,7 @@ ARG1=${1:-$NB_CPU_MAX} if [ -z "$1" ]; then echo " Argument 1 (NB_CPU) not provided, falling back on maximum number of CPUs ($NB_CPU_MAX)." fi -echo " Number of threads for acados with openMP: NB_CPU=$ARG1" +echo " Number of build threads: NB_CPU=$ARG1" echo "" ARG2=${2:-$CONDA_PREFIX} @@ -34,9 +34,18 @@ fi echo " set CMAKE_INSTALL_PREFIX=$ARG2" echo "" -ARG3=${3:-X64_AUTOMATIC} +case "$(uname -m)" in + arm64) + DEFAULT_BLASFEO_TARGET=ARMV8A_APPLE_M1 + ;; + *) + DEFAULT_BLASFEO_TARGET=X64_AUTOMATIC + ;; +esac + +ARG3=${3:-$DEFAULT_BLASFEO_TARGET} if [ -z "$3" ]; then - echo " Argument 3 (BLASFEO_TARGET) not provided, falling back on X64_AUTOMATIC" + echo " Argument 3 (BLASFEO_TARGET) not provided, falling back on $DEFAULT_BLASFEO_TARGET" fi echo " set BLASFEO_TARGET=$ARG3" echo "" @@ -54,8 +63,7 @@ mkdir acados/build cd acados/build # We must manually change the minimum required cmake version in some of acados' dependencies -sed -i "" "s/cmake_minimum_required(VERSION 3.5)/cmake_minimum_required(VERSION 3.14)/" ../external/blasfeo/CMakeLists.txt -sed -i "" "s/cmake_minimum_required(VERSION 2.6)/cmake_minimum_required(VERSION 3.14)/" ../external/qpoases/CMakeLists.txt +sed -i "" "s/cmake_minimum_required(VERSION 3.5)/cmake_minimum_required(VERSION 3.14)/" ../external/qpoases/CMakeLists.txt sed -i "" "s/CMAKE_MINIMUM_REQUIRED( VERSION 2.8 )/cmake_minimum_required(VERSION 3.14)/" ../external/qpdunes/CMakeLists.txt sed -i "" "s/cmake_minimum_required (VERSION 3.2)/cmake_minimum_required (VERSION 3.14)/" ../external/osqp/CMakeLists.txt sed -i "" "s/cmake_minimum_required (VERSION 3.2)/cmake_minimum_required (VERSION 3.14)/" ../external/osqp/lin_sys/direct/qdldl/qdldl_sources/CMakeLists.txt @@ -64,15 +72,17 @@ sed -i "" "s/cmake_minimum_required (VERSION 3.2)/cmake_minimum_required (VERSIO cmake .. \ -DCMAKE_INSTALL_PREFIX="$ARG2" \ -DACADOS_INSTALL_DIR="$ARG2" \ - -DACADOS_PYTHON=ON \ -DACADOS_WITH_OSQP=ON \ -DBLASFEO_TARGET="$ARG3" \ - -DACADOS_WITH_OPENMP=ON \ - -DACADOS_NUM_THREADS=$ARG1 + -DACADOS_WITH_OPENMP=ON # The following two won't compile on Mac # -DACADOS_WITH_QPOASES=ON \ # -DACADOS_WITH_QPDUNES=ON \ -make install -j$NB_CPU_MAX +make install -j$ARG1 + +# Acados v0.5.5 requires this generated metadata at code-generation time, +# but its CMake install target does not copy it to the installation prefix. +install -m 644 ../lib/link_libs.json ../lib/git_commit_hash "$ARG2/lib/" # Prepare the Python interface cd ../interfaces/acados_template @@ -92,22 +102,15 @@ REPLACE_ACADOS_PYTHON_BY="import site\n acados_path = site.getsitepackages()\ TO_REPLACE_T_RENDERER="if input(msg) != 'y':" REPLACE_T_RENDERER_BY="if False:" -# Change acados external library linking at run time -TO_REPLACE_LIB_PATH="libacados_ocp_solver_name = f'{lib_prefix}acados_ocp_solver_{self.model_name}{lib_ext}'" -REPLACE_LIB_PATH_BY="libacados_ocp_solver_name = f'{lib_prefix}acados_ocp_solver_{self.model_name}{lib_ext}'\n self.shared_lib_name = os.path.join(code_export_directory, libacados_ocp_solver_name)\n import site\n acados_path = site.getsitepackages()\n libacados_ocp_solver_name = f'{lib_prefix}acados_ocp_solver_{self.model_name}{lib_ext}'\n # Relink macos lib\n acados_ext_lib_path = os.path.abspath(acados_path[0]+'\/..\/..')\n os.system(\n f'install_name_tool -change libhpipm.dylib {acados_ext_lib_path}\/libhpipm.dylib {self.shared_lib_name}')\n os.system(\n f'install_name_tool -change libblasfeo.dylib {acados_ext_lib_path}\/libblasfeo.dylib {self.shared_lib_name}')" - # Perform the modifications sed -i "" "s/$TO_REPLACE_CASADI_DEP/$REPLACE_CASADI_DEP_BY/" setup.py sed -i "" "s/$TO_REPLACE_ACADOS_PYTHON/$REPLACE_ACADOS_PYTHON_BY/" acados_template/utils.py sed -i "" "s/$TO_REPLACE_ACADOS_SOURCE/$REPLACE_ACADOS_SOURCE_BY/" acados_template/utils.py sed -i "" "s/$TO_REPLACE_T_RENDERER/$REPLACE_T_RENDERER_BY/" acados_template/utils.py -sed -i "" "s/$TO_REPLACE_LIB_PATH/$REPLACE_LIB_PATH_BY/" acados_template/acados_ocp_solver.py -# Change acados external lib linking permanently -install_name_tool -change libqpOASES_e.3.1.dylib $CONDA_PREFIX/lib/libqpOASES_e.3.1.dylib $CONDA_PREFIX/lib/libacados.dylib -install_name_tool -change libhpipm.dylib $CONDA_PREFIX/lib/libhpipm.dylib $CONDA_PREFIX/lib/libacados.dylib -install_name_tool -change libblasfeo.dylib $CONDA_PREFIX/lib/libblasfeo.dylib $CONDA_PREFIX/lib/libacados.dylib -install_name_tool -change libblasfeo.dylib $CONDA_PREFIX/lib/libblasfeo.dylib $CONDA_PREFIX/lib/libhpipm.dylib +# Resolve the @rpath dependencies of the installed Acados libraries. +install_name_tool -add_rpath "$ARG2/lib" "$ARG2/lib/libacados.dylib" +install_name_tool -add_rpath "$ARG2/lib" "$ARG2/lib/libhpipm.dylib" # Install the Python interface pip install . diff --git a/tests/shard1/test_acados_interface.py b/tests/shard1/test_acados_interface.py index 6bdb878f9..9c1a41fc5 100644 --- a/tests/shard1/test_acados_interface.py +++ b/tests/shard1/test_acados_interface.py @@ -32,6 +32,29 @@ from tests.utils import TestUtils +def test_acados_v055_codegen_configuration(tmp_path, monkeypatch): + pytest.importorskip("acados_template") + from acados_template import AcadosOcp + + from bioptim.interfaces.acados_interface import _configure_acados_codegen + + monkeypatch.setenv("ACADOS_SOURCE_DIR", str(tmp_path / "source")) + acados_root = tmp_path / "installed_acados" + generated_code = tmp_path / "generated_code" + + solver = Solver.ACADOS() + solver.set_acados_dir(str(acados_root)) + solver.set_c_generated_code_path(str(generated_code)) + + acados_ocp = AcadosOcp() + _configure_acados_codegen(acados_ocp, solver) + + assert acados_ocp.code_gen_options.acados_include_path == str(acados_root / "include") + assert acados_ocp.code_gen_options.acados_lib_path == str(acados_root / "lib") + assert acados_ocp.code_gen_options.code_export_directory == str(generated_code) + assert acados_ocp.code_gen_options.json_file == "acados_ocp.json" + + @pytest.mark.parametrize("cost_type", ["LINEAR_LS", "NONLINEAR_LS"]) def test_acados_no_obj(cost_type): if platform == "win32":