From 9fc912d9d6306958e09ea9254641ba3beb6deea3 Mon Sep 17 00:00:00 2001
From: Charlie Vanaret <vanaret@zib.de>
Date: Fri, 4 Oct 2024 16:24:14 +0200
Subject: [PATCH] Uno 1.1.0 release

---
 CITATION.cff                                  |  15 +-
 CMakeLists.txt                                | 187 ++--
 INSTALL.md                                    |  62 +-
 README.md                                     |  38 +-
 bindings/AMPL/AMPLModel.cpp                   | 406 ++++++++
 bindings/AMPL/AMPLModel.hpp                   | 119 +++
 bindings/AMPL/uno_ampl.cpp                    |  77 ++
 cmake/FindBLACS.cmake                         |  31 +
 cmake/FindMETIS.cmake                         |  32 +
 cmake/FindMUMPS.cmake                         |  51 +
 cmake/FindSCOTCH.cmake                        |  41 +
 cmake/FindScaLAPACK.cmake                     |  28 +
 cmake/LibFindMacros.cmake                     | 103 ++
 uno.options                                   |  32 +-
 uno/Uno.cpp                                   | 276 +++---
 uno/Uno.hpp                                   |  76 +-
 .../ConstraintRelaxationStrategy.cpp          | 367 +++++---
 .../ConstraintRelaxationStrategy.hpp          | 164 ++--
 .../ConstraintRelaxationStrategyFactory.cpp   |  26 +-
 .../ConstraintRelaxationStrategyFactory.hpp   |  16 +-
 .../FeasibilityRestoration.cpp                | 394 ++++----
 .../FeasibilityRestoration.hpp                | 100 +-
 .../l1Relaxation.cpp                          | 536 +++++------
 .../l1Relaxation.hpp                          | 145 ++-
 .../BacktrackingLineSearch.cpp                | 230 ++---
 .../BacktrackingLineSearch.hpp                |  38 +-
 .../GlobalizationMechanism.cpp                | 139 +--
 .../GlobalizationMechanism.hpp                |  66 +-
 .../GlobalizationMechanismFactory.cpp         |  30 +-
 .../GlobalizationMechanismFactory.hpp         |  12 +-
 .../TrustRegionStrategy.cpp                   | 337 +++----
 .../TrustRegionStrategy.hpp                   |  57 +-
 .../GlobalizationStrategy.cpp                 |  18 +-
 .../GlobalizationStrategy.hpp                 |  62 +-
 .../GlobalizationStrategyFactory.cpp          |  36 +-
 .../GlobalizationStrategyFactory.hpp          |  16 +-
 .../ProgressMeasures.hpp                      |  22 +-
 .../l1MeritFunction.cpp                       | 105 ++-
 .../l1MeritFunction.hpp                       |  31 +-
 .../switching_methods/SwitchingMethod.cpp     |  66 ++
 .../switching_methods/SwitchingMethod.hpp     |  32 +
 .../filter_methods/FilterMethod.cpp           |  53 ++
 .../filter_methods/FilterMethod.hpp           |  44 +
 .../filter_methods/FletcherFilterMethod.cpp   |  69 ++
 .../filter_methods/FletcherFilterMethod.hpp   |  22 +
 .../filter_methods/WaechterFilterMethod.cpp   |  87 ++
 .../filter_methods/WaechterFilterMethod.hpp   |  26 +
 .../filter_methods/filters/Filter.cpp         | 191 ++++
 .../filter_methods/filters/Filter.hpp         |  55 ++
 .../filter_methods/filters/FilterFactory.cpp  |  23 +
 .../filter_methods/filters/FilterFactory.hpp  |  16 +
 .../filters/NonmonotoneFilter.cpp             | 104 ++
 .../filters/NonmonotoneFilter.hpp             |  27 +
 .../funnel_methods/Funnel.cpp                 |  68 ++
 .../funnel_methods/Funnel.hpp                 |  37 +
 .../funnel_methods/FunnelMethod.cpp           | 141 +++
 .../funnel_methods/FunnelMethod.hpp           |  58 ++
 uno/ingredients/subproblem/Direction.cpp      | 122 ++-
 uno/ingredients/subproblem/Direction.hpp      |  91 +-
 uno/ingredients/subproblem/HessianModel.cpp   | 126 +--
 uno/ingredients/subproblem/HessianModel.hpp   |  92 +-
 .../subproblem/HessianModelFactory.hpp        |  34 +-
 uno/ingredients/subproblem/Subproblem.hpp     |  98 +-
 .../subproblem/SubproblemFactory.cpp          |  58 +-
 .../subproblem/SubproblemFactory.hpp          |  18 +-
 .../subproblem/SubproblemStatus.hpp           |  16 +-
 .../InequalityConstrainedMethod.cpp           | 128 +--
 .../InequalityConstrainedMethod.hpp           |  60 +-
 .../LPSubproblem.cpp                          |  97 +-
 .../LPSubproblem.hpp                          |  34 +-
 .../QPSubproblem.cpp                          | 133 ++-
 .../QPSubproblem.hpp                          |  42 +-
 .../BarrierParameterUpdateStrategy.cpp        | 121 +--
 .../BarrierParameterUpdateStrategy.hpp        |  48 +-
 .../PrimalDualInteriorPointSubproblem.cpp     | 885 +++++++++---------
 .../PrimalDualInteriorPointSubproblem.hpp     | 149 +--
 uno/interfaces/AMPL/AMPLModel.cpp             |   2 +-
 uno/interfaces/AMPL/AMPLModel.hpp             |   2 +-
 uno/linear_algebra/COOSymmetricMatrix.hpp     | 230 +++--
 uno/linear_algebra/CSCSymmetricMatrix.hpp     | 237 +++--
 uno/linear_algebra/Norm.hpp                   | 246 ++---
 uno/linear_algebra/RectangularMatrix.hpp      |  56 +-
 uno/linear_algebra/SparseVector.hpp           | 240 ++---
 .../SymmetricIndefiniteLinearSystem.hpp       | 328 +++----
 uno/linear_algebra/SymmetricMatrix.hpp        | 229 +++--
 uno/linear_algebra/SymmetricMatrixFactory.hpp |  36 +-
 uno/linear_algebra/Vector.hpp                 | 182 ++--
 uno/model/BoundRelaxedModel.hpp               | 126 +--
 .../HomogeneousEqualityConstrainedModel.hpp   | 340 +++----
 uno/model/Model.cpp                           |  36 +-
 uno/model/Model.hpp                           | 176 ++--
 uno/model/ModelFactory.cpp                    |  34 +-
 uno/model/ModelFactory.hpp                    |  16 +-
 uno/model/ScaledModel.hpp                     | 244 ++---
 uno/optimization/EvaluationErrors.hpp         |  28 +-
 uno/optimization/Evaluations.hpp              |  24 +-
 uno/optimization/Iterate.cpp                  | 161 ++--
 uno/optimization/Iterate.hpp                  |  94 +-
 uno/optimization/LagrangianGradient.hpp       |  98 +-
 uno/optimization/Multipliers.hpp              |  70 +-
 uno/optimization/PrimalDualResiduals.hpp      |  28 +-
 uno/optimization/Result.cpp                   | 106 ++-
 uno/optimization/Result.hpp                   |  34 +-
 uno/optimization/TerminationStatus.hpp        |  20 +-
 uno/optimization/WarmstartInformation.hpp     | 108 +--
 uno/preprocessing/Preprocessing.cpp           | 223 ++---
 uno/preprocessing/Preprocessing.hpp           |  40 +-
 uno/preprocessing/Scaling.cpp                 |  38 +-
 uno/preprocessing/Scaling.hpp                 |  36 +-
 uno/reformulation/ElasticVariables.hpp        |  66 ++
 uno/reformulation/OptimalityProblem.hpp       | 173 ++--
 uno/reformulation/OptimizationProblem.hpp     | 149 +--
 uno/reformulation/l1RelaxedProblem.hpp        | 461 +++++----
 uno/solvers/LP/LPSolver.hpp                   |  48 +-
 uno/solvers/LP/LPSolverFactory.hpp            |  27 +-
 uno/solvers/QP/BQPDSolver.cpp                 | 472 +++++-----
 uno/solvers/QP/BQPDSolver.hpp                 | 156 +--
 uno/solvers/QP/QPSolver.hpp                   |  74 +-
 uno/solvers/QP/QPSolverFactory.hpp            |  45 +-
 uno/solvers/linear/MA57Solver.cpp             | 296 +++---
 uno/solvers/linear/MA57Solver.hpp             | 112 +--
 uno/solvers/linear/MUMPSSolver.cpp            |  96 ++
 uno/solvers/linear/MUMPSSolver.hpp            |  47 +
 .../SymmetricIndefiniteLinearSolver.hpp       |  64 +-
 ...SymmetricIndefiniteLinearSolverFactory.hpp |  46 +-
 uno/symbolic/Collection.hpp                   |  76 +-
 uno/symbolic/CollectionAdapter.hpp            |  80 +-
 uno/symbolic/Concatenation.hpp                |  84 +-
 uno/symbolic/Expression.hpp                   |   4 +-
 uno/symbolic/MatrixVectorProduct.hpp          |  56 +-
 uno/symbolic/Range.hpp                        | 110 +--
 uno/symbolic/ScalarMultiple.hpp               |  44 +-
 uno/symbolic/Sum.hpp                          |  44 +-
 uno/symbolic/UnaryNegation.hpp                |  40 +-
 uno/symbolic/VectorExpression.hpp             |  84 +-
 uno/symbolic/VectorView.hpp                   |  96 +-
 uno/tools/Infinity.hpp                        |  16 +-
 uno/tools/Logger.hpp                          | 108 +--
 uno/tools/Options.cpp                         | 328 ++++---
 uno/tools/Options.hpp                         |  42 +-
 uno/tools/Statistics.cpp                      | 263 +++---
 uno/tools/Statistics.hpp                      |  67 +-
 uno/tools/Timer.cpp                           |  22 +-
 uno/tools/Timer.hpp                           |  20 +-
 uno_ampl-completion.bash                      |   4 +-
 unotest/COOSymmetricMatrixTests.cpp           |  12 +-
 unotest/CSCSymmetricMatrixTests.cpp           |  12 +-
 unotest/CollectionAdapterTests.cpp            |   4 +-
 unotest/ConcatenationTests.cpp                |   4 +-
 unotest/MA57SolverTests.cpp                   |  24 +-
 unotest/MUMPSSolverTests.cpp                  |  80 ++
 unotest/MatrixVectorProductTests.cpp          |   2 +
 unotest/RangeTests.cpp                        |   4 +-
 unotest/ScalarMultipleTests.cpp               |   4 +-
 unotest/SparseVectorTests.cpp                 |   4 +-
 unotest/VectorTests.cpp                       |  62 ++
 unotest/unotest.cpp                           |  20 +-
 157 files changed, 9403 insertions(+), 6609 deletions(-)
 create mode 100644 bindings/AMPL/AMPLModel.cpp
 create mode 100644 bindings/AMPL/AMPLModel.hpp
 create mode 100644 bindings/AMPL/uno_ampl.cpp
 create mode 100644 cmake/FindBLACS.cmake
 create mode 100644 cmake/FindMETIS.cmake
 create mode 100644 cmake/FindMUMPS.cmake
 create mode 100644 cmake/FindSCOTCH.cmake
 create mode 100644 cmake/FindScaLAPACK.cmake
 create mode 100644 cmake/LibFindMacros.cmake
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/SwitchingMethod.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/SwitchingMethod.hpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/FilterMethod.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/FilterMethod.hpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/FletcherFilterMethod.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/FletcherFilterMethod.hpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/WaechterFilterMethod.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/WaechterFilterMethod.hpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/Filter.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/Filter.hpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/FilterFactory.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/FilterFactory.hpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/NonmonotoneFilter.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/NonmonotoneFilter.hpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/funnel_methods/Funnel.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/funnel_methods/Funnel.hpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/funnel_methods/FunnelMethod.cpp
 create mode 100644 uno/ingredients/globalization_strategy/switching_methods/funnel_methods/FunnelMethod.hpp
 create mode 100644 uno/reformulation/ElasticVariables.hpp
 create mode 100644 uno/solvers/linear/MUMPSSolver.cpp
 create mode 100644 uno/solvers/linear/MUMPSSolver.hpp
 create mode 100644 unotest/MUMPSSolverTests.cpp
 create mode 100644 unotest/VectorTests.cpp

diff --git a/CITATION.cff b/CITATION.cff
index 3d2b178..ecd9554 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -4,6 +4,17 @@ authors:
 - family-names: "Vanaret"
   given-names: "Charlie"
   orcid: "https://orcid.org/0000-0002-1131-7631"
-title: "Uno (Unifying Nonconvex Optimization)"
-version: 1.0
+title: "Uno"
+version: 1.0.0
 url: "https://github.com/cvanaret/Uno"
+preferred-citation:
+  type: unpublished
+  authors:
+  - family-names: "Vanaret"
+    given-names: "Charlie"
+    orcid: "https://orcid.org/0000-0002-1131-7631"
+  - family-names: "Leyffer"
+    given-names: "Sven"
+    orcid: "https://orcid.org/0000-0001-8839-5876"
+  title: "Unifying nonlinearly constrained nonconvex optimization"
+  year: 2024
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9837888..ec3f814 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,17 +8,19 @@ endif()
 
 # define the project name
 project(Uno VERSION 1.0
-			DESCRIPTION "Uno (Unifying Nonlinear Optimization)"
-            LANGUAGES CXX C Fortran)
-
-# set C++17 and enable other languages
+        DESCRIPTION "Uno (Unifying Nonconvex Optimization)"
+        LANGUAGES CXX C Fortran)
 set(CMAKE_CXX_STANDARD 17)
-enable_language(CXX C Fortran)
+
+include(FortranCInterface)
+FortranCInterface_VERIFY(CXX)
 
 set(CMAKE_CXX_FLAGS "-Wall -Wextra -Wnon-virtual-dtor -pedantic -Wunused-value -Wconversion -Wmaybe-uninitialized")
 set(CMAKE_CXX_FLAGS_DEBUG "-pg")
 set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG") # disable asserts
 
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake ${CMAKE_CURRENT_SOURCE_DIR}/cmake-library/finders)
+
 # optional Gtest
 option(WITH_GTEST "Enable GoogleTest" OFF)
 message(STATUS "GoogleTest: WITH_GTEST=${WITH_GTEST}")
@@ -31,8 +33,10 @@ file(GLOB UNO_SOURCE_FILES
     uno/Uno.cpp
     uno/ingredients/globalization_mechanism/*.cpp
     uno/ingredients/globalization_strategy/*.cpp
-    uno/ingredients/globalization_strategy/filter_method/*.cpp
-    uno/ingredients/globalization_strategy/filter_method/filter/*.cpp
+    uno/ingredients/globalization_strategy/switching_methods/*.cpp
+    uno/ingredients/globalization_strategy/switching_methods/filter_methods/*.cpp
+    uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/*.cpp
+    uno/ingredients/globalization_strategy/switching_methods/funnel_methods/*.cpp
     uno/ingredients/constraint_relaxation_strategy/*.cpp
     uno/ingredients/subproblem/*.cpp
     uno/ingredients/subproblem/inequality_constrained_methods/*.cpp
@@ -43,76 +47,129 @@ file(GLOB UNO_SOURCE_FILES
     uno/tools/*.cpp
 )
 
-# find libraries
+#########################
+# external dependencies #
+#########################
 set(LIBRARIES "")
-set(OPTIONAL_LIBRARIES amplsolver ma57 metis bqpd CACHE STRING "Optional libraries")
-set(REQUIRED_LIBRARIES dl blas lapack)
-
-# automatic detection of optional libraries
-foreach(library_name IN LISTS OPTIONAL_LIBRARIES)
-	find_library(${library_name} ${library_name})
-	# ${library_name} is the name of the library(e.g. ma57)
-	# ${${library_name}} is the path of the library if found, otherwise ${library_name}-NOTFOUND
-	if(${${library_name}} STREQUAL "${library_name}-NOTFOUND")
-		message(WARNING "Optional library ${library_name} was not found. Use ccmake to configure its path.")
-	else()
-		# add the library
-		list(APPEND LIBRARIES ${${library_name}})
-		# add a preprocessor definition
-		string(TOUPPER ${library_name} library_name_upper)
-		add_definitions("-D HAS_${library_name_upper}")
-		# include the corresponding directory
-		get_filename_component(directory ${${library_name}} DIRECTORY)
-		include_directories(${directory})
-        message(STATUS "Library ${library_name} was found")
-        
-        # add the corresponding sources
-        if(${library_name} STREQUAL amplsolver)
-            list(APPEND UNO_SOURCE_FILES uno/interfaces/AMPL/AMPLModel.cpp)
-        elseif(${library_name} STREQUAL bqpd)
-            list(APPEND UNO_SOURCE_FILES uno/solvers/QP/BQPDSolver.cpp uno/solvers/QP/wdotd.f)
-        elseif(${library_name} STREQUAL ma57)
-            list(APPEND UNO_SOURCE_FILES uno/solvers/linear/MA57Solver.cpp)
-        endif()
-	endif()
-endforeach(library_name)
-
-# automatic detection of required libraries
-foreach(library_name IN LISTS REQUIRED_LIBRARIES)
-	find_library(${library_name} ${library_name})
-	# ${library_name} is the name of the library(e.g. blas)
-	# ${${library_name}} is the path of the library if found, otherwise ${library_name}-NOTFOUND
-	if(${${library_name}} STREQUAL "${library_name}-NOTFOUND")
-		message(FATAL_ERROR "Required library ${library_name} was not found. Use ccmake to configure its path.")
-	else()
-		# add the library
-		list(APPEND LIBRARIES ${${library_name}})
-	endif()
-endforeach(library_name)
+
+# function that links an existing library to Uno
+function(link_to_uno library_name library_path)
+   # add the library
+   set(LIBRARIES ${LIBRARIES} ${library_path} PARENT_SCOPE)
+   # add a preprocessor definition
+   string(TOUPPER ${library_name} library_name_upper)
+   add_definitions("-D HAS_${library_name_upper}")
+   # include the corresponding directory
+   get_filename_component(directory ${library_path} DIRECTORY)
+   set(DIRECTORIES ${DIRECTORIES} ${directory} PARENT_SCOPE)
+   message(STATUS "Library ${library_name} was found.")
+endfunction()
+
+# detect optional libraries: ma57 metis bqpd
+find_library(ma57 NAMES ma57 hsl)
+if(${ma57} STREQUAL "ma57-NOTFOUND")
+   message(WARNING "Optional library MA57 was not found.")
+else()
+   list(APPEND UNO_SOURCE_FILES uno/solvers/linear/MA57Solver.cpp)
+   link_to_uno(ma57 ${ma57})
+endif()
+
+find_library(metis metis)
+if(${metis} STREQUAL "metis-NOTFOUND")
+   message(WARNING "Optional library METIS was not found.")
+else()
+   link_to_uno(metis ${metis})
+endif()
+
+find_library(bqpd bqpd)
+if(${bqpd} STREQUAL "bqpd-NOTFOUND")
+   message(WARNING "Optional library BQPD was not found.")
+else()
+   list(APPEND UNO_SOURCE_FILES uno/solvers/QP/BQPDSolver.cpp uno/solvers/QP/wdotd.f)
+   link_to_uno(bqpd ${bqpd})
+endif()
+
+find_package(MUMPS REQUIRED)
+if(${MUMPS_LIBRARY} STREQUAL "MUMPS-NOTFOUND")
+   message(WARNING "Optional library MUMPS was not found.")
+else()
+   list(APPEND UNO_SOURCE_FILES uno/solvers/linear/MUMPSSolver.cpp)
+   list(APPEND LIBRARIES ${MUMPS_LIBRARY} ${MUMPS_COMMON_LIBRARY} ${MUMPS_PORD_LIBRARY})
+   
+   list(APPEND DIRECTORIES ${MUMPS_INCLUDE_DIR})
+
+   if(${MUMPS_MPISEQ_LIBRARY} STREQUAL "MUMPS_MPISEQ_LIBRARY-NOTFOUND")
+      # parallel
+      add_definitions("-D MUMPS_PARALLEL")
+      find_package(MPI REQUIRED)
+      list(APPEND LIBRARIES MPI::MPI_CXX MPI::MPI_Fortran)
+
+      find_package(BLACS REQUIRED)
+      list(APPEND LIBRARIES ${BLACS_LIBRARY})
+      list(APPEND DIRECTORIES ${BLACS_INCLUDE_DIRS})
+
+      find_package(ScaLAPACK REQUIRED)
+      list(APPEND LIBRARIES ${ScaLAPACK_LIBRARY})
+      list(APPEND DIRECTORIES ${ScaLAPACK_INCLUDE_DIRS})
+   else()
+      # sequential
+      add_definitions("-D MUMPS_SEQUENTIAL")
+      # link dummy parallel library (provided by MUMPS distribution)
+      link_to_uno(MUMPS_MPISEQ_LIBRARY ${MUMPS_MPISEQ_LIBRARY})
+   endif()
+   
+   find_package(METIS REQUIRED)
+   list(APPEND LIBRARIES ${METIS_LIBRARY})
+   list(APPEND DIRECTORIES ${METIS_INCLUDE_DIRS})
+
+   
+   add_definitions("-D HAS_MPI")
+   add_definitions("-D HAS_MUMPS")
+endif()
+
+# detection of required libraries
+find_library(dl dl REQUIRED)
+list(APPEND LIBRARIES ${dl})
+
+find_library(blas blas REQUIRED)
+list(APPEND LIBRARIES ${blas})
+
+find_library(lapack lapack REQUIRED)
+list(APPEND LIBRARIES ${lapack})
+
 find_package(OpenMP REQUIRED)
 list(APPEND LIBRARIES OpenMP::OpenMP_CXX)
 
-#######
-# Uno #
-#######
+###############
+# Uno library #
+###############
 add_library(uno STATIC ${UNO_SOURCE_FILES})
 target_include_directories(uno PUBLIC ${DIRECTORIES})
-
-# link the libraries
 target_link_libraries(uno PUBLIC ${LIBRARIES})
 
 # copy the option file
 file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/uno.options DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
 
-#############
-# AMPL main #
-#############
-add_executable(uno_ampl uno/main.cpp)
-target_link_libraries(uno_ampl PUBLIC uno)
+######################
+# optional AMPL main #
+######################
+find_library(amplsolver amplsolver)
+if(${amplsolver} STREQUAL "amplsolver-NOTFOUND")
+   message(WARNING "Optional library amplsolver (ASL) was not found.")
+else()
+   message(STATUS "Library amplsolver was found.")
+   add_executable(uno_ampl bindings/AMPL/AMPLModel.cpp bindings/AMPL/uno_ampl.cpp)
+   
+   target_link_libraries(uno_ampl PUBLIC uno ${amplsolver})
+   add_definitions("-D HAS_AMPLSOLVER")
+   # include the corresponding directory
+   get_filename_component(directory ${amplsolver} DIRECTORY)
+   include_directories(${directory})
+endif()
 
-#########################
-# GoogleTest unit tests #
-#########################
+##################################
+# optional GoogleTest unit tests #
+##################################
 if(WITH_GTEST)
     find_package(GTest CONFIG REQUIRED)
     if(NOT ${GTest}_DIR STREQUAL "${GTest}-NOTFOUND")
diff --git a/INSTALL.md b/INSTALL.md
index 9b787e3..c55b075 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -7,28 +7,68 @@
 * download **optional** solvers:
     * BQPD (indefinite null-space QP solver): https://www.mcs.anl.gov/~leyffer/solvers.html
     * MA57 (sparse indefinite symmetric linear solver): http://www.hsl.rl.ac.uk/catalogue/ma57.html
+    * MUMPS (sparse indefinite symmetric linear solver): https://mumps-solver.org/index.php?page=dwnld
 
-* install BLAS and LAPACK: ```sudo apt-get install libblas-dev liblapack-dev```
-* install cmake (and optionally ccmake, CMake curses interface): ```sudo apt-get install cmake cmake-curses-gui```
+* to compile MUMPS in sequential mode, set the following variables at the end of your Makefile.inc:
+```console
+INCS = $(INCSEQ)
+LIBS = $(LIBSEQ)
+LIBSEQNEEDED = libseqneeded
+```
+
+* install BLAS and LAPACK:
+```console
+sudo apt-get install libblas-dev liblapack-dev
+```
+* install cmake (and optionally ccmake, CMake curses interface):
+```console
+sudo apt-get install cmake cmake-curses-gui
+```
 
 ### Compilation
 
-1. Create a `build` directory in the main directory: ```mkdir build```
-2. Move to the build directory: ```cd build/```
+1. Create a `build` directory in the main directory:
+```console
+mkdir build
+```
+2. Move to the build directory:
+```console
+cd build/
+```
 3. Execute cmake by providing the paths to the required and optional libraries ASL, BQPD and MA57:  
-```cmake -Dbqpd=path -Dma57=path -Damplsolver=path -DCMAKE_BUILD_TYPE=[Release|Debug] ..```
-4. **(or)** Use ccmake to provide the paths to the required and optional libraries: ```ccmake ..```
-5. Compile (in parallel: `n` being the number of threads, e.g. 6): ```make -jn```
+```console
+cmake -Dbqpd=path -Dma57=path -Damplsolver=path -DCMAKE_BUILD_TYPE=[Release|Debug] ..
+```
+4. **(or)** Use ccmake to provide the paths to the required and optional libraries:
+```console
+ccmake ..
+```
+5. Compile (in parallel: `n` being the number of threads, e.g. 6):
+```console
+make -jn
+```
 
 To compile the code with different configurations, simply create a `build` directory for each configuration and perform instructions 1 to 5.
 
 ### Unit tests
 
-6. Install the GoogleTest suite: ```sudo apt-get install googletest```
-7. Perform steps 2 and 3 with the flag ```-DWITH_GTEST=ON```
-8. Run the test suite: ```./run_unotest```
+6. Install the GoogleTest suite:
+```console
+sudo apt-get install googletest
+```
+7. Perform steps 2 and 3 with the flag
+```console
+-DWITH_GTEST=ON
+```
+8. Run the test suite:
+```console
+./run_unotest
+```
 
 ### Autocompletion
 
-To benefit from autocompletion, install the file `uno_ampl-completion.bash`: ```sudo cp uno_ampl-completion.bash /etc/bash_completion.d/```
+To benefit from autocompletion, install the file `uno_ampl-completion.bash`:
+```console
+sudo cp uno_ampl-completion.bash /etc/bash_completion.d/
+```
 and open a new terminal.
diff --git a/README.md b/README.md
index 023b346..9b6ecb3 100644
--- a/README.md
+++ b/README.md
@@ -16,11 +16,11 @@ $$
 \end{align}
 $$
 
-The theoretical abstract framework for unifying nonlinearly constrained nonconvex optimization was developed by [Charlie Vanaret](https://github.com/cvanaret/) (Zuse-Institut Berlin) and [Sven Leyffer](https://wiki.mcs.anl.gov/leyffer/index.php/Sven_Leyffer) (Argonne National Laboratory).  
+The theoretical abstract framework for unifying nonlinearly constrained nonconvex optimization was developed by [Charlie Vanaret](https://github.com/cvanaret/) (Argonne National Laboratory & Zuse-Institut Berlin) and [Sven Leyffer](https://wiki.mcs.anl.gov/leyffer/index.php/Sven_Leyffer) (Argonne National Laboratory). Uno was designed and implemented by Charlie Vanaret. It is released under the MIT license (see the [license file](LICENSE)).
 
-Uno was designed and implemented by Charlie Vanaret. [Silvio Traversaro](https://github.com/traversaro) contributed to the CMakeLists.
+[David Kiessling](https://github.com/david0oo) implemented the funnel method.  
+[Silvio Traversaro](https://github.com/traversaro) and [Alexis Montoison](https://github.com/amontoison) contributed to the CMakeLists.
 
-Uno is released under the MIT license (see the [license file](LICENSE)).
 
 ## Unifying nonlinearly constrained nonconvex optimization
 
@@ -36,17 +36,17 @@ The following hypergraph illustrates how some of the state-of-the-art solvers ca
    <img src="docs/figures/combination_hypergraph.png" alt="Combination hypergraph" width="75%" />
 </p>
 
-## Uno 1.0
+## Uno 1.0.0
 -->
 
-Uno 1.0 implements the following strategies:
+Uno 1.0.0 implements the following strategies:
 <p align="center">
-   <img src="docs/figures/hypergraph_uno.png" alt="Uno 1.0 hypergraph" width="65%" />
+   <img src="docs/figures/hypergraph_uno.png" alt="Uno 1.0.0 hypergraph" width="65%" />
 </p>
 
-**Any strategy combination** can be automatically generated without any programming effort from the user. Note that all combinations do not necessarily result in sensible algorithms, or even convergent approaches. For more details, check out my [presentation at the ICCOPT 2022 conference](https://www.researchgate.net/publication/362254109).
+**Any strategy combination** can be automatically generated without any programming effort from the user. Note that all combinations do not necessarily result in sensible algorithms, or even convergent approaches. For more details, check out our [preprint](https://www.researchgate.net/publication/381522383_Unifying_nonlinearly_constrained_nonconvex_optimization) or my [presentation at the ICCOPT 2022 conference](https://www.researchgate.net/publication/362254109).
 
-Uno 1.0 implements three **presets**, that is strategy combinations that correspond to existing solvers (as well as hyperparameter values found in their documentations):
+Uno 1.0.0 implements three **presets**, that is strategy combinations that correspond to existing solvers (as well as hyperparameter values found in their documentations):
 * `filtersqp` mimics filterSQP (trust-region feasibility restoration filter SQP method);
 * `ipopt` mimics IPOPT (line-search feasibility restoration filter barrier method);
 * `byrd` mimics Byrd's S $\ell_1$ QP (line-search $\ell_1$ merit S $\ell_1$ QP method).
@@ -57,7 +57,7 @@ Some of Uno combinations that correspond to existing solvers (called presets, se
 The figure below is a performance profile of Uno and state-of-the-art solvers filterSQP, IPOPT, SNOPT, MINOS, LANCELOT, LOQO and CONOPT; it shows how many problems are solved for a given budget of function evaluations (1 time, 2 times, 4 times, ..., $2^x$ times the number of objective evaluations of the best solver for each instance).
 
 <p align="center">
-   <img src="docs/figures/uno_performance_profile.png" alt="Performance profile of Uno 1.0" width="75%" />
+   <img src="docs/figures/uno_performance_profile.png" alt="Performance profile of Uno 1.0.0" width="75%" />
 </p>
 
 All log files can be found [here](https://github.com/cvanaret/nonconvex_solver_comparison).
@@ -66,7 +66,18 @@ All log files can be found [here](https://github.com/cvanaret/nonconvex_solver_c
 
 ### In an article
 
-Please be patient, we are actively working on our article.
+We have submitted our paper to the Mathematical Programming Computation journal. The preprint is available on [ResearchGate](https://www.researchgate.net/publication/381522383_Unifying_nonlinearly_constrained_nonconvex_optimization).
+
+Until it is published, you can use the following bibtex entry:
+
+```
+@unpublished{VanaretLeyffer2024,
+  author = {Vanaret, Charlie and Leyffer, Sven},
+  title = {Unifying nonlinearly constrained nonconvex optimization},
+  year = {2024},
+  note = {Submitted to Mathematical Programming Computation}
+}
+```
 
 ### On social media
 
@@ -79,14 +90,17 @@ See the [INSTALL](INSTALL.md) file.
 
 ## Solving a problem with Uno
 
+At the moment, Uno only reads models from [.nl files](https://en.wikipedia.org/wiki/Nl_(format)). A couple of CUTEst instances are available in the `/examples` directory.
+
 To solve an AMPL model, type in the `build` directory: ```./uno_ampl path_to_file/file.nl```
-A couple of CUTEst instances are available in the `/examples` directory.
+
+To use Uno with Julia/JuMP, a solution in the short term is to use the package [AmplNLWriter.jl](https://juliahub.com/ui/Packages/General/AmplNLWriter.jl) to dump JuMP models into .nl files.
 
 ### Combination of ingredients
 
 To pick a globalization mechanism, use the argument (choose one of the possible options in brackets): ```-globalization_mechanism [LS|TR]```  
 To pick a constraint relaxation strategy, use the argument: ```-constraint_relaxation_strategy [feasibility_restoration|l1_relaxation]```  
-To pick a globalization strategy, use the argument: ```-globalization_strategy [l1_merit|fletcher_filter_strategy|waechter_filter_strategy]```  
+To pick a globalization strategy, use the argument: ```-globalization_strategy [l1_merit|fletcher_filter_method|waechter_filter_method|funnel_method]```  
 To pick a subproblem method, use the argument: ```-subproblem [QP|LP|primal_dual_interior_point]```  
 The options can be combined in the same command line.
 
diff --git a/bindings/AMPL/AMPLModel.cpp b/bindings/AMPL/AMPLModel.cpp
new file mode 100644
index 0000000..d6f78af
--- /dev/null
+++ b/bindings/AMPL/AMPLModel.cpp
@@ -0,0 +1,406 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include <cassert>
+#include "AMPLModel.hpp"
+#include "linear_algebra/RectangularMatrix.hpp"
+#include "linear_algebra/SymmetricMatrix.hpp"
+#include "optimization/EvaluationErrors.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Infinity.hpp"
+#include "symbolic/Concatenation.hpp"
+
+namespace uno {
+   ASL* generate_asl(std::string file_name) {
+      ASL* asl = ASL_alloc(ASL_read_pfgh);
+      FILE* nl = jac0dim_ASL(asl, file_name.data(), static_cast<int>(file_name.size()));
+      // indices start at 0
+      asl->i.Fortran_ = 0;
+
+      int n_discrete = asl->i.nbv_ + asl->i.niv_ + asl->i.nlvbi_ + asl->i.nlvci_ + asl->i.nlvoi_;
+      if (0 < n_discrete) {
+         WARNING << "Ignoring integrality of " << n_discrete << " variables.\n";
+         asl->i.need_nl_ = 0;
+      }
+
+      // preallocate initial primal and dual solutions
+      asl->i.X0_ = static_cast<double*>(M1zapalloc_ASL(&asl->i, sizeof(double) * static_cast<size_t>(asl->i.n_var_)));
+      asl->i.pi0_ = static_cast<double*>(M1zapalloc_ASL(&asl->i, sizeof(double) * static_cast<size_t>(asl->i.n_con_)));
+
+      // read the file_name.nl file
+      pfgh_read_ASL(asl, nl, ASL_findgroups);
+      return asl;
+   }
+
+   // generate the ASL object and call the private constructor
+   AMPLModel::AMPLModel(const std::string& file_name) : AMPLModel(file_name, generate_asl(file_name)) {
+   }
+
+   AMPLModel::AMPLModel(const std::string& file_name, ASL* asl) :
+         Model(file_name, static_cast<size_t>(asl->i.n_var_), static_cast<size_t>(asl->i.n_con_), (asl->i.objtype_[0] == 1) ? -1. : 1.),
+         asl(asl),
+         // allocate vectors
+         asl_gradient(this->number_variables),
+         variable_lower_bounds(this->number_variables),
+         variable_upper_bounds(this->number_variables),
+         constraint_lower_bounds(this->number_constraints),
+         constraint_upper_bounds(this->number_constraints),
+         variable_status(this->number_variables),
+         constraint_type(this->number_constraints),
+         constraint_status(this->number_constraints),
+         equality_constraints_collection(this->equality_constraints),
+         inequality_constraints_collection(this->inequality_constraints),
+         lower_bounded_variables_collection(this->lower_bounded_variables),
+         upper_bounded_variables_collection(this->upper_bounded_variables),
+         single_lower_bounded_variables_collection(this->single_lower_bounded_variables),
+         single_upper_bounded_variables_collection(this->single_upper_bounded_variables) {
+      // evaluate the constraint Jacobian in sparse mode
+      this->asl->i.congrd_mode = 1;
+
+      // variables
+      this->lower_bounded_variables.reserve(this->number_variables);
+      this->upper_bounded_variables.reserve(this->number_variables);
+      this->single_lower_bounded_variables.reserve(this->number_variables);
+      this->single_upper_bounded_variables.reserve(this->number_variables);
+      this->generate_variables();
+
+      // constraints
+      this->equality_constraints.reserve(this->number_constraints);
+      this->inequality_constraints.reserve(this->number_constraints);
+      this->linear_constraints.reserve(this->number_constraints);
+      this->generate_constraints();
+
+      // compute number of nonzeros in the Lagrangian Hessian
+      this->set_number_hessian_nonzeros();
+   }
+
+   AMPLModel::~AMPLModel() {
+      ASL_free(&this->asl);
+   }
+
+   void AMPLModel::generate_variables() {
+      for (size_t variable_index: Range(this->number_variables)) {
+         this->variable_lower_bounds[variable_index] = (this->asl->i.LUv_ != nullptr) ? this->asl->i.LUv_[2*variable_index] : -INF<double>;
+         this->variable_upper_bounds[variable_index] = (this->asl->i.LUv_ != nullptr) ? this->asl->i.LUv_[2*variable_index + 1] : INF<double>;
+         if (this->variable_lower_bounds[variable_index] == this->variable_upper_bounds[variable_index]) {
+            WARNING << "Variable x" << variable_index << " has identical bounds\n";
+         }
+      }
+      AMPLModel::determine_bounds_types(this->variable_lower_bounds, this->variable_upper_bounds, this->variable_status);
+      // figure out the bounded variables
+      for (size_t variable_index: Range(this->number_variables)) {
+         const BoundType status = this->get_variable_bound_type(variable_index);
+         if (status == BOUNDED_LOWER || status == BOUNDED_BOTH_SIDES) {
+            this->lower_bounded_variables.push_back(variable_index);
+            if (status == BOUNDED_LOWER) {
+               this->single_lower_bounded_variables.push_back(variable_index);
+            }
+         }
+         if (status == BOUNDED_UPPER || status == BOUNDED_BOTH_SIDES) {
+            this->upper_bounded_variables.push_back(variable_index);
+            if (status == BOUNDED_UPPER) {
+               this->single_upper_bounded_variables.push_back(variable_index);
+            }
+         }
+      }
+   }
+
+   double AMPLModel::evaluate_objective(const Vector<double>& x) const {
+      int error_flag = 0;
+      double result = this->objective_sign * (*(this->asl)->p.Objval)(this->asl, 0, const_cast<double*>(x.data()), &error_flag);
+      if (0 < error_flag) {
+         throw FunctionEvaluationError();
+      }
+      return result;
+   }
+
+   // sparse gradient
+   void AMPLModel::evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const {
+      int error_flag = 0;
+      // prevent ASL to crash by catching all evaluation errors
+      Jmp_buf err_jmp_uno;
+      asl->i.err_jmp_ = &err_jmp_uno;
+      asl->i.err_jmp1_ = &err_jmp_uno;
+      if (setjmp(err_jmp_uno.jb)) {
+         error_flag = 1;
+      }
+      // evaluate the ASL gradient (always in a dense vector)
+      (*(this->asl)->p.Objgrd)(this->asl, 0, const_cast<double*>(x.data()), const_cast<double*>(this->asl_gradient.data()), &error_flag);
+      if (0 < error_flag) {
+         throw GradientEvaluationError();
+      }
+
+      // create the Uno sparse vector
+      ograd* asl_variables_tmp = this->asl->i.Ograd_[0];
+      while (asl_variables_tmp != nullptr) {
+         const size_t variable_index = static_cast<size_t>(asl_variables_tmp->varno);
+         // scale by the objective sign
+         const double partial_derivative = this->objective_sign*this->asl_gradient[variable_index];
+         gradient.insert(variable_index, partial_derivative);
+         asl_variables_tmp = asl_variables_tmp->next;
+      }
+   }
+
+   /*
+   double AMPLModel::evaluate_constraint(int j, const std::vector<double>& x) const {
+      int error_flag = 0;
+      double result = (*(this->asl)->p.Conival)(this->asl_, j, const_cast<double*>(x.data()), &error_flag);
+      if (0 < error_flag) {
+         throw FunctionNumericalError();
+      }
+      return result;
+   }
+   */
+
+   void AMPLModel::evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const {
+      int error_flag = 0;
+      (*(this->asl)->p.Conval)(this->asl, const_cast<double*>(x.data()), constraints.data(), &error_flag);
+      if (0 < error_flag) {
+         throw FunctionEvaluationError();
+      }
+   }
+
+   // sparse gradient
+   void AMPLModel::evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const {
+      // compute the AMPL sparse gradient
+      int error_flag = 0;
+      (*(this->asl)->p.Congrd)(this->asl, static_cast<int>(constraint_index), const_cast<double*>(x.data()), const_cast<double*>(this->asl_gradient.data()),
+            &error_flag);
+      if (0 < error_flag) {
+         throw GradientEvaluationError();
+      }
+
+      // construct the Uno sparse vector
+      gradient.clear();
+      cgrad* asl_variables_tmp = this->asl->i.Cgrad_[constraint_index];
+      size_t sparse_asl_index = 0;
+      while (asl_variables_tmp != nullptr) {
+         const size_t variable_index = static_cast<size_t>(asl_variables_tmp->varno);
+         gradient.insert(variable_index, this->asl_gradient[sparse_asl_index]);
+         asl_variables_tmp = asl_variables_tmp->next;
+         sparse_asl_index++;
+      }
+   }
+
+   void AMPLModel::evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const {
+      for (size_t constraint_index: Range(this->number_constraints)) {
+         constraint_jacobian[constraint_index].clear();
+         this->evaluate_constraint_gradient(x, constraint_index, constraint_jacobian[constraint_index]);
+      }
+   }
+
+   void AMPLModel::set_number_hessian_nonzeros() {
+      // compute the maximum number of nonzero elements, provided that all multipliers are non-zero
+      // int (*Sphset) (ASL*, SputInfo**, int nobj, int ow, int y, int uptri);
+      const int objective_number = -1;
+      const int upper_triangular = 1;
+      this->number_asl_hessian_nonzeros = static_cast<size_t>((*(this->asl)->p.Sphset)(this->asl, nullptr, objective_number, 1, 1, upper_triangular));
+      this->asl_hessian.reserve(this->number_asl_hessian_nonzeros);
+
+      // use Lagrangian scale: in AMPL, the Lagrangian is f + lambda.g, while Uno uses f - lambda.g
+      int error_flag{};
+      lagscale_ASL(this->asl, -1., &error_flag);
+   }
+
+   size_t AMPLModel::number_objective_gradient_nonzeros() const {
+      return static_cast<size_t>(this->asl->i.nzo_);
+   }
+
+   size_t AMPLModel::number_jacobian_nonzeros() const {
+      return static_cast<size_t>(this->asl->i.nzc_);
+   }
+
+   size_t AMPLModel::number_hessian_nonzeros() const {
+      return this->number_asl_hessian_nonzeros;
+   }
+
+   const Collection<size_t>& AMPLModel::get_equality_constraints() const {
+      return this->equality_constraints_collection;
+   }
+
+   const Collection<size_t>& AMPLModel::get_inequality_constraints() const {
+      return this->inequality_constraints_collection;
+   }
+
+   const std::vector<size_t>& AMPLModel::get_linear_constraints() const {
+      return this->linear_constraints;
+   }
+
+   const SparseVector<size_t>& AMPLModel::get_slacks() const {
+      return this->slacks;
+   }
+
+   const Collection<size_t>& AMPLModel::get_single_lower_bounded_variables() const {
+      return this->single_lower_bounded_variables_collection;
+   }
+
+   const Collection<size_t>& AMPLModel::get_single_upper_bounded_variables() const {
+      return this->single_upper_bounded_variables_collection;
+   }
+
+   const Collection<size_t>& AMPLModel::get_lower_bounded_variables() const {
+      return this->lower_bounded_variables_collection;
+   }
+
+   const Collection<size_t>& AMPLModel::get_upper_bounded_variables() const {
+      return this->upper_bounded_variables_collection;
+   }
+
+   bool are_all_zeros(const Vector<double>& multipliers) {
+      return std::all_of(multipliers.begin(), multipliers.end(), [](double xj) {
+         return xj == 0.;
+      });
+   }
+
+   size_t AMPLModel::compute_hessian_number_nonzeros(double objective_multiplier, const Vector<double>& multipliers) const {
+      // compute the sparsity
+      const int objective_number = -1;
+      const int upper_triangular = 1;
+      const bool all_zeros_multipliers = are_all_zeros(multipliers);
+      int number_nonzeros = (*(this->asl)->p.Sphset)(this->asl, nullptr, objective_number, (objective_multiplier != 0.),
+            not all_zeros_multipliers, upper_triangular);
+      return static_cast<size_t>(number_nonzeros);
+   }
+
+   void AMPLModel::evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
+         SymmetricMatrix<size_t, double>& hessian) const {
+      // register the vector of variables
+      (*(this->asl)->p.Xknown)(this->asl, const_cast<double*>(x.data()), nullptr);
+
+      // scale by the objective sign
+      objective_multiplier *= this->objective_sign;
+
+      // compute the number of nonzeros
+      [[maybe_unused]] const size_t number_nonzeros = this->fixed_hessian_sparsity ? this->number_asl_hessian_nonzeros :
+                                                      this->compute_hessian_number_nonzeros(objective_multiplier, multipliers);
+      assert(hessian.capacity >= number_nonzeros);
+
+      // evaluate the Hessian: store the matrix in a preallocated array this->asl_hessian
+      const int objective_number = -1;
+      if (this->fixed_hessian_sparsity) {
+         (*(this->asl)->p.Sphes)(this->asl, nullptr, const_cast<double*>(this->asl_hessian.data()), objective_number, &objective_multiplier,
+               const_cast<double*>(multipliers.data()));
+      }
+      else {
+         double* objective_multiplier_pointer = (objective_multiplier != 0.) ? &objective_multiplier : nullptr;
+         const bool all_zeros_multipliers = are_all_zeros(multipliers);
+         (*(this->asl)->p.Sphes)(this->asl, nullptr, const_cast<double*>(this->asl_hessian.data()), objective_number, objective_multiplier_pointer,
+               all_zeros_multipliers ? nullptr : const_cast<double*>(multipliers.data()));
+      }
+
+      // generate the sparsity pattern in the right sparse format
+      const int* asl_column_start = this->asl->i.sputinfo_->hcolstarts;
+      const int* asl_row_index = this->asl->i.sputinfo_->hrownos;
+      // check that the column pointers are sorted in increasing order
+      // TODO throw exception
+      assert(in_increasing_order(asl_column_start, this->number_variables + 1) && "AMPLModel::evaluate_lagrangian_hessian: column starts are not ordered");
+
+      // copy the nonzeros in the Hessian
+      hessian.reset();
+      for (size_t column_index: Range(this->number_variables)) {
+         for (size_t k: Range(static_cast<size_t>(asl_column_start[column_index]), static_cast<size_t>(asl_column_start[column_index + 1]))) {
+            const size_t row_index = static_cast<size_t>(asl_row_index[k]);
+            const double entry = this->asl_hessian[k];
+            hessian.insert(entry, row_index, column_index);
+         }
+         hessian.finalize_column(column_index);
+      }
+      // unregister the vector of variables
+      this->asl->i.x_known = 0;
+   }
+
+   double AMPLModel::variable_lower_bound(size_t variable_index) const {
+      return this->variable_lower_bounds[variable_index];
+   }
+
+   double AMPLModel::variable_upper_bound(size_t variable_index) const {
+      return this->variable_upper_bounds[variable_index];
+   }
+
+   BoundType AMPLModel::get_variable_bound_type(size_t variable_index) const {
+      return this->variable_status[variable_index];
+   }
+
+   double AMPLModel::constraint_lower_bound(size_t constraint_index) const {
+      return this->constraint_lower_bounds[constraint_index];
+   }
+
+   double AMPLModel::constraint_upper_bound(size_t constraint_index) const {
+      return this->constraint_upper_bounds[constraint_index];
+   }
+
+   FunctionType AMPLModel::get_constraint_type(size_t constraint_index) const {
+      return this->constraint_type[constraint_index];
+   }
+
+   BoundType AMPLModel::get_constraint_bound_type(size_t constraint_index) const {
+      return this->constraint_status[constraint_index];
+   }
+
+   // initial primal point
+   void AMPLModel::initial_primal_point(Vector<double>& x) const {
+      assert(x.size() >= this->number_variables);
+      std::copy(this->asl->i.X0_, this->asl->i.X0_ + this->number_variables, x.begin());
+   }
+
+   // initial dual point
+   void AMPLModel::initial_dual_point(Vector<double>& multipliers) const {
+      assert(multipliers.size() >= this->number_constraints);
+      std::copy(this->asl->i.pi0_, this->asl->i.pi0_ + this->number_constraints, multipliers.begin());
+   }
+
+   void AMPLModel::postprocess_solution(Iterate& /*iterate*/, TerminationStatus /*termination_status*/) const {
+      // do nothing
+   }
+
+   void AMPLModel::generate_constraints() {
+      for (size_t constraint_index: Range(this->number_constraints)) {
+         this->constraint_lower_bounds[constraint_index] = (this->asl->i.LUrhs_ != nullptr) ? this->asl->i.LUrhs_[2*constraint_index] : -INF<double>;
+         this->constraint_upper_bounds[constraint_index] = (this->asl->i.LUrhs_ != nullptr) ? this->asl->i.LUrhs_[2*constraint_index + 1] : INF<double>;
+      }
+      AMPLModel::determine_bounds_types(this->constraint_lower_bounds, this->constraint_upper_bounds, this->constraint_status);
+
+      // partition equality and inequality constraints
+      for (size_t constraint_index: Range(this->number_constraints)) {
+         if (this->get_constraint_bound_type(constraint_index) == EQUAL_BOUNDS) {
+            this->equality_constraints.push_back(constraint_index);
+         }
+         else {
+            this->inequality_constraints.push_back(constraint_index);
+         }
+      }
+
+      // AMPL orders the constraints based on the function type: nonlinear first, then linear
+      const size_t number_nonlinear_constraints = static_cast<size_t>(this->asl->i.nlc_);
+      for (size_t constraint_index: Range(number_nonlinear_constraints)) {
+         this->constraint_type[constraint_index] = NONLINEAR;
+      }
+      for (size_t constraint_index: Range(number_nonlinear_constraints, this->number_constraints)) {
+         this->constraint_type[constraint_index] = LINEAR;
+         this->linear_constraints.push_back(constraint_index);
+      }
+   }
+
+   void AMPLModel::determine_bounds_types(const std::vector<double>& lower_bounds, const std::vector<double>& upper_bounds, std::vector<BoundType>& status) {
+      assert(lower_bounds.size() == status.size());
+      assert(upper_bounds.size() == status.size());
+      // build the "status" vector as a mapping (map/transform operation) of the "bounds" vector
+      for (size_t index: Range(lower_bounds.size())) {
+         if (lower_bounds[index] == upper_bounds[index]) {
+            status[index] = EQUAL_BOUNDS;
+         }
+         else if (is_finite(lower_bounds[index]) && is_finite(upper_bounds[index])) {
+            status[index] = BOUNDED_BOTH_SIDES;
+         }
+         else if (is_finite(lower_bounds[index])) {
+            status[index] = BOUNDED_LOWER;
+         }
+         else if (is_finite(upper_bounds[index])) {
+            status[index] = BOUNDED_UPPER;
+         }
+         else {
+            status[index] = UNBOUNDED;
+         }
+      }
+   }
+} // namespace
diff --git a/bindings/AMPL/AMPLModel.hpp b/bindings/AMPL/AMPLModel.hpp
new file mode 100644
index 0000000..ebfdbc3
--- /dev/null
+++ b/bindings/AMPL/AMPLModel.hpp
@@ -0,0 +1,119 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_AMPLMODEL_H
+#define UNO_AMPLMODEL_H
+
+#include <vector>
+#include "model/Model.hpp"
+#include "linear_algebra/SparseVector.hpp"
+#include "symbolic/CollectionAdapter.hpp"
+
+// include AMPL Solver Library (ASL)
+extern "C" {
+#include "asl_pfgh.h"
+#include "getstub.h"
+}
+
+namespace uno {
+   /*! \class AMPLModel
+    * \brief AMPL model
+    *
+    *  Description of an AMPL model
+    */
+   class AMPLModel: public Model {
+   public:
+      explicit AMPLModel(const std::string& file_name);
+      ~AMPLModel() override;
+
+      [[nodiscard]] double evaluate_objective(const Vector<double>& x) const override;
+      void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const override;
+      void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const override;
+      void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const override;
+      void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const override;
+      void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
+            SymmetricMatrix<size_t, double>& hessian) const override;
+
+      [[nodiscard]] double variable_lower_bound(size_t variable_index) const override;
+      [[nodiscard]] double variable_upper_bound(size_t variable_index) const override;
+      [[nodiscard]] BoundType get_variable_bound_type(size_t variable_index) const override;
+      [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override;
+      [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override;
+      [[nodiscard]] const SparseVector<size_t>& get_slacks() const override;
+      [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override;
+      [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override;
+
+      [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override;
+      [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override;
+      [[nodiscard]] FunctionType get_constraint_type(size_t constraint_index) const override;
+      [[nodiscard]] BoundType get_constraint_bound_type(size_t constraint_index) const override;
+      [[nodiscard]] const Collection<size_t>& get_equality_constraints() const override;
+      [[nodiscard]] const Collection<size_t>& get_inequality_constraints() const override;
+      [[nodiscard]] const std::vector<size_t>& get_linear_constraints() const override;
+
+      void initial_primal_point(Vector<double>& x) const override;
+      void initial_dual_point(Vector<double>& multipliers) const override;
+      void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const override;
+
+      [[nodiscard]] size_t number_objective_gradient_nonzeros() const override;
+      [[nodiscard]] size_t number_jacobian_nonzeros() const override;
+      [[nodiscard]] size_t number_hessian_nonzeros() const override;
+
+   private:
+      // private constructor to pass the dimensions to the Model base constructor
+      AMPLModel(const std::string& file_name, ASL* asl);
+
+      // mutable: can be modified by const methods (internal state not seen by user)
+      mutable ASL* asl; /*!< Instance of the AMPL Solver Library class */
+      mutable std::vector<double> asl_gradient{};
+      mutable std::vector<double> asl_hessian{};
+      size_t number_asl_hessian_nonzeros{0}; /*!< Number of nonzero elements in the Hessian */
+
+      std::vector<double> variable_lower_bounds;
+      std::vector<double> variable_upper_bounds;
+      std::vector<double> constraint_lower_bounds;
+      std::vector<double> constraint_upper_bounds;
+      std::vector<BoundType> variable_status; /*!< Status of the variables (EQUALITY, BOUNDED_LOWER, BOUNDED_UPPER, BOUNDED_BOTH_SIDES) */
+      std::vector<FunctionType> constraint_type; /*!< Types of the constraints (LINEAR, QUADRATIC, NONLINEAR) */
+      std::vector<BoundType> constraint_status; /*!< Status of the constraints (EQUAL_BOUNDS, BOUNDED_LOWER, BOUNDED_UPPER, BOUNDED_BOTH_SIDES,
+    * UNBOUNDED) */
+      std::vector<size_t> linear_constraints;
+
+      // lists of variables and constraints + corresponding collection objects
+      std::vector<size_t> equality_constraints{};
+      CollectionAdapter<std::vector<size_t>&> equality_constraints_collection;
+      std::vector<size_t> inequality_constraints{};
+      CollectionAdapter<std::vector<size_t>&> inequality_constraints_collection;
+      SparseVector<size_t> slacks{};
+      std::vector<size_t> lower_bounded_variables;
+      CollectionAdapter<std::vector<size_t>&> lower_bounded_variables_collection;
+      std::vector<size_t> upper_bounded_variables;
+      CollectionAdapter<std::vector<size_t>&> upper_bounded_variables_collection;
+      std::vector<size_t> single_lower_bounded_variables{}; // indices of the single lower-bounded variables
+      CollectionAdapter<std::vector<size_t>&> single_lower_bounded_variables_collection;
+      std::vector<size_t> single_upper_bounded_variables{}; // indices of the single upper-bounded variables
+      CollectionAdapter<std::vector<size_t>&> single_upper_bounded_variables_collection;
+
+      void generate_variables();
+      void generate_constraints();
+
+      void set_number_hessian_nonzeros();
+      [[nodiscard]] size_t compute_hessian_number_nonzeros(double objective_multiplier, const Vector<double>& multipliers) const;
+      static void determine_bounds_types(const std::vector<double>& lower_bounds, const std::vector<double>& upper_bounds, std::vector<BoundType>& status);
+   };
+
+   // check that an array of integers is in increasing order (x[i] <= x[i+1])
+   template <typename Array>
+   bool in_increasing_order(const Array& array, size_t length) {
+      size_t index = 0;
+      while (index < length - 1) {
+         if (array[index] > array[index + 1]) {
+            return false;
+         }
+         index++;
+      }
+      return true;
+   }
+} // namespace
+
+#endif // UNO_AMPLMODEL_H
diff --git a/bindings/AMPL/uno_ampl.cpp b/bindings/AMPL/uno_ampl.cpp
new file mode 100644
index 0000000..e38900c
--- /dev/null
+++ b/bindings/AMPL/uno_ampl.cpp
@@ -0,0 +1,77 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include "ingredients/globalization_mechanism/GlobalizationMechanismFactory.hpp"
+#include "ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.hpp"
+#include "AMPLModel.hpp"
+#include "Uno.hpp"
+#include "model/ModelFactory.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Options.hpp"
+
+/*
+size_t memory_allocation_amount = 0;
+
+void* operator new(size_t size) {
+   memory_allocation_amount += size;
+   std::cout << "Memory: " << size << '\n';
+   return malloc(size);
+}
+*/
+
+namespace uno {
+   void run_uno_ampl(const std::string& model_name, const Options& options) {
+      // AMPL model
+      std::unique_ptr<Model> ampl_model = std::make_unique<AMPLModel>(model_name);
+
+      // initialize initial primal and dual points
+      Iterate initial_iterate(ampl_model->number_variables, ampl_model->number_constraints);
+      ampl_model->initial_primal_point(initial_iterate.primals);
+      ampl_model->project_onto_variable_bounds(initial_iterate.primals);
+      ampl_model->initial_dual_point(initial_iterate.multipliers.constraints);
+      initial_iterate.feasibility_multipliers.reset();
+
+      // reformulate (scale, add slacks, relax the bounds, ...) if necessary
+      std::unique_ptr<Model> model = ModelFactory::reformulate(std::move(ampl_model), initial_iterate, options);
+
+      // create the constraint relaxation strategy, the globalization mechanism and the Uno solver
+      auto constraint_relaxation_strategy = ConstraintRelaxationStrategyFactory::create(*model, options);
+      auto globalization_mechanism = GlobalizationMechanismFactory::create(*constraint_relaxation_strategy, options);
+      Uno uno = Uno(*globalization_mechanism, options);
+
+      // solve the instance
+      Result result = uno.solve(*model, initial_iterate, options);
+      Uno::print_optimization_summary(options, result);
+      // std::cout << "memory_allocation_amount = " << memory_allocation_amount << '\n';
+   }
+
+   Level Logger::level = INFO;
+} // namespace
+
+int main(int argc, char* argv[]) {
+   using namespace uno;
+   
+   if (1 < argc) {
+      // get the default options
+      Options options = Options::get_default_options("uno.options");
+      // override them with the command line arguments
+      options.get_command_line_arguments(argc, argv);
+      Logger::set_logger(options.get_string("logger"));
+
+      if (std::string(argv[1]) == "-v") {
+         Uno::print_uno_version();
+      }
+      else if (std::string(argv[1]) == "--strategies") {
+         Uno::print_available_strategies();
+      }
+      else {
+         // run Uno on the .nl file (last command line argument)
+         std::string model_name = std::string(argv[argc - 1]);
+         run_uno_ampl(model_name, options);
+      }
+   }
+   else {
+      Uno::print_uno_version();
+   }
+   return EXIT_SUCCESS;
+}
diff --git a/cmake/FindBLACS.cmake b/cmake/FindBLACS.cmake
new file mode 100644
index 0000000..73bac27
--- /dev/null
+++ b/cmake/FindBLACS.cmake
@@ -0,0 +1,31 @@
+# - Try to find BLACS
+# Once done, this will define
+#
+#  BLACS_FOUND - system has BLACS
+#  BLACS_INCLUDE_DIRS - the BLACS include directories
+#  BLACS_LIBRARY - library to link to
+
+include(LibFindMacros)
+
+# Dependencies
+# 
+
+# Use pkg-config to get hints about paths
+libfind_pkg_check_modules(BLACS_PKGCONF BLACS)
+
+
+# Only check for the library since this is a link dependency
+find_library(BLACS_LIBRARY
+  NAMES libblacs.a libblacs-openmpi.a
+  PATHS ${BLACS_PKGCONF_LIBRARY_DIRS}
+)
+
+find_library(BLACS_INIT_LIBRARY
+  NAMES libblacsCinit.a libblacsCinit-openmpi.a
+  PATHS ${BLACS_PKGCONF_LIBRARY_DIRS}
+)
+
+# Set the include dir variables and the libraries and let libfind_process do the rest.
+# NOTE: Singular variables for this library, plural for libraries this this lib depends on.
+set(BLACS_PROCESS_LIBS BLACS_LIBRARY)
+libfind_process(BLACS)
diff --git a/cmake/FindMETIS.cmake b/cmake/FindMETIS.cmake
new file mode 100644
index 0000000..ba96e00
--- /dev/null
+++ b/cmake/FindMETIS.cmake
@@ -0,0 +1,32 @@
+# - Try to find METIS
+# Once done, this will define
+#
+#  METIS_FOUND - system has METIS
+#  METIS_INCLUDE_DIRS - the METIS include directories
+#  METIS_LIBRARY - library to link to
+
+include(LibFindMacros)
+
+# Dependencies
+# 
+
+# Use pkg-config to get hints about paths
+libfind_pkg_check_modules(METIS_PKGCONF METIS)
+
+# Include dir
+find_path(METIS_INCLUDE_DIR
+  NAMES metis.h
+  PATHS ${METIS_PKGCONF_INCLUDE_DIRS}
+)
+
+# Finally the library itself
+find_library(METIS_LIBRARY
+  NAMES metis
+  PATHS ${METIS_PKGCONF_LIBRARY_DIRS}
+)
+
+# Set the include dir variables and the libraries and let libfind_process do the rest.
+# NOTE: Singular variables for this library, plural for libraries this this lib depends on.
+set(METIS_PROCESS_INCLUDES METIS_INCLUDE_DIR)
+set(METIS_PROCESS_LIBS METIS_LIBRARY)
+libfind_process(METIS)
diff --git a/cmake/FindMUMPS.cmake b/cmake/FindMUMPS.cmake
new file mode 100644
index 0000000..51cbe50
--- /dev/null
+++ b/cmake/FindMUMPS.cmake
@@ -0,0 +1,51 @@
+# - Try to find MUMPS
+# Once done, this will define
+#
+#  MUMPS_FOUND - system has MUMPS
+#  MUMPS_INCLUDE_DIRS - the MUMPS include directories
+#  MUMPS_LIBRARY - library to link to
+
+include(LibFindMacros)
+
+# Dependencies
+# 
+#libfind_package(MUMPS ScaLAPACK)
+
+# Use pkg-config to get hints about paths
+libfind_pkg_check_modules(MUMPS_PKGCONF MUMPS)
+
+# Include dir
+find_path(MUMPS_INCLUDE_DIR
+  NAMES dmumps_c.h smumps_c.h
+  PATHS ${MUMPS_PKGCONF_INCLUDE_DIRS}
+)
+
+# Finally the library itself
+find_library(MUMPS_LIBRARY
+  NAMES libdmumps.a
+  PATHS ${MUMPS_PKGCONF_LIBRARY_DIRS}
+)
+
+find_library(MUMPS_COMMON_LIBRARY 
+  NAMES libmumps_common.a
+  PATHS ${MUMPS_PKGCONF_LIBRARY_DIRS}
+)
+
+find_library(MUMPS_PORD_LIBRARY
+  NAMES libpord.a
+  PATHS ${MUMPS_PKGCONF_LIBRARY_DIRS}
+)
+
+# libseq for sequential MUMPS
+set(MUMPS_MPISEQ_DIR ${MUMPS_INCLUDE_DIR}/../libseq)
+
+find_library(MUMPS_MPISEQ_LIBRARY
+  NAMES libmpiseq.a
+  PATHS ${MUMPS_MPISEQ_DIR}
+)
+
+# Set the include dir variables and the libraries and let libfind_process do the rest.
+# NOTE: Singular variables for this library, plural for libraries this lib depends on.
+set(MUMPS_PROCESS_INCLUDES MUMPS_INCLUDE_DIR)
+set(MUMPS_PROCESS_LIBS "MUMPS_LIBRARY")
+libfind_process(MUMPS)
diff --git a/cmake/FindSCOTCH.cmake b/cmake/FindSCOTCH.cmake
new file mode 100644
index 0000000..cc01d6c
--- /dev/null
+++ b/cmake/FindSCOTCH.cmake
@@ -0,0 +1,41 @@
+# - Try to find SCOTCH
+# Once done, this will define
+#
+#  SCOTCH_FOUND - system has SCOTCH
+#  SCOTCH_INCLUDE_DIRS - the SCOTCH include directories
+#  SCOTCH_LIBRARY - library to link to
+
+include(LibFindMacros)
+
+# Dependencies
+# 
+
+# Use pkg-config to get hints about paths
+libfind_pkg_check_modules(SCOTCH_PKGCONF SCOTCH)
+
+
+# Only check for the library since this is a link dependency
+find_library(SCOTCH_LIBRARY
+  NAMES libscotch.a
+  PATHS ${SCOTCH_PKGCONF_LIBRARY_DIRS}
+)
+
+find_library(SCOTCH_ERR_LIBRARY
+  NAMES libscotcherr.a
+  PATHS ${SCOTCH_PKGCONF_LIBRARY_DIRS}
+)
+
+find_library(SCOTCH_ERR_EXIT_LIBRARY
+  NAMES libscotcherrexit.a
+  PATHS ${SCOTCH_PKGCONF_LIBRARY_DIRS}
+)
+
+find_library(SCOTCH_METIS_LIBRARY
+  NAMES libscotchmetis.a
+  PATHS ${SCOTCH_PKGCONF_LIBRARY_DIRS}
+)
+
+# Set the include dir variables and the libraries and let libfind_process do the rest.
+# NOTE: Singular variables for this library, plural for libraries this this lib depends on.
+set(SCOTCH_PROCESS_LIBS SCOTCH_LIBRARY)
+libfind_process(SCOTCH)
diff --git a/cmake/FindScaLAPACK.cmake b/cmake/FindScaLAPACK.cmake
new file mode 100644
index 0000000..4a7cef0
--- /dev/null
+++ b/cmake/FindScaLAPACK.cmake
@@ -0,0 +1,28 @@
+# - Try to find ScaLAPACK
+# Once done, this will define
+#
+#  ScaLAPACK_FOUND - system has ScaLAPACK
+#  ScaLAPACK_INCLUDE_DIRS - the ScaLAPACK include directories
+#  ScaLAPACK_LIBRARY - library to link to
+
+include(LibFindMacros)
+
+# Dependencies
+# 
+#libfind_package(BLACS ScaLAPACK)
+#libfind_package(ParMETIS ScaLAPACK)
+
+# Use pkg-config to get hints about paths
+libfind_pkg_check_modules(ScaLAPACK_PKGCONF ScaLAPACK)
+
+
+# Only check for the library since this is a link dependency
+find_library(ScaLAPACK_LIBRARY
+  NAMES libscalapack.so libscalapack-openmpi.so
+  PATHS ${ScaLAPACK_PKGCONF_LIBRARY_DIRS}
+)
+
+# Set the include dir variables and the libraries and let libfind_process do the rest.
+# NOTE: Singular variables for this library, plural for libraries this this lib depends on.
+set(ScaLAPACK_PROCESS_LIBS ScaLAPACK_LIBRARY)
+libfind_process(ScaLAPACK)
diff --git a/cmake/LibFindMacros.cmake b/cmake/LibFindMacros.cmake
new file mode 100644
index 0000000..6cadd80
--- /dev/null
+++ b/cmake/LibFindMacros.cmake
@@ -0,0 +1,103 @@
+# LibFindMacros by Tronic
+# Source: http://zi.fi/cmake/Modules/LibFindMacros.cmake
+# Referenced on CMake wiki https://gitlab.kitware.com/cmake/community/-/wikis/doc/tutorials/How-To-Find-Libraries
+# License: unknown
+
+# Works the same as find_package, but forwards the "REQUIRED" and "QUIET" arguments
+# used for the current package. For this to work, the first parameter must be the
+# prefix of the current package, then the prefix of the new package etc, which are
+# passed to find_package.
+macro (libfind_package PREFIX)
+  set (LIBFIND_PACKAGE_ARGS ${ARGN})
+  if (${PREFIX}_FIND_QUIETLY)
+    set (LIBFIND_PACKAGE_ARGS ${LIBFIND_PACKAGE_ARGS} QUIET)
+  endif (${PREFIX}_FIND_QUIETLY)
+  if (${PREFIX}_FIND_REQUIRED)
+    set (LIBFIND_PACKAGE_ARGS ${LIBFIND_PACKAGE_ARGS} REQUIRED)
+  endif (${PREFIX}_FIND_REQUIRED)
+  find_package(${LIBFIND_PACKAGE_ARGS})
+endmacro (libfind_package)
+
+# CMake developers made the UsePkgConfig system deprecated in the same release (2.6)
+# where they added pkg_check_modules. Consequently I need to support both in my scripts
+# to avoid those deprecated warnings. Here's a helper that does just that.
+# Works identically to pkg_check_modules, except that no checks are needed prior to use.
+macro (libfind_pkg_check_modules PREFIX PKGNAME)
+  if (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4)
+    include(UsePkgConfig)
+    pkgconfig(${PKGNAME} ${PREFIX}_INCLUDE_DIRS ${PREFIX}_LIBRARY_DIRS ${PREFIX}_LDFLAGS ${PREFIX}_CFLAGS)
+  else (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4)
+    find_package(PkgConfig)
+    if (PKG_CONFIG_FOUND)
+      pkg_check_modules(${PREFIX} ${PKGNAME})
+    endif (PKG_CONFIG_FOUND)
+  endif (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4)
+endmacro (libfind_pkg_check_modules)
+
+# Do the final processing once the paths have been detected.
+# If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain
+# all the variables, each of which contain one include directory.
+# Ditto for ${PREFIX}_PROCESS_LIBS and library files.
+# Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES.
+# Also handles errors in case library detection was required, etc.
+macro (libfind_process PREFIX)
+  # Skip processing if already processed during this run
+  if (NOT ${PREFIX}_FOUND)
+    # Start with the assumption that the library was found
+    set (${PREFIX}_FOUND TRUE)
+
+    # Process all includes and set _FOUND to false if any are missing
+    foreach (i ${${PREFIX}_PROCESS_INCLUDES})
+      if (${i})
+        set (${PREFIX}_INCLUDE_DIRS ${${PREFIX}_INCLUDE_DIRS} ${${i}})
+        mark_as_advanced(${i})
+      else (${i})
+        set (${PREFIX}_FOUND FALSE)
+      endif (${i})
+    endforeach (i)
+
+    # Process all libraries and set _FOUND to false if any are missing
+    foreach (i ${${PREFIX}_PROCESS_LIBS})
+      if (${i})
+        set (${PREFIX}_LIBRARIES ${${PREFIX}_LIBRARIES} ${${i}})
+        mark_as_advanced(${i})
+      else (${i})
+        set (${PREFIX}_FOUND FALSE)
+      endif (${i})
+    endforeach (i)
+
+    # Print message and/or exit on fatal error
+    if (${PREFIX}_FOUND)
+      if (NOT ${PREFIX}_FIND_QUIETLY)
+        message (STATUS "Found ${PREFIX} ${${PREFIX}_VERSION}")
+      endif (NOT ${PREFIX}_FIND_QUIETLY)
+    else (${PREFIX}_FOUND)
+      if (${PREFIX}_FIND_REQUIRED)
+        foreach (i ${${PREFIX}_PROCESS_INCLUDES} ${${PREFIX}_PROCESS_LIBS})
+          message("${i}=${${i}}")
+        endforeach (i)
+        message (FATAL_ERROR "Required library ${PREFIX} NOT FOUND.\nInstall the library (dev version) and try again. If the library is already installed, use ccmake to set the missing variables manually.")
+      endif (${PREFIX}_FIND_REQUIRED)
+    endif (${PREFIX}_FOUND)
+  endif (NOT ${PREFIX}_FOUND)
+endmacro (libfind_process)
+
+macro(libfind_library PREFIX basename)
+  set(TMP "")
+  if(MSVC80)
+    set(TMP -vc80)
+  endif(MSVC80)
+  if(MSVC90)
+    set(TMP -vc90)
+  endif(MSVC90)
+  set(${PREFIX}_LIBNAMES ${basename}${TMP})
+  if(${ARGC} GREATER 2)
+    set(${PREFIX}_LIBNAMES ${basename}${TMP}-${ARGV2})
+    string(REGEX REPLACE "\\." "_" TMP ${${PREFIX}_LIBNAMES})
+    set(${PREFIX}_LIBNAMES ${${PREFIX}_LIBNAMES} ${TMP})
+  endif(${ARGC} GREATER 2)
+  find_library(${PREFIX}_LIBRARY
+    NAMES ${${PREFIX}_LIBNAMES}
+    PATHS ${${PREFIX}_PKGCONF_LIBRARY_DIRS}
+  )
+endmacro(libfind_library)
diff --git a/uno.options b/uno.options
index 823cf8f..849ec60 100644
--- a/uno.options
+++ b/uno.options
@@ -35,12 +35,13 @@ statistics_TR_radius_column_order 10
 statistics_LS_step_length_column_order 10
 statistics_restoration_phase_column_order 20
 statistics_regularization_column_order 21
+statistics_funnel_width_column_order 25
 statistics_step_norm_column_order 31
 statistics_objective_column_order 100
-statistics_primal_infeasibility_column_order 101
-statistics_dual_infeasibility_column_order 102
-statistics_complementarity_column_order 104
-statistics_stationarity_column_order 105
+statistics_primal_feasibility_column_order 101
+statistics_dual_feasibility_column_order 102
+statistics_stationarity_column_order 104
+statistics_complementarity_column_order 105
 statistics_status_column_order 200
 
 ##### ingredients #####
@@ -104,22 +105,38 @@ linear_solver MA57
 armijo_decrease_fraction 1e-4
 armijo_tolerance 1e-9
 
+##### switching method options #####
+switching_delta 0.999
+switching_infeasibility_exponent 2
+
 ##### filter method options #####
 # filter type (standard|nonmonotone)
 filter_type standard
 
 filter_beta 0.999
 filter_gamma 0.001
-filter_delta 0.999
 filter_ubd 1e2
 filter_fact 1.25
-filter_switching_infeasibility_exponent 2
 
 filter_capacity 50
 
+# used by Waechter filter method
+filter_sufficient_infeasibility_decrease_factor 0.9
+
 # nonmonotone filter strategy
 nonmonotone_filter_number_dominated_entries 3
 
+##### funnel options #####
+# funnel options
+funnel_kappa 0.5
+funnel_beta 0.9999
+funnel_gamma 0.001
+funnel_ubd 1.0
+funnel_fact 1.5
+funnel_update_strategy 1
+
+funnel_require_acceptance_wrt_current_iterate no
+
 ##### line search options #####
 # backtracking ratio
 LS_backtracking_ratio 0.5
@@ -197,9 +214,6 @@ l1_constraint_violation_coefficient 1
 l1_small_duals_threshold 1e-10
 
 ##### feasibility restoration options #####
-# test acceptance when switching back to the optimality phase
-switch_to_optimality_requires_acceptance no
-
 # test linearized feasibility when switching back to the optimality phase
 switch_to_optimality_requires_linearized_feasibility yes
 
diff --git a/uno/Uno.cpp b/uno/Uno.cpp
index eedfd67..6839a26 100644
--- a/uno/Uno.cpp
+++ b/uno/Uno.cpp
@@ -13,149 +13,151 @@
 #include "tools/Statistics.hpp"
 #include "tools/Timer.hpp"
 
-Uno::Uno(GlobalizationMechanism& globalization_mechanism, const Options& options) :
-      globalization_mechanism(globalization_mechanism),
-      max_iterations(options.get_unsigned_int("max_iterations")),
-      time_limit(options.get_double("time_limit")) {
-}
-
-Result Uno::solve(const Model& model, Iterate& current_iterate, const Options& options) {
-   std::cout << "Problem " << model.name << '\n' << model.number_variables << " variables, " << model.number_constraints << " constraints\n\n";
-   
-   Timer timer{};
-   Statistics statistics = Uno::create_statistics(model, options);
-   // use the initial primal-dual point to initialize the strategies and generate the initial iterate
-   this->initialize(statistics, current_iterate, options);
-
-   bool termination = false;
-   current_iterate.status = TerminationStatus::NOT_OPTIMAL;
-   // allocate the trial iterate once and for all here
-   Iterate trial_iterate(current_iterate.number_variables, current_iterate.number_constraints);
-   size_t major_iterations = 0;
-   try {
-      // check for termination
-      while (not termination) {
-         major_iterations++;
+namespace uno {
+   Uno::Uno(GlobalizationMechanism& globalization_mechanism, const Options& options) :
+         globalization_mechanism(globalization_mechanism),
+         max_iterations(options.get_unsigned_int("max_iterations")),
+         time_limit(options.get_double("time_limit")) {
+   }
+
+   Result Uno::solve(const Model& model, Iterate& current_iterate, const Options& options) {
+      std::cout << "Problem " << model.name << '\n' << model.number_variables << " variables, " << model.number_constraints << " constraints\n\n";
+      
+      Timer timer{};
+      Statistics statistics = Uno::create_statistics(model, options);
+      // use the initial primal-dual point to initialize the strategies and generate the initial iterate
+      this->initialize(statistics, current_iterate, options);
+
+      bool termination = false;
+      current_iterate.status = TerminationStatus::NOT_OPTIMAL;
+      // allocate the trial iterate once and for all here
+      Iterate trial_iterate(current_iterate.number_variables, current_iterate.number_constraints);
+      size_t major_iterations = 0;
+      try {
+         // check for termination
+         while (not termination) {
+            major_iterations++;
+            statistics.start_new_line();
+            statistics.set("iter", major_iterations);
+            DEBUG << "### Outer iteration " << major_iterations << '\n';
+
+            // compute an acceptable iterate by solving a subproblem at the current point
+            this->globalization_mechanism.compute_next_iterate(statistics, model, current_iterate, trial_iterate);
+            // determine if Uno can terminate
+            termination = this->termination_criteria(trial_iterate.status, major_iterations, timer.get_duration());
+            // the trial iterate becomes the current iterate for the next iteration
+            std::swap(current_iterate, trial_iterate);
+         }
+      }
+      catch (std::exception& exception) {
          statistics.start_new_line();
-         statistics.set("iter", major_iterations);
-         DEBUG << "### Outer iteration " << major_iterations << '\n';
-
-         // compute an acceptable iterate by solving a subproblem at the current point
-         this->globalization_mechanism.compute_next_iterate(statistics, model, current_iterate, trial_iterate);
-         // determine if Uno can terminate
-         termination = this->termination_criteria(trial_iterate.status, major_iterations, timer.get_duration());
-         // copy the trial iterate into the current iterate for the next iteration
-         current_iterate = trial_iterate;
+         statistics.set("status", exception.what());
+         if (Logger::level == INFO) statistics.print_current_line();
+         DEBUG << exception.what();
       }
+      if (Logger::level == INFO) statistics.print_footer();
+
+      Uno::postprocess_iterate(model, current_iterate, current_iterate.status);
+      return this->create_result(model, current_iterate, major_iterations, timer);
    }
-   catch (std::exception& exception) {
-      statistics.start_new_line();
-      statistics.set("status", exception.what());
-      if (Logger::level == INFO) statistics.print_current_line();
-      DEBUG << exception.what();
+
+   void Uno::initialize(Statistics& statistics, Iterate& current_iterate, const Options& options) {
+      try {
+         statistics.start_new_line();
+         statistics.set("iter", 0);
+         statistics.set("status", "initial point");
+         this->globalization_mechanism.initialize(statistics, current_iterate, options);
+         options.print(true);
+         if (Logger::level == INFO) statistics.print_current_line();
+      }
+      catch (const std::exception& e) {
+         ERROR << RED << "An error occurred at the initial iterate: " << e.what() << RESET;
+         throw;
+      }
    }
-   if (Logger::level == INFO) statistics.print_footer();
-
-   Uno::postprocess_iterate(model, current_iterate, current_iterate.status);
-   return this->create_result(model, current_iterate, major_iterations, timer);
-}
-
-void Uno::initialize(Statistics& statistics, Iterate& current_iterate, const Options& options) {
-   try {
-      statistics.start_new_line();
-      statistics.set("iter", 0);
-      statistics.set("status", "initial point");
-      this->globalization_mechanism.initialize(statistics, current_iterate, options);
-      options.print(true);
-      if (Logger::level == INFO) statistics.print_current_line();
+
+   Statistics Uno::create_statistics(const Model& model, const Options& options) {
+      Statistics statistics(options);
+      statistics.add_column("iter", Statistics::int_width, options.get_int("statistics_major_column_order"));
+      statistics.add_column("step norm", Statistics::double_width - 3, options.get_int("statistics_step_norm_column_order"));
+      statistics.add_column("objective", Statistics::double_width - 4, options.get_int("statistics_objective_column_order"));
+      if (model.is_constrained()) {
+         statistics.add_column("primal feas.", Statistics::double_width - 2, options.get_int("statistics_primal_feasibility_column_order"));
+      }
+      statistics.add_column("stationarity", Statistics::double_width - 3, options.get_int("statistics_stationarity_column_order"));
+      statistics.add_column("complementarity", Statistics::double_width, options.get_int("statistics_complementarity_column_order"));
+      statistics.add_column("status", Statistics::string_width - 2, options.get_int("statistics_status_column_order"));
+      return statistics;
    }
-   catch (const std::exception& e) {
-      ERROR << RED << "An error occurred at the initial iterate: " << e.what() << RESET;
-      throw;
+
+   bool Uno::termination_criteria(TerminationStatus current_status, size_t iteration, double current_time) const {
+      return current_status != TerminationStatus::NOT_OPTIMAL || this->max_iterations <= iteration || this->time_limit <= current_time;
    }
-}
-
-Statistics Uno::create_statistics(const Model& model, const Options& options) {
-   Statistics statistics(options);
-   statistics.add_column("iter", Statistics::int_width, options.get_int("statistics_major_column_order"));
-   statistics.add_column("step norm", Statistics::double_width - 1, options.get_int("statistics_step_norm_column_order"));
-   statistics.add_column("objective", Statistics::double_width - 2, options.get_int("statistics_objective_column_order"));
-   if (model.is_constrained()) {
-      statistics.add_column("infeasibility", Statistics::double_width, options.get_int("statistics_primal_infeasibility_column_order"));
+
+   void Uno::postprocess_iterate(const Model& model, Iterate& iterate, TerminationStatus termination_status) {
+      // in case the objective was not yet evaluated, evaluate it
+      iterate.evaluate_objective(model);
+      model.postprocess_solution(iterate, termination_status);
+      DEBUG2 << "Final iterate:\n" << iterate;
    }
-   statistics.add_column("complementarity", Statistics::double_width, options.get_int("statistics_complementarity_column_order"));
-   statistics.add_column("stationarity", Statistics::double_width - 1, options.get_int("statistics_stationarity_column_order"));
-   statistics.add_column("status", Statistics::string_width, options.get_int("statistics_status_column_order"));
-   return statistics;
-}
-
-bool Uno::termination_criteria(TerminationStatus current_status, size_t iteration, double current_time) const {
-   return current_status != TerminationStatus::NOT_OPTIMAL || this->max_iterations <= iteration || this->time_limit <= current_time;
-}
-
-void Uno::postprocess_iterate(const Model& model, Iterate& iterate, TerminationStatus termination_status) {
-   // in case the objective was not yet evaluated, evaluate it
-   iterate.evaluate_objective(model);
-   model.postprocess_solution(iterate, termination_status);
-   DEBUG2 << "Final iterate:\n" << iterate;
-}
-
-Result Uno::create_result(const Model& model, Iterate& current_iterate, size_t major_iterations, const Timer& timer) {
-   const size_t number_subproblems_solved = this->globalization_mechanism.get_number_subproblems_solved();
-   const size_t number_hessian_evaluations = this->globalization_mechanism.get_hessian_evaluation_count();
-   return {std::move(current_iterate), model.number_variables, model.number_constraints, major_iterations, timer.get_duration(),
-         Iterate::number_eval_objective, Iterate::number_eval_constraints, Iterate::number_eval_objective_gradient,
-         Iterate::number_eval_jacobian, number_hessian_evaluations, number_subproblems_solved};
-}
-
-void join(const std::vector<std::string>& vector, char separator) {
-   if (not vector.empty()) {
-      std::cout << vector[0];
-      for (size_t variable_index: Range(1, vector.size())) {
-         std::cout << separator << ' ' << vector[variable_index];
+
+   Result Uno::create_result(const Model& model, Iterate& current_iterate, size_t major_iterations, const Timer& timer) {
+      const size_t number_subproblems_solved = this->globalization_mechanism.get_number_subproblems_solved();
+      const size_t number_hessian_evaluations = this->globalization_mechanism.get_hessian_evaluation_count();
+      return {std::move(current_iterate), model.number_variables, model.number_constraints, major_iterations, timer.get_duration(),
+            Iterate::number_eval_objective, Iterate::number_eval_constraints, Iterate::number_eval_objective_gradient,
+            Iterate::number_eval_jacobian, number_hessian_evaluations, number_subproblems_solved};
+   }
+
+   void join(const std::vector<std::string>& vector, char separator) {
+      if (not vector.empty()) {
+         std::cout << vector[0];
+         for (size_t variable_index: Range(1, vector.size())) {
+            std::cout << separator << ' ' << vector[variable_index];
+         }
       }
    }
-}
-
-void Uno::print_uno_version() {
-   std::cout << "Welcome in Uno 1.0\n";
-   std::cout << "To solve an AMPL model, type ./uno_ampl path_to_file/file.nl\n";
-   std::cout << "To choose a constraint relaxation strategy, use the argument -constraint_relaxation_strategy "
-                "[feasibility_restoration|l1_relaxation]\n";
-   std::cout << "To choose a subproblem method, use the argument -subproblem [QP|LP|primal_dual_interior_point]\n";
-   std::cout << "To choose a globalization mechanism, use the argument -globalization_mechanism [LS|TR]\n";
-   std::cout << "To choose a globalization strategy, use the argument -globalization_strategy "
-                "[l1_merit|fletcher_filter_method|waechter_filter_method]\n";
-   std::cout << "To choose a preset, use the argument -preset [filtersqp|ipopt|byrd]\n";
-   std::cout << "The options can be combined in the same command line. Autocompletion is possible (see README).\n";
-}
-
-void Uno::print_available_strategies() {
-   std::cout << "Available strategies:\n";
-   std::cout << "Constraint relaxation strategies: ";
-   join(ConstraintRelaxationStrategyFactory::available_strategies(), ',');
-   std::cout << '\n';
-   std::cout << "Globalization mechanisms: ";
-   join(GlobalizationMechanismFactory::available_strategies(), ',');
-   std::cout << '\n';
-   std::cout << "Globalization strategies: ";
-   join(GlobalizationStrategyFactory::available_strategies(), ',');
-   std::cout << '\n';
-   std::cout << "Subproblems: ";
-   join(SubproblemFactory::available_strategies(), ',');
-   std::cout << '\n';
-}
-
-void Uno::print_strategy_combination(const Options& options) {
-   std::string combination = options.get_string("globalization_mechanism") + " " + options.get_string("constraint_relaxation_strategy") + " " +
-                             options.get_string("globalization_strategy") + " " + options.get_string("subproblem");
-   std::cout << "\nUno (" << combination << ")\n";
-}
-
-void Uno::print_optimization_summary(const Options& options, const Result& result) {
-   Uno::print_strategy_combination(options);
-   std::cout << Timer::get_current_date();
-   std::cout << "────────────────────────────────────────\n";
-   const bool print_solution = options.get_bool("print_solution");
-   result.print(print_solution);
-}
\ No newline at end of file
+
+   void Uno::print_uno_version() {
+      std::cout << "Welcome in Uno 1.0\n";
+      std::cout << "To solve an AMPL model, type ./uno_ampl path_to_file/file.nl\n";
+      std::cout << "To choose a constraint relaxation strategy, use the argument -constraint_relaxation_strategy "
+                   "[feasibility_restoration|l1_relaxation]\n";
+      std::cout << "To choose a subproblem method, use the argument -subproblem [QP|LP|primal_dual_interior_point]\n";
+      std::cout << "To choose a globalization mechanism, use the argument -globalization_mechanism [LS|TR]\n";
+      std::cout << "To choose a globalization strategy, use the argument -globalization_strategy "
+                   "[l1_merit|fletcher_filter_method|waechter_filter_method]\n";
+      std::cout << "To choose a preset, use the argument -preset [filtersqp|ipopt|byrd]\n";
+      std::cout << "The options can be combined in the same command line. Autocompletion is possible (see README).\n";
+   }
+
+   void Uno::print_available_strategies() {
+      std::cout << "Available strategies:\n";
+      std::cout << "Constraint relaxation strategies: ";
+      join(ConstraintRelaxationStrategyFactory::available_strategies(), ',');
+      std::cout << '\n';
+      std::cout << "Globalization mechanisms: ";
+      join(GlobalizationMechanismFactory::available_strategies(), ',');
+      std::cout << '\n';
+      std::cout << "Globalization strategies: ";
+      join(GlobalizationStrategyFactory::available_strategies(), ',');
+      std::cout << '\n';
+      std::cout << "Subproblems: ";
+      join(SubproblemFactory::available_strategies(), ',');
+      std::cout << '\n';
+   }
+
+   void Uno::print_strategy_combination(const Options& options) {
+      std::string combination = options.get_string("globalization_mechanism") + " " + options.get_string("constraint_relaxation_strategy") + " " +
+                                options.get_string("globalization_strategy") + " " + options.get_string("subproblem");
+      std::cout << "\nUno (" << combination << ")\n";
+   }
+
+   void Uno::print_optimization_summary(const Options& options, const Result& result) {
+      Uno::print_strategy_combination(options);
+      std::cout << Timer::get_current_date();
+      std::cout << "────────────────────────────────────────\n";
+      const bool print_solution = options.get_bool("print_solution");
+      result.print(print_solution);
+   }
+} // namespace
diff --git a/uno/Uno.hpp b/uno/Uno.hpp
index 7c6f693..faf18de 100644
--- a/uno/Uno.hpp
+++ b/uno/Uno.hpp
@@ -7,42 +7,44 @@
 #include "optimization/Result.hpp"
 #include "optimization/TerminationStatus.hpp"
 
-// forward declarations
-class GlobalizationMechanism;
-class Model;
-class Options;
-class Statistics;
-class Timer;
-
-/*
-struct TimeOut : public std::exception {
-   [[nodiscard]] const char* what() const noexcept override {
-      return "The time limit was exceeded.\n";
-   }
-};
-*/
-
-class Uno {
-public:
-   Uno(GlobalizationMechanism& globalization_mechanism, const Options& options);
-
-   [[nodiscard]] Result solve(const Model& model, Iterate& initial_iterate, const Options& options);
-
-   static void print_uno_version();
-   static void print_available_strategies();
-   static void print_strategy_combination(const Options& options);
-   static void print_optimization_summary(const Options& options, const Result& result);
-
-private:
-   GlobalizationMechanism& globalization_mechanism; /*!< Globalization mechanism */
-   const size_t max_iterations; /*!< Maximum number of iterations */
-   const double time_limit; /*!< CPU time limit (can be inf) */
-
-   void initialize(Statistics& statistics, Iterate& current_iterate, const Options& options);
-   static Statistics create_statistics(const Model& model, const Options& options);
-   [[nodiscard]] bool termination_criteria(TerminationStatus current_status, size_t iteration, double current_time) const;
-   static void postprocess_iterate(const Model& model, Iterate& iterate, TerminationStatus termination_status);
-   Result create_result(const Model& model, Iterate& current_iterate, size_t major_iterations, const Timer& timer);
-};
+namespace uno {
+   // forward declarations
+   class GlobalizationMechanism;
+   class Model;
+   class Options;
+   class Statistics;
+   class Timer;
+
+   /*
+   struct TimeOut : public std::exception {
+      [[nodiscard]] const char* what() const noexcept override {
+         return "The time limit was exceeded.\n";
+      }
+   };
+   */
+
+   class Uno {
+   public:
+      Uno(GlobalizationMechanism& globalization_mechanism, const Options& options);
+
+      [[nodiscard]] Result solve(const Model& model, Iterate& initial_iterate, const Options& options);
+
+      static void print_uno_version();
+      static void print_available_strategies();
+      static void print_strategy_combination(const Options& options);
+      static void print_optimization_summary(const Options& options, const Result& result);
+
+   private:
+      GlobalizationMechanism& globalization_mechanism; /*!< Globalization mechanism */
+      const size_t max_iterations; /*!< Maximum number of iterations */
+      const double time_limit; /*!< CPU time limit (can be inf) */
+
+      void initialize(Statistics& statistics, Iterate& current_iterate, const Options& options);
+      static Statistics create_statistics(const Model& model, const Options& options);
+      [[nodiscard]] bool termination_criteria(TerminationStatus current_status, size_t iteration, double current_time) const;
+      static void postprocess_iterate(const Model& model, Iterate& iterate, TerminationStatus termination_status);
+      Result create_result(const Model& model, Iterate& current_iterate, size_t major_iterations, const Timer& timer);
+   };
+} // namespace
 
 #endif // UNO_H
diff --git a/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.cpp b/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.cpp
index a107d95..16afff1 100644
--- a/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.cpp
+++ b/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.cpp
@@ -2,10 +2,13 @@
 // Licensed under the MIT license. See LICENSE file in the project directory for details.
 
 #include "ConstraintRelaxationStrategy.hpp"
+#include "ingredients/globalization_strategy/GlobalizationStrategyFactory.hpp"
 #include "ingredients/subproblem/Direction.hpp"
+#include "ingredients/subproblem/SubproblemFactory.hpp"
 #include "linear_algebra/SymmetricMatrix.hpp"
 #include "model/Model.hpp"
 #include "optimization/Iterate.hpp"
+#include "optimization/LagrangianGradient.hpp"
 #include "optimization/Multipliers.hpp"
 #include "reformulation/OptimizationProblem.hpp"
 #include "symbolic/VectorView.hpp"
@@ -13,142 +16,256 @@
 #include "tools/Options.hpp"
 #include "tools/Statistics.hpp"
 
-ConstraintRelaxationStrategy::ConstraintRelaxationStrategy(const Model& model, const Options& options):
-      model(model),
-      progress_norm(norm_from_string(options.get_string("progress_norm"))),
-      residual_norm(norm_from_string(options.get_string("residual_norm"))),
-      residual_scaling_threshold(options.get_double("residual_scaling_threshold")) {
-}
-
-// infeasibility measure: constraint violation
-void ConstraintRelaxationStrategy::set_infeasibility_measure(Iterate& iterate) const {
-   iterate.evaluate_constraints(this->model);
-   iterate.progress.infeasibility = this->model.constraint_violation(iterate.evaluations.constraints, this->progress_norm);
-}
-
-// objective measure: scaled objective
-void ConstraintRelaxationStrategy::set_objective_measure(Iterate& iterate) const {
-   iterate.evaluate_objective(this->model);
-   const double objective = iterate.evaluations.objective;
-   iterate.progress.objective = [=](double objective_multiplier) {
-      return objective_multiplier * objective;
-   };
-}
-
-double ConstraintRelaxationStrategy::compute_predicted_infeasibility_reduction_model(const Iterate& current_iterate,
-      const Vector<double>& primal_direction, double step_length) const {
-   // predicted infeasibility reduction: "‖c(x)‖ - ‖c(x) + ∇c(x)^T (αd)‖"
-   const double current_constraint_violation = this->model.constraint_violation(current_iterate.evaluations.constraints, this->progress_norm);
-   const double trial_linearized_constraint_violation = this->model.constraint_violation(current_iterate.evaluations.constraints + step_length *
-         (current_iterate.evaluations.constraint_jacobian * primal_direction), this->progress_norm);
-   return current_constraint_violation - trial_linearized_constraint_violation;
-}
-
-std::function<double(double)> ConstraintRelaxationStrategy::compute_predicted_objective_reduction_model(const Iterate& current_iterate,
-      const Vector<double>& primal_direction, double step_length, const SymmetricMatrix<double>& hessian) const {
-   // predicted objective reduction: "-∇f(x)^T (αd) - α^2/2 d^T H d"
-   const double directional_derivative = dot(primal_direction, current_iterate.evaluations.objective_gradient);
-   const double quadratic_term = hessian.quadratic_product(primal_direction, primal_direction);
-   return [=](double objective_multiplier) {
-      return step_length * (-objective_multiplier*directional_derivative) - step_length*step_length/2. * quadratic_term;
-   };
-}
-
-void ConstraintRelaxationStrategy::compute_primal_dual_residuals(const OptimizationProblem& optimality_problem, const OptimizationProblem& feasibility_problem,
-      Iterate& iterate) {
-   iterate.evaluate_objective_gradient(this->model);
-   iterate.evaluate_constraints(this->model);
-   iterate.evaluate_constraint_jacobian(this->model);
-
-   // stationarity errors:
-   // - with standard multipliers and current objective multiplier (for KKT conditions)
-   // - with standard multipliers and 0 objective multiplier (for FJ conditions)
-   // - with feasibility multipliers and 0 objective multiplier (for feasibility problem)
-   this->evaluate_lagrangian_gradient(iterate, iterate.multipliers);
-   iterate.residuals.KKT_stationarity = optimality_problem.stationarity_error(iterate.lagrangian_gradient, iterate.objective_multiplier,
-         this->residual_norm);
-   iterate.residuals.FJ_stationarity = optimality_problem.stationarity_error(iterate.lagrangian_gradient, 0., this->residual_norm);
-   this->evaluate_lagrangian_gradient(iterate, iterate.feasibility_multipliers);
-   iterate.residuals.feasibility_stationarity = feasibility_problem.stationarity_error(iterate.lagrangian_gradient, 0., this->residual_norm);
-
-   // constraint violation of the original problem
-   iterate.residuals.infeasibility = this->model.constraint_violation(iterate.evaluations.constraints, this->residual_norm);
-
-   // complementarity error
-   iterate.residuals.complementarity = optimality_problem.complementarity_error(iterate.primals, iterate.evaluations.constraints,
-         iterate.multipliers, this->residual_norm);
-   iterate.residuals.feasibility_complementarity = feasibility_problem.complementarity_error(iterate.primals, iterate.evaluations.constraints,
-         iterate.feasibility_multipliers, this->residual_norm);
-
-   // scaling factors
-   iterate.residuals.stationarity_scaling = this->compute_stationarity_scaling(iterate.multipliers);
-   iterate.residuals.complementarity_scaling = this->compute_complementarity_scaling(iterate.multipliers);
-}
-
-// Lagrangian gradient split in two parts: objective contribution and constraints' contribution
-void ConstraintRelaxationStrategy::evaluate_lagrangian_gradient(Iterate& iterate, const Multipliers& multipliers) const {
-   iterate.lagrangian_gradient.objective_contribution.fill(0.);
-   iterate.lagrangian_gradient.constraints_contribution.fill(0.);
-
-   // objective gradient
-   for (auto [variable_index, derivative]: iterate.evaluations.objective_gradient) {
-      iterate.lagrangian_gradient.objective_contribution[variable_index] += derivative;
-   }
-
-   // constraints
-   for (size_t constraint_index: Range(iterate.number_constraints)) {
-      if (multipliers.constraints[constraint_index] != 0.) {
-         for (auto [variable_index, derivative]: iterate.evaluations.constraint_jacobian[constraint_index]) {
-            iterate.lagrangian_gradient.constraints_contribution[variable_index] -= multipliers.constraints[constraint_index] * derivative;
+namespace uno {
+   ConstraintRelaxationStrategy::ConstraintRelaxationStrategy(const Model& model, size_t number_variables, size_t number_constraints,
+         size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options):
+         model(model),
+         globalization_strategy(GlobalizationStrategyFactory::create(options.get_string("globalization_strategy"), options)),
+         subproblem(SubproblemFactory::create(number_variables, number_constraints, number_objective_gradient_nonzeros, number_jacobian_nonzeros,
+               number_hessian_nonzeros, options)),
+         progress_norm(norm_from_string(options.get_string("progress_norm"))),
+         residual_norm(norm_from_string(options.get_string("residual_norm"))),
+         residual_scaling_threshold(options.get_double("residual_scaling_threshold")),
+         tight_tolerance(options.get_double("tolerance")),
+         loose_tolerance(options.get_double("loose_tolerance")),
+         loose_tolerance_consecutive_iteration_threshold(options.get_unsigned_int("loose_tolerance_consecutive_iteration_threshold")),
+         unbounded_objective_threshold(options.get_double("unbounded_objective_threshold")),
+         first_order_predicted_reduction(options.get_string("globalization_mechanism") == "LS") {
+   }
+
+   void ConstraintRelaxationStrategy::set_trust_region_radius(double trust_region_radius) {
+      this->subproblem->set_trust_region_radius(trust_region_radius);
+   }
+
+   // with initial point
+   void ConstraintRelaxationStrategy::compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+         const Vector<double>& initial_point, WarmstartInformation& warmstart_information) {
+      this->subproblem->set_initial_point(initial_point);
+      this->compute_feasible_direction(statistics, current_iterate, direction, warmstart_information);
+   }
+
+   // infeasibility measure: constraint violation
+   void ConstraintRelaxationStrategy::set_infeasibility_measure(Iterate& iterate) const {
+      iterate.evaluate_constraints(this->model);
+      iterate.progress.infeasibility = this->model.constraint_violation(iterate.evaluations.constraints, this->progress_norm);
+   }
+
+   // objective measure: scaled objective
+   void ConstraintRelaxationStrategy::set_objective_measure(Iterate& iterate) const {
+      iterate.evaluate_objective(this->model);
+      const double objective = iterate.evaluations.objective;
+      iterate.progress.objective = [=](double objective_multiplier) {
+         return objective_multiplier * objective;
+      };
+   }
+
+   double ConstraintRelaxationStrategy::compute_predicted_infeasibility_reduction_model(const Iterate& current_iterate,
+         const Vector<double>& primal_direction, double step_length) const {
+      // predicted infeasibility reduction: "‖c(x)‖ - ‖c(x) + ∇c(x)^T (αd)‖"
+      const double current_constraint_violation = this->model.constraint_violation(current_iterate.evaluations.constraints, this->progress_norm);
+      const double trial_linearized_constraint_violation = this->model.constraint_violation(current_iterate.evaluations.constraints + step_length *
+                                                                                                                                      (current_iterate.evaluations.constraint_jacobian * primal_direction), this->progress_norm);
+      return current_constraint_violation - trial_linearized_constraint_violation;
+   }
+
+   std::function<double(double)> ConstraintRelaxationStrategy::compute_predicted_objective_reduction_model(const Iterate& current_iterate,
+         const Vector<double>& primal_direction, double step_length, const SymmetricMatrix<size_t, double>& hessian) const {
+      // predicted objective reduction: "-∇f(x)^T (αd) - α^2/2 d^T H d"
+      const double directional_derivative = dot(primal_direction, current_iterate.evaluations.objective_gradient);
+      const double quadratic_term = hessian.quadratic_product(primal_direction, primal_direction);
+      return [=](double objective_multiplier) {
+         return step_length * (-objective_multiplier*directional_derivative) - step_length*step_length/2. * quadratic_term;
+      };
+   }
+
+   std::function<double(double)> ConstraintRelaxationStrategy::compute_predicted_objective_reduction_model(const Iterate& current_iterate,
+         const Vector<double>& primal_direction, double step_length) const {
+      // predicted objective reduction: "-∇f(x)^T (αd)"
+      const double directional_derivative = dot(primal_direction, current_iterate.evaluations.objective_gradient);
+      return [=](double objective_multiplier) {
+         return step_length * (-objective_multiplier*directional_derivative) ;
+      };
+   }
+
+   void ConstraintRelaxationStrategy::compute_progress_measures(Iterate& current_iterate, Iterate& trial_iterate) {
+      if (this->subproblem->subproblem_definition_changed) {
+         DEBUG << "The subproblem definition changed, the globalization strategy is reset and the auxiliary measure is recomputed\n";
+         this->globalization_strategy->reset();
+         this->subproblem->set_auxiliary_measure(this->model, current_iterate);
+         this->subproblem->subproblem_definition_changed = false;
+      }
+      this->evaluate_progress_measures(trial_iterate);
+   }
+
+   void ConstraintRelaxationStrategy::compute_primal_dual_residuals(const OptimizationProblem& optimality_problem, const OptimizationProblem& feasibility_problem,
+         Iterate& iterate) {
+      iterate.evaluate_objective_gradient(this->model);
+      iterate.evaluate_constraints(this->model);
+      iterate.evaluate_constraint_jacobian(this->model);
+
+      // stationarity errors:
+      // - for KKT conditions: with standard multipliers and current objective multiplier
+      // - for FJ conditions: with standard multipliers and 0 objective multiplier
+      // - for feasibility problem: with feasibility multipliers and 0 objective multiplier
+      this->evaluate_lagrangian_gradient(iterate.residuals.lagrangian_gradient, iterate, iterate.multipliers);
+      iterate.residuals.stationarity = OptimizationProblem::stationarity_error(iterate.residuals.lagrangian_gradient, iterate.objective_multiplier,
+            this->residual_norm);
+      this->evaluate_lagrangian_gradient(iterate.feasibility_residuals.lagrangian_gradient, iterate, iterate.feasibility_multipliers);
+      iterate.feasibility_residuals.stationarity = OptimizationProblem::stationarity_error(iterate.feasibility_residuals.lagrangian_gradient, 0.,
+            this->residual_norm);
+
+      // constraint violation of the original problem
+      iterate.residuals.primal_feasibility = this->model.constraint_violation(iterate.evaluations.constraints, this->residual_norm);
+
+      // complementarity error
+      const double shift_value = 0.;
+      iterate.residuals.complementarity = optimality_problem.complementarity_error(iterate.primals, iterate.evaluations.constraints,
+            iterate.multipliers, shift_value, this->residual_norm);
+      iterate.feasibility_residuals.complementarity = feasibility_problem.complementarity_error(iterate.primals, iterate.evaluations.constraints,
+            iterate.feasibility_multipliers, shift_value, this->residual_norm);
+
+      // scaling factors
+      iterate.residuals.stationarity_scaling = this->compute_stationarity_scaling(iterate.multipliers);
+      iterate.residuals.complementarity_scaling = this->compute_complementarity_scaling(iterate.multipliers);
+      iterate.feasibility_residuals.stationarity_scaling = this->compute_stationarity_scaling(iterate.feasibility_multipliers);
+      iterate.feasibility_residuals.complementarity_scaling = this->compute_complementarity_scaling(iterate.feasibility_multipliers);
+   }
+
+   // Lagrangian gradient split in two parts: objective contribution and constraints' contribution
+   void ConstraintRelaxationStrategy::evaluate_lagrangian_gradient(LagrangianGradient<double>& lagrangian_gradient, Iterate& iterate,
+         const Multipliers& multipliers) const {
+      lagrangian_gradient.objective_contribution.fill(0.);
+      lagrangian_gradient.constraints_contribution.fill(0.);
+
+      // objective gradient
+      for (auto [variable_index, derivative]: iterate.evaluations.objective_gradient) {
+         lagrangian_gradient.objective_contribution[variable_index] += derivative;
+      }
+
+      // constraints
+      for (size_t constraint_index: Range(iterate.number_constraints)) {
+         if (multipliers.constraints[constraint_index] != 0.) {
+            for (auto [variable_index, derivative]: iterate.evaluations.constraint_jacobian[constraint_index]) {
+               lagrangian_gradient.constraints_contribution[variable_index] -= multipliers.constraints[constraint_index] * derivative;
+            }
          }
       }
+
+      // bound constraints
+      for (size_t variable_index: Range(this->model.number_variables)) {
+         lagrangian_gradient.constraints_contribution[variable_index] -= multipliers.lower_bounds[variable_index] + multipliers.upper_bounds[variable_index];
+      }
    }
 
-   // bound constraints
-   for (size_t variable_index: Range(this->model.number_variables)) {
-      iterate.lagrangian_gradient.constraints_contribution[variable_index] -= multipliers.lower_bounds[variable_index] + multipliers.upper_bounds[variable_index];
+   double ConstraintRelaxationStrategy::compute_stationarity_scaling(const Multipliers& multipliers) const {
+      const size_t total_size = this->model.get_lower_bounded_variables().size() + this->model.get_upper_bounded_variables().size() + this->model.number_constraints;
+      if (total_size == 0) {
+         return 1.;
+      }
+      else {
+         const double scaling_factor = this->residual_scaling_threshold * static_cast<double>(total_size);
+         const double multiplier_norm = norm_1(
+               view(multipliers.constraints, 0, this->model.number_constraints),
+               view(multipliers.lower_bounds, 0, this->model.number_variables),
+               view(multipliers.upper_bounds, 0, this->model.number_variables)
+         );
+         return std::max(1., multiplier_norm / scaling_factor);
+      }
    }
-}
 
-double ConstraintRelaxationStrategy::compute_stationarity_scaling(const Multipliers& multipliers) const {
-   const size_t total_size = this->model.get_lower_bounded_variables().size() + this->model.get_upper_bounded_variables().size() + this->model.number_constraints;
-   if (total_size == 0) {
-      return 1.;
+   double ConstraintRelaxationStrategy::compute_complementarity_scaling(const Multipliers& multipliers) const {
+      const size_t total_size = this->model.get_lower_bounded_variables().size() + this->model.get_upper_bounded_variables().size();
+      if (total_size == 0) {
+         return 1.;
+      }
+      else {
+         const double scaling_factor = this->residual_scaling_threshold * static_cast<double>(total_size);
+         const double bound_multiplier_norm = norm_1(
+               view(multipliers.lower_bounds, 0, this->model.number_variables),
+               view(multipliers.upper_bounds, 0, this->model.number_variables)
+         );
+         return std::max(1., bound_multiplier_norm / scaling_factor);
+      }
+   }
+
+   TerminationStatus ConstraintRelaxationStrategy::check_termination(Iterate& iterate) {
+      if (iterate.is_objective_computed && iterate.evaluations.objective < this->unbounded_objective_threshold) {
+         return TerminationStatus::UNBOUNDED;
+      }
+
+      // compute the residuals
+      this->compute_primal_dual_residuals(iterate);
+
+      // test convergence wrt the tight tolerance
+      const TerminationStatus status_tight_tolerance = this->check_first_order_convergence(iterate, this->tight_tolerance);
+      if (status_tight_tolerance != TerminationStatus::NOT_OPTIMAL || this->loose_tolerance <= this->tight_tolerance) {
+         return status_tight_tolerance;
+      }
+
+      // if not converged, check convergence wrt loose tolerance (provided it is strictly looser than the tight tolerance)
+      const TerminationStatus status_loose_tolerance = this->check_first_order_convergence(iterate, this->loose_tolerance);
+      // if converged, keep track of the number of consecutive iterations
+      if (status_loose_tolerance != TerminationStatus::NOT_OPTIMAL) {
+         this->loose_tolerance_consecutive_iterations++;
+      }
+      else {
+         this->loose_tolerance_consecutive_iterations = 0;
+         return TerminationStatus::NOT_OPTIMAL;
+      }
+      // check if loose tolerance achieved for enough consecutive iterations
+      if (this->loose_tolerance_consecutive_iteration_threshold <= this->loose_tolerance_consecutive_iterations) {
+         return status_loose_tolerance;
+      }
+      else {
+         return TerminationStatus::NOT_OPTIMAL;
+      }
    }
-   else {
-      const double scaling_factor = this->residual_scaling_threshold * static_cast<double>(total_size);
-      const double multiplier_norm = norm_1(
-            view(multipliers.constraints, 0, this->model.number_constraints),
-            view(multipliers.lower_bounds, 0, this->model.number_variables),
-            view(multipliers.upper_bounds, 0, this->model.number_variables)
-      );
-      return std::max(1., multiplier_norm / scaling_factor);
+
+   TerminationStatus ConstraintRelaxationStrategy::check_first_order_convergence(Iterate& current_iterate, double tolerance) const {
+      // evaluate termination conditions based on optimality conditions
+      const bool KKT_stationarity = (current_iterate.residuals.stationarity / current_iterate.residuals.stationarity_scaling <= tolerance);
+      const bool feasibility_stationarity = (current_iterate.feasibility_residuals.stationarity <= tolerance);
+      const bool complementarity = (current_iterate.residuals.complementarity / current_iterate.residuals.complementarity_scaling <= tolerance);
+      const bool feasibility_complementarity = (current_iterate.feasibility_residuals.complementarity <= tolerance);
+      const bool primal_feasibility = (current_iterate.residuals.primal_feasibility <= tolerance);
+      const bool no_trivial_duals = current_iterate.multipliers.not_all_zero(this->model.number_variables, tolerance);
+
+      DEBUG << "\nTermination criteria for tolerance = " << tolerance << ":\n";
+      DEBUG << "KKT stationarity: " << std::boolalpha << KKT_stationarity << '\n';
+      DEBUG << "Stationarity (feasibility): " << std::boolalpha << feasibility_stationarity << '\n';
+      DEBUG << "Complementarity: " << std::boolalpha << complementarity << '\n';
+      DEBUG << "Complementarity (feasibility): " << std::boolalpha << feasibility_complementarity << '\n';
+      DEBUG << "Primal feasibility: " << std::boolalpha << primal_feasibility << '\n';
+      DEBUG << "Not all zero multipliers: " << std::boolalpha << no_trivial_duals << "\n\n";
+
+      if (KKT_stationarity && primal_feasibility && 0. < current_iterate.objective_multiplier && complementarity) {
+         // feasible regular stationary point
+         return TerminationStatus::FEASIBLE_KKT_POINT;
+      }
+      else if (this->model.is_constrained() && feasibility_stationarity && not primal_feasibility && feasibility_complementarity) {
+         // no primal feasibility, stationary point of constraint violation
+         return TerminationStatus::INFEASIBLE_STATIONARY_POINT;
+      }
+      return TerminationStatus::NOT_OPTIMAL;
    }
-}
 
-double ConstraintRelaxationStrategy::compute_complementarity_scaling(const Multipliers& multipliers) const {
-   const size_t total_size = this->model.get_lower_bounded_variables().size() + this->model.get_upper_bounded_variables().size();
-   if (total_size == 0) {
-      return 1.;
+   void ConstraintRelaxationStrategy::set_statistics(Statistics& statistics, const Iterate& iterate) const {
+      this->set_progress_statistics(statistics, iterate);
+      this->set_dual_residuals_statistics(statistics, iterate);
    }
-   else {
-      const double scaling_factor = this->residual_scaling_threshold * static_cast<double>(total_size);
-      const double bound_multiplier_norm = norm_1(
-            view(multipliers.lower_bounds, 0, this->model.number_variables),
-            view(multipliers.upper_bounds, 0, this->model.number_variables)
-      );
-      return std::max(1., bound_multiplier_norm / scaling_factor);
+
+   void ConstraintRelaxationStrategy::set_progress_statistics(Statistics& statistics, const Iterate& iterate) const {
+      statistics.set("objective", iterate.evaluations.objective);
+      if (this->model.is_constrained()) {
+         statistics.set("primal feas.", iterate.progress.infeasibility);
+      }
    }
-}
 
-void ConstraintRelaxationStrategy::set_statistics(Statistics& statistics, const Iterate& iterate) const {
-   this->set_progress_statistics(statistics, iterate);
-   this->set_dual_residuals_statistics(statistics, iterate);
-}
+   size_t ConstraintRelaxationStrategy::get_hessian_evaluation_count() const {
+      return this->subproblem->get_hessian_evaluation_count();
+   }
 
-void ConstraintRelaxationStrategy::set_progress_statistics(Statistics& statistics, const Iterate& iterate) const {
-   statistics.set("objective", iterate.evaluations.objective);
-   if (this->model.is_constrained()) {
-      statistics.set("infeasibility", iterate.progress.infeasibility);
+   size_t ConstraintRelaxationStrategy::get_number_subproblems_solved() const {
+      return this->subproblem->number_subproblems_solved;
    }
-}
+} // namespace
\ No newline at end of file
diff --git a/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.hpp b/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.hpp
index 9899b31..45253a9 100644
--- a/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.hpp
+++ b/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.hpp
@@ -5,74 +5,98 @@
 #define UNO_CONSTRAINTRELAXATIONSTRATEGY_H
 
 #include <cstddef>
+#include <memory>
+#include "ingredients/globalization_strategy/GlobalizationStrategy.hpp"
+#include "ingredients/subproblem/Subproblem.hpp"
 #include "linear_algebra/Norm.hpp"
-
-// forward declarations
-class Direction;
-class Iterate;
-class Model;
-struct Multipliers;
-class OptimizationProblem;
-class Options;
-class Statistics;
-template <typename ElementType>
-class SymmetricMatrix;
-template <typename ElementType>
-class Vector;
-class WarmstartInformation;
-
-class ConstraintRelaxationStrategy {
-public:
-   ConstraintRelaxationStrategy(const Model& model, const Options& options);
-   virtual ~ConstraintRelaxationStrategy() = default;
-
-   virtual void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) = 0;
-   virtual void set_trust_region_radius(double trust_region_radius) = 0;
-
-   [[nodiscard]] virtual size_t maximum_number_variables() const = 0;
-   [[nodiscard]] virtual size_t maximum_number_constraints() const = 0;
-
-   // direction computation
-   virtual void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-         WarmstartInformation& warmstart_information) = 0;
-   virtual void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-         const Vector<double>& initial_point, WarmstartInformation& warmstart_information) = 0;
-   [[nodiscard]] virtual bool solving_feasibility_problem() const = 0;
-   virtual void switch_to_feasibility_problem(Statistics& statistics, Iterate& current_iterate) = 0;
-
-   // trial iterate acceptance
-   virtual void compute_progress_measures(Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction, double step_length) = 0;
-   [[nodiscard]] virtual bool is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
-         double step_length) = 0;
-
-   // primal-dual residuals
-   virtual void compute_primal_dual_residuals(Iterate& iterate) = 0;
-
-   [[nodiscard]] virtual size_t get_hessian_evaluation_count() const = 0;
-   [[nodiscard]] virtual size_t get_number_subproblems_solved() const = 0;
-
-protected:
-   const Model& model;
-   const Norm progress_norm;
-   const Norm residual_norm;
-   const double residual_scaling_threshold;
-
-   void set_objective_measure(Iterate& iterate) const;
-   void set_infeasibility_measure(Iterate& iterate) const;
-   [[nodiscard]] double compute_predicted_infeasibility_reduction_model(const Iterate& current_iterate, const Vector<double>& primal_direction,
-         double step_length) const;
-   [[nodiscard]] std::function<double(double)> compute_predicted_objective_reduction_model(const Iterate& current_iterate,
-         const Vector<double>& primal_direction, double step_length, const SymmetricMatrix<double>& hessian) const;
-
-   void compute_primal_dual_residuals(const OptimizationProblem& optimality_problem, const OptimizationProblem& feasibility_problem, Iterate& iterate);
-   void evaluate_lagrangian_gradient(Iterate& iterate, const Multipliers& multipliers) const;
-
-   [[nodiscard]] double compute_stationarity_scaling(const Multipliers& multipliers) const;
-   [[nodiscard]] double compute_complementarity_scaling(const Multipliers& multipliers) const;
-
-   void set_statistics(Statistics& statistics, const Iterate& iterate) const;
-   void set_progress_statistics(Statistics& statistics, const Iterate& iterate) const;
-   virtual void set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const = 0;
-};
-
-#endif //UNO_CONSTRAINTRELAXATIONSTRATEGY_H
+#include "optimization/TerminationStatus.hpp"
+
+namespace uno {
+   // forward declarations
+   class Direction;
+   class Iterate;
+   template <typename ElementType>
+   class LagrangianGradient;
+   class Model;
+   struct Multipliers;
+   class OptimizationProblem;
+   class Options;
+   class Statistics;
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrix;
+   template <typename ElementType>
+   class Vector;
+   struct WarmstartInformation;
+
+   class ConstraintRelaxationStrategy {
+   public:
+      ConstraintRelaxationStrategy(const Model& model, size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
+            size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options);
+      virtual ~ConstraintRelaxationStrategy() = default;
+
+      virtual void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) = 0;
+      void set_trust_region_radius(double trust_region_radius);
+
+      [[nodiscard]] virtual size_t maximum_number_variables() const = 0;
+      [[nodiscard]] virtual size_t maximum_number_constraints() const = 0;
+
+      // direction computation
+      virtual void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+            WarmstartInformation& warmstart_information) = 0;
+      void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+            const Vector<double>& initial_point, WarmstartInformation& warmstart_information);
+      [[nodiscard]] virtual bool solving_feasibility_problem() const = 0;
+      virtual void switch_to_feasibility_problem(Statistics& statistics, Iterate& current_iterate) = 0;
+
+      // trial iterate acceptance
+      [[nodiscard]] virtual bool is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
+            double step_length) = 0;
+      [[nodiscard]] TerminationStatus check_termination(Iterate& iterate);
+
+      // primal-dual residuals
+      virtual void compute_primal_dual_residuals(Iterate& iterate) = 0;
+      virtual void set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const = 0;
+
+      [[nodiscard]] size_t get_hessian_evaluation_count() const;
+      [[nodiscard]] size_t get_number_subproblems_solved() const;
+
+   protected:
+      const Model& model;
+      const std::unique_ptr<GlobalizationStrategy> globalization_strategy;
+      const std::unique_ptr<Subproblem> subproblem;
+      const Norm progress_norm;
+      const Norm residual_norm;
+      const double residual_scaling_threshold;
+      const double tight_tolerance; /*!< Tight tolerance of the termination criteria */
+      const double loose_tolerance; /*!< Loose tolerance of the termination criteria */
+      size_t loose_tolerance_consecutive_iterations{0};
+      const size_t loose_tolerance_consecutive_iteration_threshold;
+      const double unbounded_objective_threshold;
+      // first_order_predicted_reduction is true when the predicted reduction can be taken as first-order (e.g. in line-search methods)
+      const bool first_order_predicted_reduction;
+
+      void set_objective_measure(Iterate& iterate) const;
+      void set_infeasibility_measure(Iterate& iterate) const;
+      [[nodiscard]] double compute_predicted_infeasibility_reduction_model(const Iterate& current_iterate, const Vector<double>& primal_direction,
+            double step_length) const;
+      [[nodiscard]] std::function<double(double)> compute_predicted_objective_reduction_model(const Iterate& current_iterate,
+            const Vector<double>& primal_direction, double step_length, const SymmetricMatrix<size_t, double>& hessian) const;
+      [[nodiscard]] std::function<double(double)> compute_predicted_objective_reduction_model(const Iterate& current_iterate,
+            const Vector<double>& primal_direction, double step_length) const;
+      void compute_progress_measures(Iterate& current_iterate, Iterate& trial_iterate);
+      virtual void evaluate_progress_measures(Iterate& iterate) const = 0;
+
+      void compute_primal_dual_residuals(const OptimizationProblem& optimality_problem, const OptimizationProblem& feasibility_problem, Iterate& iterate);
+      void evaluate_lagrangian_gradient(LagrangianGradient<double>& lagrangian_gradient, Iterate& iterate, const Multipliers& multipliers) const;
+
+      [[nodiscard]] double compute_stationarity_scaling(const Multipliers& multipliers) const;
+      [[nodiscard]] double compute_complementarity_scaling(const Multipliers& multipliers) const;
+
+      [[nodiscard]] TerminationStatus check_first_order_convergence(Iterate& current_iterate, double tolerance) const;
+
+      void set_statistics(Statistics& statistics, const Iterate& iterate) const;
+      void set_progress_statistics(Statistics& statistics, const Iterate& iterate) const;
+   };
+} // namespace
+
+#endif //UNO_CONSTRAINTRELAXATIONSTRATEGY_H
\ No newline at end of file
diff --git a/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.cpp b/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.cpp
index f96b19f..e332346 100644
--- a/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.cpp
+++ b/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.cpp
@@ -6,17 +6,19 @@
 #include "l1Relaxation.hpp"
 #include "tools/Options.hpp"
 
-std::unique_ptr<ConstraintRelaxationStrategy> ConstraintRelaxationStrategyFactory::create(const Model& model, const Options& options) {
-   const std::string constraint_relaxation_type = options.get_string("constraint_relaxation_strategy");
-   if (constraint_relaxation_type == "feasibility_restoration") {
-      return std::make_unique<FeasibilityRestoration>(model, options);
+namespace uno {
+   std::unique_ptr<ConstraintRelaxationStrategy> ConstraintRelaxationStrategyFactory::create(const Model& model, const Options& options) {
+      const std::string constraint_relaxation_type = options.get_string("constraint_relaxation_strategy");
+      if (constraint_relaxation_type == "feasibility_restoration") {
+         return std::make_unique<FeasibilityRestoration>(model, options);
+      }
+      else if (constraint_relaxation_type == "l1_relaxation") {
+         return std::make_unique<l1Relaxation>(model, options);
+      }
+      throw std::invalid_argument("ConstraintRelaxationStrategy " + constraint_relaxation_type + " is not supported");
    }
-   else if (constraint_relaxation_type == "l1_relaxation") {
-      return std::make_unique<l1Relaxation>(model, options);
-   }
-   throw std::invalid_argument("ConstraintRelaxationStrategy " + constraint_relaxation_type + " is not supported");
-}
 
-std::vector<std::string> ConstraintRelaxationStrategyFactory::available_strategies() {
-   return {"feasibility_restoration", "l1_relaxation"};
-}
+   std::vector<std::string> ConstraintRelaxationStrategyFactory::available_strategies() {
+      return {"feasibility_restoration", "l1_relaxation"};
+   }
+} // namespace
diff --git a/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.hpp b/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.hpp
index 1c99747..9dff041 100644
--- a/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.hpp
+++ b/uno/ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategyFactory.hpp
@@ -7,13 +7,15 @@
 #include <memory>
 #include "ConstraintRelaxationStrategy.hpp"
 
-// forward declaration
-class Options;
+namespace uno {
+   // forward declaration
+   class Options;
 
-class ConstraintRelaxationStrategyFactory {
-public:
-   static std::unique_ptr<ConstraintRelaxationStrategy> create(const Model& model, const Options& options);
-   static std::vector<std::string> available_strategies();
-};
+   class ConstraintRelaxationStrategyFactory {
+   public:
+      static std::unique_ptr<ConstraintRelaxationStrategy> create(const Model& model, const Options& options);
+      static std::vector<std::string> available_strategies();
+   };
+} // namespace
 
 #endif // UNO_CONSTRAINTRELAXATIONSTRATEGYFACTORY_H
diff --git a/uno/ingredients/constraint_relaxation_strategy/FeasibilityRestoration.cpp b/uno/ingredients/constraint_relaxation_strategy/FeasibilityRestoration.cpp
index 05f34f8..139cdb8 100644
--- a/uno/ingredients/constraint_relaxation_strategy/FeasibilityRestoration.cpp
+++ b/uno/ingredients/constraint_relaxation_strategy/FeasibilityRestoration.cpp
@@ -3,9 +3,7 @@
 
 #include <functional>
 #include "FeasibilityRestoration.hpp"
-#include "ingredients/globalization_strategy/GlobalizationStrategyFactory.hpp"
 #include "ingredients/subproblem/Direction.hpp"
-#include "ingredients/subproblem/SubproblemFactory.hpp"
 #include "linear_algebra/SymmetricIndefiniteLinearSystem.hpp"
 #include "model/Model.hpp"
 #include "optimization/Iterate.hpp"
@@ -13,254 +11,212 @@
 #include "symbolic/VectorView.hpp"
 #include "tools/Options.hpp"
 
-FeasibilityRestoration::FeasibilityRestoration(const Model& model, const Options& options) :
-      ConstraintRelaxationStrategy(model, options),
-      // create the (optimality phase) optimality problem (= original model)
-      optimality_problem(model),
-      // create the (restoration phase) feasibility problem (objective multiplier = 0)
-      feasibility_problem(model, 0., options.get_double("l1_constraint_violation_coefficient")),
-      subproblem(SubproblemFactory::create(
-         // allocate the largest size necessary to solve the optimality subproblem or the feasibility subproblem
-         std::max(this->optimality_problem.number_variables, this->feasibility_problem.number_variables),
-         std::max(this->optimality_problem.number_constraints, this->feasibility_problem.number_constraints),
-         std::max(this->optimality_problem.number_objective_gradient_nonzeros(), this->feasibility_problem.number_objective_gradient_nonzeros()),
-         std::max(this->optimality_problem.number_jacobian_nonzeros(), this->feasibility_problem.number_jacobian_nonzeros()),
-         std::max(this->optimality_problem.number_hessian_nonzeros(), this->feasibility_problem.number_hessian_nonzeros()),
-         options)),
-      globalization_strategy(GlobalizationStrategyFactory::create(options.get_string("globalization_strategy"), options)),
-      linear_feasibility_tolerance(options.get_double("tolerance")),
-      switch_to_optimality_requires_acceptance(options.get_bool("switch_to_optimality_requires_acceptance")),
-      switch_to_optimality_requires_linearized_feasibility(options.get_bool("switch_to_optimality_requires_linearized_feasibility")) {
-}
+namespace uno {
+   FeasibilityRestoration::FeasibilityRestoration(const Model& model, const Options& options) :
+         // call delegating constructor
+         FeasibilityRestoration(model, OptimalityProblem(model),
+               // create the (restoration phase) feasibility problem (objective multiplier = 0)
+               l1RelaxedProblem(model, 0., options.get_double("l1_constraint_violation_coefficient")),
+               options) {
+   }
 
-void FeasibilityRestoration::initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) {
-   // statistics
-   this->subproblem->initialize_statistics(statistics, options);
-   statistics.add_column("phase", Statistics::int_width, options.get_int("statistics_restoration_phase_column_order"));
-   statistics.set("phase", "OPT");
+   // private delegating constructor
+   FeasibilityRestoration::FeasibilityRestoration(const Model& model, OptimalityProblem&& optimality_problem, l1RelaxedProblem&& feasibility_problem,
+            const Options& options) :
+         ConstraintRelaxationStrategy(model,
+               // allocate the largest size necessary to solve the optimality subproblem or the feasibility subproblem
+               std::max(optimality_problem.number_variables, feasibility_problem.number_variables),
+               std::max(optimality_problem.number_constraints, feasibility_problem.number_constraints),
+               std::max(optimality_problem.number_objective_gradient_nonzeros(), feasibility_problem.number_objective_gradient_nonzeros()),
+               std::max(optimality_problem.number_jacobian_nonzeros(), feasibility_problem.number_jacobian_nonzeros()),
+               std::max(optimality_problem.number_hessian_nonzeros(), feasibility_problem.number_hessian_nonzeros()),
+               options),
+         optimality_problem(std::forward<OptimalityProblem>(optimality_problem)),
+         feasibility_problem(std::forward<l1RelaxedProblem>(feasibility_problem)),
+         linear_feasibility_tolerance(options.get_double("tolerance")),
+         switch_to_optimality_requires_linearized_feasibility(options.get_bool("switch_to_optimality_requires_linearized_feasibility")) {
+   }
 
-   // initial iterate
-   const bool is_linearly_feasible = this->subproblem->generate_initial_iterate(this->optimality_problem, initial_iterate);
-   this->evaluate_progress_measures(initial_iterate);
-   this->compute_primal_dual_residuals(initial_iterate);
-   this->set_statistics(statistics, initial_iterate);
-   if (not is_linearly_feasible) {
-      this->switch_to_feasibility_problem(statistics, initial_iterate);
+   void FeasibilityRestoration::initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) {
+      // statistics
+      this->subproblem->initialize_statistics(statistics, options);
+      statistics.add_column("phase", Statistics::int_width, options.get_int("statistics_restoration_phase_column_order"));
       statistics.set("phase", "OPT");
-      statistics.set("status", "linearly infeas.");
-   }
-   this->globalization_strategy->initialize(statistics, initial_iterate, options);
-}
 
-void FeasibilityRestoration::compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-      WarmstartInformation& warmstart_information) {
-   /*
-   if (1e6 < norm_inf(current_iterate.multipliers.constraints)) {
-      // large duals are an indication of CQ failure
-      statistics.set("status", "large duals");
-      DEBUG << "/!\\ The duals are large\n";
-      this->switch_to_feasibility_problem(statistics, current_iterate);
-      warmstart_information.set_cold_start();
+      // initial iterate
+      this->subproblem->generate_initial_iterate(this->optimality_problem, initial_iterate);
+      this->evaluate_progress_measures(initial_iterate);
+      this->compute_primal_dual_residuals(initial_iterate);
+      this->set_statistics(statistics, initial_iterate);
+      this->globalization_strategy->initialize(statistics, initial_iterate, options);
    }
-   */
-   direction.reset();
-   // if we are in the optimality phase, solve the optimality problem
-   if (this->current_phase == Phase::OPTIMALITY) {
-      statistics.set("phase", "OPT");
-      try {
-         DEBUG << "Solving the optimality subproblem\n";
-         this->solve_subproblem(statistics, this->optimality_problem, current_iterate, current_iterate.multipliers, direction, warmstart_information);
-         if (direction.status == SubproblemStatus::INFEASIBLE) {
-            // switch to the feasibility problem, starting from the current direction
-            statistics.set("status", "infeas. subproblem");
-            DEBUG << "/!\\ The subproblem is infeasible\n";
+
+   void FeasibilityRestoration::compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+         WarmstartInformation& warmstart_information) {
+      direction.reset();
+      // if we are in the optimality phase, solve the optimality problem
+      if (this->current_phase == Phase::OPTIMALITY) {
+         statistics.set("phase", "OPT");
+         try {
+            DEBUG << "Solving the optimality subproblem\n";
+            this->solve_subproblem(statistics, this->optimality_problem, current_iterate, current_iterate.multipliers, direction, warmstart_information);
+            if (direction.status == SubproblemStatus::INFEASIBLE) {
+               // switch to the feasibility problem, starting from the current direction
+               statistics.set("status", "infeas. subproblem");
+               DEBUG << "/!\\ The subproblem is infeasible\n";
+               this->switch_to_feasibility_problem(statistics, current_iterate);
+               warmstart_information.set_cold_start();
+               this->subproblem->set_initial_point(direction.primals);
+            }
+            else {
+               return;
+            }
+         }
+         catch (const UnstableRegularization&) {
             this->switch_to_feasibility_problem(statistics, current_iterate);
             warmstart_information.set_cold_start();
-            this->subproblem->set_initial_point(direction.primals);
-         }
-         else {
-            return;
          }
       }
-      catch (const UnstableRegularization&) {
-         this->switch_to_feasibility_problem(statistics, current_iterate);
-         warmstart_information.set_cold_start();
-      }
-   }
 
-   // solve the feasibility problem (minimize the constraint violation)
-   DEBUG << "Solving the feasibility subproblem\n";
-   statistics.set("phase", "FEAS");
-   // note: failure of regularization should not happen here, since the feasibility Jacobian has full rank
-   this->solve_subproblem(statistics, this->feasibility_problem, current_iterate, current_iterate.feasibility_multipliers, direction,
-         warmstart_information);
-   std::swap(direction.multipliers, direction.feasibility_multipliers);
-}
-
-// an initial point is provided
-void FeasibilityRestoration::compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-      const Vector<double>& initial_point, WarmstartInformation& warmstart_information) {
-   this->subproblem->set_initial_point(initial_point);
-   this->compute_feasible_direction(statistics, current_iterate, direction, warmstart_information);
-}
-
-bool FeasibilityRestoration::solving_feasibility_problem() const {
-   return (this->current_phase == Phase::FEASIBILITY_RESTORATION);
-}
-
-// precondition: this->current_phase == Phase::OPTIMALITY
-void FeasibilityRestoration::switch_to_feasibility_problem(Statistics& statistics, Iterate& current_iterate) {
-   DEBUG << "Switching from optimality to restoration phase\n";
-   this->current_phase = Phase::FEASIBILITY_RESTORATION;
-   this->globalization_strategy->register_current_progress(current_iterate.progress);
-   this->subproblem->initialize_feasibility_problem(this->feasibility_problem, current_iterate);
-
-   current_iterate.set_number_variables(this->feasibility_problem.number_variables);
-   this->subproblem->set_elastic_variable_values(this->feasibility_problem, current_iterate);
-   DEBUG2 << "Current iterate:\n" << current_iterate << '\n';
+      // solve the feasibility problem (minimize the constraint violation)
+      DEBUG << "Solving the feasibility subproblem\n";
+      statistics.set("phase", "FEAS");
+      // note: failure of regularization should not happen here, since the feasibility Jacobian has full rank
+      this->solve_subproblem(statistics, this->feasibility_problem, current_iterate, current_iterate.feasibility_multipliers, direction,
+            warmstart_information);
+      std::swap(direction.multipliers, direction.feasibility_multipliers);
+   }
 
-   if (Logger::level == INFO) statistics.print_current_line();
-   statistics.start_new_line();
-}
+   bool FeasibilityRestoration::solving_feasibility_problem() const {
+      return (this->current_phase == Phase::FEASIBILITY_RESTORATION);
+   }
 
-void FeasibilityRestoration::solve_subproblem(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
-      const Multipliers& current_multipliers, Direction& direction, WarmstartInformation& warmstart_information) {
-   // upon switching to the optimality phase, set a cold start in the subproblem solver
-   if (this->switching_to_optimality_phase) {
-      this->switching_to_optimality_phase = false;
-      warmstart_information.set_cold_start();
+   // precondition: this->current_phase == Phase::OPTIMALITY
+   void FeasibilityRestoration::switch_to_feasibility_problem(Statistics& statistics, Iterate& current_iterate) {
+      DEBUG << "Switching from optimality to restoration phase\n";
+      this->current_phase = Phase::FEASIBILITY_RESTORATION;
+      this->globalization_strategy->notify_switch_to_feasibility(current_iterate.progress);
+      this->subproblem->initialize_feasibility_problem(this->feasibility_problem, current_iterate);
+      // save the progress of the current point upon switching
+      this->reference_optimality_progress = current_iterate.progress;
+
+      current_iterate.set_number_variables(this->feasibility_problem.number_variables);
+      this->subproblem->set_elastic_variable_values(this->feasibility_problem, current_iterate);
+      DEBUG2 << "Current iterate:\n" << current_iterate << '\n';
+
+      if (Logger::level == INFO) statistics.print_current_line();
+      statistics.start_new_line();
    }
 
-   direction.set_dimensions(problem.number_variables, problem.number_constraints);
-   this->subproblem->solve(statistics, problem, current_iterate, current_multipliers, direction, warmstart_information);
-   direction.norm = norm_inf(view(direction.primals, 0, this->model.number_variables));
-   DEBUG3 << direction << '\n';
-}
+   void FeasibilityRestoration::solve_subproblem(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
+         const Multipliers& current_multipliers, Direction& direction, WarmstartInformation& warmstart_information) {
+      // upon switching to the optimality phase, set a cold start in the subproblem solver
+      if (this->switching_to_optimality_phase) {
+         this->switching_to_optimality_phase = false;
+         warmstart_information.set_cold_start();
+      }
 
-void FeasibilityRestoration::compute_progress_measures(Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
-      double step_length) {
-   if (this->subproblem->subproblem_definition_changed) {
-      this->subproblem->subproblem_definition_changed = false;
-      DEBUG << "The subproblem definition changed, the globalization strategy is reset and the auxiliary measure is recomputed\n";
-      this->globalization_strategy->reset();
-      this->subproblem->set_auxiliary_measure(this->model, current_iterate);
+      direction.set_dimensions(problem.number_variables, problem.number_constraints);
+      this->subproblem->solve(statistics, problem, current_iterate, current_multipliers, direction, warmstart_information);
+      direction.norm = norm_inf(view(direction.primals, 0, this->model.number_variables));
+      DEBUG3 << direction << '\n';
    }
-   this->evaluate_progress_measures(trial_iterate);
 
-   // possibly go from restoration phase to optimality phase
-   if (this->current_phase == Phase::FEASIBILITY_RESTORATION && not this->switch_to_optimality_requires_acceptance &&
-         (not this->switch_to_optimality_requires_linearized_feasibility || this->model.constraint_violation(current_iterate.evaluations.constraints +
-         step_length*(current_iterate.evaluations.constraint_jacobian * direction.primals), this->residual_norm) <= this->linear_feasibility_tolerance) &&
-         this->globalization_strategy->is_infeasibility_sufficiently_reduced(current_iterate.progress, trial_iterate.progress)) {
-      this->switch_to_optimality_phase(current_iterate, trial_iterate);
+   bool FeasibilityRestoration::can_switch_to_optimality_phase(const Iterate& current_iterate, const Iterate& trial_iterate, const Direction& direction,
+         double step_length) {
+      return this->globalization_strategy->is_infeasibility_sufficiently_reduced(this->reference_optimality_progress, trial_iterate.progress) &&
+         (not this->switch_to_optimality_requires_linearized_feasibility ||
+         this->model.constraint_violation(current_iterate.evaluations.constraints + step_length*(current_iterate.evaluations.constraint_jacobian *
+         direction.primals), this->residual_norm) <= this->linear_feasibility_tolerance);
    }
-}
 
-void FeasibilityRestoration::switch_to_optimality_phase(Iterate& current_iterate, Iterate& trial_iterate) {
-   DEBUG << "Switching from restoration to optimality phase\n";
-   this->current_phase = Phase::OPTIMALITY;
-   this->globalization_strategy->register_current_progress(current_iterate.progress);
-   current_iterate.set_number_variables(this->optimality_problem.number_variables);
-   trial_iterate.set_number_variables(this->optimality_problem.number_variables);
-   current_iterate.objective_multiplier = trial_iterate.objective_multiplier = 1.;
+   void FeasibilityRestoration::switch_to_optimality_phase(Iterate& current_iterate, Iterate& trial_iterate) {
+      DEBUG << "Switching from restoration to optimality phase\n";
+      this->current_phase = Phase::OPTIMALITY;
+      this->globalization_strategy->notify_switch_to_optimality(current_iterate.progress);
+      current_iterate.set_number_variables(this->optimality_problem.number_variables);
+      trial_iterate.set_number_variables(this->optimality_problem.number_variables);
+      current_iterate.objective_multiplier = trial_iterate.objective_multiplier = 1.;
 
-   this->subproblem->exit_feasibility_problem(this->optimality_problem, trial_iterate);
-   this->switching_to_optimality_phase = true;
-}
+      this->subproblem->exit_feasibility_problem(this->optimality_problem, trial_iterate);
+      this->switching_to_optimality_phase = true;
+   }
 
-bool FeasibilityRestoration::is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
-      double step_length) {
-   this->subproblem->postprocess_iterate(this->current_problem(), trial_iterate);
-   this->compute_progress_measures(current_iterate, trial_iterate, direction, step_length);
+   bool FeasibilityRestoration::is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
+         double step_length) {
+      this->subproblem->postprocess_iterate(this->current_problem(), trial_iterate);
+      this->compute_progress_measures(current_iterate, trial_iterate);
+      trial_iterate.objective_multiplier = this->current_problem().get_objective_multiplier();
 
-   bool accept_iterate = false;
-   /*
-   // detect trivial multipliers
-   if (this->current_phase == Phase::FEASIBILITY_RESTORATION && not trial_iterate.multipliers.not_all_zero(this->model.number_variables, 1e-6)) {
-      DEBUG << "Trivial duals in restoration phase, trial iterate rejected\n\n";
-      statistics.set("status", "rejected (trivial duals)");
-   }
-   else
-   */
-   if (direction.norm == 0.) {
-      DEBUG << "Zero step acceptable\n";
-      trial_iterate.evaluate_objective(this->model);
-      accept_iterate = true;
-      statistics.set("status", "accepted (0 primal step)");
-   }
-   else {
-      // invoke the globalization strategy for acceptance
-      const ProgressMeasures predicted_reduction = this->compute_predicted_reduction_models(current_iterate, direction, step_length);
-      accept_iterate = this->globalization_strategy->is_iterate_acceptable(statistics, current_iterate.progress, trial_iterate.progress,
-            predicted_reduction, this->current_problem().get_objective_multiplier());
-   }
-   // possibly switch back to optimality phase if the trial iterate in feasibility restoration was accepted
-   if (accept_iterate) {
-      if (this->current_phase == Phase::FEASIBILITY_RESTORATION && this->switch_to_optimality_requires_acceptance &&
-            this->globalization_strategy->is_infeasibility_sufficiently_reduced(current_iterate.progress, trial_iterate.progress)) {
+      // possibly go from restoration phase to optimality phase
+      if (this->current_phase == Phase::FEASIBILITY_RESTORATION && this->can_switch_to_optimality_phase(current_iterate, trial_iterate, direction, step_length)) {
          this->switch_to_optimality_phase(current_iterate, trial_iterate);
       }
-      this->compute_primal_dual_residuals(trial_iterate);
-      this->set_dual_residuals_statistics(statistics, trial_iterate);
-   }
-   ConstraintRelaxationStrategy::set_progress_statistics(statistics, trial_iterate);
-   return accept_iterate;
-}
 
-void FeasibilityRestoration::compute_primal_dual_residuals(Iterate& iterate) {
-   ConstraintRelaxationStrategy::compute_primal_dual_residuals(this->optimality_problem, this->feasibility_problem, iterate);
-}
-
-const OptimizationProblem& FeasibilityRestoration::current_problem() const {
-   if (this->current_phase == Phase::OPTIMALITY) {
-      return this->optimality_problem;
-   }
-   else {
-      return this->feasibility_problem;
+      bool accept_iterate = false;
+      if (direction.norm == 0.) {
+         DEBUG << "Zero step acceptable\n";
+         trial_iterate.evaluate_objective(this->model);
+         accept_iterate = true;
+         statistics.set("status", "0 primal step");
+      }
+      else {
+         // invoke the globalization strategy for acceptance
+         const ProgressMeasures predicted_reduction = this->compute_predicted_reduction_models(current_iterate, direction, step_length);
+         accept_iterate = this->globalization_strategy->is_iterate_acceptable(statistics, current_iterate.progress, trial_iterate.progress,
+               predicted_reduction, this->current_problem().get_objective_multiplier());
+      }
+      if (accept_iterate) {
+         // this->set_dual_residuals_statistics(statistics, trial_iterate);
+      }
+      ConstraintRelaxationStrategy::set_progress_statistics(statistics, trial_iterate);
+      return accept_iterate;
    }
-}
-
-void FeasibilityRestoration::evaluate_progress_measures(Iterate& iterate) const {
-   this->set_infeasibility_measure(iterate);
-   this->set_objective_measure(iterate);
-   this->subproblem->set_auxiliary_measure(this->model, iterate);
-}
-
-ProgressMeasures FeasibilityRestoration::compute_predicted_reduction_models(Iterate& current_iterate, const Direction& direction, double step_length) {
-   return {
-      this->compute_predicted_infeasibility_reduction_model(current_iterate, direction.primals, step_length),
-      this->compute_predicted_objective_reduction_model(current_iterate, direction.primals, step_length, this->subproblem->get_lagrangian_hessian()),
-      this->subproblem->compute_predicted_auxiliary_reduction_model(this->model, current_iterate, direction.primals, step_length)
-   };
-}
 
-void FeasibilityRestoration::set_trust_region_radius(double trust_region_radius) {
-   this->subproblem->set_trust_region_radius(trust_region_radius);
-}
+   void FeasibilityRestoration::compute_primal_dual_residuals(Iterate& iterate) {
+      ConstraintRelaxationStrategy::compute_primal_dual_residuals(this->optimality_problem, this->feasibility_problem, iterate);
+   }
 
-size_t FeasibilityRestoration::maximum_number_variables() const {
-   return std::max(this->optimality_problem.number_variables, this->feasibility_problem.number_variables);
-}
+   const OptimizationProblem& FeasibilityRestoration::current_problem() const {
+      if (this->current_phase == Phase::OPTIMALITY) {
+         return this->optimality_problem;
+      }
+      else {
+         return this->feasibility_problem;
+      }
+   }
 
-size_t FeasibilityRestoration::maximum_number_constraints() const {
-   return std::max(this->optimality_problem.number_constraints, this->feasibility_problem.number_constraints);
-}
+   void FeasibilityRestoration::evaluate_progress_measures(Iterate& iterate) const {
+      this->set_infeasibility_measure(iterate);
+      this->set_objective_measure(iterate);
+      this->subproblem->set_auxiliary_measure(this->model, iterate);
+   }
 
-void FeasibilityRestoration::set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const {
-   if (this->current_phase == Phase::OPTIMALITY) {
-      statistics.set("complementarity", iterate.residuals.complementarity);
-      statistics.set("stationarity", iterate.residuals.KKT_stationarity);
+   ProgressMeasures FeasibilityRestoration::compute_predicted_reduction_models(Iterate& current_iterate, const Direction& direction, double step_length) {
+      return {
+         this->compute_predicted_infeasibility_reduction_model(current_iterate, direction.primals, step_length),
+         this->first_order_predicted_reduction ? this->compute_predicted_objective_reduction_model(current_iterate, direction.primals, step_length) :
+            this->compute_predicted_objective_reduction_model(current_iterate, direction.primals, step_length, this->subproblem->get_lagrangian_hessian()),
+         this->subproblem->compute_predicted_auxiliary_reduction_model(this->model, current_iterate, direction.primals, step_length)
+      };
    }
-   else {
-      statistics.set("complementarity", iterate.residuals.feasibility_complementarity);
-      statistics.set("stationarity", iterate.residuals.feasibility_stationarity);
+
+   size_t FeasibilityRestoration::maximum_number_variables() const {
+      return std::max(this->optimality_problem.number_variables, this->feasibility_problem.number_variables);
    }
-}
 
-size_t FeasibilityRestoration::get_hessian_evaluation_count() const {
-   return this->subproblem->get_hessian_evaluation_count();
-}
+   size_t FeasibilityRestoration::maximum_number_constraints() const {
+      return std::max(this->optimality_problem.number_constraints, this->feasibility_problem.number_constraints);
+   }
 
-size_t FeasibilityRestoration::get_number_subproblems_solved() const {
-   return this->subproblem->number_subproblems_solved;
-}
+   void FeasibilityRestoration::set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const {
+      if (this->current_phase == Phase::OPTIMALITY) {
+         statistics.set("stationarity", iterate.residuals.stationarity);
+         statistics.set("complementarity", iterate.residuals.complementarity);
+      }
+      else {
+         statistics.set("stationarity", iterate.feasibility_residuals.stationarity);
+         statistics.set("complementarity", iterate.feasibility_residuals.complementarity);
+      }
+   }
+} // namespace
diff --git a/uno/ingredients/constraint_relaxation_strategy/FeasibilityRestoration.hpp b/uno/ingredients/constraint_relaxation_strategy/FeasibilityRestoration.hpp
index 26de001..d8807eb 100644
--- a/uno/ingredients/constraint_relaxation_strategy/FeasibilityRestoration.hpp
+++ b/uno/ingredients/constraint_relaxation_strategy/FeasibilityRestoration.hpp
@@ -6,61 +6,57 @@
 
 #include <memory>
 #include "ConstraintRelaxationStrategy.hpp"
-#include "ingredients/globalization_strategy/GlobalizationStrategy.hpp"
 #include "ingredients/subproblem/Subproblem.hpp"
 #include "reformulation/OptimalityProblem.hpp"
 #include "reformulation/l1RelaxedProblem.hpp"
 
-enum class Phase {FEASIBILITY_RESTORATION = 1, OPTIMALITY = 2};
-
-class FeasibilityRestoration : public ConstraintRelaxationStrategy {
-public:
-   FeasibilityRestoration(const Model& model, const Options& options);
-
-   void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) override;
-   void set_trust_region_radius(double trust_region_radius) override;
-
-   [[nodiscard]] size_t maximum_number_variables() const override;
-   [[nodiscard]] size_t maximum_number_constraints() const override;
-
-   // direction computation
-   void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction, WarmstartInformation& warmstart_information) override;
-   void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction, const Vector<double>& initial_point,
-         WarmstartInformation& warmstart_information) override;
-   [[nodiscard]] bool solving_feasibility_problem() const override;
-   void switch_to_feasibility_problem(Statistics& statistics, Iterate& current_iterate) override;
-
-   // trial iterate acceptance
-   void compute_progress_measures(Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction, double step_length) override;
-   [[nodiscard]] bool is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
-         double step_length) override;
-
-   // primal-dual residuals
-   void compute_primal_dual_residuals(Iterate& iterate) override;
-
-   [[nodiscard]] size_t get_hessian_evaluation_count() const override;
-   [[nodiscard]] size_t get_number_subproblems_solved() const override;
-
-private:
-   const OptimalityProblem optimality_problem;
-   l1RelaxedProblem feasibility_problem;
-   std::unique_ptr<Subproblem> subproblem;
-   const std::unique_ptr<GlobalizationStrategy> globalization_strategy;
-   Phase current_phase{Phase::OPTIMALITY};
-   const double linear_feasibility_tolerance;
-   const bool switch_to_optimality_requires_acceptance;
-   const bool switch_to_optimality_requires_linearized_feasibility;
-   bool switching_to_optimality_phase{false};
-
-   [[nodiscard]] const OptimizationProblem& current_problem() const;
-   void solve_subproblem(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
-         Direction& direction, WarmstartInformation& warmstart_information);
-   void switch_to_optimality_phase(Iterate& current_iterate, Iterate& trial_iterate);
-
-   void evaluate_progress_measures(Iterate& iterate) const;
-   [[nodiscard]] ProgressMeasures compute_predicted_reduction_models(Iterate& current_iterate, const Direction& direction, double step_length);
-
-   void set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const override;
-};
+namespace uno {
+   enum class Phase {FEASIBILITY_RESTORATION = 1, OPTIMALITY = 2};
+
+   class FeasibilityRestoration : public ConstraintRelaxationStrategy {
+   public:
+      FeasibilityRestoration(const Model& model, const Options& options);
+
+      void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) override;
+
+      [[nodiscard]] size_t maximum_number_variables() const override;
+      [[nodiscard]] size_t maximum_number_constraints() const override;
+
+      // direction computation
+      void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction, WarmstartInformation& warmstart_information) override;
+      [[nodiscard]] bool solving_feasibility_problem() const override;
+      void switch_to_feasibility_problem(Statistics& statistics, Iterate& current_iterate) override;
+
+      // trial iterate acceptance
+      [[nodiscard]] bool is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
+            double step_length) override;
+
+      // primal-dual residuals
+      void compute_primal_dual_residuals(Iterate& iterate) override;
+      void set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const override;
+
+   private:
+      const OptimalityProblem optimality_problem;
+      l1RelaxedProblem feasibility_problem;
+      Phase current_phase{Phase::OPTIMALITY};
+      const double linear_feasibility_tolerance;
+      const bool switch_to_optimality_requires_linearized_feasibility;
+      bool switching_to_optimality_phase{false};
+      ProgressMeasures reference_optimality_progress{};
+
+      // delegating constructor
+      FeasibilityRestoration(const Model& model, OptimalityProblem&& optimality_problem, l1RelaxedProblem&& feasibility_problem, const Options& options);
+
+      [[nodiscard]] const OptimizationProblem& current_problem() const;
+      void solve_subproblem(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
+            Direction& direction, WarmstartInformation& warmstart_information);
+      void switch_to_optimality_phase(Iterate& current_iterate, Iterate& trial_iterate);
+
+      void evaluate_progress_measures(Iterate& iterate) const override;
+      [[nodiscard]] ProgressMeasures compute_predicted_reduction_models(Iterate& current_iterate, const Direction& direction, double step_length);
+      [[nodiscard]] bool can_switch_to_optimality_phase(const Iterate& current_iterate, const Iterate& trial_iterate, const Direction& direction,
+            double step_length);
+   };
+} // namespace
 
 #endif //UNO_FEASIBILITYRESTORATION_H
diff --git a/uno/ingredients/constraint_relaxation_strategy/l1Relaxation.cpp b/uno/ingredients/constraint_relaxation_strategy/l1Relaxation.cpp
index 6c13a73..8932011 100644
--- a/uno/ingredients/constraint_relaxation_strategy/l1Relaxation.cpp
+++ b/uno/ingredients/constraint_relaxation_strategy/l1Relaxation.cpp
@@ -3,9 +3,7 @@
 
 #include <cassert>
 #include "l1Relaxation.hpp"
-#include "ingredients/globalization_strategy/GlobalizationStrategyFactory.hpp"
 #include "ingredients/subproblem/Direction.hpp"
-#include "ingredients/subproblem/SubproblemFactory.hpp"
 #include "optimization/Iterate.hpp"
 #include "optimization/WarmstartInformation.hpp"
 #include "symbolic/VectorView.hpp"
@@ -18,303 +16,279 @@
  * http://epubs.siam.org/doi/pdf/10.1137/080738222
  */
 
-l1Relaxation::l1Relaxation(const Model& model, const Options& options) :
-      ConstraintRelaxationStrategy(model, options),
-      // create the l1 feasibility problem (objective multiplier = 0)
-      feasibility_problem(model, 0., options.get_double("l1_constraint_violation_coefficient")),
-      // create the l1 relaxed problem
-      l1_relaxed_problem(model, options.get_double("l1_relaxation_initial_parameter"), options.get_double("l1_constraint_violation_coefficient")),
-      subproblem(SubproblemFactory::create(this->l1_relaxed_problem.number_variables, this->l1_relaxed_problem.number_constraints,
-            this->l1_relaxed_problem.number_objective_gradient_nonzeros(), this->l1_relaxed_problem.number_jacobian_nonzeros(),
-            this->l1_relaxed_problem.number_hessian_nonzeros(), options)),
-      globalization_strategy(GlobalizationStrategyFactory::create(options.get_string("globalization_strategy"), options)),
-      penalty_parameter(options.get_double("l1_relaxation_initial_parameter")),
-      tolerance(options.get_double("tolerance")),
-      parameters({
-         options.get_bool("l1_relaxation_fixed_parameter"),
-         options.get_double("l1_relaxation_decrease_factor"),
-         options.get_double("l1_relaxation_epsilon1"),
-         options.get_double("l1_relaxation_epsilon2"),
-         options.get_double("l1_relaxation_residual_small_threshold")
-      }),
-      small_duals_threshold(options.get_double("l1_small_duals_threshold")),
-      trial_multipliers(this->l1_relaxed_problem.number_variables, model.number_constraints) {
-}
-
-void l1Relaxation::initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) {
-   // statistics
-   this->subproblem->initialize_statistics(statistics, options);
-   statistics.add_column("penalty param.", Statistics::double_width, options.get_int("statistics_penalty_parameter_column_order"));
-   statistics.set("penalty param.", this->penalty_parameter);
-
-   // initial iterate
-   this->subproblem->set_elastic_variable_values(this->l1_relaxed_problem, initial_iterate);
-   const bool is_linearly_feasible = this->subproblem->generate_initial_iterate(this->l1_relaxed_problem, initial_iterate);
-   this->evaluate_progress_measures(initial_iterate);
-   this->compute_primal_dual_residuals(initial_iterate);
-   this->set_statistics(statistics, initial_iterate);
-   if (not is_linearly_feasible) {
-      this->switch_to_feasibility_problem(statistics, initial_iterate);
-      statistics.set("status", "linearly infeas.");
+namespace uno {
+   l1Relaxation::l1Relaxation(const Model& model, const Options& options) :
+         // call delegating constructor
+         l1Relaxation(model,
+               // create the l1 feasibility problem (objective multiplier = 0)
+               l1RelaxedProblem(model, 0., options.get_double("l1_constraint_violation_coefficient")),
+               // create the l1 relaxed problem
+               l1RelaxedProblem(model, options.get_double("l1_relaxation_initial_parameter"), options.get_double("l1_constraint_violation_coefficient")),
+               options) {
    }
-   this->globalization_strategy->initialize(statistics, initial_iterate, options);
-}
-
-void l1Relaxation::compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-      WarmstartInformation& warmstart_information) {
-   statistics.set("penalty param.", this->penalty_parameter);
-   direction.reset();
-   this->solve_sequence_of_relaxed_subproblems(statistics, current_iterate, direction, warmstart_information);
-}
-
-// an initial point is provided
-void l1Relaxation::compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-      const Vector<double>& initial_point, WarmstartInformation& warmstart_information) {
-   this->subproblem->set_initial_point(initial_point);
-   this->compute_feasible_direction(statistics, current_iterate, direction, warmstart_information);
-}
-
-bool l1Relaxation::solving_feasibility_problem() const {
-   return (this->penalty_parameter == 0.);
-}
-
-void l1Relaxation::switch_to_feasibility_problem(Statistics& /*statistics*/, Iterate& /*current_iterate*/) {
-   throw std::runtime_error("l1Relaxation::switch_to_feasibility_problem is not implemented\n");
-}
-
-// use Byrd's steering rules to update the penalty parameter and compute a descent direction
-void l1Relaxation::solve_sequence_of_relaxed_subproblems(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-      WarmstartInformation& warmstart_information) {
-   // stage a: compute a direction for the current penalty parameter
-   this->solve_l1_relaxed_problem(statistics, current_iterate, direction, this->penalty_parameter, warmstart_information);
-   // from now on, only the penalty parameter, therefore the objective, changes
-   warmstart_information.only_objective_changed();
-
-   // penalty update: if penalty parameter is already 0 or fixed by the user, no need to decrease it
-   if (0. < this->penalty_parameter && not this->parameters.fixed_parameter) {
-      double linearized_residual = this->model.constraint_violation(current_iterate.evaluations.constraints +
-            current_iterate.evaluations.constraint_jacobian * direction.primals, Norm::L1);
-      DEBUG << "Linearized infeasibility mk(dk): " << linearized_residual << "\n\n";
-
-      // if the current direction is already feasible, terminate
-      if (this->tolerance < linearized_residual) {
-         const double current_penalty_parameter = this->penalty_parameter;
-
-         // stage c: compute the lowest possible constraint violation (penalty parameter = 0)
-         DEBUG << "Compute ideal solution by solving the feasibility problem:\n";
-         this->subproblem->initialize_feasibility_problem(this->feasibility_problem, current_iterate);
-         Direction feasibility_direction(direction.number_variables, direction.number_constraints);
-         this->solve_subproblem(statistics, this->feasibility_problem, current_iterate, current_iterate.feasibility_multipliers, feasibility_direction,
-               warmstart_information);
-         std::swap(direction.multipliers, direction.feasibility_multipliers);
-         const double residual_lowest_violation = this->model.constraint_violation(current_iterate.evaluations.constraints +
-               current_iterate.evaluations.constraint_jacobian * feasibility_direction.primals, Norm::L1);
-         DEBUG << "Lowest linearized infeasibility mk(dk): " << residual_lowest_violation << '\n';
-         this->subproblem->exit_feasibility_problem(this->feasibility_problem, current_iterate);
-
-         // stage f: update the penalty parameter based on the current dual error
-         this->decrease_parameter_aggressively(current_iterate, feasibility_direction);
-         if (this->penalty_parameter < current_penalty_parameter) {
-            this->solve_l1_relaxed_problem(statistics, current_iterate, direction, this->penalty_parameter, warmstart_information);
-            linearized_residual = this->model.constraint_violation(current_iterate.evaluations.constraints +
-                  current_iterate.evaluations.constraint_jacobian * direction.primals, Norm::L1);
-         }
 
-         // stage d: further decrease penalty parameter to reach a fraction of the ideal decrease
-         this->enforce_linearized_residual_sufficient_decrease(statistics, current_iterate, direction, linearized_residual,
-               residual_lowest_violation, warmstart_information);
-         // stage e: further decrease penalty parameter to guarantee a descent direction for the l1 merit function
-         this->enforce_descent_direction_for_l1_merit(statistics, current_iterate, direction, feasibility_direction, warmstart_information);
+   // private delegating constructor
+   l1Relaxation::l1Relaxation(const Model& model, l1RelaxedProblem&& feasibility_problem, l1RelaxedProblem&& l1_relaxed_problem, const Options& options) :
+         ConstraintRelaxationStrategy(model, l1_relaxed_problem.number_variables, l1_relaxed_problem.number_constraints,
+               l1_relaxed_problem.number_objective_gradient_nonzeros(), l1_relaxed_problem.number_jacobian_nonzeros(),
+               l1_relaxed_problem.number_hessian_nonzeros(), options),
+         feasibility_problem(std::forward<l1RelaxedProblem>(feasibility_problem)),
+         l1_relaxed_problem(std::forward<l1RelaxedProblem>(l1_relaxed_problem)),
+         penalty_parameter(options.get_double("l1_relaxation_initial_parameter")),
+         tolerance(options.get_double("tolerance")),
+         parameters({
+               options.get_bool("l1_relaxation_fixed_parameter"),
+               options.get_double("l1_relaxation_decrease_factor"),
+               options.get_double("l1_relaxation_epsilon1"),
+               options.get_double("l1_relaxation_epsilon2"),
+               options.get_double("l1_relaxation_residual_small_threshold")
+         }),
+         small_duals_threshold(options.get_double("l1_small_duals_threshold")),
+         trial_multipliers(this->l1_relaxed_problem.number_variables, model.number_constraints) {
+   }
 
-         // save the dual feasibility direction
-         direction.feasibility_multipliers = feasibility_direction.multipliers;
-      }
+   void l1Relaxation::initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) {
+      // statistics
+      this->subproblem->initialize_statistics(statistics, options);
+      statistics.add_column("penalty param.", Statistics::double_width, options.get_int("statistics_penalty_parameter_column_order"));
+      statistics.set("penalty param.", this->penalty_parameter);
+
+      // initial iterate
+      this->subproblem->set_elastic_variable_values(this->l1_relaxed_problem, initial_iterate);
+      this->subproblem->generate_initial_iterate(this->l1_relaxed_problem, initial_iterate);
+      this->evaluate_progress_measures(initial_iterate);
+      this->compute_primal_dual_residuals(initial_iterate);
+      this->set_statistics(statistics, initial_iterate);
+      this->globalization_strategy->initialize(statistics, initial_iterate, options);
    }
-}
-
-void l1Relaxation::solve_subproblem(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
-      const Multipliers& current_multipliers, Direction& direction, const WarmstartInformation& warmstart_information) {
-   DEBUG << "Solving the subproblem with penalty parameter " << problem.get_objective_multiplier() << "\n\n";
-
-   // solve the subproblem
-   direction.set_dimensions(problem.number_variables, problem.number_constraints);
-   this->subproblem->solve(statistics, problem, current_iterate, current_multipliers, direction, warmstart_information);
-   direction.norm = norm_inf(view(direction.primals, 0, this->model.number_variables));
-   DEBUG3 << direction << '\n';
-   assert(direction.status == SubproblemStatus::OPTIMAL && "The subproblem was not solved to optimality");
-}
-
-void l1Relaxation::solve_l1_relaxed_problem(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-      double current_penalty_parameter, const WarmstartInformation& warmstart_information) {
-   this->l1_relaxed_problem.set_objective_multiplier(current_penalty_parameter);
-   this->solve_subproblem(statistics, this->l1_relaxed_problem, current_iterate, current_iterate.multipliers, direction, warmstart_information);
-}
-
-void l1Relaxation::decrease_parameter_aggressively(Iterate& current_iterate, const Direction& direction) {
-   this->trial_multipliers.constraints = current_iterate.feasibility_multipliers.constraints + direction.feasibility_multipliers.constraints;
-   this->trial_multipliers.lower_bounds = current_iterate.feasibility_multipliers.lower_bounds + direction.feasibility_multipliers.lower_bounds;
-   this->trial_multipliers.upper_bounds = current_iterate.feasibility_multipliers.upper_bounds + direction.feasibility_multipliers.upper_bounds;
-
-   // there must be at least a nonzero dual to avoid trivial stationary points
-   if (this->trial_multipliers.not_all_zero(this->model.number_variables, this->small_duals_threshold)) {
-      // compute the ideal error (with a zero penalty parameter)
-      const double infeasible_dual_error = l1Relaxation::compute_infeasible_dual_error(current_iterate);
-      DEBUG << "Ideal dual error: " << infeasible_dual_error << '\n';
-      const double scaled_error = infeasible_dual_error / std::max(1., current_iterate.residuals.infeasibility);
-      this->penalty_parameter = std::min(this->penalty_parameter, scaled_error * scaled_error);
-      DEBUG << "Further aggressively decrease the penalty parameter to " << this->penalty_parameter << '\n';
+
+   void l1Relaxation::compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+         WarmstartInformation& warmstart_information) {
+      statistics.set("penalty param.", this->penalty_parameter);
+      direction.reset();
+      this->solve_sequence_of_relaxed_subproblems(statistics, current_iterate, direction, warmstart_information);
    }
-   else {
-      DEBUG << RED << "l1Relaxation: all multipliers are almost 0. The penalty parameter won't be decreased" << RESET << '\n';
+
+   bool l1Relaxation::solving_feasibility_problem() const {
+      return (this->penalty_parameter == 0.);
    }
-}
-
-// measure that combines KKT error and complementarity error
-double l1Relaxation::compute_infeasible_dual_error(Iterate& current_iterate) {
-   // stationarity error
-   this->evaluate_lagrangian_gradient(current_iterate, this->trial_multipliers);
-   double error = norm_1(current_iterate.lagrangian_gradient.constraints_contribution);
-
-   // complementarity error
-   error += this->feasibility_problem.complementarity_error(current_iterate.primals, current_iterate.evaluations.constraints,
-         this->trial_multipliers, Norm::L1);
-   return error;
-}
-
-void l1Relaxation::enforce_linearized_residual_sufficient_decrease(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-      double linearized_residual, double residual_lowest_violation, WarmstartInformation& warmstart_information) {
-   while (0. < this->penalty_parameter && not this->linearized_residual_sufficient_decrease(current_iterate, linearized_residual,
-         residual_lowest_violation)) {
-      // decrease the penalty parameter and re-solve the problem
-      this->penalty_parameter /= this->parameters.decrease_factor;
-      DEBUG << "Further decrease the penalty parameter to " << this->penalty_parameter << '\n';
+
+   void l1Relaxation::switch_to_feasibility_problem(Statistics& /*statistics*/, Iterate& /*current_iterate*/) {
+      throw std::runtime_error("l1Relaxation::switch_to_feasibility_problem is not implemented\n");
+   }
+
+   // use Byrd's steering rules to update the penalty parameter and compute a descent direction
+   void l1Relaxation::solve_sequence_of_relaxed_subproblems(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+         WarmstartInformation& warmstart_information) {
+      // stage a: compute a direction for the current penalty parameter
       this->solve_l1_relaxed_problem(statistics, current_iterate, direction, this->penalty_parameter, warmstart_information);
+      // from now on, only the penalty parameter, therefore the objective, changes
+      warmstart_information.only_objective_changed();
+
+      // penalty update: if penalty parameter is already 0 or fixed by the user, no need to decrease it
+      if (0. < this->penalty_parameter && not this->parameters.fixed_parameter) {
+         double linearized_residual = this->model.constraint_violation(current_iterate.evaluations.constraints +
+               current_iterate.evaluations.constraint_jacobian * direction.primals, Norm::L1);
+         DEBUG << "Linearized infeasibility mk(dk): " << linearized_residual << "\n\n";
+
+         // if the current direction is already feasible, terminate
+         if (this->tolerance < linearized_residual) {
+            const double current_penalty_parameter = this->penalty_parameter;
+
+            // stage c: compute the lowest possible constraint violation (penalty parameter = 0)
+            DEBUG << "Compute ideal solution by solving the feasibility problem:\n";
+            this->subproblem->initialize_feasibility_problem(this->feasibility_problem, current_iterate);
+            Direction feasibility_direction(direction.number_variables, direction.number_constraints);
+            this->solve_subproblem(statistics, this->feasibility_problem, current_iterate, current_iterate.feasibility_multipliers, feasibility_direction,
+                  warmstart_information);
+            std::swap(direction.multipliers, direction.feasibility_multipliers);
+            const double residual_lowest_violation = this->model.constraint_violation(current_iterate.evaluations.constraints +
+                  current_iterate.evaluations.constraint_jacobian * feasibility_direction.primals, Norm::L1);
+            DEBUG << "Lowest linearized infeasibility mk(dk): " << residual_lowest_violation << '\n';
+            this->subproblem->exit_feasibility_problem(this->feasibility_problem, current_iterate);
+
+            // stage f: update the penalty parameter based on the current dual error
+            this->decrease_parameter_aggressively(current_iterate, feasibility_direction);
+            if (this->penalty_parameter < current_penalty_parameter) {
+               this->solve_l1_relaxed_problem(statistics, current_iterate, direction, this->penalty_parameter, warmstart_information);
+               linearized_residual = this->model.constraint_violation(current_iterate.evaluations.constraints +
+                     current_iterate.evaluations.constraint_jacobian * direction.primals, Norm::L1);
+            }
+
+            // stage d: further decrease penalty parameter to reach a fraction of the ideal decrease
+            this->enforce_linearized_residual_sufficient_decrease(statistics, current_iterate, direction, linearized_residual,
+                  residual_lowest_violation, warmstart_information);
+            // stage e: further decrease penalty parameter to guarantee a descent direction for the l1 merit function
+            this->enforce_descent_direction_for_l1_merit(statistics, current_iterate, direction, feasibility_direction, warmstart_information);
+
+            // save the dual feasibility direction
+            direction.feasibility_multipliers = feasibility_direction.multipliers;
+         }
+      }
+   }
 
-      // recompute the linearized residual
-      linearized_residual = this->model.constraint_violation(current_iterate.evaluations.constraints +
-            current_iterate.evaluations.constraint_jacobian * direction.primals, Norm::L1);
-      DEBUG << "Linearized infeasibility mk(dk): " << linearized_residual << "\n\n";
+   void l1Relaxation::solve_subproblem(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
+         const Multipliers& current_multipliers, Direction& direction, const WarmstartInformation& warmstart_information) {
+      DEBUG << "Solving the subproblem with penalty parameter " << problem.get_objective_multiplier() << "\n\n";
+
+      // solve the subproblem
+      direction.set_dimensions(problem.number_variables, problem.number_constraints);
+      this->subproblem->solve(statistics, problem, current_iterate, current_multipliers, direction, warmstart_information);
+      direction.norm = norm_inf(view(direction.primals, 0, this->model.number_variables));
+      DEBUG3 << direction << '\n';
+      assert(direction.status == SubproblemStatus::OPTIMAL && "The subproblem was not solved to optimality");
    }
-   DEBUG << "Condition enforce_linearized_residual_sufficient_decrease is true\n";
-}
 
-bool l1Relaxation::linearized_residual_sufficient_decrease(const Iterate& current_iterate, double linearized_residual,
-      double residual_lowest_violation) const {
-   if (residual_lowest_violation <= this->parameters.residual_small_threshold) {
-      return (linearized_residual <= this->parameters.residual_small_threshold);
+   void l1Relaxation::solve_l1_relaxed_problem(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+         double current_penalty_parameter, const WarmstartInformation& warmstart_information) {
+      this->l1_relaxed_problem.set_objective_multiplier(current_penalty_parameter);
+      this->solve_subproblem(statistics, this->l1_relaxed_problem, current_iterate, current_iterate.multipliers, direction, warmstart_information);
    }
-   const double linearized_residual_reduction = current_iterate.progress.infeasibility - linearized_residual;
-   const double lowest_linearized_residual_reduction = current_iterate.progress.infeasibility - residual_lowest_violation;
-   if (lowest_linearized_residual_reduction < 0.) {
-      WARNING << "The lowest_linearized_residual_reduction quantity is negative. Negative curvature in your problem\n";
+
+   void l1Relaxation::decrease_parameter_aggressively(Iterate& current_iterate, const Direction& direction) {
+      this->trial_multipliers.constraints = current_iterate.feasibility_multipliers.constraints + direction.feasibility_multipliers.constraints;
+      this->trial_multipliers.lower_bounds = current_iterate.feasibility_multipliers.lower_bounds + direction.feasibility_multipliers.lower_bounds;
+      this->trial_multipliers.upper_bounds = current_iterate.feasibility_multipliers.upper_bounds + direction.feasibility_multipliers.upper_bounds;
+
+      // there must be at least a nonzero dual to avoid trivial stationary points
+      if (this->trial_multipliers.not_all_zero(this->model.number_variables, this->small_duals_threshold)) {
+         // compute the ideal error (with a zero penalty parameter)
+         const double infeasible_dual_error = l1Relaxation::compute_infeasible_dual_error(current_iterate);
+         DEBUG << "Ideal dual error: " << infeasible_dual_error << '\n';
+         const double scaled_error = infeasible_dual_error / std::max(1., current_iterate.residuals.primal_feasibility);
+         this->penalty_parameter = std::min(this->penalty_parameter, scaled_error * scaled_error);
+         DEBUG << "Further aggressively decrease the penalty parameter to " << this->penalty_parameter << '\n';
+      }
+      else {
+         DEBUG << RED << "l1Relaxation: all multipliers are almost 0. The penalty parameter won't be decreased" << RESET << '\n';
+      }
    }
-   return (linearized_residual_reduction >= this->parameters.epsilon1 * lowest_linearized_residual_reduction);
-}
-
-void l1Relaxation::enforce_descent_direction_for_l1_merit(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-      const Direction& feasibility_direction, WarmstartInformation& warmstart_information) {
-   while (0. < this->penalty_parameter && not this->is_descent_direction_for_l1_merit_function(current_iterate, direction, feasibility_direction)) {
-      // decrease the penalty parameter and re-solve the problem
-      this->penalty_parameter /= this->parameters.decrease_factor;
-      DEBUG << "Further decrease the penalty parameter to " << this->penalty_parameter << '\n';
-      this->solve_l1_relaxed_problem(statistics, current_iterate, direction, this->penalty_parameter, warmstart_information);
+
+   // measure that combines KKT error and complementarity error
+   double l1Relaxation::compute_infeasible_dual_error(Iterate& current_iterate) {
+      // stationarity error
+      this->evaluate_lagrangian_gradient(current_iterate.residuals.lagrangian_gradient, current_iterate, this->trial_multipliers);
+      double error = norm_1(current_iterate.residuals.lagrangian_gradient.constraints_contribution);
+
+      // complementarity error
+      const double shift_value = 0.;
+      error += this->feasibility_problem.complementarity_error(current_iterate.primals, current_iterate.evaluations.constraints,
+            this->trial_multipliers, shift_value, Norm::L1);
+      return error;
    }
-   DEBUG << "Condition enforce_descent_direction_for_l1_merit is true\n\n";
-}
-
-bool l1Relaxation::is_descent_direction_for_l1_merit_function(const Iterate& current_iterate, const Direction& direction,
-      const Direction& feasibility_direction) const {
-   const double predicted_l1_merit_reduction = current_iterate.residuals.infeasibility - direction.subproblem_objective;
-   const double lowest_decrease_objective = current_iterate.residuals.infeasibility - feasibility_direction.subproblem_objective;
-   return (predicted_l1_merit_reduction >= this->parameters.epsilon2 * lowest_decrease_objective);
-}
-
-void l1Relaxation::compute_progress_measures(Iterate& current_iterate, Iterate& trial_iterate, const Direction& /*direction*/, double /*step_length*/) {
-   if (this->subproblem->subproblem_definition_changed) {
-      DEBUG << "The subproblem definition changed\n";
-      this->globalization_strategy->reset();
-      this->subproblem->subproblem_definition_changed = false;
+
+   void l1Relaxation::enforce_linearized_residual_sufficient_decrease(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+         double linearized_residual, double residual_lowest_violation, WarmstartInformation& warmstart_information) {
+      while (0. < this->penalty_parameter && not this->linearized_residual_sufficient_decrease(current_iterate, linearized_residual,
+            residual_lowest_violation)) {
+         // decrease the penalty parameter and re-solve the problem
+         this->penalty_parameter /= this->parameters.decrease_factor;
+         DEBUG << "Further decrease the penalty parameter to " << this->penalty_parameter << '\n';
+         this->solve_l1_relaxed_problem(statistics, current_iterate, direction, this->penalty_parameter, warmstart_information);
+
+         // recompute the linearized residual
+         linearized_residual = this->model.constraint_violation(current_iterate.evaluations.constraints +
+               current_iterate.evaluations.constraint_jacobian * direction.primals, Norm::L1);
+         DEBUG << "Linearized infeasibility mk(dk): " << linearized_residual << "\n\n";
+      }
+      DEBUG << "Condition enforce_linearized_residual_sufficient_decrease is true\n";
    }
-   this->evaluate_progress_measures(current_iterate);
-   this->evaluate_progress_measures(trial_iterate);
-
-   trial_iterate.objective_multiplier = this->l1_relaxed_problem.get_objective_multiplier();
-}
-
-bool l1Relaxation::is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
-      double step_length) {
-   this->subproblem->postprocess_iterate(this->l1_relaxed_problem, trial_iterate);
-   this->compute_progress_measures(current_iterate, trial_iterate, direction, step_length);
-
-   bool accept_iterate = false;
-   if (direction.norm == 0.) {
-      DEBUG << "Zero step acceptable\n";
-      trial_iterate.evaluate_objective(this->model);
-      accept_iterate = true;
-      statistics.set("status", "accepted (0 primal step)");
+
+   bool l1Relaxation::linearized_residual_sufficient_decrease(const Iterate& current_iterate, double linearized_residual,
+         double residual_lowest_violation) const {
+      if (residual_lowest_violation <= this->parameters.residual_small_threshold) {
+         return (linearized_residual <= this->parameters.residual_small_threshold);
+      }
+      const double linearized_residual_reduction = current_iterate.progress.infeasibility - linearized_residual;
+      const double lowest_linearized_residual_reduction = current_iterate.progress.infeasibility - residual_lowest_violation;
+      if (lowest_linearized_residual_reduction < 0.) {
+         WARNING << "The lowest_linearized_residual_reduction quantity is negative. Negative curvature in your problem\n";
+      }
+      return (linearized_residual_reduction >= this->parameters.epsilon1 * lowest_linearized_residual_reduction);
+   }
+
+   void l1Relaxation::enforce_descent_direction_for_l1_merit(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+         const Direction& feasibility_direction, WarmstartInformation& warmstart_information) {
+      while (0. < this->penalty_parameter && not this->is_descent_direction_for_l1_merit_function(current_iterate, direction, feasibility_direction)) {
+         // decrease the penalty parameter and re-solve the problem
+         this->penalty_parameter /= this->parameters.decrease_factor;
+         DEBUG << "Further decrease the penalty parameter to " << this->penalty_parameter << '\n';
+         this->solve_l1_relaxed_problem(statistics, current_iterate, direction, this->penalty_parameter, warmstart_information);
+      }
+      DEBUG << "Condition enforce_descent_direction_for_l1_merit is true\n\n";
+   }
+
+   bool l1Relaxation::is_descent_direction_for_l1_merit_function(const Iterate& current_iterate, const Direction& direction,
+         const Direction& feasibility_direction) const {
+      const double predicted_l1_merit_reduction = current_iterate.residuals.primal_feasibility - direction.subproblem_objective;
+      const double lowest_decrease_objective = current_iterate.residuals.primal_feasibility - feasibility_direction.subproblem_objective;
+      return (predicted_l1_merit_reduction >= this->parameters.epsilon2 * lowest_decrease_objective);
+   }
+
+   bool l1Relaxation::is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
+         double step_length) {
+      this->subproblem->postprocess_iterate(this->l1_relaxed_problem, trial_iterate);
+      this->compute_progress_measures(current_iterate, trial_iterate);
+      trial_iterate.objective_multiplier = this->l1_relaxed_problem.get_objective_multiplier();
+
+      bool accept_iterate = false;
+      if (direction.norm == 0.) {
+         DEBUG << "Zero step acceptable\n";
+         trial_iterate.evaluate_objective(this->model);
+         accept_iterate = true;
+         statistics.set("status", "0 primal step");
+      }
+      else {
+         // invoke the globalization strategy for acceptance
+         const ProgressMeasures predicted_reduction = this->compute_predicted_reduction_models(current_iterate, direction, step_length);
+         accept_iterate = this->globalization_strategy->is_iterate_acceptable(statistics, current_iterate.progress, trial_iterate.progress,
+               predicted_reduction, this->penalty_parameter);
+      }
+      if (accept_iterate) {
+         this->check_exact_relaxation(trial_iterate);
+         // this->set_dual_residuals_statistics(statistics, trial_iterate);
+      }
+      this->set_progress_statistics(statistics, trial_iterate);
+      return accept_iterate;
    }
-   else {
-      // invoke the globalization strategy for acceptance
-      const ProgressMeasures predicted_reduction = this->compute_predicted_reduction_models(current_iterate, direction, step_length);
-      accept_iterate = this->globalization_strategy->is_iterate_acceptable(statistics, current_iterate.progress, trial_iterate.progress,
-            predicted_reduction, this->penalty_parameter);
+
+   void l1Relaxation::compute_primal_dual_residuals(Iterate& iterate) {
+      ConstraintRelaxationStrategy::compute_primal_dual_residuals(this->l1_relaxed_problem, this->feasibility_problem, iterate);
    }
-   if (accept_iterate) {
-      this->check_exact_relaxation(trial_iterate);
-      this->compute_primal_dual_residuals(trial_iterate);
-      this->set_dual_residuals_statistics(statistics, trial_iterate);
+
+   void l1Relaxation::evaluate_progress_measures(Iterate& iterate) const {
+      this->set_infeasibility_measure(iterate);
+      this->set_objective_measure(iterate);
+      this->subproblem->set_auxiliary_measure(this->model, iterate);
+   }
+
+   ProgressMeasures l1Relaxation::compute_predicted_reduction_models(Iterate& current_iterate, const Direction& direction, double step_length) {
+      return {
+         this->compute_predicted_infeasibility_reduction_model(current_iterate, direction.primals, step_length),
+         this->first_order_predicted_reduction ? this->compute_predicted_objective_reduction_model(current_iterate, direction.primals, step_length) :
+            this->compute_predicted_objective_reduction_model(current_iterate, direction.primals, step_length, this->subproblem->get_lagrangian_hessian()),
+         this->subproblem->compute_predicted_auxiliary_reduction_model(this->model, current_iterate, direction.primals, step_length)
+      };
    }
-   this->set_progress_statistics(statistics, trial_iterate);
-   return accept_iterate;
-}
-
-void l1Relaxation::compute_primal_dual_residuals(Iterate& iterate) {
-   ConstraintRelaxationStrategy::compute_primal_dual_residuals(this->l1_relaxed_problem, this->feasibility_problem, iterate);
-}
-
-void l1Relaxation::evaluate_progress_measures(Iterate& iterate) const {
-   this->set_infeasibility_measure(iterate);
-   this->set_objective_measure(iterate);
-   this->subproblem->set_auxiliary_measure(this->model, iterate);
-}
-
-ProgressMeasures l1Relaxation::compute_predicted_reduction_models(Iterate& current_iterate, const Direction& direction, double step_length) {
-   return {
-      this->compute_predicted_infeasibility_reduction_model(current_iterate, direction.primals, step_length),
-      this->compute_predicted_objective_reduction_model(current_iterate, direction.primals, step_length, this->subproblem->get_lagrangian_hessian()),
-      this->subproblem->compute_predicted_auxiliary_reduction_model(this->model, current_iterate, direction.primals, step_length)
-   };
-}
-
-void l1Relaxation::set_trust_region_radius(double trust_region_radius) {
-   this->subproblem->set_trust_region_radius(trust_region_radius);
-}
-
-size_t l1Relaxation::maximum_number_variables() const {
-   return this->l1_relaxed_problem.number_variables;
-}
-
-size_t l1Relaxation::maximum_number_constraints() const {
-   return this->l1_relaxed_problem.number_constraints;
-}
-
-// for information purposes, check that l1 is an exact relaxation
-void l1Relaxation::check_exact_relaxation(Iterate& iterate) const {
-   const double norm_inf_multipliers = norm_inf(iterate.multipliers.constraints);
-   if (0. < norm_inf_multipliers && this->penalty_parameter <= 1./norm_inf_multipliers) {
-      DEBUG << "The value of the penalty parameter is consistent with an exact relaxation\n\n";
+
+   size_t l1Relaxation::maximum_number_variables() const {
+      return this->l1_relaxed_problem.number_variables;
    }
-}
 
-void l1Relaxation::set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const {
-   statistics.set("complementarity", iterate.residuals.complementarity);
-   statistics.set("stationarity", iterate.residuals.KKT_stationarity);
-}
+   size_t l1Relaxation::maximum_number_constraints() const {
+      return this->l1_relaxed_problem.number_constraints;
+   }
 
-size_t l1Relaxation::get_hessian_evaluation_count() const {
-   return this->subproblem->get_hessian_evaluation_count();
-}
+   // for information purposes, check that l1 is an exact relaxation
+   void l1Relaxation::check_exact_relaxation(Iterate& iterate) const {
+      const double norm_inf_multipliers = norm_inf(iterate.multipliers.constraints);
+      if (0. < norm_inf_multipliers && this->penalty_parameter <= 1./norm_inf_multipliers) {
+         DEBUG << "The value of the penalty parameter is consistent with an exact relaxation\n\n";
+      }
+   }
 
-size_t l1Relaxation::get_number_subproblems_solved() const {
-   return this->subproblem->number_subproblems_solved;
-}
+   void l1Relaxation::set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const {
+      statistics.set("stationarity", iterate.residuals.stationarity);
+      statistics.set("complementarity", iterate.residuals.complementarity);
+   }
+} // namespace
diff --git a/uno/ingredients/constraint_relaxation_strategy/l1Relaxation.hpp b/uno/ingredients/constraint_relaxation_strategy/l1Relaxation.hpp
index 702c658..cefe3d5 100644
--- a/uno/ingredients/constraint_relaxation_strategy/l1Relaxation.hpp
+++ b/uno/ingredients/constraint_relaxation_strategy/l1Relaxation.hpp
@@ -6,83 +6,78 @@
 
 #include <memory>
 #include "ConstraintRelaxationStrategy.hpp"
-#include "ingredients/globalization_strategy/GlobalizationStrategy.hpp"
 #include "ingredients/subproblem/Subproblem.hpp"
 #include "reformulation/l1RelaxedProblem.hpp"
 
-struct l1RelaxationParameters {
-   bool fixed_parameter;
-   double decrease_factor;
-   double epsilon1;
-   double epsilon2;
-   double residual_small_threshold;
-};
-
-class l1Relaxation : public ConstraintRelaxationStrategy {
-public:
-   l1Relaxation(const Model& model, const Options& options);
-
-   void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) override;
-   void set_trust_region_radius(double trust_region_radius) override;
-
-   [[nodiscard]] size_t maximum_number_variables() const override;
-   [[nodiscard]] size_t maximum_number_constraints() const override;
-
-   // direction computation
-   void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-         WarmstartInformation& warmstart_information) override;
-   void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction, const Vector<double>& initial_point,
-         WarmstartInformation& warmstart_information) override;
-   [[nodiscard]] bool solving_feasibility_problem() const override;
-   void switch_to_feasibility_problem(Statistics& statistics, Iterate& current_iterate) override;
-
-   // trial iterate acceptance
-   void compute_progress_measures(Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction, double step_length) override;
-   [[nodiscard]] bool is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
-         double step_length) override;
-
-   // primal-dual residuals
-   void compute_primal_dual_residuals(Iterate& iterate) override;
-
-   [[nodiscard]] size_t get_hessian_evaluation_count() const override;
-   [[nodiscard]] size_t get_number_subproblems_solved() const override;
-
-protected:
-   const l1RelaxedProblem feasibility_problem;
-   l1RelaxedProblem l1_relaxed_problem;
-   std::unique_ptr<Subproblem> subproblem;
-   const std::unique_ptr<GlobalizationStrategy> globalization_strategy;
-   double penalty_parameter;
-   const double tolerance;
-   const l1RelaxationParameters parameters;
-   const double small_duals_threshold;
-   // preallocated temporary multipliers
-   Multipliers trial_multipliers;
-
-   void solve_sequence_of_relaxed_subproblems(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-         WarmstartInformation& warmstart_information);
-   void solve_l1_relaxed_problem(Statistics& statistics, Iterate& current_iterate, Direction& direction, double current_penalty_parameter,
-         const WarmstartInformation& warmstart_information);
-   void solve_subproblem(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
-         Direction& direction, const WarmstartInformation& warmstart_information);
-
-   // functions that decrease the penalty parameter to enforce particular conditions
-   void decrease_parameter_aggressively(Iterate& current_iterate, const Direction& direction);
-   double compute_infeasible_dual_error(Iterate& current_iterate);
-   void enforce_linearized_residual_sufficient_decrease(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-         double linearized_residual, double residual_lowest_violation, WarmstartInformation& warmstart_information);
-   [[nodiscard]] bool linearized_residual_sufficient_decrease(const Iterate& current_iterate, double linearized_residual,
-         double residual_lowest_violation) const;
-   void enforce_descent_direction_for_l1_merit(Statistics& statistics, Iterate& current_iterate, Direction& direction,
-         const Direction& feasibility_direction, WarmstartInformation& warmstart_information);
-   [[nodiscard]] bool is_descent_direction_for_l1_merit_function(const Iterate& current_iterate, const Direction& direction,
-         const Direction& feasibility_direction) const;
-
-   void evaluate_progress_measures(Iterate& iterate) const;
-   [[nodiscard]] ProgressMeasures compute_predicted_reduction_models(Iterate& current_iterate, const Direction& direction, double step_length);
-
-   void set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const override;
-   void check_exact_relaxation(Iterate& iterate) const;
-};
+namespace uno {
+   struct l1RelaxationParameters {
+      bool fixed_parameter;
+      double decrease_factor;
+      double epsilon1;
+      double epsilon2;
+      double residual_small_threshold;
+   };
+
+   class l1Relaxation : public ConstraintRelaxationStrategy {
+   public:
+      l1Relaxation(const Model& model, const Options& options);
+
+      void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) override;
+
+      [[nodiscard]] size_t maximum_number_variables() const override;
+      [[nodiscard]] size_t maximum_number_constraints() const override;
+
+      // direction computation
+      void compute_feasible_direction(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+            WarmstartInformation& warmstart_information) override;
+      [[nodiscard]] bool solving_feasibility_problem() const override;
+      void switch_to_feasibility_problem(Statistics& statistics, Iterate& current_iterate) override;
+
+      // trial iterate acceptance
+      [[nodiscard]] bool is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
+            double step_length) override;
+
+      // primal-dual residuals
+      void compute_primal_dual_residuals(Iterate& iterate) override;
+      void set_dual_residuals_statistics(Statistics& statistics, const Iterate& iterate) const override;
+
+   protected:
+      const l1RelaxedProblem feasibility_problem;
+      l1RelaxedProblem l1_relaxed_problem;
+      double penalty_parameter;
+      const double tolerance;
+      const l1RelaxationParameters parameters;
+      const double small_duals_threshold;
+      // preallocated temporary multipliers
+      Multipliers trial_multipliers;
+
+      // delegating constructor
+      l1Relaxation(const Model& model, l1RelaxedProblem&& feasibility_problem, l1RelaxedProblem&& l1_relaxed_problem, const Options& options);
+
+      void solve_sequence_of_relaxed_subproblems(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+            WarmstartInformation& warmstart_information);
+      void solve_l1_relaxed_problem(Statistics& statistics, Iterate& current_iterate, Direction& direction, double current_penalty_parameter,
+            const WarmstartInformation& warmstart_information);
+      void solve_subproblem(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
+            Direction& direction, const WarmstartInformation& warmstart_information);
+
+      // functions that decrease the penalty parameter to enforce particular conditions
+      void decrease_parameter_aggressively(Iterate& current_iterate, const Direction& direction);
+      double compute_infeasible_dual_error(Iterate& current_iterate);
+      void enforce_linearized_residual_sufficient_decrease(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+            double linearized_residual, double residual_lowest_violation, WarmstartInformation& warmstart_information);
+      [[nodiscard]] bool linearized_residual_sufficient_decrease(const Iterate& current_iterate, double linearized_residual,
+            double residual_lowest_violation) const;
+      void enforce_descent_direction_for_l1_merit(Statistics& statistics, Iterate& current_iterate, Direction& direction,
+            const Direction& feasibility_direction, WarmstartInformation& warmstart_information);
+      [[nodiscard]] bool is_descent_direction_for_l1_merit_function(const Iterate& current_iterate, const Direction& direction,
+            const Direction& feasibility_direction) const;
+
+      void evaluate_progress_measures(Iterate& iterate) const override;
+      [[nodiscard]] ProgressMeasures compute_predicted_reduction_models(Iterate& current_iterate, const Direction& direction, double step_length);
+
+      void check_exact_relaxation(Iterate& iterate) const;
+   };
+} // namespace
 
 #endif //UNO_L1RELAXATION_H
diff --git a/uno/ingredients/globalization_mechanism/BacktrackingLineSearch.cpp b/uno/ingredients/globalization_mechanism/BacktrackingLineSearch.cpp
index c5712c0..68e23a3 100644
--- a/uno/ingredients/globalization_mechanism/BacktrackingLineSearch.cpp
+++ b/uno/ingredients/globalization_mechanism/BacktrackingLineSearch.cpp
@@ -4,6 +4,7 @@
 #include <cassert>
 #include "ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.hpp"
 #include "BacktrackingLineSearch.hpp"
+#include "model/Model.hpp"
 #include "optimization/EvaluationErrors.hpp"
 #include "optimization/Iterate.hpp"
 #include "optimization/WarmstartInformation.hpp"
@@ -11,130 +12,143 @@
 #include "tools/Options.hpp"
 #include "tools/Statistics.hpp"
 
-BacktrackingLineSearch::BacktrackingLineSearch(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options):
-      GlobalizationMechanism(constraint_relaxation_strategy, options),
-      backtracking_ratio(options.get_double("LS_backtracking_ratio")),
-      minimum_step_length(options.get_double("LS_min_step_length")),
-      scale_duals_with_step_length(options.get_bool("LS_scale_duals_with_step_length")) {
-   // check the initial and minimal step lengths
-   assert(0 < this->backtracking_ratio && this->backtracking_ratio < 1. && "The LS backtracking ratio should be in (0, 1)");
-   assert(0 < this->minimum_step_length && this->minimum_step_length < 1. && "The LS minimum step length should be in (0, 1)");
-}
-
-void BacktrackingLineSearch::initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) {
-   statistics.add_column("LS iter", Statistics::int_width + 2, options.get_int("statistics_minor_column_order"));
-   statistics.add_column("step length", Statistics::double_width - 3, options.get_int("statistics_LS_step_length_column_order"));
-   
-   this->constraint_relaxation_strategy.initialize(statistics, initial_iterate, options);
-}
-
-void BacktrackingLineSearch::compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) {
-   WarmstartInformation warmstart_information{};
-   warmstart_information.set_hot_start();
-   DEBUG2 << "Current iterate\n" << current_iterate << '\n';
-
-   // compute the direction
-   this->constraint_relaxation_strategy.compute_feasible_direction(statistics, current_iterate, this->direction, warmstart_information);
-   BacktrackingLineSearch::check_unboundedness(this->direction);
-
-   // backtrack along the direction
-   this->total_number_iterations = 0;
-   this->backtrack_along_direction(statistics, model, current_iterate, trial_iterate, this->direction, warmstart_information);
-}
-
-// backtrack on the primal-dual step length computed by the subproblem
-void BacktrackingLineSearch::backtrack_along_direction(Statistics& statistics, const Model& model, Iterate& current_iterate,
-      Iterate& trial_iterate, const Direction& direction, WarmstartInformation& warmstart_information) {
-   // most subproblem methods return a step length of 1. Interior-point methods however apply the fraction-to-boundary condition
-   double step_length = 1.;
-   bool reached_small_step_length = false;
-   size_t number_iterations = 0;
-   while (not reached_small_step_length) {
-      this->total_number_iterations++;
-      number_iterations++;
-      if (1 < number_iterations) {
-         statistics.start_new_line();
-      }
-      BacktrackingLineSearch::print_iteration(number_iterations, step_length);
-      statistics.set("step length", step_length);
-
-      try {
-         // assemble the trial iterate by going a fraction along the direction
-         GlobalizationMechanism::assemble_trial_iterate(model, current_iterate, trial_iterate, direction, step_length,
-               // scale or not the constraint dual direction with the LS step length
-               this->scale_duals_with_step_length ? step_length : 1.);
-
-         // check whether the trial iterate is accepted
-         if (this->constraint_relaxation_strategy.is_iterate_acceptable(statistics, current_iterate, trial_iterate, direction, step_length)) {
-            // check termination criteria
-            trial_iterate.status = this->check_termination(model, trial_iterate);
-            this->set_statistics(statistics, trial_iterate, direction, step_length);
-            if (Logger::level == INFO) statistics.print_current_line();
-            return;
+namespace uno {
+   BacktrackingLineSearch::BacktrackingLineSearch(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options):
+         GlobalizationMechanism(constraint_relaxation_strategy),
+         backtracking_ratio(options.get_double("LS_backtracking_ratio")),
+         minimum_step_length(options.get_double("LS_min_step_length")),
+         scale_duals_with_step_length(options.get_bool("LS_scale_duals_with_step_length")) {
+      // check the initial and minimal step lengths
+      assert(0 < this->backtracking_ratio && this->backtracking_ratio < 1. && "The LS backtracking ratio should be in (0, 1)");
+      assert(0 < this->minimum_step_length && this->minimum_step_length < 1. && "The LS minimum step length should be in (0, 1)");
+   }
+
+   void BacktrackingLineSearch::initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) {
+      statistics.add_column("LS iter", Statistics::int_width + 2, options.get_int("statistics_minor_column_order"));
+      statistics.add_column("step length", Statistics::double_width - 4, options.get_int("statistics_LS_step_length_column_order"));
+      
+      this->constraint_relaxation_strategy.initialize(statistics, initial_iterate, options);
+   }
+
+   void BacktrackingLineSearch::compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) {
+      WarmstartInformation warmstart_information{};
+      warmstart_information.set_hot_start();
+      DEBUG2 << "Current iterate\n" << current_iterate << '\n';
+
+      // compute the direction
+      this->constraint_relaxation_strategy.compute_feasible_direction(statistics, current_iterate, this->direction, warmstart_information);
+      BacktrackingLineSearch::check_unboundedness(this->direction);
+
+      // backtrack along the direction
+      this->total_number_iterations = 0;
+      this->backtrack_along_direction(statistics, model, current_iterate, trial_iterate, warmstart_information);
+   }
+
+   // backtrack on the primal-dual step length computed by the subproblem
+   void BacktrackingLineSearch::backtrack_along_direction(Statistics& statistics, const Model& model, Iterate& current_iterate,
+         Iterate& trial_iterate, WarmstartInformation& warmstart_information) {
+      // most subproblem methods return a step length of 1. Interior-point methods however apply the fraction-to-boundary condition
+      double step_length = 1.;
+      bool reached_small_step_length = false;
+      size_t number_iterations = 0;
+      while (not reached_small_step_length) {
+         this->total_number_iterations++;
+         number_iterations++;
+         if (1 < number_iterations) {
+            statistics.start_new_line();
+         }
+         BacktrackingLineSearch::print_iteration(number_iterations, step_length);
+         statistics.set("step length", step_length);
+
+         try {
+            // assemble the trial iterate by going a fraction along the direction
+            GlobalizationMechanism::assemble_trial_iterate(model, current_iterate, trial_iterate, this->direction, step_length,
+                  // scale or not the constraint dual direction with the LS step length
+                  this->scale_duals_with_step_length ? step_length : 1.);
+
+            // check whether the trial iterate is accepted
+            const bool is_acceptable = this->constraint_relaxation_strategy.is_iterate_acceptable(statistics, current_iterate, trial_iterate,
+                  this->direction, step_length);
+            this->set_statistics(statistics, trial_iterate, this->direction, step_length);
+            if (is_acceptable) {
+               trial_iterate.status = this->constraint_relaxation_strategy.check_termination(trial_iterate);
+               this->constraint_relaxation_strategy.set_dual_residuals_statistics(statistics, trial_iterate);
+               if (Logger::level == INFO) statistics.print_current_line();
+               return;
+            }
+         }
+         catch (const EvaluationError& e) {
+            this->set_statistics(statistics);
+            statistics.set("status", "eval. error");
          }
+
          // small step length
-         else if (step_length < this->minimum_step_length) {
+         if (step_length < this->minimum_step_length) {
             DEBUG << "The line search step length is smaller than " << this->minimum_step_length << '\n';
             reached_small_step_length = true;
-            this->set_statistics(statistics, trial_iterate, direction, step_length);
          }
          else {
-            this->set_statistics(statistics, trial_iterate, direction, step_length);
             step_length = this->decrease_step_length(step_length);
          }
          if (Logger::level == INFO) statistics.print_current_line();
+      } // end while loop
+
+      // reached a small step length: revert to solving the feasibility problem
+      if (this->constraint_relaxation_strategy.solving_feasibility_problem()) {
+         throw std::runtime_error("Feasibility LS failed");
       }
-      catch (const EvaluationError& e) {
-         this->set_statistics(statistics);
-         statistics.set("status", "eval. error");
-         if (Logger::level == INFO) statistics.print_current_line();
-         step_length = this->decrease_step_length(step_length);
+      else if (not model.is_constrained()) {
+         throw std::runtime_error("Regular LS failed");
       }
-   }
+      else {
+         // check if we can terminate at a first-order point
+         trial_iterate.status = this->constraint_relaxation_strategy.check_termination(trial_iterate);
+         if (trial_iterate.status != TerminationStatus::NOT_OPTIMAL) {
+            statistics.set("status", "small step size");
+            this->constraint_relaxation_strategy.set_dual_residuals_statistics(statistics, trial_iterate);
+            return;
+         }
 
-   // reached a small step length: revert to solving the feasibility problem
-   if (this->constraint_relaxation_strategy.solving_feasibility_problem()) {
-      throw std::runtime_error("Feasibility LS failed");
+         // switch to solving the feasibility problem
+         warmstart_information.set_cold_start();
+         statistics.set("status", "LS failed");
+         this->constraint_relaxation_strategy.switch_to_feasibility_problem(statistics, current_iterate);
+         warmstart_information.set_cold_start();
+         this->constraint_relaxation_strategy.compute_feasible_direction(statistics, current_iterate, this->direction, this->direction.primals,
+               warmstart_information);
+         BacktrackingLineSearch::check_unboundedness(this->direction);
+         this->backtrack_along_direction(statistics, model, current_iterate, trial_iterate, warmstart_information);
+      }
    }
-   else {
-      warmstart_information.set_cold_start();
-      statistics.set("status", "LS failed");
-      this->constraint_relaxation_strategy.switch_to_feasibility_problem(statistics, current_iterate);
-      warmstart_information.set_cold_start();
-      this->constraint_relaxation_strategy.compute_feasible_direction(statistics, current_iterate, this->direction, this->direction.primals,
-            warmstart_information);
-      BacktrackingLineSearch::check_unboundedness(this->direction);
-      this->backtrack_along_direction(statistics, model, current_iterate, trial_iterate, this->direction, warmstart_information);
+
+   // step length follows the following sequence: 1, ratio, ratio^2, ratio^3, ...
+   double BacktrackingLineSearch::decrease_step_length(double step_length) const {
+      step_length *= this->backtracking_ratio;
+      assert(0 < step_length && step_length <= 1 && "The line-search step length is not in (0, 1]");
+      return step_length;
    }
-}
-
-// step length follows the following sequence: 1, ratio, ratio^2, ratio^3, ...
-double BacktrackingLineSearch::decrease_step_length(double step_length) const {
-   step_length *= this->backtracking_ratio;
-   assert(0 < step_length && step_length <= 1 && "The line-search step length is not in (0, 1]");
-   return step_length;
-}
-
-void BacktrackingLineSearch::check_unboundedness(const Direction& direction) {
-   if (direction.status == SubproblemStatus::UNBOUNDED_PROBLEM) {
-      throw std::runtime_error("The subproblem is unbounded, this should not happen. If the subproblem has curvature, use regularization. If not, "
-                               "use a trust-region method.\n");
+
+   void BacktrackingLineSearch::check_unboundedness(const Direction& direction) {
+      if (direction.status == SubproblemStatus::UNBOUNDED_PROBLEM) {
+         throw std::runtime_error("The subproblem is unbounded, this should not happen. If the subproblem has curvature, use regularization. If not, "
+                                  "use a trust-region method.\n");
+      }
    }
-}
 
-void BacktrackingLineSearch::set_statistics(Statistics& statistics) const {
-   statistics.set("LS iter", this->total_number_iterations);
-}
+   void BacktrackingLineSearch::set_statistics(Statistics& statistics) const {
+      statistics.set("LS iter", this->total_number_iterations);
+   }
 
-void BacktrackingLineSearch::set_statistics(Statistics& statistics, const Iterate& trial_iterate, const Direction& direction,
-      double primal_dual_step_length) const {
-   if (trial_iterate.is_objective_computed) {
-      statistics.set("objective", trial_iterate.evaluations.objective);
+   void BacktrackingLineSearch::set_statistics(Statistics& statistics, const Iterate& trial_iterate, const Direction& direction,
+         double primal_dual_step_length) const {
+      if (trial_iterate.is_objective_computed) {
+         statistics.set("objective", trial_iterate.evaluations.objective);
+      }
+      statistics.set("step norm", primal_dual_step_length * direction.norm);
+      this->set_statistics(statistics);
    }
-   statistics.set("step norm", primal_dual_step_length * direction.norm);
-   this->set_statistics(statistics);
-}
 
-void BacktrackingLineSearch::print_iteration(size_t number_iterations, double primal_dual_step_length) {
-   DEBUG << "\n\tLINE SEARCH iteration " << number_iterations << ", step_length " << primal_dual_step_length << '\n';
-}
+   void BacktrackingLineSearch::print_iteration(size_t number_iterations, double primal_dual_step_length) {
+      DEBUG << "\n\tLINE SEARCH iteration " << number_iterations << ", step_length " << primal_dual_step_length << '\n';
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_mechanism/BacktrackingLineSearch.hpp b/uno/ingredients/globalization_mechanism/BacktrackingLineSearch.hpp
index fed4356..a3c4985 100644
--- a/uno/ingredients/globalization_mechanism/BacktrackingLineSearch.hpp
+++ b/uno/ingredients/globalization_mechanism/BacktrackingLineSearch.hpp
@@ -6,26 +6,28 @@
 
 #include "GlobalizationMechanism.hpp"
 
-class BacktrackingLineSearch : public GlobalizationMechanism {
-public:
-   BacktrackingLineSearch(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options);
+namespace uno {
+   class BacktrackingLineSearch : public GlobalizationMechanism {
+   public:
+      BacktrackingLineSearch(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options);
 
-   void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) override;
-   void compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) override;
+      void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) override;
+      void compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) override;
 
-private:
-   const double backtracking_ratio;
-   const double minimum_step_length;
-   const bool scale_duals_with_step_length;
-   size_t total_number_iterations{0}; /*!< Total number of iterations (optimality and feasibility) */
+   private:
+      const double backtracking_ratio;
+      const double minimum_step_length;
+      const bool scale_duals_with_step_length;
+      size_t total_number_iterations{0}; /*!< Total number of iterations (optimality and feasibility) */
 
-   void backtrack_along_direction(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate,
-         const Direction& direction, WarmstartInformation& warmstart_information);
-   [[nodiscard]] double decrease_step_length(double step_length) const;
-   static void check_unboundedness(const Direction& direction);
-   void set_statistics(Statistics& statistics) const;
-   void set_statistics(Statistics& statistics, const Iterate& trial_iterate, const Direction& direction, double primal_dual_step_length) const;
-   static void print_iteration(size_t number_iterations, double primal_dual_step_length);
-};
+      void backtrack_along_direction(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate,
+            WarmstartInformation& warmstart_information);
+      [[nodiscard]] double decrease_step_length(double step_length) const;
+      static void check_unboundedness(const Direction& direction);
+      void set_statistics(Statistics& statistics) const;
+      void set_statistics(Statistics& statistics, const Iterate& trial_iterate, const Direction& direction, double primal_dual_step_length) const;
+      static void print_iteration(size_t number_iterations, double primal_dual_step_length);
+   };
+} // namespace
 
 #endif // UNO_BACKTRACKINGLINESEARCH_H
diff --git a/uno/ingredients/globalization_mechanism/GlobalizationMechanism.cpp b/uno/ingredients/globalization_mechanism/GlobalizationMechanism.cpp
index 9902aa2..ad2a0fd 100644
--- a/uno/ingredients/globalization_mechanism/GlobalizationMechanism.cpp
+++ b/uno/ingredients/globalization_mechanism/GlobalizationMechanism.cpp
@@ -7,107 +7,40 @@
 #include "optimization/Iterate.hpp"
 #include "symbolic/Expression.hpp"
 #include "tools/Options.hpp"
-#include "tools/Statistics.hpp"
 
-GlobalizationMechanism::GlobalizationMechanism(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options) :
-      constraint_relaxation_strategy(constraint_relaxation_strategy),
-      direction(this->constraint_relaxation_strategy.maximum_number_variables(), this->constraint_relaxation_strategy.maximum_number_constraints()),
-      tight_tolerance(options.get_double("tolerance")),
-      loose_tolerance(options.get_double("loose_tolerance")),
-      loose_tolerance_consecutive_iteration_threshold(options.get_unsigned_int("loose_tolerance_consecutive_iteration_threshold")),
-      progress_norm(norm_from_string(options.get_string("progress_norm"))),
-      unbounded_objective_threshold(options.get_double("unbounded_objective_threshold")) {
-}
-
-void GlobalizationMechanism::assemble_trial_iterate(const Model& model, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
-      double primal_step_length, double dual_step_length) {
-   trial_iterate.set_number_variables(current_iterate.primals.size());
-   // take primal step
-   trial_iterate.primals = current_iterate.primals + primal_step_length * direction.primals;
-   // project the trial iterate onto the bounds to avoid numerical errors
-   model.project_onto_variable_bounds(trial_iterate.primals);
-   // take dual step: line-search carried out only on constraint multipliers. Bound multipliers updated with full step
-   trial_iterate.multipliers.constraints = current_iterate.multipliers.constraints + dual_step_length * direction.multipliers.constraints;
-   trial_iterate.multipliers.lower_bounds = current_iterate.multipliers.lower_bounds + direction.multipliers.lower_bounds;
-   trial_iterate.multipliers.upper_bounds = current_iterate.multipliers.upper_bounds + direction.multipliers.upper_bounds;
-   trial_iterate.feasibility_multipliers.constraints = current_iterate.feasibility_multipliers.constraints + dual_step_length * direction.feasibility_multipliers.constraints;
-   trial_iterate.feasibility_multipliers.lower_bounds = current_iterate.feasibility_multipliers.lower_bounds + direction.feasibility_multipliers.lower_bounds;
-   trial_iterate.feasibility_multipliers.upper_bounds = current_iterate.feasibility_multipliers.upper_bounds + direction.feasibility_multipliers.upper_bounds;
-   trial_iterate.progress.reset();
-   trial_iterate.is_objective_computed = false;
-   trial_iterate.is_objective_gradient_computed = false;
-   trial_iterate.are_constraints_computed = false;
-   trial_iterate.is_constraint_jacobian_computed = false;
-   trial_iterate.status = TerminationStatus::NOT_OPTIMAL;
-}
-
-TerminationStatus GlobalizationMechanism::check_termination(const Model& model, Iterate& current_iterate) {
-   // test convergence wrt the tight tolerance
-   const TerminationStatus status_tight_tolerance = this->check_convergence_with_given_tolerance(model, current_iterate, this->tight_tolerance);
-   if (status_tight_tolerance != TerminationStatus::NOT_OPTIMAL || this->loose_tolerance <= this->tight_tolerance) {
-      return status_tight_tolerance;
-   }
-
-   // if not converged, check convergence wrt loose tolerance (provided it is strictly looser than the tight tolerance)
-   const TerminationStatus status_loose_tolerance = this->check_convergence_with_given_tolerance(model, current_iterate, this->loose_tolerance);
-   // if converged, keep track of the number of consecutive iterations
-   if (status_loose_tolerance != TerminationStatus::NOT_OPTIMAL) {
-      this->loose_tolerance_consecutive_iterations++;
-   }
-   else {
-      this->loose_tolerance_consecutive_iterations = 0;
-      return TerminationStatus::NOT_OPTIMAL;
-   }
-   // check if loose tolerance achieved for enough consecutive iterations
-   if (this->loose_tolerance_consecutive_iteration_threshold <= this->loose_tolerance_consecutive_iterations) {
-      return status_loose_tolerance;
-   }
-   else {
-      return TerminationStatus::NOT_OPTIMAL;
-   }
-}
-
-TerminationStatus GlobalizationMechanism::check_convergence_with_given_tolerance(const Model& model, Iterate& current_iterate, double tolerance) const {
-   // evaluate termination conditions based on optimality conditions
-   const bool KKT_stationarity = (current_iterate.residuals.KKT_stationarity / current_iterate.residuals.stationarity_scaling <= tolerance);
-   const bool FJ_stationarity = (current_iterate.residuals.FJ_stationarity <= tolerance);
-   const bool feasibility_stationarity = (current_iterate.residuals.feasibility_stationarity <= tolerance);
-   const bool complementarity = (current_iterate.residuals.complementarity / current_iterate.residuals.complementarity_scaling <= tolerance);
-   const bool feasibility_complementarity = (current_iterate.residuals.feasibility_complementarity <= tolerance);
-   const bool primal_feasibility = (current_iterate.residuals.infeasibility <= tolerance);
-   const bool no_trivial_duals = current_iterate.multipliers.not_all_zero(model.number_variables, tolerance);
-
-   DEBUG << "\nTermination criteria for tolerance = " << tolerance << ":\n";
-   DEBUG << "KKT stationarity: " << std::boolalpha << KKT_stationarity << '\n';
-   DEBUG << "FJ stationarity: " << std::boolalpha << FJ_stationarity << '\n';
-   DEBUG << "Stationarity (feasibility): " << std::boolalpha << feasibility_stationarity << '\n';
-   DEBUG << "Complementarity: " << std::boolalpha << complementarity << '\n';
-   DEBUG << "Complementarity (feasibility): " << std::boolalpha << feasibility_complementarity << '\n';
-   DEBUG << "Primal feasibility: " << std::boolalpha << primal_feasibility << '\n';
-   DEBUG << "Not all zero multipliers: " << std::boolalpha << no_trivial_duals << "\n\n";
-
-   if (current_iterate.is_objective_computed && current_iterate.evaluations.objective < this->unbounded_objective_threshold) {
-      return TerminationStatus::UNBOUNDED;
-   }
-   else if (KKT_stationarity && primal_feasibility && 0. < current_iterate.objective_multiplier && complementarity) {
-      // feasible regular stationary point
-      return TerminationStatus::FEASIBLE_KKT_POINT;
-   }
-   else if (FJ_stationarity && model.is_constrained() && primal_feasibility && complementarity && no_trivial_duals) {
-      // feasible but violation of CQ
-      return TerminationStatus::FEASIBLE_FJ_POINT;
-   }
-   else if (feasibility_stationarity && model.is_constrained() && not primal_feasibility && feasibility_complementarity) {
-      // no primal feasibility, stationary point of constraint violation
-      return TerminationStatus::INFEASIBLE_STATIONARY_POINT;
-   }
-   return TerminationStatus::NOT_OPTIMAL;
-}
-
-size_t GlobalizationMechanism::get_hessian_evaluation_count() const {
-   return this->constraint_relaxation_strategy.get_hessian_evaluation_count();
-}
-
-size_t GlobalizationMechanism::get_number_subproblems_solved() const {
-   return this->constraint_relaxation_strategy.get_number_subproblems_solved();
-}
+namespace uno {
+   GlobalizationMechanism::GlobalizationMechanism(ConstraintRelaxationStrategy& constraint_relaxation_strategy) :
+         constraint_relaxation_strategy(constraint_relaxation_strategy),
+         direction(this->constraint_relaxation_strategy.maximum_number_variables(), this->constraint_relaxation_strategy.maximum_number_constraints()) {
+   }
+
+   void GlobalizationMechanism::assemble_trial_iterate(const Model& model, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
+         double primal_step_length, double dual_step_length) {
+      trial_iterate.set_number_variables(current_iterate.primals.size());
+      // take primal step
+      trial_iterate.primals = current_iterate.primals + primal_step_length * direction.primals;
+      // project the trial iterate onto the bounds to avoid numerical errors
+      model.project_onto_variable_bounds(trial_iterate.primals);
+      // take dual step: line-search carried out only on constraint multipliers. Bound multipliers updated with full step
+      trial_iterate.multipliers.constraints = current_iterate.multipliers.constraints + dual_step_length * direction.multipliers.constraints;
+      trial_iterate.multipliers.lower_bounds = current_iterate.multipliers.lower_bounds + direction.multipliers.lower_bounds;
+      trial_iterate.multipliers.upper_bounds = current_iterate.multipliers.upper_bounds + direction.multipliers.upper_bounds;
+      trial_iterate.feasibility_multipliers.constraints = current_iterate.feasibility_multipliers.constraints + dual_step_length * direction.feasibility_multipliers.constraints;
+      trial_iterate.feasibility_multipliers.lower_bounds = current_iterate.feasibility_multipliers.lower_bounds + direction.feasibility_multipliers.lower_bounds;
+      trial_iterate.feasibility_multipliers.upper_bounds = current_iterate.feasibility_multipliers.upper_bounds + direction.feasibility_multipliers.upper_bounds;
+      trial_iterate.progress.reset();
+      trial_iterate.is_objective_computed = false;
+      trial_iterate.is_objective_gradient_computed = false;
+      trial_iterate.are_constraints_computed = false;
+      trial_iterate.is_constraint_jacobian_computed = false;
+      trial_iterate.status = TerminationStatus::NOT_OPTIMAL;
+   }
+
+   size_t GlobalizationMechanism::get_hessian_evaluation_count() const {
+      return this->constraint_relaxation_strategy.get_hessian_evaluation_count();
+   }
+
+   size_t GlobalizationMechanism::get_number_subproblems_solved() const {
+      return this->constraint_relaxation_strategy.get_number_subproblems_solved();
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_mechanism/GlobalizationMechanism.hpp b/uno/ingredients/globalization_mechanism/GlobalizationMechanism.hpp
index bb15d70..fd573b4 100644
--- a/uno/ingredients/globalization_mechanism/GlobalizationMechanism.hpp
+++ b/uno/ingredients/globalization_mechanism/GlobalizationMechanism.hpp
@@ -5,42 +5,34 @@
 #define UNO_GLOBALIZATIONMECHANISM_H
 
 #include "ingredients/subproblem/Direction.hpp"
-#include "linear_algebra/Norm.hpp"
-#include "optimization/TerminationStatus.hpp"
-
-// forward declarations
-class ConstraintRelaxationStrategy;
-class Iterate;
-class Model;
-class Options;
-class Statistics;
-
-class GlobalizationMechanism {
-public:
-   GlobalizationMechanism(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options);
-   virtual ~GlobalizationMechanism() = default;
-
-   virtual void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) = 0;
-   virtual void compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) = 0;
-
-   [[nodiscard]] size_t get_hessian_evaluation_count() const;
-   [[nodiscard]] size_t get_number_subproblems_solved() const;
-
-protected:
-   // reference to allow polymorphism
-   ConstraintRelaxationStrategy& constraint_relaxation_strategy; /*!< Constraint relaxation strategy */
-   Direction direction;
-   const double tight_tolerance; /*!< Tight tolerance of the termination criteria */
-   const double loose_tolerance; /*!< Loose tolerance of the termination criteria */
-   size_t loose_tolerance_consecutive_iterations{0};
-   const size_t loose_tolerance_consecutive_iteration_threshold;
-   const Norm progress_norm;
-   const double unbounded_objective_threshold;
-
-   static void assemble_trial_iterate(const Model& model, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
-         double primal_step_length, double dual_step_length);
-   [[nodiscard]] TerminationStatus check_termination(const Model& model, Iterate& current_iterate);
-   [[nodiscard]] TerminationStatus check_convergence_with_given_tolerance(const Model& model, Iterate& current_iterate, double tolerance) const;
-};
+
+namespace uno {
+   // forward declarations
+   class ConstraintRelaxationStrategy;
+   class Iterate;
+   class Model;
+   class Options;
+   class Statistics;
+
+   class GlobalizationMechanism {
+   public:
+      explicit GlobalizationMechanism(ConstraintRelaxationStrategy& constraint_relaxation_strategy);
+      virtual ~GlobalizationMechanism() = default;
+
+      virtual void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) = 0;
+      virtual void compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) = 0;
+
+      [[nodiscard]] size_t get_hessian_evaluation_count() const;
+      [[nodiscard]] size_t get_number_subproblems_solved() const;
+
+   protected:
+      // reference to allow polymorphism
+      ConstraintRelaxationStrategy& constraint_relaxation_strategy; /*!< Constraint relaxation strategy */
+      Direction direction;
+
+      static void assemble_trial_iterate(const Model& model, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
+            double primal_step_length, double dual_step_length);
+   };
+} // namespace
 
 #endif // UNO_GLOBALIZATIONMECHANISM_H
diff --git a/uno/ingredients/globalization_mechanism/GlobalizationMechanismFactory.cpp b/uno/ingredients/globalization_mechanism/GlobalizationMechanismFactory.cpp
index cb3898b..51d0655 100644
--- a/uno/ingredients/globalization_mechanism/GlobalizationMechanismFactory.cpp
+++ b/uno/ingredients/globalization_mechanism/GlobalizationMechanismFactory.cpp
@@ -5,18 +5,20 @@
 #include "ingredients/globalization_mechanism/TrustRegionStrategy.hpp"
 #include "ingredients/globalization_mechanism/BacktrackingLineSearch.hpp"
 
-std::unique_ptr<GlobalizationMechanism> GlobalizationMechanismFactory::create(
-      ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options) {
-   const std::string& mechanism_type = options.get_string("globalization_mechanism");
-    if (mechanism_type == "TR") {
-        return std::make_unique<TrustRegionStrategy>(constraint_relaxation_strategy, options);
-    }
-    else if (mechanism_type == "LS") {
-        return std::make_unique<BacktrackingLineSearch>(constraint_relaxation_strategy, options);
-    }
-    throw std::invalid_argument("GlobalizationMechanism " + mechanism_type + " is not supported");
-}
+namespace uno {
+   std::unique_ptr<GlobalizationMechanism> GlobalizationMechanismFactory::create(
+         ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options) {
+      const std::string& mechanism_type = options.get_string("globalization_mechanism");
+       if (mechanism_type == "TR") {
+           return std::make_unique<TrustRegionStrategy>(constraint_relaxation_strategy, options);
+       }
+       else if (mechanism_type == "LS") {
+           return std::make_unique<BacktrackingLineSearch>(constraint_relaxation_strategy, options);
+       }
+       throw std::invalid_argument("GlobalizationMechanism " + mechanism_type + " is not supported");
+   }
 
-std::vector<std::string> GlobalizationMechanismFactory::available_strategies() {
-   return {"TR", "LS"};
-}
+   std::vector<std::string> GlobalizationMechanismFactory::available_strategies() {
+      return {"TR", "LS"};
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_mechanism/GlobalizationMechanismFactory.hpp b/uno/ingredients/globalization_mechanism/GlobalizationMechanismFactory.hpp
index 5f1f576..f89ed09 100644
--- a/uno/ingredients/globalization_mechanism/GlobalizationMechanismFactory.hpp
+++ b/uno/ingredients/globalization_mechanism/GlobalizationMechanismFactory.hpp
@@ -9,10 +9,12 @@
 #include "ingredients/constraint_relaxation_strategy/ConstraintRelaxationStrategy.hpp"
 #include "tools/Options.hpp"
 
-class GlobalizationMechanismFactory {
-public:
-   static std::unique_ptr<GlobalizationMechanism> create(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options);
-   static std::vector<std::string> available_strategies();
-};
+namespace uno {
+   class GlobalizationMechanismFactory {
+   public:
+      static std::unique_ptr<GlobalizationMechanism> create(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options);
+      static std::vector<std::string> available_strategies();
+   };
+} // namespace
 
 #endif // UNO_GLOBALIZATIONMECHANISMFACTORY_H
diff --git a/uno/ingredients/globalization_mechanism/TrustRegionStrategy.cpp b/uno/ingredients/globalization_mechanism/TrustRegionStrategy.cpp
index d03c89b..118fa24 100644
--- a/uno/ingredients/globalization_mechanism/TrustRegionStrategy.cpp
+++ b/uno/ingredients/globalization_mechanism/TrustRegionStrategy.cpp
@@ -13,192 +13,193 @@
 #include "tools/Options.hpp"
 #include "tools/Statistics.hpp"
 
-TrustRegionStrategy::TrustRegionStrategy(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options) :
-      GlobalizationMechanism(constraint_relaxation_strategy, options),
-      radius(options.get_double("TR_radius")),
-      increase_factor(options.get_double("TR_increase_factor")),
-      decrease_factor(options.get_double("TR_decrease_factor")),
-      aggressive_decrease_factor(options.get_double("TR_aggressive_decrease_factor")),
-      activity_tolerance(options.get_double("TR_activity_tolerance")),
-      minimum_radius(options.get_double("TR_min_radius")),
-      radius_reset_threshold(options.get_double("TR_radius_reset_threshold")) {
-   assert(0 < this->radius && "The trust-region radius should be positive");
-   assert(1. < this->increase_factor && "The trust-region increase factor should be > 1");
-   assert(1. < this->decrease_factor && "The trust-region decrease factor should be > 1");
-}
-
-void TrustRegionStrategy::initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) {
-   statistics.add_column("TR iter", Statistics::int_width + 3, options.get_int("statistics_minor_column_order"));
-   statistics.add_column("TR radius", Statistics::double_width - 3, options.get_int("statistics_TR_radius_column_order"));
-   statistics.set("TR radius", this->radius);
-   
-   this->constraint_relaxation_strategy.set_trust_region_radius(this->radius);
-   this->constraint_relaxation_strategy.initialize(statistics, initial_iterate, options);
-}
-
-void TrustRegionStrategy::compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) {
-   WarmstartInformation warmstart_information{};
-   warmstart_information.set_hot_start();
-   DEBUG2 << "Current iterate\n" << current_iterate << '\n';
-
-   size_t number_iterations = 0;
-   while (true) { // TODO not very elegant
-      try {
-         number_iterations++;
-         this->print_iteration(number_iterations);
-         if (1 < number_iterations) {
-            statistics.start_new_line();
-         }
-         // compute the direction within the trust region
-         this->constraint_relaxation_strategy.set_trust_region_radius(this->radius);
-         this->constraint_relaxation_strategy.compute_feasible_direction(statistics, current_iterate, this->direction, warmstart_information);
-
-         // deal with errors in the subproblem
-         if (this->direction.status == SubproblemStatus::UNBOUNDED_PROBLEM) {
-            // the subproblem is always bounded, but the objective may exceed a very large negative value
-            this->set_statistics(statistics, this->direction, number_iterations);
-            statistics.set("status", "unbounded subproblem");
-            if (Logger::level == INFO) statistics.print_current_line();
-            this->decrease_radius_aggressively();
-            warmstart_information.set_cold_start();
+namespace uno {
+   TrustRegionStrategy::TrustRegionStrategy(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options) :
+         GlobalizationMechanism(constraint_relaxation_strategy),
+         radius(options.get_double("TR_radius")),
+         increase_factor(options.get_double("TR_increase_factor")),
+         decrease_factor(options.get_double("TR_decrease_factor")),
+         aggressive_decrease_factor(options.get_double("TR_aggressive_decrease_factor")),
+         activity_tolerance(options.get_double("TR_activity_tolerance")),
+         minimum_radius(options.get_double("TR_min_radius")),
+         radius_reset_threshold(options.get_double("TR_radius_reset_threshold")),
+         tolerance(options.get_double("tolerance")) {
+      assert(0 < this->radius && "The trust-region radius should be positive");
+      assert(1. < this->increase_factor && "The trust-region increase factor should be > 1");
+      assert(1. < this->decrease_factor && "The trust-region decrease factor should be > 1");
+   }
+
+   void TrustRegionStrategy::initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) {
+      statistics.add_column("TR iter", Statistics::int_width + 2, options.get_int("statistics_minor_column_order"));
+      statistics.add_column("TR radius", Statistics::double_width - 4, options.get_int("statistics_TR_radius_column_order"));
+      statistics.set("TR radius", this->radius);
+      
+      this->constraint_relaxation_strategy.set_trust_region_radius(this->radius);
+      this->constraint_relaxation_strategy.initialize(statistics, initial_iterate, options);
+   }
+
+   void TrustRegionStrategy::compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) {
+      WarmstartInformation warmstart_information{};
+      warmstart_information.set_hot_start();
+      DEBUG2 << "Current iterate\n" << current_iterate << '\n';
+
+      size_t number_iterations = 0;
+      bool termination = false;
+      while (not termination) {
+         try {
+            number_iterations++;
+            this->print_iteration(number_iterations);
+            if (1 < number_iterations) {
+               statistics.start_new_line();
+            }
+            // compute the direction within the trust region
+            this->constraint_relaxation_strategy.set_trust_region_radius(this->radius);
+            this->constraint_relaxation_strategy.compute_feasible_direction(statistics, current_iterate, this->direction, warmstart_information);
+
+            // deal with errors in the subproblem
+            if (this->direction.status == SubproblemStatus::UNBOUNDED_PROBLEM) {
+               // the subproblem is always bounded, but the objective may exceed a very large negative value
+               this->set_statistics(statistics, this->direction, number_iterations);
+               statistics.set("status", "unbounded subproblem");
+               if (Logger::level == INFO) statistics.print_current_line();
+               this->decrease_radius_aggressively();
+               warmstart_information.set_cold_start();
+            }
+            else if (this->direction.status == SubproblemStatus::ERROR) {
+               this->set_statistics(statistics, this->direction, number_iterations);
+               statistics.set("status", "solver error");
+               if (Logger::level == INFO) statistics.print_current_line();
+               this->decrease_radius(this->direction.norm);
+               warmstart_information.set_cold_start();
+            }
+            else {
+               // take full primal-dual step
+               GlobalizationMechanism::assemble_trial_iterate(model, current_iterate, trial_iterate, this->direction, 1., 1.);
+               this->reset_active_trust_region_multipliers(model, this->direction, trial_iterate);
+
+               // check whether the trial iterate (current iterate + full step) is acceptable
+               const bool is_acceptable = this->is_iterate_acceptable(statistics, current_iterate, trial_iterate, this->direction, number_iterations);
+               if (is_acceptable) {
+                  this->constraint_relaxation_strategy.set_dual_residuals_statistics(statistics, trial_iterate);
+                  this->reset_radius();
+                  termination = true;
+               }
+               else {
+                  this->decrease_radius(this->direction.norm);
+                  // after the first iteration, only the variable bounds are updated
+                  warmstart_information.only_variable_bounds_changed();
+               }
+               if (Logger::level == INFO) statistics.print_current_line();
+            }
          }
-         else if (this->direction.status == SubproblemStatus::ERROR) {
-            this->set_statistics(statistics, this->direction, number_iterations);
-            statistics.set("status", "solver error");
+         // if an evaluation error occurs, decrease the radius
+         catch (const EvaluationError& e) {
+            this->set_statistics(statistics, number_iterations);
+            statistics.set("status", "eval. error");
             if (Logger::level == INFO) statistics.print_current_line();
-            this->decrease_radius(this->direction.norm);
+            this->decrease_radius();
             warmstart_information.set_cold_start();
          }
-         else {
-            // take full primal-dual step
-            GlobalizationMechanism::assemble_trial_iterate(model, current_iterate, trial_iterate, this->direction, 1., 1.);
-            this->reset_active_trust_region_multipliers(model, this->direction, trial_iterate);
-
-            // check whether the trial iterate (current iterate + full step) is acceptable
-            if (this->is_iterate_acceptable(statistics, model, current_iterate, trial_iterate, this->direction, number_iterations)) {
-               this->reset_radius();
-               return;
-            }
-            else {
-               this->decrease_radius(this->direction.norm);
-               // after the first iteration, only the variable bounds are updated
-               warmstart_information.only_variable_bounds_changed();
-            }
+      }
+   }
+
+   void TrustRegionStrategy::reset_active_trust_region_multipliers(const Model& model, const Direction& direction, Iterate& trial_iterate) const {
+      assert(0 < this->radius && "The trust-region radius should be positive");
+      // set multipliers for bound constraints active at trust region to 0 (except if one of the original bounds is active)
+      for (size_t variable_index: direction.active_bounds.at_lower_bound) {
+         if (variable_index < model.number_variables && std::abs(direction.primals[variable_index] + this->radius) <= this->activity_tolerance &&
+             this->activity_tolerance < std::abs(trial_iterate.primals[variable_index] - model.variable_lower_bound(variable_index))) {
+            trial_iterate.multipliers.lower_bounds[variable_index] = 0.;
          }
       }
-      // if an evaluation error occurs, decrease the radius
-      catch (const EvaluationError& e) {
-         this->set_statistics(statistics, number_iterations);
-         statistics.set("status", "eval. error");
-         if (Logger::level == INFO) statistics.print_current_line();
-         this->decrease_radius();
-         warmstart_information.set_cold_start();
+      for (size_t variable_index: direction.active_bounds.at_upper_bound) {
+         if (variable_index < model.number_variables && std::abs(direction.primals[variable_index] - this->radius) <= this->activity_tolerance &&
+             this->activity_tolerance < std::abs(model.variable_upper_bound(variable_index) - trial_iterate.primals[variable_index])) {
+            trial_iterate.multipliers.upper_bounds[variable_index] = 0.;
+         }
       }
    }
-   throw std::runtime_error("TR strategy failed for unknown reasons, this should not happen.");
-}
-
-// the trial iterate is accepted by the constraint relaxation strategy or if the step is small and we cannot switch to solving the feasibility problem
-bool TrustRegionStrategy::is_iterate_acceptable(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate,
-      const Direction& direction, size_t number_iterations) {
-   // direction.primal_dual_step_length is usually 1, can be lower if reduced by fraction-to-boundary rule
-   bool accept_iterate = this->constraint_relaxation_strategy.is_iterate_acceptable(statistics, current_iterate, trial_iterate, direction, 1.);
-
-   this->set_statistics(statistics, trial_iterate, direction, number_iterations);
-   if (Logger::level == INFO) statistics.print_current_line();
-         
-   if (accept_iterate) {
-      // possibly increase the radius if trust region is active
-      this->possibly_increase_radius(direction.norm);
-
-      trial_iterate.status = this->check_termination(model, trial_iterate);
-      accept_iterate = true;
-   }
-   else if (this->radius < this->minimum_radius) { // rejected, but small radius
-      accept_iterate = this->check_termination_with_small_step(model, trial_iterate);
-   }
-   return accept_iterate;
-}
-
-void TrustRegionStrategy::possibly_increase_radius(double step_norm) {
-   // increase the radius if the trust-region is active
-   if (step_norm >= this->radius - this->activity_tolerance) {
-      this->radius *= this->increase_factor;
-      DEBUG << "Trust-region radius increased to " << this->radius << '\n';
+
+   // the trial iterate is accepted by the constraint relaxation strategy or if the step is small and we cannot switch to solving the feasibility problem
+   bool TrustRegionStrategy::is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate,
+         const Direction& direction, size_t number_iterations) {
+      // direction.primal_dual_step_length is usually 1, can be lower if reduced by fraction-to-boundary rule
+      bool accept_iterate = this->constraint_relaxation_strategy.is_iterate_acceptable(statistics, current_iterate, trial_iterate, direction, 1.);
+      this->set_statistics(statistics, trial_iterate, direction, number_iterations);
+      if (accept_iterate) {
+         trial_iterate.status = this->constraint_relaxation_strategy.check_termination(trial_iterate);
+         // possibly increase the radius if trust region is active
+         this->possibly_increase_radius(direction.norm);
+      }
+      else if (this->radius < this->minimum_radius) { // rejected, but small radius
+         accept_iterate = this->check_termination_with_small_step(trial_iterate);
+      }
+      return accept_iterate;
    }
-}
-
-void TrustRegionStrategy::decrease_radius(double step_norm) {
-   // reduce the radius to a value smaller than the primal step norm (otherwise, the reduction won't have an effect)
-   this->radius = std::min(this->radius, step_norm) / this->decrease_factor;
-   DEBUG << "Trust-region radius decreased to " << this->radius << '\n';
-}
-
-void TrustRegionStrategy::decrease_radius() {
-   this->radius /= this->decrease_factor;
-   DEBUG << "Trust-region radius decreased to " << this->radius << '\n';
-}
-
-void TrustRegionStrategy::decrease_radius_aggressively() {
-   this->radius /= this->aggressive_decrease_factor;
-   DEBUG << "Trust-region radius decreased to " << this->radius << '\n';
-}
-
-void TrustRegionStrategy::reset_radius() {
-   this->radius = std::max(this->radius, this->radius_reset_threshold);
-}
-
-void TrustRegionStrategy::reset_active_trust_region_multipliers(const Model& model, const Direction& direction, Iterate& trial_iterate) const {
-   assert(0 < this->radius && "The trust-region radius should be positive");
-   // set multipliers for bound constraints active at trust region to 0 (except if one of the original bounds is active)
-   for (size_t variable_index: direction.active_set.bounds.at_lower_bound) {
-      if (variable_index < model.number_variables && std::abs(direction.primals[variable_index] + this->radius) <= this->activity_tolerance &&
-            this->activity_tolerance < std::abs(trial_iterate.primals[variable_index] - model.variable_lower_bound(variable_index))) {
-         trial_iterate.multipliers.lower_bounds[variable_index] = 0.;
+
+   bool TrustRegionStrategy::check_termination_with_small_step(Iterate& trial_iterate) const {
+      // terminate with a feasible point
+      if (trial_iterate.progress.infeasibility <= this->tolerance) {
+         trial_iterate.status = TerminationStatus::FEASIBLE_SMALL_STEP;
+         this->constraint_relaxation_strategy.compute_primal_dual_residuals(trial_iterate);
+         return true;
+      }
+      else if (this->constraint_relaxation_strategy.solving_feasibility_problem()) { // terminate with an infeasible point
+         trial_iterate.status = TerminationStatus::INFEASIBLE_SMALL_STEP;
+         this->constraint_relaxation_strategy.compute_primal_dual_residuals(trial_iterate);
+         return true;
+      }
+      else { // do not terminate, infeasible non stationary
+         return false;
       }
    }
-   for (size_t variable_index: direction.active_set.bounds.at_upper_bound) {
-      if (variable_index < model.number_variables && std::abs(direction.primals[variable_index] - this->radius) <= this->activity_tolerance &&
-            this->activity_tolerance < std::abs(model.variable_upper_bound(variable_index) - trial_iterate.primals[variable_index])) {
-         trial_iterate.multipliers.upper_bounds[variable_index] = 0.;
+
+   void TrustRegionStrategy::possibly_increase_radius(double step_norm) {
+      // increase the radius if the trust-region is active
+      if (step_norm >= this->radius - this->activity_tolerance) {
+         this->radius *= this->increase_factor;
+         DEBUG << "Trust-region radius increased to " << this->radius << '\n';
       }
    }
-}
-
-bool TrustRegionStrategy::check_termination_with_small_step(const Model& /*model*/, Iterate& trial_iterate) const {
-   // terminate with a feasible point
-   if (trial_iterate.progress.infeasibility <= this->tight_tolerance) {
-      trial_iterate.status = TerminationStatus::FEASIBLE_SMALL_STEP;
-      this->constraint_relaxation_strategy.compute_primal_dual_residuals(trial_iterate);
-      return true;
+
+   void TrustRegionStrategy::decrease_radius(double step_norm) {
+      // reduce the radius to a value smaller than the primal step norm (otherwise, the reduction won't have an effect)
+      this->radius = std::min(this->radius, step_norm) / this->decrease_factor;
+      DEBUG << "Trust-region radius decreased to " << this->radius << '\n';
    }
-   else if (this->constraint_relaxation_strategy.solving_feasibility_problem()) { // terminate with an infeasible point
-      trial_iterate.status = TerminationStatus::INFEASIBLE_SMALL_STEP;
-      this->constraint_relaxation_strategy.compute_primal_dual_residuals(trial_iterate);
-      return true;
+
+   void TrustRegionStrategy::decrease_radius() {
+      this->radius /= this->decrease_factor;
+      DEBUG << "Trust-region radius decreased to " << this->radius << '\n';
    }
-   else { // do not terminate, infeasible non stationary
-      return false;
+
+   void TrustRegionStrategy::decrease_radius_aggressively() {
+      this->radius /= this->aggressive_decrease_factor;
+      DEBUG << "Trust-region radius decreased to " << this->radius << '\n';
    }
-}
 
-void TrustRegionStrategy::set_statistics(Statistics& statistics, size_t number_iterations) const {
-   statistics.set("TR iter", number_iterations);
-   statistics.set("TR radius", this->radius);
-}
+   void TrustRegionStrategy::reset_radius() {
+      this->radius = std::max(this->radius, this->radius_reset_threshold);
+   }
 
-void TrustRegionStrategy::set_statistics(Statistics& statistics, const Direction& direction, size_t number_iterations) const {
-   statistics.set("step norm", direction.norm);
-   this->set_statistics(statistics, number_iterations);
-}
+   void TrustRegionStrategy::set_statistics(Statistics& statistics, size_t number_iterations) const {
+      statistics.set("TR iter", number_iterations);
+      statistics.set("TR radius", this->radius);
+   }
 
-void TrustRegionStrategy::set_statistics(Statistics& statistics, const Iterate& trial_iterate, const Direction& direction, size_t number_iterations) const {
-   if (trial_iterate.is_objective_computed) {
-      statistics.set("objective", trial_iterate.evaluations.objective);
+   void TrustRegionStrategy::set_statistics(Statistics& statistics, const Direction& direction, size_t number_iterations) const {
+      statistics.set("step norm", direction.norm);
+      this->set_statistics(statistics, number_iterations);
    }
-   this->set_statistics(statistics, direction, number_iterations);
-}
 
-void TrustRegionStrategy::print_iteration(size_t number_iterations) {
-   DEBUG << "\n\t### Trust-region inner iteration " << number_iterations << " with radius " << this->radius << "\n\n";
-}
+   void TrustRegionStrategy::set_statistics(Statistics& statistics, const Iterate& trial_iterate, const Direction& direction, size_t number_iterations) const {
+      if (trial_iterate.is_objective_computed) {
+         statistics.set("objective", trial_iterate.evaluations.objective);
+      }
+      this->set_statistics(statistics, direction, number_iterations);
+   }
+
+   void TrustRegionStrategy::print_iteration(size_t number_iterations) {
+      DEBUG << "\n\t### Trust-region inner iteration " << number_iterations << " with radius " << this->radius << "\n\n";
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_mechanism/TrustRegionStrategy.hpp b/uno/ingredients/globalization_mechanism/TrustRegionStrategy.hpp
index eda619a..e836362 100644
--- a/uno/ingredients/globalization_mechanism/TrustRegionStrategy.hpp
+++ b/uno/ingredients/globalization_mechanism/TrustRegionStrategy.hpp
@@ -6,35 +6,38 @@
 
 #include "GlobalizationMechanism.hpp"
 
-class TrustRegionStrategy : public GlobalizationMechanism {
-public:
-   TrustRegionStrategy(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options);
+namespace uno {
+   class TrustRegionStrategy : public GlobalizationMechanism {
+   public:
+      TrustRegionStrategy(ConstraintRelaxationStrategy& constraint_relaxation_strategy, const Options& options);
 
-   void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) override;
-   void compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) override;
+      void initialize(Statistics& statistics, Iterate& initial_iterate, const Options& options) override;
+      void compute_next_iterate(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate) override;
 
-private:
-   double radius; /*!< Current trust region radius */
-   const double increase_factor;
-   const double decrease_factor;
-   const double aggressive_decrease_factor;
-   const double activity_tolerance;
-   const double minimum_radius;
-   const double radius_reset_threshold;
+   private:
+      double radius; /*!< Current trust region radius */
+      const double increase_factor;
+      const double decrease_factor;
+      const double aggressive_decrease_factor;
+      const double activity_tolerance;
+      const double minimum_radius;
+      const double radius_reset_threshold;
+      const double tolerance;
 
-   bool is_iterate_acceptable(Statistics& statistics, const Model& model, Iterate& current_iterate, Iterate& trial_iterate,
-         const Direction& direction, size_t number_iterations);
-   void possibly_increase_radius(double step_norm);
-   void decrease_radius(double step_norm);
-   void decrease_radius();
-   void decrease_radius_aggressively();
-   void reset_radius();
-   void reset_active_trust_region_multipliers(const Model& model, const Direction& direction, Iterate& trial_iterate) const;
-   bool check_termination_with_small_step(const Model& model, Iterate& trial_iterate) const;
-   void set_statistics(Statistics& statistics, size_t number_iterations) const;
-   void set_statistics(Statistics& statistics, const Direction& direction, size_t number_iterations) const;
-   void set_statistics(Statistics& statistics, const Iterate& trial_iterate, const Direction& direction, size_t number_iterations) const;
-   void print_iteration(size_t number_iterations);
-};
+      bool is_iterate_acceptable(Statistics& statistics, Iterate& current_iterate, Iterate& trial_iterate, const Direction& direction,
+            size_t number_iterations);
+      void possibly_increase_radius(double step_norm);
+      void decrease_radius(double step_norm);
+      void decrease_radius();
+      void decrease_radius_aggressively();
+      void reset_radius();
+      void reset_active_trust_region_multipliers(const Model& model, const Direction& direction, Iterate& trial_iterate) const;
+      bool check_termination_with_small_step(Iterate& trial_iterate) const;
+      void set_statistics(Statistics& statistics, size_t number_iterations) const;
+      void set_statistics(Statistics& statistics, const Direction& direction, size_t number_iterations) const;
+      void set_statistics(Statistics& statistics, const Iterate& trial_iterate, const Direction& direction, size_t number_iterations) const;
+      void print_iteration(size_t number_iterations);
+   };
+} // namespace
 
 #endif // UNO_TRUSTREGIONSTRATEGY_H
diff --git a/uno/ingredients/globalization_strategy/GlobalizationStrategy.cpp b/uno/ingredients/globalization_strategy/GlobalizationStrategy.cpp
index 82a222e..cf70708 100644
--- a/uno/ingredients/globalization_strategy/GlobalizationStrategy.cpp
+++ b/uno/ingredients/globalization_strategy/GlobalizationStrategy.cpp
@@ -5,12 +5,14 @@
 #include "GlobalizationStrategy.hpp"
 #include "tools/Options.hpp"
 
-GlobalizationStrategy::GlobalizationStrategy(const Options& options):
-   armijo_decrease_fraction(options.get_double("armijo_decrease_fraction")),
-   armijo_tolerance(options.get_double("armijo_tolerance")),
-   protect_actual_reduction_against_roundoff(options.get_bool("protect_actual_reduction_against_roundoff")) {
-}
+namespace uno {
+   GlobalizationStrategy::GlobalizationStrategy(const Options& options):
+      armijo_decrease_fraction(options.get_double("armijo_decrease_fraction")),
+      armijo_tolerance(options.get_double("armijo_tolerance")),
+      protect_actual_reduction_against_roundoff(options.get_bool("protect_actual_reduction_against_roundoff")) {
+   }
 
-bool GlobalizationStrategy::armijo_sufficient_decrease(double predicted_reduction, double actual_reduction) const {
-   return (actual_reduction >= this->armijo_decrease_fraction * std::max(0., predicted_reduction - this->armijo_tolerance));
-}
+   bool GlobalizationStrategy::armijo_sufficient_decrease(double predicted_reduction, double actual_reduction) const {
+      return (actual_reduction >= this->armijo_decrease_fraction * std::max(0., predicted_reduction - this->armijo_tolerance));
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/GlobalizationStrategy.hpp b/uno/ingredients/globalization_strategy/GlobalizationStrategy.hpp
index 45e531b..f135003 100644
--- a/uno/ingredients/globalization_strategy/GlobalizationStrategy.hpp
+++ b/uno/ingredients/globalization_strategy/GlobalizationStrategy.hpp
@@ -4,34 +4,38 @@
 #ifndef UNO_GLOBALIZATIONSTRATEGY_H
 #define UNO_GLOBALIZATIONSTRATEGY_H
 
-// forward declarations
-class Iterate;
-struct ProgressMeasures;
-class Statistics;
-class Options;
-
-/*! \class GlobalizationStrategy
- *  Ingredient that accepts or rejects a trial iterate
- */
-class GlobalizationStrategy {
-public:
-   explicit GlobalizationStrategy(const Options& options);
-   virtual ~GlobalizationStrategy() = default;
-
-   virtual void initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) = 0;
-   [[nodiscard]] virtual bool is_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
-         const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction, double objective_multiplier) = 0;
-   [[nodiscard]] virtual bool is_infeasibility_sufficiently_reduced(const ProgressMeasures& current_progress, const ProgressMeasures& trial_progress) const = 0;
-
-   virtual void reset() = 0;
-   virtual void register_current_progress(const ProgressMeasures& current_progress) = 0;
-
-protected:
-   const double armijo_decrease_fraction; /*!< Sufficient reduction constant */
-   const double armijo_tolerance;
-   const bool protect_actual_reduction_against_roundoff;
-
-   [[nodiscard]] bool armijo_sufficient_decrease(double predicted_reduction, double actual_reduction) const;
-};
+namespace uno {
+   // forward declarations
+   class Iterate;
+   struct ProgressMeasures;
+   class Statistics;
+   class Options;
+
+   /*! \class GlobalizationStrategy
+    *  Ingredient that accepts or rejects a trial iterate
+    */
+   class GlobalizationStrategy {
+   public:
+      explicit GlobalizationStrategy(const Options& options);
+      virtual ~GlobalizationStrategy() = default;
+
+      virtual void initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) = 0;
+      [[nodiscard]] virtual bool is_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+            const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction, double objective_multiplier) = 0;
+      [[nodiscard]] virtual bool is_infeasibility_sufficiently_reduced(const ProgressMeasures& reference_progress, const ProgressMeasures& trial_progress) const = 0;
+
+      virtual void reset() = 0;
+
+      virtual void notify_switch_to_feasibility(const ProgressMeasures& current_progress) = 0;
+      virtual void notify_switch_to_optimality(const ProgressMeasures& current_progress) = 0;
+
+   protected:
+      const double armijo_decrease_fraction; /*!< Sufficient reduction constant */
+      const double armijo_tolerance;
+      const bool protect_actual_reduction_against_roundoff;
+
+      [[nodiscard]] bool armijo_sufficient_decrease(double predicted_reduction, double actual_reduction) const;
+   };
+} // namespace
 
 #endif // UNO_GLOBALIZATIONSTRATEGY_H
diff --git a/uno/ingredients/globalization_strategy/GlobalizationStrategyFactory.cpp b/uno/ingredients/globalization_strategy/GlobalizationStrategyFactory.cpp
index b0bc4ba..8971eb9 100644
--- a/uno/ingredients/globalization_strategy/GlobalizationStrategyFactory.cpp
+++ b/uno/ingredients/globalization_strategy/GlobalizationStrategyFactory.cpp
@@ -4,22 +4,28 @@
 #include <stdexcept>
 #include "GlobalizationStrategyFactory.hpp"
 #include "l1MeritFunction.hpp"
-#include "filter_method/FletcherFilterMethod.hpp"
-#include "filter_method/WaechterFilterMethod.hpp"
+#include "switching_methods/filter_methods/FletcherFilterMethod.hpp"
+#include "switching_methods/filter_methods/WaechterFilterMethod.hpp"
+#include "switching_methods/funnel_methods/FunnelMethod.hpp"
 
-std::unique_ptr <GlobalizationStrategy> GlobalizationStrategyFactory::create(const std::string& strategy_type, const Options& options) {
-   if (strategy_type == "l1_merit") {
-      return std::make_unique<l1MeritFunction>(options);
+namespace uno {
+   std::unique_ptr <GlobalizationStrategy> GlobalizationStrategyFactory::create(const std::string& strategy_type, const Options& options) {
+      if (strategy_type == "l1_merit") {
+         return std::make_unique<l1MeritFunction>(options);
+      }
+      else if (strategy_type == "fletcher_filter_method") {
+         return std::make_unique<FletcherFilterMethod>(options);
+      }
+      else if (strategy_type == "waechter_filter_method") {
+         return std::make_unique<WaechterFilterMethod>(options);
+      }
+      else if (strategy_type == "funnel_method") {
+         return std::make_unique<FunnelMethod>(options);
+      }
+      throw std::invalid_argument("GlobalizationStrategy " + strategy_type + " is not supported");
    }
-   else if (strategy_type == "fletcher_filter_method") {
-      return std::make_unique<FletcherFilterMethod>(options);
-   }
-   else if (strategy_type == "waechter_filter_method") {
-      return std::make_unique<WaechterFilterMethod>(options);
-   }
-   throw std::invalid_argument("GlobalizationStrategy " + strategy_type + " is not supported");
-}
 
-std::vector<std::string> GlobalizationStrategyFactory::available_strategies() {
-   return {"l1_merit", "fletcher_filter_strategy", "waechter_filter_strategy"};
+   std::vector<std::string> GlobalizationStrategyFactory::available_strategies() {
+      return {"l1_merit", "fletcher_filter_method", "waechter_filter_method", "funnel_method"};
+   }
 }
diff --git a/uno/ingredients/globalization_strategy/GlobalizationStrategyFactory.hpp b/uno/ingredients/globalization_strategy/GlobalizationStrategyFactory.hpp
index b6ba6fa..76c906f 100644
--- a/uno/ingredients/globalization_strategy/GlobalizationStrategyFactory.hpp
+++ b/uno/ingredients/globalization_strategy/GlobalizationStrategyFactory.hpp
@@ -8,13 +8,15 @@
 #include <vector>
 #include "GlobalizationStrategy.hpp"
 
-// forward declaration
-class Options;
+namespace uno {
+   // forward declaration
+   class Options;
 
-class GlobalizationStrategyFactory {
-public:
-   static std::unique_ptr<GlobalizationStrategy> create(const std::string& strategy_type, const Options& options);
-   static std::vector<std::string> available_strategies();
-};
+   class GlobalizationStrategyFactory {
+   public:
+      static std::unique_ptr<GlobalizationStrategy> create(const std::string& strategy_type, const Options& options);
+      static std::vector<std::string> available_strategies();
+   };
+} // namespace
 
 #endif // UNO_GLOBALIZATIONSTRATEGYFACTORY_H
diff --git a/uno/ingredients/globalization_strategy/ProgressMeasures.hpp b/uno/ingredients/globalization_strategy/ProgressMeasures.hpp
index 9a12c9e..c80aac3 100644
--- a/uno/ingredients/globalization_strategy/ProgressMeasures.hpp
+++ b/uno/ingredients/globalization_strategy/ProgressMeasures.hpp
@@ -7,16 +7,18 @@
 #include <functional>
 #include "tools/Infinity.hpp"
 
-struct ProgressMeasures {
-   double infeasibility{}; // constraint violation
-   std::function<double(double objective_multiplier)> objective{}; // objective measure (scaled by penalty parameter): objective, Lagrangian
-   double auxiliary{}; // auxiliary terms (independent of penalty parameter): barrier terms, proximal term, ...
+namespace uno {
+   struct ProgressMeasures {
+      double infeasibility{}; // constraint violation
+      std::function<double(double objective_multiplier)> objective{}; // objective measure (scaled by penalty parameter): objective, Lagrangian
+      double auxiliary{}; // auxiliary terms (independent of penalty parameter): barrier terms, proximal term, ...
 
-   void reset() {
-      this->infeasibility = INF<double>;
-      this->objective = [](double) { return INF<double>; };
-      this->auxiliary = INF<double>;
-   }
-};
+      void reset() {
+         this->infeasibility = INF<double>;
+         this->objective = [](double) { return INF<double>; };
+         this->auxiliary = INF<double>;
+      }
+   };
+} // namespace
 
 #endif // UNO_PROGRESSMEASURES_H
diff --git a/uno/ingredients/globalization_strategy/l1MeritFunction.cpp b/uno/ingredients/globalization_strategy/l1MeritFunction.cpp
index 954a544..6be8a3e 100644
--- a/uno/ingredients/globalization_strategy/l1MeritFunction.cpp
+++ b/uno/ingredients/globalization_strategy/l1MeritFunction.cpp
@@ -7,64 +7,69 @@
 #include "tools/Options.hpp"
 #include "tools/Statistics.hpp"
 
-l1MeritFunction::l1MeritFunction(const Options& options): GlobalizationStrategy(options) {
-}
-
-void l1MeritFunction::initialize(Statistics& statistics, const Iterate& /*initial_iterate*/, const Options& options) {
-   statistics.add_column("penalty param.", Statistics::double_width, options.get_int("statistics_penalty_parameter_column_order"));
-}
+namespace uno {
+   l1MeritFunction::l1MeritFunction(const Options& options): GlobalizationStrategy(options) {
+   }
 
-void l1MeritFunction::reset() {
-}
+   void l1MeritFunction::initialize(Statistics& statistics, const Iterate& /*initial_iterate*/, const Options& options) {
+      statistics.add_column("penalty param.", Statistics::double_width, options.get_int("statistics_penalty_parameter_column_order"));
+   }
 
-void l1MeritFunction::register_current_progress(const ProgressMeasures& /*current_progress*/) {
-}
+   void l1MeritFunction::reset() {
+   }
 
-bool l1MeritFunction::is_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
-      const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction, double objective_multiplier) {
-   // predicted reduction with all contributions. This quantity should be positive (= negative directional derivative)
-   double constrained_predicted_reduction = l1MeritFunction::constrained_merit_function(predicted_reduction, objective_multiplier);
-   DEBUG << "Constrained predicted reduction: " << constrained_predicted_reduction << '\n';
-   if (constrained_predicted_reduction <= 0.) {
-      WARNING << YELLOW << "The direction is not a descent direction for the merit function. You should decrease the penalty parameter.\n" << RESET;
+   void l1MeritFunction::notify_switch_to_feasibility(const ProgressMeasures& /*current_progress*/) {
    }
-   // compute current exact penalty
-   const double current_merit_value = l1MeritFunction::constrained_merit_function(current_progress, objective_multiplier);
-   const double trial_merit_value = l1MeritFunction::constrained_merit_function(trial_progress, objective_multiplier);
-   const double actual_reduction = this->compute_merit_actual_reduction(current_merit_value, trial_merit_value);
-   DEBUG << "Current merit: " << current_merit_value << '\n';
-   DEBUG << "Trial merit:   " << trial_merit_value << '\n';
-   DEBUG << "Actual reduction: " << current_merit_value << " - " << trial_merit_value << " = " << actual_reduction << '\n';
-   statistics.set("penalty param.", objective_multiplier);
 
-   // Armijo sufficient decrease condition
-   const bool accept = this->armijo_sufficient_decrease(constrained_predicted_reduction, actual_reduction);
-   if (accept) {
-      DEBUG << "Trial iterate was accepted by satisfying Armijo condition\n";
-      this->smallest_known_infeasibility = std::min(this->smallest_known_infeasibility, trial_progress.infeasibility);
-      statistics.set("status", "accepted (Armijo)");
+   void l1MeritFunction::notify_switch_to_optimality(const ProgressMeasures& /*current_progress*/) {
    }
-   else {
-      statistics.set("status", "rejected (Armijo)");
+
+   bool l1MeritFunction::is_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+         const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction, double objective_multiplier) {
+      // predicted reduction with all contributions. This quantity should be positive (= negative directional derivative)
+      double constrained_predicted_reduction = l1MeritFunction::constrained_merit_function(predicted_reduction, objective_multiplier);
+      DEBUG << "Constrained predicted reduction: " << constrained_predicted_reduction << '\n';
+      if (constrained_predicted_reduction <= 0.) {
+         WARNING << YELLOW << "The direction is not a descent direction for the merit function. You should decrease the penalty parameter.\n" << RESET;
+      }
+      // compute current exact penalty
+      const double current_merit_value = l1MeritFunction::constrained_merit_function(current_progress, objective_multiplier);
+      const double trial_merit_value = l1MeritFunction::constrained_merit_function(trial_progress, objective_multiplier);
+      const double actual_reduction = this->compute_merit_actual_reduction(current_merit_value, trial_merit_value);
+      DEBUG << "Current merit: " << current_merit_value << '\n';
+      DEBUG << "Trial merit:   " << trial_merit_value << '\n';
+      DEBUG << "Actual reduction: " << current_merit_value << " - " << trial_merit_value << " = " << actual_reduction << '\n';
+      statistics.set("penalty param.", objective_multiplier);
+
+      // Armijo sufficient decrease condition
+      const bool accept = this->armijo_sufficient_decrease(constrained_predicted_reduction, actual_reduction);
+      if (accept) {
+         DEBUG << "Trial iterate was accepted by satisfying Armijo condition\n";
+         this->smallest_known_infeasibility = std::min(this->smallest_known_infeasibility, trial_progress.infeasibility);
+         statistics.set("status", "accepted (Armijo)");
+      }
+      else {
+         statistics.set("status", "rejected (Armijo)");
+      }
+      return accept;
    }
-   return accept;
-}
 
-bool l1MeritFunction::is_infeasibility_sufficiently_reduced(const ProgressMeasures& /*current_progress*/, const ProgressMeasures& trial_progress) const {
-   // if the trial infeasibility improves upon the best known infeasibility
-   // TODO put constant in option file
-   return (trial_progress.infeasibility <= 0.9*this->smallest_known_infeasibility);
-}
+   bool l1MeritFunction::is_infeasibility_sufficiently_reduced(const ProgressMeasures& /*current_progress*/, const ProgressMeasures& trial_progress) const {
+      // if the trial infeasibility improves upon the best known infeasibility
+      // TODO put constant in option file
+      return (trial_progress.infeasibility <= 0.9*this->smallest_known_infeasibility);
+   }
 
-double l1MeritFunction::constrained_merit_function(const ProgressMeasures& progress, double objective_multiplier) {
-   return progress.objective(objective_multiplier) + progress.auxiliary + progress.infeasibility;
-}
+   double l1MeritFunction::constrained_merit_function(const ProgressMeasures& progress, double objective_multiplier) {
+      return progress.objective(objective_multiplier) + progress.auxiliary + progress.infeasibility;
+   }
 
-double l1MeritFunction::compute_merit_actual_reduction(double current_merit_value, double trial_merit_value) const {
-   double actual_reduction = current_merit_value - trial_merit_value;
-   if (this->protect_actual_reduction_against_roundoff) {
-      static double machine_epsilon = std::numeric_limits<double>::epsilon();
-      actual_reduction += 10. * machine_epsilon * std::abs(current_merit_value);
+   double l1MeritFunction::compute_merit_actual_reduction(double current_merit_value, double trial_merit_value) const {
+      double actual_reduction = current_merit_value - trial_merit_value;
+      if (this->protect_actual_reduction_against_roundoff) {
+         static double machine_epsilon = std::numeric_limits<double>::epsilon();
+         actual_reduction += 10. * machine_epsilon * std::abs(current_merit_value);
+      }
+      return actual_reduction;
    }
-   return actual_reduction;
-}
\ No newline at end of file
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/l1MeritFunction.hpp b/uno/ingredients/globalization_strategy/l1MeritFunction.hpp
index 345f845..48d1ed9 100644
--- a/uno/ingredients/globalization_strategy/l1MeritFunction.hpp
+++ b/uno/ingredients/globalization_strategy/l1MeritFunction.hpp
@@ -7,22 +7,25 @@
 #include "GlobalizationStrategy.hpp"
 #include "tools/Infinity.hpp"
 
-class l1MeritFunction : public GlobalizationStrategy {
-public:
-   explicit l1MeritFunction(const Options& options);
+namespace uno {
+   class l1MeritFunction : public GlobalizationStrategy {
+   public:
+      explicit l1MeritFunction(const Options& options);
 
-   void initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) override;
-   [[nodiscard]] bool is_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
-         const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction, double objective_multiplier) override;
-   [[nodiscard]] bool is_infeasibility_sufficiently_reduced(const ProgressMeasures& current_progress, const ProgressMeasures& trial_progress) const override;
-   void reset() override;
-   void register_current_progress(const ProgressMeasures& current_progress) override;
+      void initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) override;
+      [[nodiscard]] bool is_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+            const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction, double objective_multiplier) override;
+      [[nodiscard]] bool is_infeasibility_sufficiently_reduced(const ProgressMeasures& current_progress, const ProgressMeasures& trial_progress) const override;
+      void reset() override;
+      void notify_switch_to_feasibility(const ProgressMeasures& current_progress) override;
+      void notify_switch_to_optimality(const ProgressMeasures& current_progress) override;
 
-protected:
-   double smallest_known_infeasibility{INF<double>};
+   protected:
+      double smallest_known_infeasibility{INF<double>};
 
-   [[nodiscard]] static double constrained_merit_function(const ProgressMeasures& progress, double objective_multiplier);
-   [[nodiscard]] double compute_merit_actual_reduction(double current_merit_value, double trial_merit_value) const;
-};
+      [[nodiscard]] static double constrained_merit_function(const ProgressMeasures& progress, double objective_multiplier);
+      [[nodiscard]] double compute_merit_actual_reduction(double current_merit_value, double trial_merit_value) const;
+   };
+} // namespace
 
 #endif // UNO_MERITFUNCTION_H
diff --git a/uno/ingredients/globalization_strategy/switching_methods/SwitchingMethod.cpp b/uno/ingredients/globalization_strategy/switching_methods/SwitchingMethod.cpp
new file mode 100644
index 0000000..645480d
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/SwitchingMethod.cpp
@@ -0,0 +1,66 @@
+// Copyright (c) 2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include <cmath>
+#include "SwitchingMethod.hpp"
+#include "../ProgressMeasures.hpp"
+#include "optimization/Iterate.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Options.hpp"
+#include "tools/Statistics.hpp"
+
+namespace uno {
+   SwitchingMethod::SwitchingMethod(const Options& options): GlobalizationStrategy(options),
+      delta(options.get_double("switching_delta")),
+      switching_infeasibility_exponent(options.get_double("switching_infeasibility_exponent")) { }
+
+   double SwitchingMethod::unconstrained_merit_function(const ProgressMeasures& progress) {
+      return progress.objective(1.) + progress.auxiliary;
+   }
+
+   bool SwitchingMethod::switching_condition(double predicted_reduction, double current_infeasibility) const {
+      return predicted_reduction > this->delta * std::pow(current_infeasibility, this->switching_infeasibility_exponent);
+   }
+
+   /* check acceptability of step
+    * switching methods enforce an *unconstrained* sufficient decrease condition
+    * precondition: feasible step
+    * */
+   bool SwitchingMethod::is_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+         const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction, double objective_multiplier) {
+      this->set_statistics(statistics);
+      const bool solving_feasibility_problem = (objective_multiplier == 0.);
+      if (solving_feasibility_problem) {
+         return this->is_feasibility_iterate_acceptable(statistics, current_progress, trial_progress, predicted_reduction);
+      }
+      else {
+         return this->is_regular_iterate_acceptable(statistics, current_progress, trial_progress, predicted_reduction);
+      }
+   }
+
+   // solving the feasibility problem = working on infeasibility only (no filter acceptability test)
+   bool SwitchingMethod::is_feasibility_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+         const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction) {
+      // drop the objective measure and focus on infeasibility and auxiliary terms (barrier, proximal, ...)
+      const double current_merit = current_progress.infeasibility + current_progress.auxiliary;
+      const double trial_merit = trial_progress.infeasibility + trial_progress.auxiliary;
+      const double predicted_merit_reduction = predicted_reduction.infeasibility + predicted_reduction.auxiliary;
+      const double actual_merit_reduction = current_merit - trial_merit;
+      DEBUG << "Current merit = " << current_merit << '\n';
+      DEBUG << "Trial merit = " << trial_merit << '\n';
+      DEBUG << "Predicted merit reduction = " << predicted_merit_reduction << '\n';
+      DEBUG << "Actual merit reduction = " << actual_merit_reduction << '\n';
+
+      bool accept = false;
+      if (this->armijo_sufficient_decrease(predicted_merit_reduction, actual_merit_reduction)) {
+         DEBUG << "Trial iterate (h-type) was accepted by satisfying the Armijo condition\n";
+         accept = true;
+      }
+      else {
+         DEBUG << "Trial iterate (h-type) was rejected by violating the Armijo condition\n";
+      }
+      Iterate::number_eval_objective--;
+      statistics.set("status", std::string(accept ? "accepted" : "rejected") + " (restoration)");
+      return accept;
+   }
+} // namespace
\ No newline at end of file
diff --git a/uno/ingredients/globalization_strategy/switching_methods/SwitchingMethod.hpp b/uno/ingredients/globalization_strategy/switching_methods/SwitchingMethod.hpp
new file mode 100644
index 0000000..779d499
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/SwitchingMethod.hpp
@@ -0,0 +1,32 @@
+// Copyright (c) 2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_SWITCHINGMETHOD_H
+#define UNO_SWITCHINGMETHOD_H
+
+#include "../GlobalizationStrategy.hpp"
+
+namespace uno {
+   class SwitchingMethod : public GlobalizationStrategy {
+   public:
+      explicit SwitchingMethod(const Options& options);
+      ~SwitchingMethod() override = default;
+
+      bool is_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress, const ProgressMeasures& trial_progress,
+            const ProgressMeasures& predicted_reduction, double objective_multiplier) override;
+
+   protected:
+      const double delta;
+      const double switching_infeasibility_exponent;
+
+      [[nodiscard]] virtual bool is_regular_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+            const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction) = 0;
+      [[nodiscard]] static double unconstrained_merit_function(const ProgressMeasures& progress);
+      [[nodiscard]] bool switching_condition(double predicted_reduction, double current_infeasibility) const;
+      [[nodiscard]] bool is_feasibility_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+            const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction);
+      virtual void set_statistics(Statistics& statistics) const = 0;
+   };
+} // namespace
+
+#endif // UNO_SWITCHINGMETHOD_H
\ No newline at end of file
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FilterMethod.cpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FilterMethod.cpp
new file mode 100644
index 0000000..c383dc0
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FilterMethod.cpp
@@ -0,0 +1,53 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include "FilterMethod.hpp"
+#include "filters/FilterFactory.hpp"
+#include "optimization/Iterate.hpp"
+#include "tools/Options.hpp"
+#include "tools/Statistics.hpp"
+
+namespace uno {
+   FilterMethod::FilterMethod(const Options& options) :
+         SwitchingMethod(options),
+         filter(FilterFactory::create(options)),
+         parameters({
+            options.get_double("filter_ubd"),
+            options.get_double("filter_fact"),
+         }) {
+   }
+
+   void FilterMethod::initialize(Statistics& /*statistics*/, const Iterate& initial_iterate, const Options& /*options*/) {
+      // set the filter upper bound
+      const double upper_bound = std::max(this->parameters.upper_bound, this->parameters.infeasibility_factor * initial_iterate.progress.infeasibility);
+      this->filter->set_infeasibility_upper_bound(upper_bound);
+   }
+
+   void FilterMethod::reset() {
+      this->filter->reset();
+   }
+
+   void FilterMethod::notify_switch_to_feasibility(const ProgressMeasures& current_progress) {
+      const double current_objective_measure = SwitchingMethod::unconstrained_merit_function(current_progress);
+      this->filter->add(current_progress.infeasibility, current_objective_measure);
+   }
+
+   void FilterMethod::notify_switch_to_optimality(const ProgressMeasures& current_progress) {
+      const double current_objective_measure = SwitchingMethod::unconstrained_merit_function(current_progress);
+      this->filter->add(current_progress.infeasibility, current_objective_measure);
+   }
+
+   double FilterMethod::compute_actual_objective_reduction(double current_objective_measure, double current_infeasibility, double trial_objective_measure) {
+      double actual_reduction = this->filter->compute_actual_objective_reduction(current_objective_measure, current_infeasibility, trial_objective_measure);
+      if (this->protect_actual_reduction_against_roundoff) {
+         // TODO put constant in option file
+         static double machine_epsilon = std::numeric_limits<double>::epsilon();
+         actual_reduction += 10. * machine_epsilon * std::abs(current_objective_measure);
+      }
+      return actual_reduction;
+   }
+
+   void FilterMethod::set_statistics(Statistics& /*statistics*/) const {
+      // do nothing
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FilterMethod.hpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FilterMethod.hpp
new file mode 100644
index 0000000..bc1e28d
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FilterMethod.hpp
@@ -0,0 +1,44 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_FILTERMETHOD_H
+#define UNO_FILTERMETHOD_H
+
+#include "../SwitchingMethod.hpp"
+#include "filters/Filter.hpp"
+
+namespace uno {
+   // forward declaration
+   class Options;
+
+   /*! \class TwoPhaseConstants
+    * \brief Constants for filter strategy
+    *
+    *  Set of constants to control the filter strategy
+    */
+   struct FilterStrategyParameters {
+      double upper_bound;
+      double infeasibility_factor;
+   };
+
+   class FilterMethod: public SwitchingMethod {
+   public:
+      explicit FilterMethod(const Options& options);
+      ~FilterMethod() override = default;
+
+      void initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) override;
+      void reset() override;
+      void notify_switch_to_feasibility(const ProgressMeasures& current_progress) override;
+      void notify_switch_to_optimality(const ProgressMeasures& current_progress) override;
+
+   protected:
+      // pointer to allow polymorphism
+      const std::unique_ptr<Filter> filter;
+      const FilterStrategyParameters parameters; /*!< Set of constants */
+
+      [[nodiscard]] double compute_actual_objective_reduction(double current_objective_measure, double current_infeasibility, double trial_objective_measure);
+      void set_statistics(Statistics& statistics) const override;
+   };
+} // namespace
+
+#endif // UNO_FILTERMETHOD_H
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FletcherFilterMethod.cpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FletcherFilterMethod.cpp
new file mode 100644
index 0000000..b03c17d
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FletcherFilterMethod.cpp
@@ -0,0 +1,69 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include "FletcherFilterMethod.hpp"
+#include "../../ProgressMeasures.hpp"
+#include "optimization/Iterate.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Statistics.hpp"
+
+namespace uno {
+   FletcherFilterMethod::FletcherFilterMethod(const Options& options): FilterMethod(options) {
+   }
+
+   bool FletcherFilterMethod::is_regular_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+         const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction) {
+      // in filter methods, we construct an unconstrained measure by ignoring infeasibility and scaling the objective measure by 1
+      const double current_merit = FilterMethod::unconstrained_merit_function(current_progress);
+      const double trial_merit = FilterMethod::unconstrained_merit_function(trial_progress);
+      const double merit_predicted_reduction = FilterMethod::unconstrained_merit_function(predicted_reduction);
+      DEBUG << "Current: (infeasibility, objective + auxiliary) = (" << current_progress.infeasibility << ", " << current_merit << ")\n";
+      DEBUG << "Trial:   (infeasibility, objective + auxiliary) = (" << trial_progress.infeasibility << ", " << trial_merit << ")\n";
+      DEBUG << "Current filter:\n" << *this->filter << '\n';
+      DEBUG << "Unconstrained predicted reduction = " << merit_predicted_reduction << '\n';
+
+      std::string scenario;
+      bool accept = false;
+      if (this->filter->acceptable(trial_progress.infeasibility, trial_merit)) {
+         if (this->filter->acceptable_wrt_current_iterate(current_progress.infeasibility, current_merit, trial_progress.infeasibility, trial_merit)) {
+            // switching condition: check whether the unconstrained predicted reduction is sufficiently positive
+            if (this->switching_condition(merit_predicted_reduction, current_progress.infeasibility)) {
+               // unconstrained Armijo sufficient decrease condition: predicted reduction should be positive (f-type)
+               const double merit_actual_reduction = this->compute_actual_objective_reduction(current_merit, current_progress.infeasibility, trial_merit);
+               DEBUG << "Unconstrained actual reduction = " << merit_actual_reduction << '\n';
+               if (this->armijo_sufficient_decrease(merit_predicted_reduction, merit_actual_reduction)) {
+                  DEBUG << "Trial iterate (f-type) was accepted by satisfying the Armijo condition\n";
+                  accept = true;
+               }
+               else { // switching condition holds, but not Armijo condition
+                  DEBUG << "Trial iterate (f-type) was rejected by violating the Armijo condition\n";
+               }
+               scenario = "f-type";
+            }
+               // switching condition violated: predicted reduction is not promising (h-type)
+            else {
+               DEBUG << "Trial iterate (h-type) was accepted by violating the switching condition\n";
+               accept = true;
+               this->filter->add(current_progress.infeasibility, current_merit);
+               DEBUG << "Current iterate was added to the filter\n";
+               scenario = "h-type";
+            }
+         }
+         else {
+            DEBUG << "Trial iterate not acceptable with respect to current point\n";
+            scenario = "current point";
+         }
+      }
+      else {
+         DEBUG << "Trial iterate not filter acceptable\n";
+         scenario = "filter";
+      }
+      statistics.set("status", std::string(accept ? "accepted" : "rejected") + " (" + scenario + ")");
+      return accept;
+   }
+
+   bool FletcherFilterMethod::is_infeasibility_sufficiently_reduced(const ProgressMeasures& /*reference_progress*/, const ProgressMeasures& trial_progress) const {
+      // if the trial infeasibility improves upon the best known infeasibility
+      return this->filter->infeasibility_sufficient_reduction(this->filter->get_smallest_infeasibility(), trial_progress.infeasibility);
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FletcherFilterMethod.hpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FletcherFilterMethod.hpp
new file mode 100644
index 0000000..622c108
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/FletcherFilterMethod.hpp
@@ -0,0 +1,22 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_LEYFFERFILTERMETHOD_H
+#define UNO_LEYFFERFILTERMETHOD_H
+
+#include "FilterMethod.hpp"
+
+namespace uno {
+   class FletcherFilterMethod : public FilterMethod {
+   public:
+      explicit FletcherFilterMethod(const Options& options);
+
+      [[nodiscard]] bool is_infeasibility_sufficiently_reduced(const ProgressMeasures& reference_progress, const ProgressMeasures& trial_progress) const override;
+
+   protected:
+      [[nodiscard]] bool is_regular_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+            const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction) override;
+   };
+} // namespace
+
+#endif // UNO_LEYFFERFILTERMETHOD_H
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/WaechterFilterMethod.cpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/WaechterFilterMethod.cpp
new file mode 100644
index 0000000..026ce20
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/WaechterFilterMethod.cpp
@@ -0,0 +1,87 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include "WaechterFilterMethod.hpp"
+#include "../../ProgressMeasures.hpp"
+#include "optimization/Iterate.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Options.hpp"
+#include "tools/Statistics.hpp"
+
+namespace uno {
+   WaechterFilterMethod::WaechterFilterMethod(const Options& options):
+         FilterMethod(options),
+         sufficient_infeasibility_decrease_factor(options.get_double("filter_sufficient_infeasibility_decrease_factor")) {
+   }
+
+   void WaechterFilterMethod::initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) {
+      this->initial_infeasibility = initial_iterate.progress.infeasibility;
+      FilterMethod::initialize(statistics, initial_iterate, options);
+   }
+
+   bool WaechterFilterMethod::is_regular_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+         const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction) {
+      // in filter methods, we construct an unconstrained measure by ignoring infeasibility and scaling the objective measure by 1
+      const double current_merit = FilterMethod::unconstrained_merit_function(current_progress);
+      const double trial_merit = FilterMethod::unconstrained_merit_function(trial_progress);
+      const double merit_predicted_reduction = FilterMethod::unconstrained_merit_function(predicted_reduction);
+      DEBUG << "Current (infeasibility, objective + auxiliary) = (" << current_progress.infeasibility << ", " << current_merit << ")\n";
+      DEBUG << "Trial   (infeasibility, objective + auxiliary) = (" << trial_progress.infeasibility << ", " << trial_merit << ")\n";
+      DEBUG << "Current filter:\n" << *this->filter;
+      DEBUG << "Unconstrained predicted reduction = " << merit_predicted_reduction << '\n';
+
+      std::string scenario;
+      bool accept = false;
+      if (this->filter->acceptable(trial_progress.infeasibility, trial_merit)) {
+         const double merit_actual_reduction = this->compute_actual_objective_reduction(current_merit, current_progress.infeasibility, trial_merit);
+         DEBUG << "Unconstrained actual reduction = " << merit_actual_reduction << '\n';
+
+         // TODO put this coefficient in the option file
+         const bool small_infeasibility = current_progress.infeasibility <= 1e-4 * std::max(1., this->initial_infeasibility);
+         const bool switching = (0. < merit_predicted_reduction) && this->switching_condition(merit_predicted_reduction, current_progress.infeasibility);
+         const bool sufficient_decrease = this->armijo_sufficient_decrease(merit_predicted_reduction, merit_actual_reduction);
+
+         // switching condition: the unconstrained predicted reduction is sufficiently positive
+         if (small_infeasibility && switching) {
+            DEBUG << "Switching condition satisfied\n";
+            // unconstrained Armijo sufficient decrease condition: predicted reduction should be positive (f-type)
+            if (sufficient_decrease) {
+               DEBUG << "Trial iterate (f-type) was accepted by satisfying Armijo condition\n";
+               accept = true;
+            }
+            else {
+               DEBUG << "Armijo condition not satisfied\n";
+            }
+            scenario = "f-type";
+         }
+         else {
+            DEBUG << "Switching condition violated\n";
+            if (this->filter->acceptable_wrt_current_iterate(current_progress.infeasibility, current_merit, trial_progress.infeasibility, trial_merit)) {
+               DEBUG << "Trial iterate (h-type) acceptable with respect to current point\n";
+               accept = true;
+            }
+            else {
+               DEBUG << "Trial iterate (h-type) not acceptable with respect to current point\n";
+            }
+            scenario = "h-type";
+         }
+         // possibly augment the filter
+         if (accept && (not switching || not sufficient_decrease)) {
+            DEBUG << "Adding current iterate to the filter\n";
+            this->filter->add(current_progress.infeasibility, current_merit);
+         }
+      }
+      else {
+         DEBUG << "Trial iterate not filter acceptable\n";
+         statistics.set("status", "rejected (filter)");
+         scenario = "filter";
+      }
+      statistics.set("status", std::string(accept ? "accepted" : "rejected") + " (" + scenario + ")");
+      return accept;
+   }
+
+   bool WaechterFilterMethod::is_infeasibility_sufficiently_reduced(const ProgressMeasures& reference_progress, const ProgressMeasures& trial_progress) const {
+      return trial_progress.infeasibility <= this->sufficient_infeasibility_decrease_factor * reference_progress.infeasibility &&
+         this->filter->acceptable(trial_progress.infeasibility, FilterMethod::unconstrained_merit_function(trial_progress));
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/WaechterFilterMethod.hpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/WaechterFilterMethod.hpp
new file mode 100644
index 0000000..6823d1d
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/WaechterFilterMethod.hpp
@@ -0,0 +1,26 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_WAECHTERFILTERMETHOD_H
+#define UNO_WAECHTERFILTERMETHOD_H
+
+#include "FilterMethod.hpp"
+
+namespace uno {
+   class WaechterFilterMethod : public FilterMethod {
+   public:
+      explicit WaechterFilterMethod(const Options& options);
+
+      void initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) override;
+      [[nodiscard]] bool is_infeasibility_sufficiently_reduced(const ProgressMeasures& reference_progress, const ProgressMeasures& trial_progress) const override;
+
+   protected:
+      double initial_infeasibility{INF<double>};
+      const double sufficient_infeasibility_decrease_factor;
+
+      [[nodiscard]] bool is_regular_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+            const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction) override;
+   };
+} // namespace
+
+#endif // UNO_WAECHTERFILTERMETHOD_H
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/Filter.cpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/Filter.cpp
new file mode 100644
index 0000000..7b6a827
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/Filter.cpp
@@ -0,0 +1,191 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include <iostream>
+#include <iomanip>
+#include <algorithm>
+#include "Filter.hpp"
+#include "symbolic/Range.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Options.hpp"
+
+namespace uno {
+   Filter::Filter(const Options& options) :
+         capacity(options.get_unsigned_int("filter_capacity")),
+         infeasibility(this->capacity),
+         objective(this->capacity),
+         parameters({options.get_double("filter_beta"), options.get_double("filter_gamma")}) {
+   }
+
+   void Filter::reset() {
+      this->number_entries = 0;
+   }
+
+   bool Filter::is_empty() const {
+      return (this->number_entries == 0);
+   }
+
+   double Filter::get_smallest_infeasibility() const {
+      if (not this->is_empty()) {
+         // left-most entry has the lowest infeasibility
+         return this->infeasibility[0];
+      }
+      else { // empty filter
+         return this->infeasibility_upper_bound;
+      }
+   }
+
+   void Filter::set_infeasibility_upper_bound(double new_upper_bound) {
+      this->infeasibility_upper_bound = new_upper_bound;
+   }
+
+   void Filter::left_shift(size_t start, size_t shift_size) {
+      for (size_t position: Range(start, this->number_entries - shift_size)) {
+         this->infeasibility[position] = this->infeasibility[position + shift_size];
+         this->objective[position] = this->objective[position + shift_size];
+      }
+   }
+
+   void Filter::right_shift(size_t start, size_t shift_size) {
+      for (size_t position: BackwardRange(this->number_entries, start)) {
+         this->infeasibility[position] = this->infeasibility[position - shift_size];
+         this->objective[position] = this->objective[position - shift_size];
+      }
+   }
+
+   //  add (infeasibility, objective) to the filter
+   void Filter::add(double current_infeasibility, double current_objective) {
+      // remove dominated filter entries
+      // find position in filter without margin
+      size_t start_position = 0;
+      while (start_position < this->number_entries && this->infeasibility[start_position] < current_infeasibility) {
+         start_position++;
+      }
+
+      // find redundant entries starting from position
+      size_t end_position = start_position;
+      while (end_position < this->number_entries && current_objective <= this->objective[end_position]) {
+         end_position++;
+      }
+
+      // remove entries [position:end_position] from filter
+      const size_t number_redundant_entries = end_position - start_position;
+      if (0 < number_redundant_entries) {
+         this->left_shift(start_position, number_redundant_entries);
+         this->number_entries -= number_redundant_entries;
+      }
+
+      // check sufficient space available for new entry (remove last entry, if not)
+      if (this->number_entries >= this->capacity) {
+         const double largest_filter_infeasibility = std::max(this->infeasibility_upper_bound, this->infeasibility[this->number_entries - 1]);
+         this->set_infeasibility_upper_bound(this->parameters.beta * largest_filter_infeasibility);
+         // create space in filter: remove last entry
+         this->number_entries--;
+      }
+
+      // add new entry to the filter at position
+      start_position = 0;
+      while (start_position < this->number_entries && not this->infeasibility_sufficient_reduction(this->infeasibility[start_position], current_infeasibility)) {
+         start_position++;
+      }
+      // shift entries by one to right to make room for new entry
+      if (start_position < this->number_entries) {
+         this->right_shift(start_position, 1);
+      }
+      // add new entry to filter
+      this->infeasibility[start_position] = current_infeasibility;
+      this->objective[start_position] = current_objective;
+      this->number_entries++;
+   }
+
+   bool Filter::acceptable_wrt_upper_bound(double trial_infeasibility) const {
+      return this->infeasibility_sufficient_reduction(this->infeasibility_upper_bound, trial_infeasibility);
+   }
+
+   // return true if (infeasibility, objective) acceptable, false otherwise
+   bool Filter::acceptable(double trial_infeasibility, double trial_objective) {
+      // check upper bound first
+      if (not this->acceptable_wrt_upper_bound(trial_infeasibility)) {
+         DEBUG << "Rejected because of filter upper bound\n";
+         return false;
+      }
+
+      // TODO: use binary search
+      size_t position = 0;
+      while (position < this->number_entries && not this->infeasibility_sufficient_reduction(this->infeasibility[position], trial_infeasibility)) {
+         position++;
+      }
+
+      // check acceptability
+      if (position == 0) {
+         return true; // acceptable as left-most entry
+      }
+      // until here, the objective measure was not evaluated
+      else if (this->objective_sufficient_reduction(this->objective[position - 1], trial_objective, trial_infeasibility)) {
+         return true; // point acceptable
+      }
+      DEBUG << "Rejected because of filter domination\n";
+      return false;
+   }
+
+   //! check acceptability wrt current point
+   bool Filter::acceptable_wrt_current_iterate(double current_infeasibility, double current_objective, double trial_infeasibility,
+         double trial_objective) const {
+      return this->infeasibility_sufficient_reduction(current_infeasibility, trial_infeasibility) ||
+         this->objective_sufficient_reduction(current_objective, trial_objective, trial_infeasibility);
+   }
+
+   double Filter::compute_actual_objective_reduction(double current_objective, double /*current_infeasibility*/, double trial_objective) {
+      return current_objective - trial_objective;
+   }
+
+   bool Filter::infeasibility_sufficient_reduction(double current_infeasibility, double trial_infeasibility) const {
+      return (trial_infeasibility < this->parameters.beta * current_infeasibility);
+   }
+
+   bool Filter::objective_sufficient_reduction(double current_objective, double trial_objective, double trial_infeasibility) const {
+      return (trial_objective <= current_objective - this->parameters.gamma * trial_infeasibility);
+   }
+
+   std::string to_string(double number) {
+      std::ostringstream stream;
+      stream << std::defaultfloat << std::setprecision(7) << number;
+      return stream.str();
+   }
+
+   void print_line(std::ostream& stream, const std::string& infeasibility, const std::string& objective) {
+      const size_t fixed_length_column1 = 14;
+      const size_t fixed_length_column2 = 10;
+
+      // compute lengths of columns
+      const size_t infeasibility_length = infeasibility.size();
+      const size_t number_infeasibility_spaces = (infeasibility_length < fixed_length_column1) ? fixed_length_column1 - infeasibility_length : 0;
+      const size_t objective_length = objective.size();
+      const size_t number_objective_spaces = (objective_length < fixed_length_column2) ? fixed_length_column2 - objective_length : 0;
+
+      // print line
+      stream << "│ " << infeasibility;
+      for ([[maybe_unused]] size_t k: Range(number_infeasibility_spaces)) {
+         stream << ' ';
+      }
+      stream << "│ " << objective;
+      for ([[maybe_unused]] size_t k: Range(number_objective_spaces)) {
+         stream << ' ';
+      }
+      stream << "│\n";
+   }
+
+   // print the content of the filter
+   std::ostream& operator<<(std::ostream& stream, Filter& filter) {
+      stream << "┌───────────────┬───────────┐\n";
+      stream << "│ infeasibility │ objective │\n";
+      stream << "├───────────────┼───────────┤\n";
+      for (size_t position: Range(filter.number_entries)) {
+         print_line(stream, to_string(filter.infeasibility[position]), to_string(filter.objective[position]));
+      }
+      // print upper bound
+      print_line(stream, to_string(filter.infeasibility_upper_bound), "-");
+      stream << "└───────────────┴───────────┘\n";
+      return stream;
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/Filter.hpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/Filter.hpp
new file mode 100644
index 0000000..4592971
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/Filter.hpp
@@ -0,0 +1,55 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_FILTER_H
+#define UNO_FILTER_H
+
+#include <vector>
+#include <memory>
+#include "tools/Infinity.hpp"
+
+namespace uno {
+   // forward declaration
+   class Options;
+
+   struct FilterParameters {
+      double beta; /*!< Margin around filter */
+      double gamma; /*!< Margin around filter (sloping margin) */
+   };
+
+   class Filter {
+   public:
+      explicit Filter(const Options& options);
+      virtual ~Filter() = default;
+
+      void reset();
+      [[nodiscard]] double get_smallest_infeasibility() const;
+      void set_infeasibility_upper_bound(double new_upper_bound);
+
+      virtual void add(double current_infeasibility, double current_objective);
+      [[nodiscard]] virtual bool acceptable(double trial_infeasibility, double trial_objective);
+      [[nodiscard]] virtual bool acceptable_wrt_current_iterate(double current_infeasibility, double current_objective, double trial_infeasibility,
+            double trial_objective) const;
+      [[nodiscard]] virtual double compute_actual_objective_reduction(double current_objective, double current_infeasibility, double trial_objective);
+
+      [[nodiscard]] bool infeasibility_sufficient_reduction(double current_infeasibility, double trial_infeasibility) const;
+      [[nodiscard]] bool objective_sufficient_reduction(double current_objective, double trial_objective, double trial_infeasibility) const;
+
+      friend std::ostream& operator<<(std::ostream& stream, Filter& filter);
+
+   protected:
+      const size_t capacity; /*!< Max filter size */
+      std::vector<double> infeasibility{}; // infeasibility increases
+      std::vector<double> objective{}; // objective decreases
+      double infeasibility_upper_bound{INF<double>}; /*!< Upper bound on infeasibility measure */
+      size_t number_entries{0};
+      const FilterParameters parameters; /*!< Set of parameters */
+
+      [[nodiscard]] bool is_empty() const;
+      [[nodiscard]] bool acceptable_wrt_upper_bound(double trial_infeasibility) const;
+      void left_shift(size_t start, size_t shift_size);
+      void right_shift(size_t start, size_t shift_size);
+   };
+} // namespace
+
+#endif // UNO_FILTER_H
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/FilterFactory.cpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/FilterFactory.cpp
new file mode 100644
index 0000000..15f8b3e
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/FilterFactory.cpp
@@ -0,0 +1,23 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include <stdexcept>
+#include "FilterFactory.hpp"
+#include "NonmonotoneFilter.hpp"
+#include "tools/Options.hpp"
+
+namespace uno {
+   // FilterFactory class
+   std::unique_ptr<Filter> FilterFactory::create(const Options& options) {
+      const std::string& filter_type = options.get_string("filter_type");
+      if (filter_type == "standard") {
+         return std::make_unique<Filter>(options);
+      }
+      else if (filter_type == "nonmonotone") {
+         return std::make_unique<NonmonotoneFilter>(options);
+      }
+      else {
+         throw std::invalid_argument("Filter type " + filter_type + " does not exist");
+      }
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/FilterFactory.hpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/FilterFactory.hpp
new file mode 100644
index 0000000..9efce4d
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/FilterFactory.hpp
@@ -0,0 +1,16 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_FILTERFACTORY_H
+#define UNO_FILTERFACTORY_H
+
+#include "Filter.hpp"
+
+namespace uno {
+   class FilterFactory {
+   public:
+      static std::unique_ptr<Filter> create(const Options& options);
+   };
+} // namespace
+
+#endif // UNO_FILTERFACTORY_H
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/NonmonotoneFilter.cpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/NonmonotoneFilter.cpp
new file mode 100644
index 0000000..fb9c4ad
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/NonmonotoneFilter.cpp
@@ -0,0 +1,104 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include "NonmonotoneFilter.hpp"
+#include "symbolic/Range.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Options.hpp"
+
+namespace uno {
+   NonmonotoneFilter::NonmonotoneFilter(const Options& options) :
+         Filter(options), max_number_dominated_entries(options.get_unsigned_int("nonmonotone_filter_number_dominated_entries")) {
+   }
+
+   //! add (infeasibility_measure, objective_measure) to the filter
+   void NonmonotoneFilter::add(double current_infeasibility, double current_objective) {
+      // find entries in filter that are dominated by M other entries
+      for (size_t entry_index: Range(this->number_entries)) {
+         size_t number_dominated = 0;
+         // check whether ith entry dominated by (infeasibility_measure,objective_measure)
+         if ((this->objective[entry_index] > current_objective) && (this->infeasibility[entry_index] > current_infeasibility)) {
+            number_dominated = 1;
+         }
+         // find other filter entries that dominate ith entry
+         for (size_t other_entry_index: Range(this->number_entries)) {
+            if ((this->objective[entry_index] > this->objective[other_entry_index]) && (this->infeasibility[entry_index] > this->infeasibility[other_entry_index])) {
+               number_dominated++;
+            }
+         }
+         if (number_dominated > this->max_number_dominated_entries) {
+            // remove this entry
+            this->left_shift(entry_index, 1);
+            this->number_entries--;
+         }
+      }
+
+      // check sufficient space available
+      if (this->number_entries >= this->capacity) {
+         // create space in filter: remove entry 1 (oldest entry)
+         this->left_shift(1, 1);
+         this->number_entries--;
+      }
+
+      // add new entry to filter in position this->number_entries
+      this->infeasibility[this->number_entries] = current_infeasibility;
+      this->objective[this->number_entries] = current_objective;
+      this->number_entries++;
+   }
+
+   size_t NonmonotoneFilter::compute_number_dominated_entries(double trial_infeasibility, double trial_objective) const {
+      size_t number_dominated_entries = 0;
+      for (size_t entry_index: Range(this->number_entries)) {
+         if (not this->objective_sufficient_reduction(this->objective[entry_index], trial_objective, trial_infeasibility) &&
+               not this->infeasibility_sufficient_reduction(this->infeasibility[entry_index], trial_infeasibility)) {
+            number_dominated_entries++;
+         }
+         else if ((trial_objective >= this->objective[entry_index] - this->parameters.gamma * trial_infeasibility) &&
+                  (trial_infeasibility > this->parameters.beta * this->infeasibility[entry_index])) {
+            number_dominated_entries++;
+         }
+      }
+      return number_dominated_entries;
+   }
+
+   //! accept: check if (infeasibility_measure, objective_measure) acceptable
+   bool NonmonotoneFilter::acceptable(double trial_infeasibility, double trial_objective) {
+      // check upper bound first
+      if (not this->acceptable_wrt_upper_bound(trial_infeasibility)) {
+         DEBUG << "Rejected because of filter upper bound\n";
+         return false;
+      }
+
+      // check acceptability ** by counting how many entries dominate **
+      // until here, the objective measure was not evaluated
+      const size_t number_dominated_entries = this->compute_number_dominated_entries(trial_infeasibility, trial_objective);
+      return (number_dominated_entries <= this->max_number_dominated_entries);
+   }
+
+   //! check acceptability wrt current point
+   bool NonmonotoneFilter::acceptable_wrt_current_iterate(double current_infeasibility, double current_objective,
+         double trial_infeasibility, double trial_objective) const {
+      // check acceptability wrt current point (non-monotone)
+      size_t number_dominated_entries = this->compute_number_dominated_entries(trial_infeasibility, trial_objective);
+      if (not this->objective_sufficient_reduction(current_objective, trial_objective, trial_infeasibility) &&
+          (trial_infeasibility > this->parameters.beta * current_infeasibility)) {
+         number_dominated_entries++;
+      }
+      return (number_dominated_entries <= this->max_number_dominated_entries);
+   }
+
+   // compute_actual_reduction: check nonmonotone sufficient reduction condition
+   double NonmonotoneFilter::compute_actual_objective_reduction(double current_objective, double current_infeasibility, double trial_objective) {
+      // check NON-MONOTONE sufficient reduction condition
+      // max penalty among most recent entries
+      double max_objective = current_objective;
+      for (size_t entry_index: Range(this->max_number_dominated_entries)) {
+         const size_t index = this->number_entries - entry_index;
+         const double gamma = (current_infeasibility < this->infeasibility[index]) ? 1 / this->parameters.gamma : this->parameters.gamma;
+         const double dash_objective = this->objective[index] + gamma * (this->infeasibility[index] - current_infeasibility);
+         max_objective = std::max(max_objective, dash_objective);
+      }
+      // non-monotone actual reduction
+      return max_objective - trial_objective;
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/NonmonotoneFilter.hpp b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/NonmonotoneFilter.hpp
new file mode 100644
index 0000000..d36f94f
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/filter_methods/filters/NonmonotoneFilter.hpp
@@ -0,0 +1,27 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_NONMONOTONEFILTER_H
+#define UNO_NONMONOTONEFILTER_H
+
+#include "Filter.hpp"
+
+namespace uno {
+   class NonmonotoneFilter : public Filter {
+   public:
+      explicit NonmonotoneFilter(const Options& options);
+
+      void add(double current_infeasibility, double current_objective) override;
+      [[nodiscard]] bool acceptable(double trial_infeasibility, double trial_objective) override;
+      [[nodiscard]] bool acceptable_wrt_current_iterate(double current_infeasibility, double current_objective, double trial_infeasibility,
+            double trial_objective) const override;
+      [[nodiscard]] double compute_actual_objective_reduction(double current_objective, double current_infeasibility, double trial_objective) override;
+
+   protected:
+      const size_t max_number_dominated_entries; /*!< Memory of filter */
+
+      [[nodiscard]] size_t compute_number_dominated_entries(double trial_infeasibility, double trial_objective) const;
+   };
+} // namespace
+
+#endif // UNO_NONMONOTONEFILTER_H
diff --git a/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/Funnel.cpp b/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/Funnel.cpp
new file mode 100644
index 0000000..9fa5e91
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/Funnel.cpp
@@ -0,0 +1,68 @@
+// Copyright (c) 2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include "Funnel.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Options.hpp"
+
+namespace uno {
+   Funnel::Funnel(const Options& options):
+         margin(options.get_double("funnel_beta")),
+         update_strategy(options.get_int("funnel_update_strategy")),
+         kappa(options.get_double("funnel_kappa")) {
+   }
+
+   void Funnel::set_infeasibility_upper_bound(double new_upper_bound) {
+      this->width = new_upper_bound;
+   }
+
+   double Funnel::current_width() const {
+      return this->width;
+   }
+
+   /* check if the trial iterate is inside the current funnel */
+   bool Funnel::acceptable(double trial_infeasibility) const {
+      return (trial_infeasibility <= this->width);
+   }
+
+   /* check if the funnel sufficient decrease condition is satisfied */
+   bool Funnel::sufficient_decrease_condition(double trial_infeasibility) const {
+      return (trial_infeasibility <= this->margin * this->width);
+   }
+
+   void Funnel::update(double current_infeasibility, double trial_infeasibility) {
+      if (this->update_strategy == 1) {
+         if (trial_infeasibility <= current_infeasibility) {
+            this->width = std::max(this->margin * this->width,
+                  Funnel::convex_combination(current_infeasibility, trial_infeasibility, this->kappa));
+         }
+         else {
+            DEBUG << "Trial infeasibility higher than current infeasibility" << '\n';
+            this->width = this->margin * this->width;
+         }
+      }
+      else if (this->update_strategy == 2) {
+         this->width = Funnel::convex_combination(this->width, trial_infeasibility, this->kappa);
+      }
+      else if (this->update_strategy == 3) {
+         this->width = this->margin * this->width;
+      }
+      else {
+         throw std::runtime_error("Funnel update strategy " + std::to_string(this->update_strategy) + " is unknown.");
+      }
+      DEBUG << "\t\tNew funnel parameter is: " << this->width << '\n';
+   }
+
+   void Funnel::update_restoration(double current_infeasibility) {
+      this->width = Funnel::convex_combination(this->width, current_infeasibility, this->kappa);
+      DEBUG << "\t\tNew funnel parameter is: " << this->width << '\n';
+   }
+
+   void Funnel::print() const {
+      DEBUG << "Current funnel width: " << this->width << '\n';
+   }
+
+   double Funnel::convex_combination(double a, double b, double coefficient) {
+      return coefficient*a + (1. - coefficient)*b;
+   }
+} // namespace
\ No newline at end of file
diff --git a/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/Funnel.hpp b/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/Funnel.hpp
new file mode 100644
index 0000000..9bed9d7
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/Funnel.hpp
@@ -0,0 +1,37 @@
+// Copyright (c) 2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_FUNNEL_H
+#define UNO_FUNNEL_H
+
+#include "tools/Infinity.hpp"
+
+namespace uno {
+   // forward reference
+   class Options;
+
+   class Funnel {
+   public:
+      explicit Funnel(const Options& options);
+      ~Funnel() = default;
+
+      void set_infeasibility_upper_bound(double new_upper_bound);
+      [[nodiscard]] double current_width() const;
+      [[nodiscard]] bool acceptable(double trial_infeasibility) const;
+      [[nodiscard]] bool sufficient_decrease_condition(double trial_infeasibility) const;
+      void update(double current_infeasibility, double trial_infeasibility);
+      void update_restoration(double current_infeasibility);
+
+      void print() const;
+
+   protected:
+      double width{INF<double>};
+      const double margin;
+      const int update_strategy;
+      const double kappa;
+
+      [[nodiscard]] static double convex_combination(double a, double b, double coefficient);
+   };
+} // namespace
+
+#endif // UNO_FUNNEL_H
\ No newline at end of file
diff --git a/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/FunnelMethod.cpp b/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/FunnelMethod.cpp
new file mode 100644
index 0000000..4a047e8
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/FunnelMethod.cpp
@@ -0,0 +1,141 @@
+// Copyright (c) 2024 David Kiessling, Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include "FunnelMethod.hpp"
+#include "../../ProgressMeasures.hpp"
+#include "optimization/Iterate.hpp"
+#include "tools/Options.hpp"
+#include "tools/Logger.hpp"
+#include "tools/Statistics.hpp"
+
+namespace uno {
+   FunnelMethod::FunnelMethod(const Options& options) :
+         SwitchingMethod(options),
+         funnel(options),
+         parameters({
+               options.get_double("funnel_ubd"),
+               options.get_double("funnel_fact"),
+               options.get_double("funnel_beta"),
+               options.get_double("funnel_gamma"),
+         }),
+         require_acceptance_wrt_current_iterate(options.get_bool("funnel_require_acceptance_wrt_current_iterate")) {
+   }
+
+   void FunnelMethod::initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) {
+      const double upper_bound = std::max(this->parameters.initial_upper_bound, this->parameters.infeasibility_factor * initial_iterate.progress.infeasibility);
+      this->funnel.set_infeasibility_upper_bound(upper_bound);
+      DEBUG << "Current funnel width: " << this->funnel.current_width() << '\n';
+
+      statistics.add_column("funnel width", Statistics::double_width, options.get_int("statistics_funnel_width_column_order"));
+      statistics.set("funnel width", this->funnel.current_width());
+   }
+
+   bool FunnelMethod::is_regular_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+         const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction) {
+      bool accept = false;
+      std::string scenario;
+      if (this->funnel.acceptable(trial_progress.infeasibility)) {
+         // in filter and funnel methods, we construct an unconstrained measure by ignoring infeasibility and scaling the objective measure by 1
+         const double current_merit = SwitchingMethod::unconstrained_merit_function(current_progress);
+         const double trial_merit = SwitchingMethod::unconstrained_merit_function(trial_progress);
+         const double merit_predicted_reduction = SwitchingMethod::unconstrained_merit_function(predicted_reduction);
+         DEBUG << "Current: (infeasibility, objective + auxiliary) = (" << current_progress.infeasibility << ", " << current_merit << ")\n";
+         DEBUG << "Trial:   (infeasibility, objective + auxiliary) = (" << trial_progress.infeasibility << ", " << trial_merit << ")\n";
+         DEBUG << "Unconstrained predicted reduction = " << merit_predicted_reduction << '\n';
+
+         // IF require_acceptance_wrt_current_iterate == false, then condition always fulfilled, we never check
+         // If true, then first part is false, and we always check wrt current iterate
+         if (not this->require_acceptance_wrt_current_iterate ||
+             this->acceptable_wrt_current_iterate(current_progress.infeasibility, current_merit, trial_progress.infeasibility, trial_merit)) {
+            // f-type step
+            if (this->switching_condition(merit_predicted_reduction, current_progress.infeasibility)) {
+               DEBUG << "Trial iterate satisfies switching condition\n";
+               // unconstrained Armijo sufficient decrease condition (predicted reduction should be positive)
+               const double objective_actual_reduction = this->compute_actual_objective_reduction(current_merit, trial_merit);
+               DEBUG << "Unconstrained actual reduction = " << objective_actual_reduction << '\n';
+               if (this->armijo_sufficient_decrease(merit_predicted_reduction, objective_actual_reduction)) {
+                  DEBUG << "Trial iterate (f-type) was ACCEPTED by satisfying Armijo condition\n";
+                  accept = true;
+               }
+               else { // switching condition holds, but not Armijo condition
+                  DEBUG << "Trial iterate (f-type) was REJECTED by violating the Armijo condition\n";
+               }
+               scenario = "f-type";
+            }
+               // h-type step
+            else if (this->funnel.sufficient_decrease_condition(trial_progress.infeasibility)) {
+               DEBUG << "\t\tTrial iterate  (h-type) ACCEPTED by violating the switching condition ...\n";
+               accept = true;
+
+               DEBUG << "\t\tEntering funnel reduction mechanism\n";
+               this->funnel.update(current_progress.infeasibility, trial_progress.infeasibility);
+               statistics.set("funnel width", this->funnel.current_width());
+               scenario = "h-type";
+            }
+            else {
+               DEBUG << "\t\tTrial iterate REJECTED by violating switching and funnel sufficient decrease condition\n";
+               scenario = "none of f/h-type";
+            }
+         }
+         else {
+            DEBUG << "Trial iterate not acceptable with respect to current point\n";
+            scenario = "current point";
+         }
+      }
+      else {
+         DEBUG << "\t\tTrial iterate REJECTED. Not in funnel\n";
+         scenario = "not in funnel";
+      }
+
+      statistics.set("status", std::string(accept ? "accepted" : "rejected") + " (" + scenario + ")");
+      if (accept) {
+         this->funnel.print();
+      }
+      return accept;
+   }
+
+   bool FunnelMethod::is_infeasibility_sufficiently_reduced(const ProgressMeasures& reference_progress, const ProgressMeasures& trial_progress) const {
+      return this->funnel.acceptable(trial_progress.infeasibility) &&
+             trial_progress.infeasibility <= this->parameters.beta * reference_progress.infeasibility;
+   }
+
+   void FunnelMethod::reset() {
+      // do nothing
+   }
+
+   void FunnelMethod::notify_switch_to_feasibility(const ProgressMeasures& /*current_progress_measures*/) {
+   }
+
+   void FunnelMethod::notify_switch_to_optimality(const ProgressMeasures& current_progress_measures) {
+      DEBUG << "Funnel is reduced after restoration phase\n";
+      this->funnel.update_restoration(current_progress_measures.infeasibility);
+   }
+
+   // check acceptability wrt current point
+   bool FunnelMethod::acceptable_wrt_current_iterate(double current_infeasibility, double current_objective, double trial_infeasibility,
+         double trial_objective) const {
+      return this->infeasibility_sufficient_reduction(current_infeasibility, trial_infeasibility) ||
+         this->objective_sufficient_reduction(current_objective, trial_objective, trial_infeasibility);
+   }
+
+   bool FunnelMethod::infeasibility_sufficient_reduction(double current_infeasibility, double trial_infeasibility) const {
+      return (trial_infeasibility < this->parameters.beta * current_infeasibility);
+   }
+
+   bool FunnelMethod::objective_sufficient_reduction(double current_objective, double trial_objective, double trial_infeasibility) const {
+      return (trial_objective <= current_objective - this->parameters.gamma * trial_infeasibility);
+   }
+
+   double FunnelMethod::compute_actual_objective_reduction(double current_objective_measure, double trial_objective_measure) {
+      double actual_reduction = current_objective_measure - trial_objective_measure;
+      if (this->protect_actual_reduction_against_roundoff) {
+         static double machine_epsilon = std::numeric_limits<double>::epsilon();
+         actual_reduction += 10. * machine_epsilon * std::abs(current_objective_measure);
+      }
+      return actual_reduction;
+   }
+
+   void FunnelMethod::set_statistics(Statistics& statistics) const {
+      statistics.set("funnel width", this->funnel.current_width());
+   }
+} // namespace
diff --git a/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/FunnelMethod.hpp b/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/FunnelMethod.hpp
new file mode 100644
index 0000000..bf42b69
--- /dev/null
+++ b/uno/ingredients/globalization_strategy/switching_methods/funnel_methods/FunnelMethod.hpp
@@ -0,0 +1,58 @@
+// Copyright (c) 2024 David Kiessling, Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_FUNNELSTRATEGY_H
+#define UNO_FUNNELSTRATEGY_H
+
+#include "../SwitchingMethod.hpp"
+#include "Funnel.hpp"
+#include "tools/Infinity.hpp"
+
+/*! \class TwoPhaseConstants
+ * \brief Constants for funnel strategy
+ *
+ *  Set of constants to control the funnel strategy
+ */
+namespace uno {
+   struct FunnelMethodParameters {
+      double initial_upper_bound;
+      double infeasibility_factor;
+      double beta; /*!< Margin around funnel */
+      double gamma; /*!< For acceptability wrt current point. Margin around objective value */
+   };
+
+   /*! \class FunnelMethod
+    * \brief Step acceptance strategy based on a funnel
+    *
+    *  Strategy that accepts or declines a trial step
+    */
+   class FunnelMethod: public SwitchingMethod {
+   public:
+      explicit FunnelMethod(const Options& options);
+      ~FunnelMethod() override = default;
+
+      void initialize(Statistics& statistics, const Iterate& initial_iterate, const Options& options) override;
+      [[nodiscard]] bool is_infeasibility_sufficiently_reduced(const ProgressMeasures& reference_progress,
+            const ProgressMeasures& trial_progress) const override;
+      void reset() override;
+      void notify_switch_to_feasibility(const ProgressMeasures& current_progress_measures) override;
+      void notify_switch_to_optimality(const ProgressMeasures& current_progress_measures) override;
+
+      [[nodiscard]] bool acceptable_wrt_current_iterate(double current_infeasibility, double current_objective, double trial_infeasibility,
+            double trial_objective) const;
+      [[nodiscard]] bool infeasibility_sufficient_reduction(double current_infeasibility, double trial_infeasibility) const;
+      [[nodiscard]] bool objective_sufficient_reduction(double current_objective, double trial_objective, double trial_infeasibility) const;
+
+   protected:
+      Funnel funnel;
+      const FunnelMethodParameters parameters; /*!< Set of constants */
+      const bool require_acceptance_wrt_current_iterate;
+
+      [[nodiscard]] bool is_regular_iterate_acceptable(Statistics& statistics, const ProgressMeasures& current_progress,
+            const ProgressMeasures& trial_progress, const ProgressMeasures& predicted_reduction) override;
+      [[nodiscard]] double compute_actual_objective_reduction(double current_optimality_measure, double trial_optimality_measure);
+      void set_statistics(Statistics& statistics) const override;
+   };
+} // namespace
+
+#endif // UNO_FUNNELSTRATEGY_H
diff --git a/uno/ingredients/subproblem/Direction.cpp b/uno/ingredients/subproblem/Direction.cpp
index 28bd971..2666d79 100644
--- a/uno/ingredients/subproblem/Direction.cpp
+++ b/uno/ingredients/subproblem/Direction.cpp
@@ -6,75 +6,71 @@
 #include "linear_algebra/Vector.hpp"
 #include "symbolic/VectorView.hpp"
 
-Direction::Direction(size_t number_variables, size_t number_constraints) :
-      number_variables(number_variables), number_constraints(number_constraints),
-      primals(number_variables), multipliers(number_variables, number_constraints), feasibility_multipliers(number_variables, number_constraints),
-      active_set(number_variables, number_constraints) {
-}
+namespace uno {
+   Direction::Direction(size_t number_variables, size_t number_constraints) :
+         number_variables(number_variables), number_constraints(number_constraints),
+         primals(number_variables), multipliers(number_variables, number_constraints), feasibility_multipliers(number_variables, number_constraints),
+         active_bounds(number_variables) {
+   }
 
-void Direction::set_dimensions(size_t new_number_variables, size_t new_number_constraints) {
-   this->number_variables = new_number_variables;
-   this->number_constraints = new_number_constraints;
-}
+   void Direction::set_dimensions(size_t new_number_variables, size_t new_number_constraints) {
+      this->number_variables = new_number_variables;
+      this->number_constraints = new_number_constraints;
+   }
 
-void Direction::reset() {
-   this->primals.fill(0.);
-   this->multipliers.reset();
-   this->feasibility_multipliers.reset();
-   this->active_set.constraints.at_lower_bound.clear();
-   this->active_set.constraints.at_upper_bound.clear();
-   this->active_set.bounds.at_lower_bound.clear();
-   this->active_set.bounds.at_upper_bound.clear();
-}
+   void Direction::reset() {
+      this->primals.fill(0.);
+      this->multipliers.reset();
+      this->feasibility_multipliers.reset();
+      this->active_bounds.at_lower_bound.clear();
+      this->active_bounds.at_upper_bound.clear();
+   }
 
-std::string status_to_string(SubproblemStatus status) {
-   switch (status) {
-      case SubproblemStatus::OPTIMAL:
-         return "optimal subproblem";
-      case SubproblemStatus::UNBOUNDED_PROBLEM:
-         return "unbounded subproblem";
-      case SubproblemStatus::INFEASIBLE:
-         return "infeasible subproblem";
-      default:
-         return "unknown status, something went wrong";
+   std::string status_to_string(SubproblemStatus status) {
+      switch (status) {
+         case SubproblemStatus::OPTIMAL:
+            return "optimal subproblem";
+         case SubproblemStatus::UNBOUNDED_PROBLEM:
+            return "unbounded subproblem";
+         case SubproblemStatus::INFEASIBLE:
+            return "infeasible subproblem";
+         default:
+            return "unknown status, something went wrong";
+      }
    }
-}
 
-std::ostream& operator<<(std::ostream& stream, const Direction& direction) {
-   stream << "Direction:\n";
-   stream << "│ status: " << status_to_string(direction.status) << '\n';
+   std::ostream& operator<<(std::ostream& stream, const Direction& direction) {
+      stream << "Direction:\n";
+      stream << "│ status: " << status_to_string(direction.status) << '\n';
 
-   stream << "│ primals = "; print_vector(stream, view(direction.primals, 0, direction.number_variables));
-   stream << "│ constraint multipliers = "; print_vector(stream, direction.multipliers.constraints);
-   stream << "│ lower bound multipliers = "; print_vector(stream, direction.multipliers.lower_bounds);
-   stream << "│ upper bound multipliers = "; print_vector(stream, direction.multipliers.upper_bounds);
-   stream << "│ feasibility constraint multipliers = "; print_vector(stream, direction.feasibility_multipliers.constraints);
-   stream << "│ feasibility lower bound multipliers = "; print_vector(stream, direction.feasibility_multipliers.lower_bounds);
-   stream << "│ feasibility upper bound multipliers = "; print_vector(stream, direction.feasibility_multipliers.upper_bounds);
+      stream << "│ primals = ";
+      print_vector(stream, view(direction.primals, 0, direction.number_variables));
+      stream << "│ constraint multipliers = ";
+      print_vector(stream, direction.multipliers.constraints);
+      stream << "│ lower bound multipliers = ";
+      print_vector(stream, direction.multipliers.lower_bounds);
+      stream << "│ upper bound multipliers = ";
+      print_vector(stream, direction.multipliers.upper_bounds);
+      stream << "│ feasibility constraint multipliers = ";
+      print_vector(stream, direction.feasibility_multipliers.constraints);
+      stream << "│ feasibility lower bound multipliers = ";
+      print_vector(stream, direction.feasibility_multipliers.lower_bounds);
+      stream << "│ feasibility upper bound multipliers = ";
+      print_vector(stream, direction.feasibility_multipliers.upper_bounds);
 
-   stream << "│ objective = " << direction.subproblem_objective << '\n';
-   stream << "│ norm = " << direction.norm << '\n';
+      stream << "│ objective = " << direction.subproblem_objective << '\n';
+      stream << "│ norm = " << direction.norm << '\n';
 
-   stream << "│ bound constraints active at lower bound =";
-   for (size_t variable_index: direction.active_set.bounds.at_lower_bound) {
-      stream << " x" << variable_index;
-   }
-   stream << '\n';
-   stream << "│ bound constraints active at upper bound =";
-   for (size_t variable_index: direction.active_set.bounds.at_upper_bound) {
-      stream << " x" << variable_index;
-   }
-   stream << '\n';
-
-   stream << "│ constraints at lower bound =";
-   for (size_t constraint_index: direction.active_set.constraints.at_lower_bound) {
-      stream << " c" << constraint_index;
-   }
-   stream << '\n';
-   stream << "│ constraints at upper bound =";
-   for (size_t constraint_index: direction.active_set.constraints.at_upper_bound) {
-      stream << " c" << constraint_index;
+      stream << "│ bound constraints active at lower bound =";
+      for (size_t variable_index: direction.active_bounds.at_lower_bound) {
+         stream << " x" << variable_index;
+      }
+      stream << '\n';
+      stream << "│ bound constraints active at upper bound =";
+      for (size_t variable_index: direction.active_bounds.at_upper_bound) {
+         stream << " x" << variable_index;
+      }
+      stream << '\n';
+      return stream;
    }
-   stream << '\n';
-   return stream;
-}
\ No newline at end of file
+} // namespace
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/Direction.hpp b/uno/ingredients/subproblem/Direction.hpp
index 53d1780..257185d 100644
--- a/uno/ingredients/subproblem/Direction.hpp
+++ b/uno/ingredients/subproblem/Direction.hpp
@@ -11,53 +11,48 @@
 #include "optimization/Multipliers.hpp"
 #include "tools/Infinity.hpp"
 
-// forward declaration
-template <typename ElementType>
-class Vector;
-
-/*! \struct ConstraintActivity
-* \brief Constraints at lower or upper bound at the optimum solution
-*
-*  Description of the active or infeasible constraints: at lower or upper bound at the optimum solution
-*/
-struct ActiveConstraints {
-   std::vector<size_t> at_lower_bound; /*!< List of constraint indices at their lower bound */
-   std::vector<size_t> at_upper_bound; /*!< List of constraint indices at their upper bound */
-
-   explicit ActiveConstraints(size_t capacity) {
-      this->at_lower_bound.reserve(capacity);
-      this->at_upper_bound.reserve(capacity);
-   }
-};
-
-struct ActiveSet {
-   ActiveConstraints constraints; /*!< List of general constraints */
-   ActiveConstraints bounds; /*!< List of bound constraints */
-
-   ActiveSet(size_t number_variables, size_t number_constraints): constraints(number_constraints), bounds(number_variables) { }
-};
-
-class Direction {
-public:
-   Direction(size_t number_variables, size_t number_constraints);
-
-   size_t number_variables;
-   size_t number_constraints;
-
-   Vector<double> primals; /*!< Primal variables */
-   Multipliers multipliers; /*!< Multipliers */
-   Multipliers feasibility_multipliers; /*!< Multipliers */
-
-   SubproblemStatus status{SubproblemStatus::OPTIMAL}; /*!< Status of the solution */
-
-   double norm{INF<double>}; /*!< Norm of \f$x\f$ */
-   double subproblem_objective{INF<double>}; /*!< Objective value */
-   ActiveSet active_set; /*!< Active set */
-
-   void set_dimensions(size_t new_number_variables, size_t new_number_constraints);
-   void reset();
-
-   friend std::ostream& operator<<(std::ostream& stream, const Direction& direction);
-};
+namespace uno {
+   // forward declaration
+   template <typename ElementType>
+   class Vector;
+
+   /*! \struct ConstraintActivity
+   * \brief Constraints at lower or upper bound at the optimum solution
+   *
+   *  Description of the active or infeasible constraints: at lower or upper bound at the optimum solution
+   */
+   struct ActiveConstraints {
+      std::vector<size_t> at_lower_bound; /*!< List of constraint indices at their lower bound */
+      std::vector<size_t> at_upper_bound; /*!< List of constraint indices at their upper bound */
+
+      explicit ActiveConstraints(size_t capacity) {
+         this->at_lower_bound.reserve(capacity);
+         this->at_upper_bound.reserve(capacity);
+      }
+   };
+
+   class Direction {
+   public:
+      Direction(size_t number_variables, size_t number_constraints);
+
+      size_t number_variables;
+      size_t number_constraints;
+
+      Vector<double> primals; /*!< Primal variables */
+      Multipliers multipliers; /*!< Multipliers */
+      Multipliers feasibility_multipliers; /*!< Multipliers */
+
+      SubproblemStatus status{SubproblemStatus::OPTIMAL}; /*!< Status of the solution */
+
+      double norm{INF<double>}; /*!< Norm of \f$x\f$ */
+      double subproblem_objective{INF<double>}; /*!< Objective value */
+      ActiveConstraints active_bounds; /*!< Active bound constraints */
+
+      void set_dimensions(size_t new_number_variables, size_t new_number_constraints);
+      void reset();
+
+      friend std::ostream& operator<<(std::ostream& stream, const Direction& direction);
+   };
+} // namespace
 
 #endif // UNO_DIRECTION_H
diff --git a/uno/ingredients/subproblem/HessianModel.cpp b/uno/ingredients/subproblem/HessianModel.cpp
index 539838c..3fcf0f6 100644
--- a/uno/ingredients/subproblem/HessianModel.cpp
+++ b/uno/ingredients/subproblem/HessianModel.cpp
@@ -8,74 +8,76 @@
 #include "tools/Options.hpp"
 #include "tools/Statistics.hpp"
 
-HessianModel::HessianModel(size_t dimension, size_t maximum_number_nonzeros, const std::string& sparse_format, bool use_regularization) :
-      hessian(SymmetricMatrixFactory<double>::create(sparse_format, dimension, maximum_number_nonzeros, use_regularization)) {
-}
+namespace uno {
+   HessianModel::HessianModel(size_t dimension, size_t maximum_number_nonzeros, const std::string& sparse_format, bool use_regularization) :
+         hessian(SymmetricMatrixFactory<size_t, double>::create(sparse_format, dimension, maximum_number_nonzeros, use_regularization)) {
+   }
 
-// Exact Hessian
-ExactHessian::ExactHessian(size_t dimension, size_t maximum_number_nonzeros, const Options& options) :
-   HessianModel(dimension, maximum_number_nonzeros, options.get_string("sparse_format"), /* use_regularization = */false) {
-}
+   // Exact Hessian
+   ExactHessian::ExactHessian(size_t dimension, size_t maximum_number_nonzeros, const Options& options) :
+         HessianModel(dimension, maximum_number_nonzeros, options.get_string("sparse_format"), /* use_regularization = */false) {
+   }
 
-void ExactHessian::evaluate(Statistics& /*statistics*/, const OptimizationProblem& problem, const Vector<double>& primal_variables,
-      const Vector<double>& constraint_multipliers) {
-   // evaluate Lagrangian Hessian
-   this->hessian->dimension = problem.number_variables;
-   problem.evaluate_lagrangian_hessian(primal_variables, constraint_multipliers, *this->hessian);
-   this->evaluation_count++;
-}
+   void ExactHessian::evaluate(Statistics& /*statistics*/, const OptimizationProblem& problem, const Vector<double>& primal_variables,
+         const Vector<double>& constraint_multipliers) {
+      // evaluate Lagrangian Hessian
+      this->hessian->dimension = problem.number_variables;
+      problem.evaluate_lagrangian_hessian(primal_variables, constraint_multipliers, *this->hessian);
+      this->evaluation_count++;
+   }
 
-// Convexified Hessian
-ConvexifiedHessian::ConvexifiedHessian(size_t dimension, size_t maximum_number_nonzeros, const Options& options):
-      HessianModel(dimension, maximum_number_nonzeros, options.get_string("sparse_format"), /* use_regularization = */true),
-      // inertia-based convexification needs a linear solver
-      linear_solver(SymmetricIndefiniteLinearSolverFactory::create(options.get_string("linear_solver"), dimension, maximum_number_nonzeros)),
-      regularization_initial_value(options.get_double("regularization_initial_value")),
-      regularization_increase_factor(options.get_double("regularization_increase_factor")) {
-}
+   // Convexified Hessian
+   ConvexifiedHessian::ConvexifiedHessian(size_t dimension, size_t maximum_number_nonzeros, const Options& options):
+         HessianModel(dimension, maximum_number_nonzeros, options.get_string("sparse_format"), /* use_regularization = */true),
+         // inertia-based convexification needs a linear solver
+         linear_solver(SymmetricIndefiniteLinearSolverFactory::create(options.get_string("linear_solver"), dimension, maximum_number_nonzeros)),
+         regularization_initial_value(options.get_double("regularization_initial_value")),
+         regularization_increase_factor(options.get_double("regularization_increase_factor")) {
+   }
 
-void ConvexifiedHessian::evaluate(Statistics& statistics, const OptimizationProblem& problem, const Vector<double>& primal_variables,
-      const Vector<double>& constraint_multipliers) {
-   // evaluate Lagrangian Hessian
-   this->hessian->dimension = problem.number_variables;
-   problem.evaluate_lagrangian_hessian(primal_variables, constraint_multipliers, *this->hessian);
-   this->evaluation_count++;
-   // regularize (only on the original variables) to convexify the problem
-   DEBUG2 << "hessian before convexification: " << *this->hessian;
-   this->regularize(statistics, *this->hessian, problem.get_number_original_variables());
-}
+   void ConvexifiedHessian::evaluate(Statistics& statistics, const OptimizationProblem& problem, const Vector<double>& primal_variables,
+         const Vector<double>& constraint_multipliers) {
+      // evaluate Lagrangian Hessian
+      this->hessian->dimension = problem.number_variables;
+      problem.evaluate_lagrangian_hessian(primal_variables, constraint_multipliers, *this->hessian);
+      this->evaluation_count++;
+      // regularize (only on the original variables) to convexify the problem
+      DEBUG2 << "hessian before convexification: " << *this->hessian;
+      this->regularize(statistics, *this->hessian, problem.get_number_original_variables());
+   }
 
-// Nocedal and Wright, p51
-void ConvexifiedHessian::regularize(Statistics& statistics, SymmetricMatrix<double>& hessian, size_t number_original_variables) {
-   const double smallest_diagonal_entry = hessian.smallest_diagonal_entry();
-   DEBUG << "The minimal diagonal entry of the matrix is " << hessian.smallest_diagonal_entry() << '\n';
+   // Nocedal and Wright, p51
+   void ConvexifiedHessian::regularize(Statistics& statistics, SymmetricMatrix<size_t, double>& hessian, size_t number_original_variables) {
+      const double smallest_diagonal_entry = hessian.smallest_diagonal_entry(number_original_variables);
+      DEBUG << "The minimal diagonal entry of the matrix is " << smallest_diagonal_entry << '\n';
 
-   double regularization_factor = (smallest_diagonal_entry <= 0.) ? this->regularization_initial_value - smallest_diagonal_entry : 0.;
-   bool good_inertia = false;
-   bool symbolic_factorization_performed = false;
-   while (not good_inertia) {
-      DEBUG << "Testing factorization with regularization factor " << regularization_factor << '\n';
-      if (0. < regularization_factor) {
-         hessian.set_regularization([=](size_t variable_index) {
-            return (variable_index < number_original_variables) ? regularization_factor : 0.;
-         });
-      }
-      // perform the symbolic factorization only once
-      if (not symbolic_factorization_performed) {
-         this->linear_solver->do_symbolic_factorization(hessian);
-         symbolic_factorization_performed = true;
-      }
-      this->linear_solver->do_numerical_factorization(hessian);
+      double regularization_factor = (smallest_diagonal_entry <= 0.) ? this->regularization_initial_value - smallest_diagonal_entry : 0.;
+      bool good_inertia = false;
+      bool symbolic_factorization_performed = false;
+      while (not good_inertia) {
+         DEBUG << "Testing factorization with regularization factor " << regularization_factor << '\n';
+         if (0. < regularization_factor) {
+            hessian.set_regularization([=](size_t variable_index) {
+               return (variable_index < number_original_variables) ? regularization_factor : 0.;
+            });
+         }
+         // perform the symbolic factorization only once
+         if (not symbolic_factorization_performed) {
+            this->linear_solver->do_symbolic_factorization(hessian);
+            symbolic_factorization_performed = true;
+         }
+         this->linear_solver->do_numerical_factorization(hessian);
 
-      if (this->linear_solver->rank() == number_original_variables && this->linear_solver->number_negative_eigenvalues() == 0) {
-         good_inertia = true;
-         DEBUG << "Factorization was a success\n";
-      }
-      else {
-         DEBUG << "rank: " << this->linear_solver->rank() << ", negative eigenvalues: " << this->linear_solver->number_negative_eigenvalues() << '\n';
-         regularization_factor = (regularization_factor == 0.) ? this->regularization_initial_value : this->regularization_increase_factor * regularization_factor;
-         assert(is_finite(regularization_factor) && "The regularization coefficient diverged");
+         if (this->linear_solver->rank() == number_original_variables && this->linear_solver->number_negative_eigenvalues() == 0) {
+            good_inertia = true;
+            DEBUG << "Factorization was a success\n";
+         }
+         else {
+            DEBUG << "rank: " << this->linear_solver->rank() << ", negative eigenvalues: " << this->linear_solver->number_negative_eigenvalues() << '\n';
+            regularization_factor = (regularization_factor == 0.) ? this->regularization_initial_value : this->regularization_increase_factor * regularization_factor;
+            assert(is_finite(regularization_factor) && "The regularization coefficient diverged");
+         }
       }
+      statistics.set("regularization", regularization_factor);
    }
-   statistics.set("regularization", regularization_factor);
-}
\ No newline at end of file
+} // namespace
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/HessianModel.hpp b/uno/ingredients/subproblem/HessianModel.hpp
index 6ff81bf..5c07566 100644
--- a/uno/ingredients/subproblem/HessianModel.hpp
+++ b/uno/ingredients/subproblem/HessianModel.hpp
@@ -8,48 +8,50 @@
 #include <vector>
 #include "solvers/linear/SymmetricIndefiniteLinearSolver.hpp"
 
-// forward declarations
-class OptimizationProblem;
-class Options;
-class Statistics;
-template <typename ElementType>
-class Vector;
-
-class HessianModel {
-public:
-   HessianModel(size_t dimension, size_t maximum_number_nonzeros, const std::string& sparse_format, bool use_regularization);
-   virtual ~HessianModel() = default;
-
-   std::unique_ptr<SymmetricMatrix<double>> hessian;
-   size_t evaluation_count{0};
-
-   virtual void evaluate(Statistics& statistics, const OptimizationProblem& problem, const Vector<double>& primal_variables,
-         const Vector<double>& constraint_multipliers) = 0;
-};
-
-// Exact Hessian
-class ExactHessian : public HessianModel {
-public:
-   ExactHessian(size_t dimension, size_t maximum_number_nonzeros, const Options& options);
-
-   void evaluate(Statistics& statistics, const OptimizationProblem& problem, const Vector<double>& primal_variables,
-         const Vector<double>& constraint_multipliers) override;
-};
-
-// Hessian with convexification (inertia correction)
-class ConvexifiedHessian : public HessianModel {
-public:
-   ConvexifiedHessian(size_t dimension, size_t maximum_number_nonzeros, const Options& options);
-
-   void evaluate(Statistics& statistics, const OptimizationProblem& problem, const Vector<double>& primal_variables,
-         const Vector<double>& constraint_multipliers) override;
-
-protected:
-   std::unique_ptr<SymmetricIndefiniteLinearSolver<double>> linear_solver; /*!< Solver that computes the inertia */
-   const double regularization_initial_value{};
-   const double regularization_increase_factor{};
-
-   void regularize(Statistics& statistics, SymmetricMatrix<double>& hessian, size_t number_original_variables);
-};
-
-#endif // UNO_HESSIANMODEL_H
+namespace uno {
+   // forward declarations
+   class OptimizationProblem;
+   class Options;
+   class Statistics;
+   template <typename ElementType>
+   class Vector;
+
+   class HessianModel {
+   public:
+      HessianModel(size_t dimension, size_t maximum_number_nonzeros, const std::string& sparse_format, bool use_regularization);
+      virtual ~HessianModel() = default;
+
+      std::unique_ptr<SymmetricMatrix<size_t, double>> hessian;
+      size_t evaluation_count{0};
+
+      virtual void evaluate(Statistics& statistics, const OptimizationProblem& problem, const Vector<double>& primal_variables,
+            const Vector<double>& constraint_multipliers) = 0;
+   };
+
+   // Exact Hessian
+   class ExactHessian : public HessianModel {
+   public:
+      ExactHessian(size_t dimension, size_t maximum_number_nonzeros, const Options& options);
+
+      void evaluate(Statistics& statistics, const OptimizationProblem& problem, const Vector<double>& primal_variables,
+            const Vector<double>& constraint_multipliers) override;
+   };
+
+   // Hessian with convexification (inertia correction)
+   class ConvexifiedHessian : public HessianModel {
+   public:
+      ConvexifiedHessian(size_t dimension, size_t maximum_number_nonzeros, const Options& options);
+
+      void evaluate(Statistics& statistics, const OptimizationProblem& problem, const Vector<double>& primal_variables,
+            const Vector<double>& constraint_multipliers) override;
+
+   protected:
+      std::unique_ptr<SymmetricIndefiniteLinearSolver<size_t, double>> linear_solver; /*!< Solver that computes the inertia */
+      const double regularization_initial_value{};
+      const double regularization_increase_factor{};
+
+      void regularize(Statistics& statistics, SymmetricMatrix<size_t, double>& hessian, size_t number_original_variables);
+   };
+} // namespace
+
+#endif // UNO_HESSIANMODEL_H
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/HessianModelFactory.hpp b/uno/ingredients/subproblem/HessianModelFactory.hpp
index 989f6d6..c6ba1c0 100644
--- a/uno/ingredients/subproblem/HessianModelFactory.hpp
+++ b/uno/ingredients/subproblem/HessianModelFactory.hpp
@@ -4,23 +4,25 @@
 #ifndef UNO_HESSIANMODELFACTORY_H
 #define UNO_HESSIANMODELFACTORY_H
 
-class HessianModelFactory {
-public:
-   static std::unique_ptr<HessianModel> create(const std::string& hessian_model, size_t dimension, size_t maximum_number_nonzeros,
-         bool convexify, const Options& options);
-};
+namespace uno {
+   class HessianModelFactory {
+   public:
+      static std::unique_ptr<HessianModel> create(const std::string& hessian_model, size_t dimension, size_t maximum_number_nonzeros,
+            bool convexify, const Options& options);
+   };
 
-inline std::unique_ptr<HessianModel> HessianModelFactory::create(const std::string& hessian_model, size_t dimension, size_t maximum_number_nonzeros,
-      bool convexify, const Options& options) {
-   if (hessian_model == "exact") {
-      if (convexify) {
-         return std::make_unique<ConvexifiedHessian>(dimension, maximum_number_nonzeros, options);
-      }
-      else {
-         return std::make_unique<ExactHessian>(dimension, maximum_number_nonzeros, options);
+   inline std::unique_ptr<HessianModel> HessianModelFactory::create(const std::string& hessian_model, size_t dimension, size_t maximum_number_nonzeros,
+         bool convexify, const Options& options) {
+      if (hessian_model == "exact") {
+         if (convexify) {
+            return std::make_unique<ConvexifiedHessian>(dimension, maximum_number_nonzeros, options);
+         }
+         else {
+            return std::make_unique<ExactHessian>(dimension, maximum_number_nonzeros, options);
+         }
       }
+      throw std::invalid_argument("Hessian model " + hessian_model + " does not exist");
    }
-   throw std::invalid_argument("Hessian model " + hessian_model + " does not exist");
-}
+} // namespace
 
-#endif // UNO_HESSIANMODELFACTORY_H
\ No newline at end of file
+#endif // UNO_HESSIANMODELFACTORY_H
diff --git a/uno/ingredients/subproblem/Subproblem.hpp b/uno/ingredients/subproblem/Subproblem.hpp
index b6d9372..9835969 100644
--- a/uno/ingredients/subproblem/Subproblem.hpp
+++ b/uno/ingredients/subproblem/Subproblem.hpp
@@ -8,58 +8,64 @@
 #include "optimization/Evaluations.hpp"
 #include "tools/Infinity.hpp"
 
-// forward declarations
-class Direction;
-class Iterate;
-class l1RelaxedProblem;
-class Model;
-class Multipliers;
-class OptimizationProblem;
-class Options;
-class Statistics;
-template <typename ElementType>
-class SymmetricMatrix;
-template <typename ElementType>
-class Vector;
-struct WarmstartInformation;
+namespace uno {
+   // forward declarations
+   class Direction;
+   class Iterate;
+   class l1RelaxedProblem;
+   class Model;
+   struct Multipliers;
+   class OptimizationProblem;
+   class Options;
+   class Statistics;
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrix;
+   template <typename ElementType>
+   class Vector;
+   struct WarmstartInformation;
 
-/*! \class Subproblem
- * \brief Subproblem
- */
-class Subproblem {
-public:
-   Subproblem(size_t number_variables, size_t number_constraints);
-   virtual ~Subproblem() = default;
+   /*! \class Subproblem
+    * \brief Subproblem
+    */
+   class Subproblem {
+   public:
+      explicit Subproblem() = default;
+      virtual ~Subproblem() = default;
 
-   // virtual methods implemented by subclasses
-   virtual void initialize_statistics(Statistics& statistics, const Options& options) = 0;
-   virtual bool generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) = 0;
-   virtual void solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
-         Direction& direction, const WarmstartInformation& warmstart_information) = 0;
+      // virtual methods implemented by subclasses
+      virtual void initialize_statistics(Statistics& statistics, const Options& options) = 0;
+      virtual void generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) = 0;
+      virtual void solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
+            Direction& direction, const WarmstartInformation& warmstart_information) = 0;
 
-   void set_trust_region_radius(double new_trust_region_radius);
-   virtual void initialize_feasibility_problem(const l1RelaxedProblem& problem, Iterate& current_iterate) = 0;
-   virtual void set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& current_iterate) = 0;
-   virtual void exit_feasibility_problem(const OptimizationProblem& problem, Iterate& trial_iterate) = 0;
+      void set_trust_region_radius(double new_trust_region_radius);
+      virtual void initialize_feasibility_problem(const l1RelaxedProblem& problem, Iterate& current_iterate) = 0;
+      virtual void set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& current_iterate) = 0;
+      virtual void exit_feasibility_problem(const OptimizationProblem& problem, Iterate& trial_iterate) = 0;
 
-   // progress measures
-   [[nodiscard]] virtual const SymmetricMatrix<double>& get_lagrangian_hessian() const = 0;
-   virtual void set_auxiliary_measure(const Model& model, Iterate& iterate) = 0;
-   [[nodiscard]] virtual double compute_predicted_auxiliary_reduction_model(const Model& model, const Iterate& current_iterate,
-         const Vector<double>& primal_direction, double step_length) const = 0;
+      // progress measures
+      [[nodiscard]] virtual const SymmetricMatrix<size_t, double>& get_lagrangian_hessian() const = 0;
+      virtual void set_auxiliary_measure(const Model& model, Iterate& iterate) = 0;
+      [[nodiscard]] virtual double compute_predicted_auxiliary_reduction_model(const Model& model, const Iterate& current_iterate,
+            const Vector<double>& primal_direction, double step_length) const = 0;
 
-   virtual void postprocess_iterate(const OptimizationProblem& problem, Iterate& iterate) = 0;
+      virtual void postprocess_iterate(const OptimizationProblem& problem, Iterate& iterate) = 0;
 
-   [[nodiscard]] virtual size_t get_hessian_evaluation_count() const = 0;
-   virtual void set_initial_point(const Vector<double>& initial_point) = 0;
+      [[nodiscard]] virtual size_t get_hessian_evaluation_count() const = 0;
+      virtual void set_initial_point(const Vector<double>& initial_point) = 0;
 
-   size_t number_subproblems_solved{0};
-   // when the parameterization of the subproblem (e.g. penalty or barrier parameter) is updated, signal it
-   bool subproblem_definition_changed{false};
+      size_t number_subproblems_solved{0};
+      // when the parameterization of the subproblem (e.g. penalty or barrier parameter) is updated, signal it
+      bool subproblem_definition_changed{false};
 
-protected:
-   Evaluations evaluations;
-   double trust_region_radius{INF<double>};
-};
+   protected:
+      double trust_region_radius{INF<double>};
+   };
 
-#endif // UNO_SUBPROBLEM_H
+   inline void Subproblem::set_trust_region_radius(double new_trust_region_radius) {
+      assert(0. < new_trust_region_radius && "The trust-region radius should be positive.");
+      this->trust_region_radius = new_trust_region_radius;
+   }
+} // namespace
+
+#endif // UNO_SUBPROBLEM_H
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/SubproblemFactory.cpp b/uno/ingredients/subproblem/SubproblemFactory.cpp
index d6224b8..0a70dc1 100644
--- a/uno/ingredients/subproblem/SubproblemFactory.cpp
+++ b/uno/ingredients/subproblem/SubproblemFactory.cpp
@@ -9,34 +9,36 @@
 #include "solvers/linear/SymmetricIndefiniteLinearSolverFactory.hpp"
 #include "tools/Options.hpp"
 
-std::unique_ptr<Subproblem> SubproblemFactory::create(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
-      size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options) {
-   const std::string subproblem_strategy = options.get_string("subproblem");
-   // active-set methods
-   if (subproblem_strategy == "QP") {
-      return std::make_unique<QPSubproblem>(number_variables, number_constraints, number_objective_gradient_nonzeros, number_jacobian_nonzeros,
-            number_hessian_nonzeros, options);
+namespace uno {
+   std::unique_ptr<Subproblem> SubproblemFactory::create(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
+         size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options) {
+      const std::string subproblem_strategy = options.get_string("subproblem");
+      // active-set methods
+      if (subproblem_strategy == "QP") {
+         return std::make_unique<QPSubproblem>(number_variables, number_constraints, number_objective_gradient_nonzeros, number_jacobian_nonzeros,
+               number_hessian_nonzeros, options);
+      }
+      else if (subproblem_strategy == "LP") {
+         return std::make_unique<LPSubproblem>(number_variables, number_constraints, number_objective_gradient_nonzeros, number_jacobian_nonzeros,
+               options);
+      }
+      // interior-point method
+      else if (subproblem_strategy == "primal_dual_interior_point") {
+         return std::make_unique<PrimalDualInteriorPointSubproblem>(number_variables, number_constraints, number_jacobian_nonzeros,
+               number_hessian_nonzeros, options);
+      }
+      throw std::invalid_argument("Subproblem strategy " + subproblem_strategy + " is not supported");
    }
-   else if (subproblem_strategy == "LP") {
-      return std::make_unique<LPSubproblem>(number_variables, number_constraints, number_objective_gradient_nonzeros, number_jacobian_nonzeros,
-            options);
-   }
-   // interior-point method
-   else if (subproblem_strategy == "primal_dual_interior_point") {
-      return std::make_unique<PrimalDualInteriorPointSubproblem>(number_variables, number_constraints, number_jacobian_nonzeros,
-            number_hessian_nonzeros, options);
-   }
-   throw std::invalid_argument("Subproblem strategy " + subproblem_strategy + " is not supported");
-}
 
-std::vector<std::string> SubproblemFactory::available_strategies() {
-   std::vector<std::string> strategies{};
-   if (not QPSolverFactory::available_solvers().empty()) {
-      strategies.emplace_back("QP");
-      strategies.emplace_back("LP");
-   }
-   if (not SymmetricIndefiniteLinearSolverFactory::available_solvers().empty()) {
-      strategies.emplace_back("primal_dual_interior_point");
+   std::vector<std::string> SubproblemFactory::available_strategies() {
+      std::vector<std::string> strategies{};
+      if (not QPSolverFactory::available_solvers().empty()) {
+         strategies.emplace_back("QP");
+         strategies.emplace_back("LP");
+      }
+      if (not SymmetricIndefiniteLinearSolverFactory::available_solvers().empty()) {
+         strategies.emplace_back("primal_dual_interior_point");
+      }
+      return strategies;
    }
-   return strategies;
-}
+} // namespace
diff --git a/uno/ingredients/subproblem/SubproblemFactory.hpp b/uno/ingredients/subproblem/SubproblemFactory.hpp
index 1b5016b..e470df6 100644
--- a/uno/ingredients/subproblem/SubproblemFactory.hpp
+++ b/uno/ingredients/subproblem/SubproblemFactory.hpp
@@ -7,15 +7,17 @@
 #include <memory>
 #include "Subproblem.hpp"
 
-// forward declaration
-class Options;
+namespace uno {
+   // forward declaration
+   class Options;
 
-class SubproblemFactory {
-	public:
-		static std::unique_ptr<Subproblem> create(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
-            size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options);
+   class SubproblemFactory {
+      public:
+         static std::unique_ptr<Subproblem> create(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
+               size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options);
 
-      static std::vector<std::string> available_strategies();
-};
+         static std::vector<std::string> available_strategies();
+   };
+} // namespace
 
 #endif // UNO_SUBPROBLEMFACTORY_H
diff --git a/uno/ingredients/subproblem/SubproblemStatus.hpp b/uno/ingredients/subproblem/SubproblemStatus.hpp
index 853a656..f56eec8 100644
--- a/uno/ingredients/subproblem/SubproblemStatus.hpp
+++ b/uno/ingredients/subproblem/SubproblemStatus.hpp
@@ -4,11 +4,13 @@
 #ifndef UNO_SUBPROBLEMSTATUS_H
 #define UNO_SUBPROBLEMSTATUS_H
 
-enum class SubproblemStatus {
-   OPTIMAL = 0,
-   UNBOUNDED_PROBLEM,
-   INFEASIBLE,
-   ERROR
-};
+namespace uno {
+   enum class SubproblemStatus {
+      OPTIMAL = 0,
+      UNBOUNDED_PROBLEM,
+      INFEASIBLE,
+      ERROR
+   };
+} // namespace
 
-#endif // UNO_SUBPROBLEMSTATUS_H
\ No newline at end of file
+#endif // UNO_SUBPROBLEMSTATUS_H
diff --git a/uno/ingredients/subproblem/inequality_constrained_methods/InequalityConstrainedMethod.cpp b/uno/ingredients/subproblem/inequality_constrained_methods/InequalityConstrainedMethod.cpp
index 9214959..2f920c6 100644
--- a/uno/ingredients/subproblem/inequality_constrained_methods/InequalityConstrainedMethod.cpp
+++ b/uno/ingredients/subproblem/inequality_constrained_methods/InequalityConstrainedMethod.cpp
@@ -8,77 +8,83 @@
 #include "tools/Options.hpp"
 #include "symbolic/VectorView.hpp"
 
-InequalityConstrainedMethod::InequalityConstrainedMethod(size_t number_variables, size_t number_constraints):
-      Subproblem(number_variables, number_constraints),
-      initial_point(number_variables),
-      direction_lower_bounds(number_variables),
-      direction_upper_bounds(number_variables),
-      linearized_constraints_lower_bounds(number_constraints),
-      linearized_constraints_upper_bounds(number_constraints) {
-}
-
-void InequalityConstrainedMethod::initialize_statistics(Statistics& /*statistics*/, const Options& /*options*/) {
-}
+namespace uno {
+   InequalityConstrainedMethod::InequalityConstrainedMethod(size_t number_variables, size_t number_constraints):
+         Subproblem(),
+         initial_point(number_variables),
+         direction_lower_bounds(number_variables),
+         direction_upper_bounds(number_variables),
+         linearized_constraints_lower_bounds(number_constraints),
+         linearized_constraints_upper_bounds(number_constraints),
+         objective_gradient(number_variables),
+         constraints(number_constraints),
+         constraint_jacobian(number_constraints, number_variables) {
+   }
 
-void InequalityConstrainedMethod::set_initial_point(const Vector<double>& point) {
-   this->initial_point = point;
-}
+   void InequalityConstrainedMethod::initialize_statistics(Statistics& /*statistics*/, const Options& /*options*/) {
+   }
 
-void InequalityConstrainedMethod::initialize_feasibility_problem(const l1RelaxedProblem& /*problem*/, Iterate& /*current_iterate*/) {
-   // do nothing
-}
+   void InequalityConstrainedMethod::set_initial_point(const Vector<double>& point) {
+      // copy the point into the member
+      this->initial_point = point;
+   }
 
-void InequalityConstrainedMethod::set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& current_iterate) {
-   problem.set_elastic_variable_values(current_iterate, [&](Iterate& iterate, size_t /*j*/, size_t elastic_index, double /*jacobian_coefficient*/) {
-      iterate.primals[elastic_index] = 0.;
-      iterate.multipliers.lower_bounds[elastic_index] = 1.;
-   });
-}
+   void InequalityConstrainedMethod::initialize_feasibility_problem(const l1RelaxedProblem& /*problem*/, Iterate& /*current_iterate*/) {
+      // do nothing
+   }
 
-void InequalityConstrainedMethod::exit_feasibility_problem(const OptimizationProblem& /*problem*/, Iterate& /*trial_iterate*/) {
-   // do nothing
-}
+   void InequalityConstrainedMethod::set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& current_iterate) {
+      problem.set_elastic_variable_values(current_iterate, [&](Iterate& iterate, size_t /*j*/, size_t elastic_index, double /*jacobian_coefficient*/) {
+         iterate.primals[elastic_index] = 0.;
+         iterate.multipliers.lower_bounds[elastic_index] = 1.;
+      });
+   }
 
-void InequalityConstrainedMethod::set_direction_bounds(const OptimizationProblem& problem, const Iterate& current_iterate) {
-   // bounds of original variables intersected with trust region
-   for (size_t variable_index: Range(problem.get_number_original_variables())) {
-      this->direction_lower_bounds[variable_index] = std::max(-this->trust_region_radius,
-            problem.variable_lower_bound(variable_index) - current_iterate.primals[variable_index]);
-      this->direction_upper_bounds[variable_index] = std::min(this->trust_region_radius,
-            problem.variable_upper_bound(variable_index) - current_iterate.primals[variable_index]);
+   void InequalityConstrainedMethod::exit_feasibility_problem(const OptimizationProblem& /*problem*/, Iterate& /*trial_iterate*/) {
+      // do nothing
    }
-   // bounds of additional variables (no trust region!)
-   for (size_t variable_index: Range(problem.get_number_original_variables(), problem.number_variables)) {
-      this->direction_lower_bounds[variable_index] = problem.variable_lower_bound(variable_index) - current_iterate.primals[variable_index];
-      this->direction_upper_bounds[variable_index] = problem.variable_upper_bound(variable_index) - current_iterate.primals[variable_index];
+
+   void InequalityConstrainedMethod::set_direction_bounds(const OptimizationProblem& problem, const Iterate& current_iterate) {
+      // bounds of original variables intersected with trust region
+      for (size_t variable_index: Range(problem.get_number_original_variables())) {
+         this->direction_lower_bounds[variable_index] = std::max(-this->trust_region_radius,
+               problem.variable_lower_bound(variable_index) - current_iterate.primals[variable_index]);
+         this->direction_upper_bounds[variable_index] = std::min(this->trust_region_radius,
+               problem.variable_upper_bound(variable_index) - current_iterate.primals[variable_index]);
+      }
+      // bounds of additional variables (no trust region!)
+      for (size_t variable_index: Range(problem.get_number_original_variables(), problem.number_variables)) {
+         this->direction_lower_bounds[variable_index] = problem.variable_lower_bound(variable_index) - current_iterate.primals[variable_index];
+         this->direction_upper_bounds[variable_index] = problem.variable_upper_bound(variable_index) - current_iterate.primals[variable_index];
+      }
    }
-}
 
-void InequalityConstrainedMethod::set_linearized_constraint_bounds(const OptimizationProblem& problem, const std::vector<double>& current_constraints) {
-   for (size_t constraint_index: Range(problem.number_constraints)) {
-      this->linearized_constraints_lower_bounds[constraint_index] = problem.constraint_lower_bound(constraint_index) -
-            current_constraints[constraint_index];
-      this->linearized_constraints_upper_bounds[constraint_index] = problem.constraint_upper_bound(constraint_index) -
-            current_constraints[constraint_index];
+   void InequalityConstrainedMethod::set_linearized_constraint_bounds(const OptimizationProblem& problem, const std::vector<double>& current_constraints) {
+      for (size_t constraint_index: Range(problem.number_constraints)) {
+         this->linearized_constraints_lower_bounds[constraint_index] = problem.constraint_lower_bound(constraint_index) -
+               current_constraints[constraint_index];
+         this->linearized_constraints_upper_bounds[constraint_index] = problem.constraint_upper_bound(constraint_index) -
+               current_constraints[constraint_index];
+      }
    }
-}
 
-void InequalityConstrainedMethod::compute_dual_displacements(const Multipliers& current_multipliers, Multipliers& direction_multipliers) {
-   // compute dual *displacements* (active-set methods usually compute the new duals, not the displacements)
-   view(direction_multipliers.constraints, 0, current_multipliers.constraints.size()) -= current_multipliers.constraints;
-   view(direction_multipliers.lower_bounds, 0, current_multipliers.lower_bounds.size()) -= current_multipliers.lower_bounds;
-   view(direction_multipliers.upper_bounds, 0, current_multipliers.upper_bounds.size()) -= current_multipliers.upper_bounds;
-}
+   void InequalityConstrainedMethod::compute_dual_displacements(const Multipliers& current_multipliers, Multipliers& direction_multipliers) {
+      // compute dual *displacements* (active-set methods usually compute the new duals, not the displacements)
+      view(direction_multipliers.constraints, 0, current_multipliers.constraints.size()) -= current_multipliers.constraints;
+      view(direction_multipliers.lower_bounds, 0, current_multipliers.lower_bounds.size()) -= current_multipliers.lower_bounds;
+      view(direction_multipliers.upper_bounds, 0, current_multipliers.upper_bounds.size()) -= current_multipliers.upper_bounds;
+   }
 
-// auxiliary measure is 0 in inequality-constrained methods
-void InequalityConstrainedMethod::set_auxiliary_measure(const Model& /*model*/, Iterate& iterate) {
-   iterate.progress.auxiliary = 0.;
-}
+   // auxiliary measure is 0 in inequality-constrained methods
+   void InequalityConstrainedMethod::set_auxiliary_measure(const Model& /*model*/, Iterate& iterate) {
+      iterate.progress.auxiliary = 0.;
+   }
 
-double InequalityConstrainedMethod::compute_predicted_auxiliary_reduction_model(const Model& /*model*/, const Iterate& /*current_iterate*/,
-      const Vector<double>& /*primal_direction*/, double /*step_length*/) const {
-   return 0.;
-}
+   double InequalityConstrainedMethod::compute_predicted_auxiliary_reduction_model(const Model& /*model*/, const Iterate& /*current_iterate*/,
+         const Vector<double>& /*primal_direction*/, double /*step_length*/) const {
+      return 0.;
+   }
 
-void InequalityConstrainedMethod::postprocess_iterate(const OptimizationProblem& /*problem*/, Iterate& /*iterate*/) {
-}
+   void InequalityConstrainedMethod::postprocess_iterate(const OptimizationProblem& /*problem*/, Iterate& /*iterate*/) {
+   }
+} // namespace
diff --git a/uno/ingredients/subproblem/inequality_constrained_methods/InequalityConstrainedMethod.hpp b/uno/ingredients/subproblem/inequality_constrained_methods/InequalityConstrainedMethod.hpp
index c7fb54c..8cf1a82 100644
--- a/uno/ingredients/subproblem/inequality_constrained_methods/InequalityConstrainedMethod.hpp
+++ b/uno/ingredients/subproblem/inequality_constrained_methods/InequalityConstrainedMethod.hpp
@@ -7,32 +7,38 @@
 #include "ingredients/subproblem/Subproblem.hpp"
 #include "linear_algebra/Vector.hpp"
 
-class InequalityConstrainedMethod : public Subproblem {
-public:
-   InequalityConstrainedMethod(size_t number_variables, size_t number_constraints);
-   ~InequalityConstrainedMethod() override = default;
-   
-   void initialize_statistics(Statistics& statistics, const Options& options) override;
-   void set_initial_point(const Vector<double>& point) override;
-   void initialize_feasibility_problem(const l1RelaxedProblem& problem, Iterate& current_iterate) override;
-   void set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& current_iterate) override;
-   void exit_feasibility_problem(const OptimizationProblem& problem, Iterate& trial_iterate) override;
-
-   void set_auxiliary_measure(const Model& model, Iterate& iterate) override;
-   [[nodiscard]] double compute_predicted_auxiliary_reduction_model(const Model& model, const Iterate&, const Vector<double>&, double) const override;
-
-   void postprocess_iterate(const OptimizationProblem& model, Iterate& iterate) override;
-
-protected:
-   Vector<double> initial_point{};
-   std::vector<double> direction_lower_bounds{};
-   std::vector<double> direction_upper_bounds{};
-   std::vector<double> linearized_constraints_lower_bounds{};
-   std::vector<double> linearized_constraints_upper_bounds{};
-
-   void set_direction_bounds(const OptimizationProblem& problem, const Iterate& current_iterate);
-   void set_linearized_constraint_bounds(const OptimizationProblem& problem, const std::vector<double>& current_constraints);
-   static void compute_dual_displacements(const Multipliers& current_multipliers, Multipliers& direction_multipliers);
-};
+namespace uno {
+   class InequalityConstrainedMethod : public Subproblem {
+   public:
+      InequalityConstrainedMethod(size_t number_variables, size_t number_constraints);
+      ~InequalityConstrainedMethod() override = default;
+      
+      void initialize_statistics(Statistics& statistics, const Options& options) override;
+      void set_initial_point(const Vector<double>& point) override;
+      void initialize_feasibility_problem(const l1RelaxedProblem& problem, Iterate& current_iterate) override;
+      void set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& current_iterate) override;
+      void exit_feasibility_problem(const OptimizationProblem& problem, Iterate& trial_iterate) override;
+
+      void set_auxiliary_measure(const Model& model, Iterate& iterate) override;
+      [[nodiscard]] double compute_predicted_auxiliary_reduction_model(const Model& model, const Iterate&, const Vector<double>&, double) const override;
+
+      void postprocess_iterate(const OptimizationProblem& model, Iterate& iterate) override;
+
+   protected:
+      Vector<double> initial_point{};
+      std::vector<double> direction_lower_bounds{};
+      std::vector<double> direction_upper_bounds{};
+      std::vector<double> linearized_constraints_lower_bounds{};
+      std::vector<double> linearized_constraints_upper_bounds{};
+
+      SparseVector<double> objective_gradient; /*!< Sparse Jacobian of the objective */
+      std::vector<double> constraints; /*!< Constraint values (size \f$m)\f$ */
+      RectangularMatrix<double> constraint_jacobian; /*!< Sparse Jacobian of the constraints */
+
+      void set_direction_bounds(const OptimizationProblem& problem, const Iterate& current_iterate);
+      void set_linearized_constraint_bounds(const OptimizationProblem& problem, const std::vector<double>& current_constraints);
+      static void compute_dual_displacements(const Multipliers& current_multipliers, Multipliers& direction_multipliers);
+   };
+} // namespace
 
 #endif // UNO_INEQUALITYCONSTRAINEDMETHOD_H
diff --git a/uno/ingredients/subproblem/inequality_constrained_methods/LPSubproblem.cpp b/uno/ingredients/subproblem/inequality_constrained_methods/LPSubproblem.cpp
index f09d299..60c49d3 100644
--- a/uno/ingredients/subproblem/inequality_constrained_methods/LPSubproblem.cpp
+++ b/uno/ingredients/subproblem/inequality_constrained_methods/LPSubproblem.cpp
@@ -9,60 +9,61 @@
 #include "solvers/LP/LPSolverFactory.hpp"
 #include "tools/Options.hpp"
 
-LPSubproblem::LPSubproblem(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
-      size_t number_jacobian_nonzeros, const Options& options) :
-      InequalityConstrainedMethod(number_variables, number_constraints),
-      solver(LPSolverFactory::create(options.get_string("LP_solver"), number_variables, number_constraints,
-            number_objective_gradient_nonzeros, number_jacobian_nonzeros, options)),
-      zero_hessian(COOSymmetricMatrix<double>::zero(number_variables)) {
-}
-
-bool LPSubproblem::generate_initial_iterate(const OptimizationProblem& /*problem*/, Iterate& /*initial_iterate*/) {
-   return true;
-}
+namespace uno {
+   LPSubproblem::LPSubproblem(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
+         size_t number_jacobian_nonzeros, const Options& options) :
+         InequalityConstrainedMethod(number_variables, number_constraints),
+         solver(LPSolverFactory::create(options.get_string("LP_solver"), number_variables, number_constraints,
+               number_objective_gradient_nonzeros, number_jacobian_nonzeros, options)),
+         zero_hessian(COOSymmetricMatrix<size_t, double>::zero(number_variables)) {
+   }
 
-void LPSubproblem::evaluate_functions(const OptimizationProblem& problem, Iterate& current_iterate, const WarmstartInformation& warmstart_information) {
-   // objective gradient
-   if (warmstart_information.objective_changed) {
-      problem.evaluate_objective_gradient(current_iterate, this->evaluations.objective_gradient);
+   void LPSubproblem::generate_initial_iterate(const OptimizationProblem& /*problem*/, Iterate& /*initial_iterate*/) {
    }
-   // constraints and constraint Jacobian
-   if (warmstart_information.constraints_changed) {
-      problem.evaluate_constraints(current_iterate, this->evaluations.constraints);
-      problem.evaluate_constraint_jacobian(current_iterate, this->evaluations.constraint_jacobian);
+
+   void LPSubproblem::evaluate_functions(const OptimizationProblem& problem, Iterate& current_iterate, const WarmstartInformation& warmstart_information) {
+      // objective gradient
+      if (warmstart_information.objective_changed) {
+         problem.evaluate_objective_gradient(current_iterate, this->objective_gradient);
+      }
+      // constraints and constraint Jacobian
+      if (warmstart_information.constraints_changed) {
+         problem.evaluate_constraints(current_iterate, this->constraints);
+         problem.evaluate_constraint_jacobian(current_iterate, this->constraint_jacobian);
+      }
    }
-}
 
-void LPSubproblem::solve(Statistics& /*statistics*/, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
-      Direction& direction, const WarmstartInformation& warmstart_information) {
-   // evaluate the functions at the current iterate
-   this->evaluate_functions(problem, current_iterate, warmstart_information);
+   void LPSubproblem::solve(Statistics& /*statistics*/, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
+         Direction& direction, const WarmstartInformation& warmstart_information) {
+      // evaluate the functions at the current iterate
+      this->evaluate_functions(problem, current_iterate, warmstart_information);
 
-   // set bounds of the variable displacements
-   if (warmstart_information.variable_bounds_changed) {
-      this->set_direction_bounds(problem, current_iterate);
-   }
+      // set bounds of the variable displacements
+      if (warmstart_information.variable_bounds_changed) {
+         this->set_direction_bounds(problem, current_iterate);
+      }
 
-   // set bounds of the linearized constraints
-   if (warmstart_information.constraint_bounds_changed) {
-      this->set_linearized_constraint_bounds(problem, this->evaluations.constraints);
-   }
+      // set bounds of the linearized constraints
+      if (warmstart_information.constraint_bounds_changed) {
+         this->set_linearized_constraint_bounds(problem, this->constraints);
+      }
 
-   // solve the LP
-   this->solver->solve_LP(problem.number_variables, problem.number_constraints, this->direction_lower_bounds, this->direction_upper_bounds,
-         this->linearized_constraints_lower_bounds, this->linearized_constraints_upper_bounds, this->evaluations.objective_gradient,
-         this->evaluations.constraint_jacobian, this->initial_point, direction, warmstart_information);
-   InequalityConstrainedMethod::compute_dual_displacements(current_multipliers, direction.multipliers);
-   this->number_subproblems_solved++;
-   // reset the initial point
-   this->initial_point.fill(0.);
-}
+      // solve the LP
+      this->solver->solve_LP(problem.number_variables, problem.number_constraints, this->direction_lower_bounds, this->direction_upper_bounds,
+            this->linearized_constraints_lower_bounds, this->linearized_constraints_upper_bounds, this->objective_gradient,
+            this->constraint_jacobian, this->initial_point, direction, warmstart_information);
+      InequalityConstrainedMethod::compute_dual_displacements(current_multipliers, direction.multipliers);
+      this->number_subproblems_solved++;
+      // reset the initial point
+      this->initial_point.fill(0.);
+   }
 
-const SymmetricMatrix<double>& LPSubproblem::get_lagrangian_hessian() const {
-   return this->zero_hessian;
-}
+   const SymmetricMatrix<size_t, double>& LPSubproblem::get_lagrangian_hessian() const {
+      return this->zero_hessian;
+   }
 
-size_t LPSubproblem::get_hessian_evaluation_count() const {
-   // no second order evaluation is used
-   return 0;
-}
+   size_t LPSubproblem::get_hessian_evaluation_count() const {
+      // no second order evaluation is used
+      return 0;
+   }
+} // namespace
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/inequality_constrained_methods/LPSubproblem.hpp b/uno/ingredients/subproblem/inequality_constrained_methods/LPSubproblem.hpp
index 36561d1..f82bce7 100644
--- a/uno/ingredients/subproblem/inequality_constrained_methods/LPSubproblem.hpp
+++ b/uno/ingredients/subproblem/inequality_constrained_methods/LPSubproblem.hpp
@@ -9,23 +9,25 @@
 #include "linear_algebra/COOSymmetricMatrix.hpp"
 #include "solvers/LP/LPSolver.hpp"
 
-class LPSubproblem : public InequalityConstrainedMethod {
-public:
-   LPSubproblem(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros,
-         const Options& options);
+namespace uno {
+   class LPSubproblem : public InequalityConstrainedMethod {
+   public:
+      LPSubproblem(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros,
+            const Options& options);
 
-   [[nodiscard]] bool generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) override;
-   void solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
-         Direction& direction, const WarmstartInformation& warmstart_information) override;
-   [[nodiscard]] const SymmetricMatrix<double>& get_lagrangian_hessian() const override;
-   [[nodiscard]] size_t get_hessian_evaluation_count() const override;
+      void generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) override;
+      void solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
+            Direction& direction, const WarmstartInformation& warmstart_information) override;
+      [[nodiscard]] const SymmetricMatrix<size_t, double>& get_lagrangian_hessian() const override;
+      [[nodiscard]] size_t get_hessian_evaluation_count() const override;
 
-private:
-   // pointer to allow polymorphism
-   const std::unique_ptr<LPSolver> solver; /*!< Solver that solves the subproblem */
-   const COOSymmetricMatrix<double> zero_hessian;
+   private:
+      // pointer to allow polymorphism
+      const std::unique_ptr<LPSolver> solver; /*!< Solver that solves the subproblem */
+      const COOSymmetricMatrix<size_t, double> zero_hessian;
 
-   void evaluate_functions(const OptimizationProblem& problem, Iterate& current_iterate, const WarmstartInformation& warmstart_information);
-};
+      void evaluate_functions(const OptimizationProblem& problem, Iterate& current_iterate, const WarmstartInformation& warmstart_information);
+   };
+} // namespace
 
-#endif // UNO_LPSUBPROBLEM_H
+#endif // UNO_LPSUBPROBLEM_H
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/inequality_constrained_methods/QPSubproblem.cpp b/uno/ingredients/subproblem/inequality_constrained_methods/QPSubproblem.cpp
index 16c5bb7..5e59d00 100644
--- a/uno/ingredients/subproblem/inequality_constrained_methods/QPSubproblem.cpp
+++ b/uno/ingredients/subproblem/inequality_constrained_methods/QPSubproblem.cpp
@@ -13,84 +13,81 @@
 #include "tools/Options.hpp"
 #include "tools/Statistics.hpp"
 
-QPSubproblem::QPSubproblem(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
-      size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options) :
-      InequalityConstrainedMethod(number_variables, number_constraints),
-      use_regularization(options.get_string("globalization_mechanism") != "TR" || options.get_bool("convexify_QP")),
-      enforce_linear_constraints_at_initial_iterate(options.get_bool("enforce_linear_constraints")),
-      // if no trust region is used, the problem should be convexified to guarantee boundedness
-      hessian_model(HessianModelFactory::create(options.get_string("hessian_model"), number_variables,
-            number_hessian_nonzeros + number_variables, this->use_regularization, options)),
-      // maximum number of Hessian nonzeros = number nonzeros + possible diagonal inertia correction
-      solver(QPSolverFactory::create(options.get_string("QP_solver"), number_variables, number_constraints,
-            number_objective_gradient_nonzeros, number_jacobian_nonzeros,
-            // if the QP solver is used during preprocessing, we need to allocate the Hessian with at least number_variables elements
-            std::max(this->enforce_linear_constraints_at_initial_iterate ? number_variables : 0, hessian_model->hessian->capacity),
-            options)) {
-}
-
-void QPSubproblem::initialize_statistics(Statistics& statistics, const Options& options) {
-   if (this->use_regularization) {
-      statistics.add_column("regularization", Statistics::double_width, options.get_int("statistics_regularization_column_order"));
+namespace uno {
+   QPSubproblem::QPSubproblem(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros,
+         size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options) :
+         InequalityConstrainedMethod(number_variables, number_constraints),
+         use_regularization(options.get_string("globalization_mechanism") != "TR" || options.get_bool("convexify_QP")),
+         enforce_linear_constraints_at_initial_iterate(options.get_bool("enforce_linear_constraints")),
+         // if no trust region is used, the problem should be convexified to guarantee boundedness
+         hessian_model(HessianModelFactory::create(options.get_string("hessian_model"), number_variables,
+               number_hessian_nonzeros + number_variables, this->use_regularization, options)),
+         // maximum number of Hessian nonzeros = number nonzeros + possible diagonal inertia correction
+         solver(QPSolverFactory::create(options.get_string("QP_solver"), number_variables, number_constraints,
+               number_objective_gradient_nonzeros, number_jacobian_nonzeros,
+               // if the QP solver is used during preprocessing, we need to allocate the Hessian with at least number_variables elements
+               std::max(this->enforce_linear_constraints_at_initial_iterate ? number_variables : 0, hessian_model->hessian->capacity),
+               options)) {
    }
-}
 
-bool QPSubproblem::generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) {
-   if (this->enforce_linear_constraints_at_initial_iterate) {
-      const bool is_feasible = Preprocessing::enforce_linear_constraints(problem.model, initial_iterate.primals, initial_iterate.multipliers,
-            *this->solver);
-      if (not is_feasible) {
-         return false;
+   void QPSubproblem::initialize_statistics(Statistics& statistics, const Options& options) {
+      if (this->use_regularization) {
+         statistics.add_column("regularization", Statistics::double_width, options.get_int("statistics_regularization_column_order"));
       }
    }
-   return true;
-}
 
-void QPSubproblem::evaluate_functions(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
-      const Multipliers& current_multipliers, const WarmstartInformation& warmstart_information) {
-   // Lagrangian Hessian
-   if (warmstart_information.objective_changed || warmstart_information.constraints_changed) {
-      this->hessian_model->evaluate(statistics, problem, current_iterate.primals, current_multipliers.constraints);
-   }
-   // objective gradient, constraints and constraint Jacobian
-   if (warmstart_information.objective_changed) {
-      problem.evaluate_objective_gradient(current_iterate, this->evaluations.objective_gradient);
+   void QPSubproblem::generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) {
+      if (this->enforce_linear_constraints_at_initial_iterate) {
+         Preprocessing::enforce_linear_constraints(problem.model, initial_iterate.primals, initial_iterate.multipliers, *this->solver);
+      }
    }
-   if (warmstart_information.constraints_changed) {
-      problem.evaluate_constraints(current_iterate, this->evaluations.constraints);
-      problem.evaluate_constraint_jacobian(current_iterate, this->evaluations.constraint_jacobian);
+
+   void QPSubproblem::evaluate_functions(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
+         const Multipliers& current_multipliers, const WarmstartInformation& warmstart_information) {
+      // Lagrangian Hessian
+      if (warmstart_information.objective_changed || warmstart_information.constraints_changed) {
+         this->hessian_model->evaluate(statistics, problem, current_iterate.primals, current_multipliers.constraints);
+      }
+      // objective gradient, constraints and constraint Jacobian
+      if (warmstart_information.objective_changed) {
+         problem.evaluate_objective_gradient(current_iterate, this->objective_gradient);
+      }
+      if (warmstart_information.constraints_changed) {
+         problem.evaluate_constraints(current_iterate, this->constraints);
+         problem.evaluate_constraint_jacobian(current_iterate, this->constraint_jacobian);
+      }
    }
-}
 
-void QPSubproblem::solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
-      Direction& direction, const WarmstartInformation& warmstart_information) {
-   // evaluate the functions at the current iterate
-   this->evaluate_functions(statistics, problem, current_iterate, current_multipliers, warmstart_information);
+   void QPSubproblem::solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
+         Direction& direction, const WarmstartInformation& warmstart_information) {
+      // evaluate the functions at the current iterate
+      this->evaluate_functions(statistics, problem, current_iterate, current_multipliers, warmstart_information);
 
-   // set bounds of the variable displacements
-   if (warmstart_information.variable_bounds_changed) {
-      this->set_direction_bounds(problem, current_iterate);
-   }
+      // set bounds of the variable displacements
+      if (warmstart_information.variable_bounds_changed) {
+         this->set_direction_bounds(problem, current_iterate);
+      }
 
-   // set bounds of the linearized constraints
-   if (warmstart_information.constraint_bounds_changed) {
-      this->set_linearized_constraint_bounds(problem, this->evaluations.constraints);
-   }
+      // set bounds of the linearized constraints
+      if (warmstart_information.constraint_bounds_changed) {
+         this->set_linearized_constraint_bounds(problem, this->constraints);
+      }
 
-   // solve the QP
-   this->solver->solve_QP(problem.number_variables, problem.number_constraints, this->direction_lower_bounds, this->direction_upper_bounds,
-         this->linearized_constraints_lower_bounds, this->linearized_constraints_upper_bounds, this->evaluations.objective_gradient,
-         this->evaluations.constraint_jacobian, *this->hessian_model->hessian, this->initial_point, direction, warmstart_information);
-   InequalityConstrainedMethod::compute_dual_displacements(current_multipliers, direction.multipliers);
-   this->number_subproblems_solved++;
-   // reset the initial point
-   this->initial_point.fill(0.);
-}
+      // solve the QP
+      this->solver->solve_QP(problem.number_variables, problem.number_constraints, this->direction_lower_bounds, this->direction_upper_bounds,
+            this->linearized_constraints_lower_bounds, this->linearized_constraints_upper_bounds, this->objective_gradient,
+            this->constraint_jacobian, *this->hessian_model->hessian, this->initial_point, direction, warmstart_information);
+      InequalityConstrainedMethod::compute_dual_displacements(current_multipliers, direction.multipliers);
+      this->number_subproblems_solved++;
+      // reset the initial point
+      this->initial_point.fill(0.);
+   }
 
-const SymmetricMatrix<double>& QPSubproblem::get_lagrangian_hessian() const {
-   return *this->hessian_model->hessian;
-}
+   const SymmetricMatrix<size_t, double>& QPSubproblem::get_lagrangian_hessian() const {
+      return *this->hessian_model->hessian;
+   }
 
-size_t QPSubproblem::get_hessian_evaluation_count() const {
-   return this->hessian_model->evaluation_count;
-}
+   size_t QPSubproblem::get_hessian_evaluation_count() const {
+      return this->hessian_model->evaluation_count;
+   }
+} // namespace
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/inequality_constrained_methods/QPSubproblem.hpp b/uno/ingredients/subproblem/inequality_constrained_methods/QPSubproblem.hpp
index 367693d..a4037ef 100644
--- a/uno/ingredients/subproblem/inequality_constrained_methods/QPSubproblem.hpp
+++ b/uno/ingredients/subproblem/inequality_constrained_methods/QPSubproblem.hpp
@@ -8,27 +8,29 @@
 #include "ingredients/subproblem/HessianModel.hpp"
 #include "solvers/QP/QPSolver.hpp"
 
-class QPSubproblem : public InequalityConstrainedMethod {
-public:
-   QPSubproblem(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros,
-         size_t number_hessian_nonzeros, const Options& options);
+namespace uno {
+   class QPSubproblem : public InequalityConstrainedMethod {
+   public:
+      QPSubproblem(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros,
+            size_t number_hessian_nonzeros, const Options& options);
 
-   void initialize_statistics(Statistics& statistics, const Options& options) override;
-   [[nodiscard]] bool generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) override;
-   void solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
-         Direction& direction, const WarmstartInformation& warmstart_information) override;
-   [[nodiscard]] const SymmetricMatrix<double>& get_lagrangian_hessian() const override;
-   [[nodiscard]] size_t get_hessian_evaluation_count() const override;
+      void initialize_statistics(Statistics& statistics, const Options& options) override;
+      void generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) override;
+      void solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
+            Direction& direction, const WarmstartInformation& warmstart_information) override;
+      [[nodiscard]] const SymmetricMatrix<size_t, double>& get_lagrangian_hessian() const override;
+      [[nodiscard]] size_t get_hessian_evaluation_count() const override;
 
-protected:
-   const bool use_regularization;
-   const bool enforce_linear_constraints_at_initial_iterate;
-   // pointers to allow polymorphism
-   const std::unique_ptr<HessianModel> hessian_model; /*!< Strategy to evaluate or approximate the Hessian */
-   const std::unique_ptr<QPSolver> solver; /*!< Solver that solves the subproblem */
+   protected:
+      const bool use_regularization;
+      const bool enforce_linear_constraints_at_initial_iterate;
+      // pointers to allow polymorphism
+      const std::unique_ptr<HessianModel> hessian_model; /*!< Strategy to evaluate or approximate the Hessian */
+      const std::unique_ptr<QPSolver> solver; /*!< Solver that solves the subproblem */
 
-   void evaluate_functions(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
-         const WarmstartInformation& warmstart_information);
-};
+      void evaluate_functions(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
+            const WarmstartInformation& warmstart_information);
+   };
+} // namespace
 
-#endif // UNO_QPSUBPROBLEM_H
+#endif // UNO_QPSUBPROBLEM_H
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/interior_point_methods/BarrierParameterUpdateStrategy.cpp b/uno/ingredients/subproblem/interior_point_methods/BarrierParameterUpdateStrategy.cpp
index d62320e..5cfe86e 100644
--- a/uno/ingredients/subproblem/interior_point_methods/BarrierParameterUpdateStrategy.cpp
+++ b/uno/ingredients/subproblem/interior_point_methods/BarrierParameterUpdateStrategy.cpp
@@ -10,70 +10,73 @@
 #include "tools/Logger.hpp"
 #include "tools/Options.hpp"
 
-BarrierParameterUpdateStrategy::BarrierParameterUpdateStrategy(const Options& options):
-   barrier_parameter(options.get_double("barrier_initial_parameter")),
-   tolerance(options.get_double("tolerance")),
-   parameters({
-      options.get_double("barrier_k_mu"),
-      options.get_double("barrier_theta_mu"),
-      options.get_double("barrier_k_epsilon"),
-      options.get_double("barrier_update_fraction")
-   }) {
-}
-
-double BarrierParameterUpdateStrategy::get_barrier_parameter() const {
-   return this->barrier_parameter;
-}
+namespace uno {
+   BarrierParameterUpdateStrategy::BarrierParameterUpdateStrategy(const Options& options):
+      barrier_parameter(options.get_double("barrier_initial_parameter")),
+      tolerance(options.get_double("tolerance")),
+      parameters({
+         options.get_double("barrier_k_mu"),
+         options.get_double("barrier_theta_mu"),
+         options.get_double("barrier_k_epsilon"),
+         options.get_double("barrier_update_fraction")
+      }) {
+   }
 
-void BarrierParameterUpdateStrategy::set_barrier_parameter(double new_barrier_parameter) {
-   assert(0. <= new_barrier_parameter && "The barrier parameter should be positive.");
-   this->barrier_parameter = new_barrier_parameter;
-}
+   double BarrierParameterUpdateStrategy::get_barrier_parameter() const {
+      return this->barrier_parameter;
+   }
 
-bool BarrierParameterUpdateStrategy::update_barrier_parameter(const OptimizationProblem& problem, const Iterate& current_iterate) {
-   // primal-dual errors
-   const double scaled_stationarity = current_iterate.residuals.KKT_stationarity / current_iterate.residuals.stationarity_scaling;
-   double primal_dual_error = std::max({
-      scaled_stationarity,
-      current_iterate.residuals.infeasibility,
-         current_iterate.residuals.complementarity / current_iterate.residuals.complementarity_scaling
-   });
-   DEBUG << "Max scaled primal-dual error for barrier subproblem is " << primal_dual_error << '\n';
+   void BarrierParameterUpdateStrategy::set_barrier_parameter(double new_barrier_parameter) {
+      assert(0. <= new_barrier_parameter && "The barrier parameter should be positive.");
+      this->barrier_parameter = new_barrier_parameter;
+   }
 
-   // update the barrier parameter (Eq. 7 in IPOPT paper)
-   const double tolerance_fraction = this->tolerance / this->parameters.update_fraction;
-   bool parameter_updated = false;
-   while (primal_dual_error <= this->parameters.k_epsilon * this->barrier_parameter && tolerance_fraction < this->barrier_parameter) {
-      this->barrier_parameter = std::max(tolerance_fraction, std::min(this->parameters.k_mu * this->barrier_parameter,
-            std::pow(this->barrier_parameter, this->parameters.theta_mu)));
-      DEBUG << "Barrier parameter mu updated to " << this->barrier_parameter << '\n';
-      // update complementarity error
-      double scaled_complementarity_error = BarrierParameterUpdateStrategy::compute_shifted_complementarity_error(problem, current_iterate,
-            this->barrier_parameter) / current_iterate.residuals.complementarity_scaling;
-      primal_dual_error = std::max({
+   bool BarrierParameterUpdateStrategy::update_barrier_parameter(const OptimizationProblem& problem, const Iterate& current_iterate,
+         const PrimalDualResiduals& residuals) {
+      // primal-dual errors
+      const double scaled_stationarity = residuals.stationarity / residuals.stationarity_scaling;
+      double primal_dual_error = std::max({
          scaled_stationarity,
-         current_iterate.residuals.infeasibility,
-         scaled_complementarity_error
+         residuals.primal_feasibility,
+         residuals.complementarity / residuals.complementarity_scaling
       });
       DEBUG << "Max scaled primal-dual error for barrier subproblem is " << primal_dual_error << '\n';
-      parameter_updated = true;
-   }
-   return parameter_updated;
-}
 
-double BarrierParameterUpdateStrategy::compute_shifted_complementarity_error(const OptimizationProblem& problem, const Iterate& iterate,
-      double shift_value) {
-   const VectorExpression shifted_bound_complementarity(Range(problem.number_variables), [&](size_t variable_index) {
-      double result = 0.;
-      if (0. < iterate.multipliers.lower_bounds[variable_index]) { // lower bound
-         result = std::max(result, std::abs(iterate.multipliers.lower_bounds[variable_index] *
-            (iterate.primals[variable_index] - problem.variable_lower_bound(variable_index)) - shift_value));
+      // update the barrier parameter (Eq. 7 in IPOPT paper)
+      const double tolerance_fraction = this->tolerance / this->parameters.update_fraction;
+      bool parameter_updated = false;
+      while (primal_dual_error <= this->parameters.k_epsilon * this->barrier_parameter && tolerance_fraction < this->barrier_parameter) {
+         this->barrier_parameter = std::max(tolerance_fraction, std::min(this->parameters.k_mu * this->barrier_parameter,
+               std::pow(this->barrier_parameter, this->parameters.theta_mu)));
+         DEBUG << "Barrier parameter mu updated to " << this->barrier_parameter << '\n';
+         // update complementarity error
+         double scaled_complementarity_error = BarrierParameterUpdateStrategy::compute_shifted_complementarity_error(problem, current_iterate,
+               this->barrier_parameter) / residuals.complementarity_scaling;
+         primal_dual_error = std::max({
+            scaled_stationarity,
+            residuals.primal_feasibility,
+            scaled_complementarity_error
+         });
+         DEBUG << "Max scaled primal-dual error for barrier subproblem is " << primal_dual_error << '\n';
+         parameter_updated = true;
       }
-      if (iterate.multipliers.upper_bounds[variable_index] < 0.) { // upper bound
-         result = std::max(result, std::abs(iterate.multipliers.upper_bounds[variable_index] *
-            (iterate.primals[variable_index] - problem.variable_upper_bound(variable_index)) - shift_value));
-      }
-      return result;
-   });
-   return norm_inf(shifted_bound_complementarity); // TODO use a generic norm
-}
+      return parameter_updated;
+   }
+
+   double BarrierParameterUpdateStrategy::compute_shifted_complementarity_error(const OptimizationProblem& problem, const Iterate& iterate,
+         double shift_value) {
+      const VectorExpression shifted_bound_complementarity(Range(problem.number_variables), [&](size_t variable_index) {
+         double result = 0.;
+         if (0. < iterate.multipliers.lower_bounds[variable_index]) { // lower bound
+            result = std::max(result, std::abs(iterate.multipliers.lower_bounds[variable_index] *
+               (iterate.primals[variable_index] - problem.variable_lower_bound(variable_index)) - shift_value));
+         }
+         if (iterate.multipliers.upper_bounds[variable_index] < 0.) { // upper bound
+            result = std::max(result, std::abs(iterate.multipliers.upper_bounds[variable_index] *
+               (iterate.primals[variable_index] - problem.variable_upper_bound(variable_index)) - shift_value));
+         }
+         return result;
+      });
+      return norm_inf(shifted_bound_complementarity); // TODO use a generic norm
+   }
+} // namespace
diff --git a/uno/ingredients/subproblem/interior_point_methods/BarrierParameterUpdateStrategy.hpp b/uno/ingredients/subproblem/interior_point_methods/BarrierParameterUpdateStrategy.hpp
index 33209a5..de2f830 100644
--- a/uno/ingredients/subproblem/interior_point_methods/BarrierParameterUpdateStrategy.hpp
+++ b/uno/ingredients/subproblem/interior_point_methods/BarrierParameterUpdateStrategy.hpp
@@ -4,31 +4,35 @@
 #ifndef UNO_BARRIERPARAMETERUPDATESTRATEGY_H
 #define UNO_BARRIERPARAMETERUPDATESTRATEGY_H
 
-// forward declarations
-class Iterate;
-class OptimizationProblem;
-class Options;
+namespace uno {
+   // forward declarations
+   class Iterate;
+   class OptimizationProblem;
+   class Options;
+   class PrimalDualResiduals;
 
-struct UpdateParameters {
-   double k_mu;
-   double theta_mu;
-   double k_epsilon;
-   double update_fraction;
-};
+   struct UpdateParameters {
+      double k_mu;
+      double theta_mu;
+      double k_epsilon;
+      double update_fraction;
+   };
 
-class BarrierParameterUpdateStrategy {
-public:
-   explicit BarrierParameterUpdateStrategy(const Options& options);
-   [[nodiscard]] double get_barrier_parameter() const;
-   void set_barrier_parameter(double new_barrier_parameter);
-   [[nodiscard]] bool update_barrier_parameter(const OptimizationProblem& problem, const Iterate& current_iterate);
+   class BarrierParameterUpdateStrategy {
+   public:
+      explicit BarrierParameterUpdateStrategy(const Options& options);
+      [[nodiscard]] double get_barrier_parameter() const;
+      void set_barrier_parameter(double new_barrier_parameter);
+      [[nodiscard]] bool update_barrier_parameter(const OptimizationProblem& problem, const Iterate& current_iterate,
+            const PrimalDualResiduals& residuals);
 
-protected:
-   double barrier_parameter;
-   const double tolerance;
-   const UpdateParameters parameters;
+   protected:
+      double barrier_parameter;
+      const double tolerance;
+      const UpdateParameters parameters;
 
-   [[nodiscard]] static double compute_shifted_complementarity_error(const OptimizationProblem& problem, const Iterate& iterate, double shift_value);
-};
+      [[nodiscard]] static double compute_shifted_complementarity_error(const OptimizationProblem& problem, const Iterate& iterate, double shift_value);
+   };
+} // namespace
 
 #endif // UNO_BARRIERPARAMETERUPDATESTRATEGY_H
diff --git a/uno/ingredients/subproblem/interior_point_methods/PrimalDualInteriorPointSubproblem.cpp b/uno/ingredients/subproblem/interior_point_methods/PrimalDualInteriorPointSubproblem.cpp
index 6853607..4aed688 100644
--- a/uno/ingredients/subproblem/interior_point_methods/PrimalDualInteriorPointSubproblem.cpp
+++ b/uno/ingredients/subproblem/interior_point_methods/PrimalDualInteriorPointSubproblem.cpp
@@ -13,494 +13,501 @@
 #include "symbolic/VectorView.hpp"
 #include "tools/Infinity.hpp"
 
-PrimalDualInteriorPointSubproblem::PrimalDualInteriorPointSubproblem(size_t number_variables, size_t number_constraints,
+namespace uno {
+   PrimalDualInteriorPointSubproblem::PrimalDualInteriorPointSubproblem(size_t number_variables, size_t number_constraints,
          size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options):
-      Subproblem(number_variables, number_constraints),
-      augmented_system(options.get_string("sparse_format"), number_variables + number_constraints,
-            number_hessian_nonzeros
-            + number_variables /* diagonal barrier terms for bound constraints */
-            + number_jacobian_nonzeros /* Jacobian */,
-            true, /* use regularization */
-            options),
-      // the Hessian is not convexified. Instead, the augmented system will be.
-      hessian_model(HessianModelFactory::create(options.get_string("hessian_model"), number_variables, number_hessian_nonzeros, false, options)),
-      linear_solver(SymmetricIndefiniteLinearSolverFactory::create(options.get_string("linear_solver"), number_variables + number_constraints,
-            number_hessian_nonzeros
-            + number_variables + number_constraints /* regularization */
-            + 2 * number_variables /* diagonal barrier terms */
-            + number_jacobian_nonzeros /* Jacobian */)),
-      barrier_parameter_update_strategy(options),
-      previous_barrier_parameter(options.get_double("barrier_initial_parameter")),
-      default_multiplier(options.get_double("barrier_default_multiplier")),
-      parameters({
-            options.get_double("barrier_tau_min"),
-            options.get_double("barrier_k_sigma"),
-            options.get_double("barrier_regularization_exponent"),
-            options.get_double("barrier_small_direction_factor"),
-            options.get_double("barrier_push_variable_to_interior_k1"),
-            options.get_double("barrier_push_variable_to_interior_k2")
-      }),
-      least_square_multiplier_max_norm(options.get_double("least_square_multiplier_max_norm")),
-      damping_factor(options.get_double("barrier_damping_factor")) {
-}
-
-inline void PrimalDualInteriorPointSubproblem::initialize_statistics(Statistics& statistics, const Options& options) {
-   statistics.add_column("regularization", Statistics::double_width - 1, options.get_int("statistics_regularization_column_order"));
-   statistics.add_column("barrier param.", Statistics::double_width - 1, options.get_int("statistics_barrier_parameter_column_order"));
-}
-
-inline bool PrimalDualInteriorPointSubproblem::generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) {
-   if (problem.has_inequality_constraints()) {
-      throw std::runtime_error("The problem has inequality constraints. Create an instance of HomogeneousEqualityConstrainedModel.\n");
-   }
+         Subproblem(),
+         objective_gradient(2 * number_variables), // original variables + barrier terms
+         constraints(number_constraints),
+         constraint_jacobian(number_constraints, number_variables),
+         // the Hessian is not convexified. Instead, the augmented system will be.
+         hessian_model(HessianModelFactory::create(options.get_string("hessian_model"), number_variables, number_hessian_nonzeros, false, options)),
+         augmented_system(options.get_string("sparse_format"), number_variables + number_constraints,
+               number_hessian_nonzeros
+               + number_variables /* diagonal barrier terms for bound constraints */
+               + number_jacobian_nonzeros /* Jacobian */,
+               true, /* use regularization */
+               options),
+         linear_solver(SymmetricIndefiniteLinearSolverFactory::create(options.get_string("linear_solver"), number_variables + number_constraints,
+               number_hessian_nonzeros
+               + number_variables + number_constraints /* regularization */
+               + 2 * number_variables /* diagonal barrier terms */
+               + number_jacobian_nonzeros /* Jacobian */)),
+         barrier_parameter_update_strategy(options),
+         previous_barrier_parameter(options.get_double("barrier_initial_parameter")),
+         default_multiplier(options.get_double("barrier_default_multiplier")),
+         parameters({
+               options.get_double("barrier_tau_min"),
+               options.get_double("barrier_k_sigma"),
+               options.get_double("barrier_regularization_exponent"),
+               options.get_double("barrier_small_direction_factor"),
+               options.get_double("barrier_push_variable_to_interior_k1"),
+               options.get_double("barrier_push_variable_to_interior_k2")
+         }),
+         least_square_multiplier_max_norm(options.get_double("least_square_multiplier_max_norm")),
+         damping_factor(options.get_double("barrier_damping_factor")) {
+   }
+
+   inline void PrimalDualInteriorPointSubproblem::initialize_statistics(Statistics& statistics, const Options& options) {
+      statistics.add_column("regularization", Statistics::double_width - 1, options.get_int("statistics_regularization_column_order"));
+      statistics.add_column("barrier param.", Statistics::double_width - 1, options.get_int("statistics_barrier_parameter_column_order"));
+   }
+
+   inline void PrimalDualInteriorPointSubproblem::generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) {
+      if (problem.has_inequality_constraints()) {
+         throw std::runtime_error("The problem has inequality constraints. Create an instance of HomogeneousEqualityConstrainedModel.\n");
+      }
 
-   // TODO: enforce linear constraints at initial point
-   //if (options.get_bool("enforce_linear_constraints")) {
-   //   Preprocessing::enforce_linear_constraints(problem.model, initial_iterate.primals, initial_iterate.multipliers, this->solver);
-   //}
+      // TODO: enforce linear constraints at initial point
+      //if (options.get_bool("enforce_linear_constraints")) {
+      //   Preprocessing::enforce_linear_constraints(problem.model, initial_iterate.primals, initial_iterate.multipliers, this->solver);
+      //}
 
-   // make the initial point strictly feasible wrt the bounds
-   for (size_t variable_index: Range(problem.number_variables)) {
-      initial_iterate.primals[variable_index] = PrimalDualInteriorPointSubproblem::push_variable_to_interior(initial_iterate.primals[variable_index],
-            problem.variable_lower_bound(variable_index), problem.variable_upper_bound(variable_index));
-   }
+      // make the initial point strictly feasible wrt the bounds
+      for (size_t variable_index: Range(problem.number_variables)) {
+         initial_iterate.primals[variable_index] = PrimalDualInteriorPointSubproblem::push_variable_to_interior(initial_iterate.primals[variable_index],
+               problem.variable_lower_bound(variable_index), problem.variable_upper_bound(variable_index));
+      }
 
-   // set the slack variables (if any)
-   if (not problem.model.get_slacks().is_empty()) {
-      // evaluate the constraints at the original point
-      initial_iterate.evaluate_constraints(problem.model);
+      // set the slack variables (if any)
+      if (not problem.model.get_slacks().is_empty()) {
+         // evaluate the constraints at the original point
+         initial_iterate.evaluate_constraints(problem.model);
 
-      // set the slacks to the constraint values
-      for (const auto [constraint_index, slack_index]: problem.model.get_slacks()) {
-         initial_iterate.primals[slack_index] = PrimalDualInteriorPointSubproblem::push_variable_to_interior(initial_iterate.evaluations.constraints[constraint_index],
-               problem.variable_lower_bound(slack_index), problem.variable_upper_bound(slack_index));
+         // set the slacks to the constraint values
+         for (const auto [constraint_index, slack_index]: problem.model.get_slacks()) {
+            initial_iterate.primals[slack_index] = PrimalDualInteriorPointSubproblem::push_variable_to_interior(initial_iterate.evaluations.constraints[constraint_index],
+                  problem.variable_lower_bound(slack_index), problem.variable_upper_bound(slack_index));
+         }
+         // since the slacks have been set, the function evaluations should also be updated
+         initial_iterate.is_objective_gradient_computed = false;
+         initial_iterate.are_constraints_computed = false;
+         initial_iterate.is_constraint_jacobian_computed = false;
       }
-      // since the slacks have been set, the function evaluations should also be updated
-      initial_iterate.is_objective_gradient_computed = false;
-      initial_iterate.are_constraints_computed = false;
-      initial_iterate.is_constraint_jacobian_computed = false;
-   }
 
-   // set the bound multipliers
-   for (const size_t variable_index: problem.get_lower_bounded_variables()) {
-      initial_iterate.multipliers.lower_bounds[variable_index] = this->default_multiplier;
-   }
-   for (const size_t variable_index: problem.get_upper_bounded_variables()) {
-      initial_iterate.multipliers.upper_bounds[variable_index] = -this->default_multiplier;
+      // set the bound multipliers
+      for (const size_t variable_index: problem.get_lower_bounded_variables()) {
+         initial_iterate.multipliers.lower_bounds[variable_index] = this->default_multiplier;
+      }
+      for (const size_t variable_index: problem.get_upper_bounded_variables()) {
+         initial_iterate.multipliers.upper_bounds[variable_index] = -this->default_multiplier;
+      }
+
+      // compute least-square multipliers
+      if (problem.is_constrained()) {
+         this->compute_least_square_multipliers(problem, initial_iterate, initial_iterate.multipliers.constraints);
+      }
    }
 
-   // compute least-square multipliers
-   if (problem.is_constrained()) {
-      this->compute_least_square_multipliers(problem, initial_iterate, initial_iterate.multipliers.constraints);
+   double PrimalDualInteriorPointSubproblem::barrier_parameter() const {
+      return this->barrier_parameter_update_strategy.get_barrier_parameter();
    }
-   return true;
-}
-
-double PrimalDualInteriorPointSubproblem::barrier_parameter() const {
-   return this->barrier_parameter_update_strategy.get_barrier_parameter();
-}
-
-double PrimalDualInteriorPointSubproblem::push_variable_to_interior(double variable_value, double lower_bound, double upper_bound) const {
-   const double range = upper_bound - lower_bound;
-   const double perturbation_lb = std::min(this->parameters.push_variable_to_interior_k1 * std::max(1., std::abs(lower_bound)),
-         this->parameters.push_variable_to_interior_k2 * range);
-   const double perturbation_ub = std::min(this->parameters.push_variable_to_interior_k1 * std::max(1., std::abs(upper_bound)),
-         this->parameters.push_variable_to_interior_k2 * range);
-   variable_value = std::max(variable_value, lower_bound + perturbation_lb);
-   variable_value = std::min(variable_value, upper_bound - perturbation_ub);
-   return variable_value;
-}
-
-void PrimalDualInteriorPointSubproblem::evaluate_functions(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
-      const Multipliers& current_multipliers, const WarmstartInformation& warmstart_information) {
-   // barrier Lagrangian Hessian
-   if (warmstart_information.objective_changed || warmstart_information.constraints_changed) {
-      // original Lagrangian Hessian
-      this->hessian_model->evaluate(statistics, problem, current_iterate.primals, current_multipliers.constraints);
-
-      // diagonal barrier terms (grouped by variable)
-      for (size_t variable_index: Range(problem.number_variables)) {
-         double diagonal_barrier_term = 0.;
-         if (is_finite(problem.variable_lower_bound(variable_index))) { // lower bounded
-            const double distance_to_bound = current_iterate.primals[variable_index] - problem.variable_lower_bound(variable_index);
-            diagonal_barrier_term += current_multipliers.lower_bounds[variable_index] / distance_to_bound;
-         }
-         if (is_finite(problem.variable_upper_bound(variable_index))) { // upper bounded
-            const double distance_to_bound = current_iterate.primals[variable_index] - problem.variable_upper_bound(variable_index);
-            diagonal_barrier_term += current_multipliers.upper_bounds[variable_index] / distance_to_bound;
-         }
-         this->hessian_model->hessian->insert(diagonal_barrier_term, variable_index, variable_index);
-      }
+
+   double PrimalDualInteriorPointSubproblem::push_variable_to_interior(double variable_value, double lower_bound, double upper_bound) const {
+      const double range = upper_bound - lower_bound;
+      const double perturbation_lb = std::min(this->parameters.push_variable_to_interior_k1 * std::max(1., std::abs(lower_bound)),
+            this->parameters.push_variable_to_interior_k2 * range);
+      const double perturbation_ub = std::min(this->parameters.push_variable_to_interior_k1 * std::max(1., std::abs(upper_bound)),
+            this->parameters.push_variable_to_interior_k2 * range);
+      variable_value = std::max(variable_value, lower_bound + perturbation_lb);
+      variable_value = std::min(variable_value, upper_bound - perturbation_ub);
+      return variable_value;
    }
 
-   // barrier objective gradient
-   if (warmstart_information.objective_changed) {
-      // original objective gradient
-      problem.evaluate_objective_gradient(current_iterate, this->evaluations.objective_gradient);
+   void PrimalDualInteriorPointSubproblem::evaluate_functions(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
+         const Multipliers& current_multipliers, const WarmstartInformation& warmstart_information) {
+      // barrier Lagrangian Hessian
+      if (warmstart_information.objective_changed || warmstart_information.constraints_changed) {
+         // original Lagrangian Hessian
+         this->hessian_model->evaluate(statistics, problem, current_iterate.primals, current_multipliers.constraints);
 
-      // barrier terms
-      // TODO: the allocated size for objective_gradient is probably too small
-      for (size_t variable_index: Range(problem.number_variables)) {
-         double barrier_term = 0.;
-         if (is_finite(problem.variable_lower_bound(variable_index))) { // lower bounded
-            barrier_term += -this->barrier_parameter()/(current_iterate.primals[variable_index] - problem.variable_lower_bound(variable_index));
-            // damping
-            if (not is_finite(problem.variable_upper_bound(variable_index))) {
-               barrier_term += this->damping_factor * this->barrier_parameter();
+         // diagonal barrier terms (grouped by variable)
+         for (size_t variable_index: Range(problem.number_variables)) {
+            double diagonal_barrier_term = 0.;
+            if (is_finite(problem.variable_lower_bound(variable_index))) { // lower bounded
+               const double distance_to_bound = current_iterate.primals[variable_index] - problem.variable_lower_bound(variable_index);
+               diagonal_barrier_term += current_multipliers.lower_bounds[variable_index] / distance_to_bound;
             }
+            if (is_finite(problem.variable_upper_bound(variable_index))) { // upper bounded
+               const double distance_to_bound = current_iterate.primals[variable_index] - problem.variable_upper_bound(variable_index);
+               diagonal_barrier_term += current_multipliers.upper_bounds[variable_index] / distance_to_bound;
+            }
+            this->hessian_model->hessian->insert(diagonal_barrier_term, variable_index, variable_index);
          }
-         if (is_finite(problem.variable_upper_bound(variable_index))) { // upper bounded
-            barrier_term += -this->barrier_parameter()/(current_iterate.primals[variable_index] - problem.variable_upper_bound(variable_index));
-            // damping
-            if (not is_finite(problem.variable_lower_bound(variable_index))) {
-               barrier_term -= this->damping_factor * this->barrier_parameter();
+      }
+
+      // barrier objective gradient
+      if (warmstart_information.objective_changed) {
+         // original objective gradient
+         problem.evaluate_objective_gradient(current_iterate, this->objective_gradient);
+
+         // barrier terms
+         for (size_t variable_index: Range(problem.number_variables)) {
+            double barrier_term = 0.;
+            if (is_finite(problem.variable_lower_bound(variable_index))) { // lower bounded
+               barrier_term += -this->barrier_parameter()/(current_iterate.primals[variable_index] - problem.variable_lower_bound(variable_index));
+               // damping
+               if (not is_finite(problem.variable_upper_bound(variable_index))) {
+                  barrier_term += this->damping_factor * this->barrier_parameter();
+               }
+            }
+            if (is_finite(problem.variable_upper_bound(variable_index))) { // upper bounded
+               barrier_term += -this->barrier_parameter()/(current_iterate.primals[variable_index] - problem.variable_upper_bound(variable_index));
+               // damping
+               if (not is_finite(problem.variable_lower_bound(variable_index))) {
+                  barrier_term -= this->damping_factor * this->barrier_parameter();
+               }
             }
+            this->objective_gradient.insert(variable_index, barrier_term);
          }
-         this->evaluations.objective_gradient.insert(variable_index, barrier_term);
       }
-   }
 
-   // constraints and Jacobian
-   if (warmstart_information.constraints_changed) {
-      problem.evaluate_constraints(current_iterate, this->evaluations.constraints);
-      problem.evaluate_constraint_jacobian(current_iterate, this->evaluations.constraint_jacobian);
+      // constraints and Jacobian
+      if (warmstart_information.constraints_changed) {
+         problem.evaluate_constraints(current_iterate, this->constraints);
+         problem.evaluate_constraint_jacobian(current_iterate, this->constraint_jacobian);
+      }
    }
-}
 
-void PrimalDualInteriorPointSubproblem::solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
-      const Multipliers& current_multipliers, Direction& direction, const WarmstartInformation& warmstart_information) {
-   if (problem.has_inequality_constraints()) {
-      throw std::runtime_error("The problem has inequality constraints. Create an instance of HomogeneousEqualityConstrainedModel.\n");
-   }
-   if (is_finite(this->trust_region_radius)) {
-      throw std::runtime_error("The interior-point subproblem has a trust region. This is not implemented yet.\n");
-   }
+   void PrimalDualInteriorPointSubproblem::solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,
+         const Multipliers& current_multipliers, Direction& direction, const WarmstartInformation& warmstart_information) {
+      if (problem.has_inequality_constraints()) {
+         throw std::runtime_error("The problem has inequality constraints. Create an instance of HomogeneousEqualityConstrainedModel.\n");
+      }
+      if (is_finite(this->trust_region_radius)) {
+         throw std::runtime_error("The interior-point subproblem has a trust region. This is not implemented yet.\n");
+      }
 
-   // possibly update the barrier parameter
-   if (not this->solving_feasibility_problem) {
-      this->update_barrier_parameter(problem, current_iterate);
+      // possibly update the barrier parameter
+      const auto residuals = this->solving_feasibility_problem ? current_iterate.feasibility_residuals : current_iterate.residuals;
+      this->update_barrier_parameter(problem, current_iterate, residuals);
+      statistics.set("barrier param.", this->barrier_parameter());
+
+      // evaluate the functions at the current iterate
+      this->evaluate_functions(statistics, problem, current_iterate, current_multipliers, warmstart_information);
+
+      // compute the primal-dual solution
+      this->assemble_augmented_system(statistics, problem, current_multipliers);
+      this->augmented_system.solve(*this->linear_solver);
+      assert(direction.status == SubproblemStatus::OPTIMAL && "The primal-dual perturbed subproblem was not solved to optimality");
+      this->number_subproblems_solved++;
+
+      this->assemble_primal_dual_direction(problem, current_iterate.primals, current_multipliers, direction.primals, direction.multipliers);
+      direction.subproblem_objective = this->evaluate_subproblem_objective(direction);
+   }
+
+   void PrimalDualInteriorPointSubproblem::assemble_augmented_system(Statistics& statistics, const OptimizationProblem& problem,
+         const Multipliers& current_multipliers) {
+      // assemble, factorize and regularize the augmented matrix
+      this->augmented_system.assemble_matrix(*this->hessian_model->hessian, this->constraint_jacobian, problem.number_variables, problem.number_constraints);
+      this->augmented_system.factorize_matrix(problem.model, *this->linear_solver);
+      const double dual_regularization_parameter = std::pow(this->barrier_parameter(), this->parameters.regularization_exponent);
+      this->augmented_system.regularize_matrix(statistics, problem.model, *this->linear_solver, problem.number_variables, problem.number_constraints,
+            dual_regularization_parameter);
+      [[maybe_unused]] auto[number_pos_eigenvalues, number_neg_eigenvalues, number_zero_eigenvalues] = this->linear_solver->get_inertia();
+      assert(number_pos_eigenvalues == problem.number_variables && number_neg_eigenvalues == problem.number_constraints && number_zero_eigenvalues == 0);
+
+      // rhs
+      this->assemble_augmented_rhs(problem, current_multipliers);
+   }
+
+   void PrimalDualInteriorPointSubproblem::initialize_feasibility_problem(const l1RelaxedProblem& /*problem*/, Iterate& /*current_iterate*/) {
+      this->solving_feasibility_problem = true;
+      this->subproblem_definition_changed = true;
+
+      // temporarily update the objective multiplier
+      this->previous_barrier_parameter = this->barrier_parameter();
+      const double new_barrier_parameter = std::max(this->barrier_parameter(), norm_inf(this->constraints));
+      this->barrier_parameter_update_strategy.set_barrier_parameter(new_barrier_parameter);
+      DEBUG << "Barrier parameter mu temporarily updated to " << this->barrier_parameter() << '\n';
+
+      // set the bound multipliers
+      /*
+      for (const size_t variable_index: problem.get_lower_bounded_variables()) {
+         current_iterate.feasibility_multipliers.lower_bounds[variable_index] = std::min(this->default_multiplier, problem.constraint_violation_coefficient);
+      }
+      for (const size_t variable_index: problem.get_upper_bounded_variables()) {
+         current_iterate.feasibility_multipliers.upper_bounds[variable_index] = -this->default_multiplier;
+      }
+       */
+   }
+
+   // set the elastic variables of the current iterate
+   void PrimalDualInteriorPointSubproblem::set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& current_iterate) {
+      DEBUG << "Setting the elastic variables\n";
+      // c(x) - p + n = 0
+      // analytical expression for p and n:
+      // (mu_over_rho - jacobian_coefficient*this->barrier_constraints[j] + std::sqrt(radical))/2.
+      // where jacobian_coefficient = -1 for p, +1 for n
+      // Note: IPOPT uses a '+' sign because they define the Lagrangian as f(x) + \lambda^T c(x)
+      const double barrier_parameter = this->barrier_parameter();
+      const auto elastic_setting_function = [&](Iterate& iterate, size_t constraint_index, size_t elastic_index, double jacobian_coefficient) {
+         // precomputations
+         const double constraint_j = this->constraints[constraint_index];
+         const double mu_over_rho = barrier_parameter; // here, rho = 1
+         const double radical = std::pow(constraint_j, 2) + std::pow(mu_over_rho, 2);
+         const double sqrt_radical = std::sqrt(radical);
+
+         iterate.primals[elastic_index] = (mu_over_rho - jacobian_coefficient * constraint_j + sqrt_radical) / 2.;
+         iterate.feasibility_multipliers.lower_bounds[elastic_index] = barrier_parameter / iterate.primals[elastic_index];
+         assert(0. < iterate.primals[elastic_index] && "The elastic variable is not strictly positive.");
+         assert(0. < iterate.feasibility_multipliers.lower_bounds[elastic_index] && "The elastic dual is not strictly positive.");
+      };
+      problem.set_elastic_variable_values(current_iterate, elastic_setting_function);
+   }
+
+   void PrimalDualInteriorPointSubproblem::exit_feasibility_problem(const OptimizationProblem& problem, Iterate& trial_iterate) {
+      assert(this->solving_feasibility_problem && "The barrier subproblem did not know it was solving the feasibility problem.");
+      this->barrier_parameter_update_strategy.set_barrier_parameter(this->previous_barrier_parameter);
+      this->solving_feasibility_problem = false;
+      this->compute_least_square_multipliers(problem, trial_iterate, trial_iterate.multipliers.constraints);
+   }
+
+   const SymmetricMatrix<size_t, double>& PrimalDualInteriorPointSubproblem::get_lagrangian_hessian() const {
+      return *this->hessian_model->hessian;
+   }
+
+   void PrimalDualInteriorPointSubproblem::set_auxiliary_measure(const Model& model, Iterate& iterate) {
+      // auxiliary measure: barrier terms
+      double barrier_terms = 0.;
+      for (const size_t variable_index: model.get_lower_bounded_variables()) {
+         barrier_terms -= std::log(iterate.primals[variable_index] - model.variable_lower_bound(variable_index));
+      }
+      for (const size_t variable_index: model.get_upper_bounded_variables()) {
+         barrier_terms -= std::log(model.variable_upper_bound(variable_index) - iterate.primals[variable_index]);
+      }
+      // damping
+      for (const size_t variable_index: model.get_single_lower_bounded_variables()) {
+         barrier_terms += this->damping_factor*(iterate.primals[variable_index] - model.variable_lower_bound(variable_index));
+      }
+      for (const size_t variable_index: model.get_single_upper_bounded_variables()) {
+         barrier_terms += this->damping_factor*(model.variable_upper_bound(variable_index) - iterate.primals[variable_index]);
+      }
+      barrier_terms *= this->barrier_parameter();
+      assert(not std::isnan(barrier_terms) && "The auxiliary measure is not an number.");
+      iterate.progress.auxiliary = barrier_terms;
    }
-   statistics.set("barrier param.", this->barrier_parameter());
-
-   // evaluate the functions at the current iterate
-   this->evaluate_functions(statistics, problem, current_iterate, current_multipliers, warmstart_information);
 
-   // set up the augmented system (with the correct inertia)
-   this->assemble_augmented_system(statistics, problem, current_multipliers);
-
-   // compute the primal-dual solution
-   this->augmented_system.solve(*this->linear_solver);
-   assert(direction.status == SubproblemStatus::OPTIMAL && "The primal-dual perturbed subproblem was not solved to optimality");
-   this->number_subproblems_solved++;
-   this->assemble_primal_dual_direction(problem, current_iterate.primals, current_multipliers, direction.primals, direction.multipliers);
-   direction.subproblem_objective = this->evaluate_subproblem_objective(direction);
-
-   // determine if the direction is a "small direction" (Section 3.9 of the Ipopt paper) TODO
-   const bool is_small_step = PrimalDualInteriorPointSubproblem::is_small_step(problem, current_iterate, direction);
-   if (is_small_step) {
-      DEBUG << "This is a small step\n";
-   }
-}
-
-void PrimalDualInteriorPointSubproblem::assemble_augmented_system(Statistics& statistics, const OptimizationProblem& problem,
-      const Multipliers& current_multipliers) {
-   // assemble, factorize and regularize the augmented matrix
-   this->augmented_system.assemble_matrix(*this->hessian_model->hessian, this->evaluations.constraint_jacobian,
-         problem.number_variables, problem.number_constraints);
-   this->augmented_system.factorize_matrix(problem.model, *this->linear_solver);
-   const double dual_regularization_parameter = std::pow(this->barrier_parameter(), this->parameters.regularization_exponent);
-   this->augmented_system.regularize_matrix(statistics, problem.model, *this->linear_solver, problem.number_variables, problem.number_constraints,
-         dual_regularization_parameter);
-   [[maybe_unused]] auto[number_pos_eigenvalues, number_neg_eigenvalues, number_zero_eigenvalues] = this->linear_solver->get_inertia();
-   assert(number_pos_eigenvalues == problem.number_variables && number_neg_eigenvalues == problem.number_constraints && number_zero_eigenvalues == 0);
-
-   // assemble the right-hand side
-   this->generate_augmented_rhs(problem, current_multipliers);
-}
-
-void PrimalDualInteriorPointSubproblem::initialize_feasibility_problem(const l1RelaxedProblem& /*problem*/, Iterate& /*current_iterate*/) {
-   this->solving_feasibility_problem = true;
-   this->subproblem_definition_changed = true;
-
-   // temporarily update the objective multiplier
-   this->previous_barrier_parameter = this->barrier_parameter();
-   const double new_barrier_parameter = std::max(this->barrier_parameter(), norm_inf(this->evaluations.constraints));
-   this->barrier_parameter_update_strategy.set_barrier_parameter(new_barrier_parameter);
-   DEBUG << "Barrier parameter mu temporarily updated to " << this->barrier_parameter() << '\n';
-
-   // set the bound multipliers
-   /*
-   for (const size_t variable_index: problem.get_lower_bounded_variables()) {
-      current_iterate.feasibility_multipliers.lower_bounds[variable_index] = std::min(this->default_multiplier, problem.constraint_violation_coefficient);
-   }
-   for (const size_t variable_index: problem.get_upper_bounded_variables()) {
-      current_iterate.feasibility_multipliers.upper_bounds[variable_index] = -this->default_multiplier;
+   double PrimalDualInteriorPointSubproblem::compute_predicted_auxiliary_reduction_model(const Model& model, const Iterate& current_iterate,
+         const Vector<double>& primal_direction, double step_length) const {
+      const double directional_derivative = this->compute_barrier_term_directional_derivative(model, current_iterate, primal_direction);
+      return step_length * (-directional_derivative);
+      // }, "α*(μ*X^{-1} e^T d)"};
    }
-    */
-}
-
-// set the elastic variables of the current iterate
-void PrimalDualInteriorPointSubproblem::set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& current_iterate) {
-   DEBUG << "Setting the elastic variables\n";
-   // c(x) - p + n = 0
-   // analytical expression for p and n:
-   // (mu_over_rho - jacobian_coefficient*this->barrier_constraints[j] + std::sqrt(radical))/2.
-   // where jacobian_coefficient = -1 for p, +1 for n
-   // Note: IPOPT uses a '+' sign because they define the Lagrangian as f(x) + \lambda^T c(x)
-   const double barrier_parameter = this->barrier_parameter();
-   const auto elastic_setting_function = [&](Iterate& iterate, size_t constraint_index, size_t elastic_index, double jacobian_coefficient) {
-      // precomputations
-      const double constraint_j = this->evaluations.constraints[constraint_index];
-      const double mu_over_rho = barrier_parameter; // here, rho = 1
-      const double radical = std::pow(constraint_j, 2) + std::pow(mu_over_rho, 2);
-      const double sqrt_radical = std::sqrt(radical);
-
-      iterate.primals[elastic_index] = (mu_over_rho - jacobian_coefficient * constraint_j + sqrt_radical) / 2.;
-      iterate.feasibility_multipliers.lower_bounds[elastic_index] = barrier_parameter / iterate.primals[elastic_index];
-      assert(0. < iterate.primals[elastic_index] && "The elastic variable is not strictly positive.");
-      assert(0. < iterate.feasibility_multipliers.lower_bounds[elastic_index] && "The elastic dual is not strictly positive.");
-   };
-   problem.set_elastic_variable_values(current_iterate, elastic_setting_function);
-}
-
-void PrimalDualInteriorPointSubproblem::exit_feasibility_problem(const OptimizationProblem& problem, Iterate& trial_iterate) {
-   assert(this->solving_feasibility_problem && "The barrier subproblem did not know it was solving the feasibility problem.");
-   this->barrier_parameter_update_strategy.set_barrier_parameter(this->previous_barrier_parameter);
-   this->solving_feasibility_problem = false;
-   this->compute_least_square_multipliers(problem, trial_iterate, trial_iterate.multipliers.constraints);
-}
-
-const SymmetricMatrix<double>& PrimalDualInteriorPointSubproblem::get_lagrangian_hessian() const {
-   return *this->hessian_model->hessian;
-}
-
-void PrimalDualInteriorPointSubproblem::set_auxiliary_measure(const Model& model, Iterate& iterate) {
-   // auxiliary measure: barrier terms
-   double barrier_terms = 0.;
-   for (const size_t variable_index: model.get_lower_bounded_variables()) {
-      barrier_terms -= std::log(iterate.primals[variable_index] - model.variable_lower_bound(variable_index));
-   }
-   for (const size_t variable_index: model.get_upper_bounded_variables()) {
-      barrier_terms -= std::log(model.variable_upper_bound(variable_index) - iterate.primals[variable_index]);
-   }
-   // damping
-   for (const size_t variable_index: model.get_single_lower_bounded_variables()) {
-      barrier_terms += this->damping_factor*(iterate.primals[variable_index] - model.variable_lower_bound(variable_index));
-   }
-   for (const size_t variable_index: model.get_single_upper_bounded_variables()) {
-      barrier_terms += this->damping_factor*(model.variable_upper_bound(variable_index) - iterate.primals[variable_index]);
-   }
-   barrier_terms *= this->barrier_parameter();
-   assert(not std::isnan(barrier_terms) && "The auxiliary measure is not an number.");
-   iterate.progress.auxiliary = barrier_terms;
-}
-
-double PrimalDualInteriorPointSubproblem::compute_predicted_auxiliary_reduction_model(const Model& model, const Iterate& current_iterate,
-      const Vector<double>& primal_direction, double step_length) const {
-   const double directional_derivative = this->compute_barrier_term_directional_derivative(model, current_iterate, primal_direction);
-   return step_length * (-directional_derivative);
-   // }, "α*(μ*X^{-1} e^T d)"};
-}
-
-double PrimalDualInteriorPointSubproblem::compute_barrier_term_directional_derivative(const Model& model, const Iterate& current_iterate,
-      const Vector<double>& primal_direction) const {
-   double directional_derivative = 0.;
-   for (const size_t variable_index: model.get_lower_bounded_variables()) {
-      directional_derivative += -this->barrier_parameter() / (current_iterate.primals[variable_index] -
-            model.variable_lower_bound(variable_index)) * primal_direction[variable_index];
+
+   double PrimalDualInteriorPointSubproblem::compute_barrier_term_directional_derivative(const Model& model, const Iterate& current_iterate,
+         const Vector<double>& primal_direction) const {
+      double directional_derivative = 0.;
+      for (const size_t variable_index: model.get_lower_bounded_variables()) {
+         directional_derivative += -this->barrier_parameter() / (current_iterate.primals[variable_index] -
+                                                                 model.variable_lower_bound(variable_index)) * primal_direction[variable_index];
+      }
+      for (const size_t variable_index: model.get_upper_bounded_variables()) {
+         directional_derivative += -this->barrier_parameter() / (current_iterate.primals[variable_index] -
+                                                                 model.variable_upper_bound(variable_index)) * primal_direction[variable_index];
+      }
+      // damping
+      for (const size_t variable_index: model.get_single_lower_bounded_variables()) {
+         directional_derivative += this->damping_factor * this->barrier_parameter() * primal_direction[variable_index];
+      }
+      for (const size_t variable_index: model.get_single_upper_bounded_variables()) {
+         directional_derivative -= this->damping_factor * this->barrier_parameter() * primal_direction[variable_index];
+      }
+      return directional_derivative;
    }
-   for (const size_t variable_index: model.get_upper_bounded_variables()) {
-      directional_derivative += -this->barrier_parameter() / (current_iterate.primals[variable_index] -
-            model.variable_upper_bound(variable_index)) * primal_direction[variable_index];
+
+   void PrimalDualInteriorPointSubproblem::update_barrier_parameter(const OptimizationProblem& problem, const Iterate& current_iterate,
+         const PrimalDualResiduals& residuals) {
+      const bool barrier_parameter_updated = this->barrier_parameter_update_strategy.update_barrier_parameter(problem, current_iterate, residuals);
+      // the barrier parameter may have been changed earlier when entering restoration
+      this->subproblem_definition_changed = this->subproblem_definition_changed || barrier_parameter_updated;
    }
-   // damping
-   for (const size_t variable_index: model.get_single_lower_bounded_variables()) {
-      directional_derivative += this->damping_factor * this->barrier_parameter() * primal_direction[variable_index];
+
+   // Section 3.9 in IPOPT paper
+   bool PrimalDualInteriorPointSubproblem::is_small_step(const OptimizationProblem& problem, const Vector<double>& current_primals,
+         const Vector<double>& direction_primals) const {
+      const VectorExpression relative_direction_size(Range(problem.number_variables), [&](size_t variable_index) {
+         return direction_primals[variable_index] / (1 + std::abs(current_primals[variable_index]));
+      });
+      static double machine_epsilon = std::numeric_limits<double>::epsilon();
+      return (norm_inf(relative_direction_size) <= this->parameters.small_direction_factor * machine_epsilon);
    }
-   for (const size_t variable_index: model.get_single_upper_bounded_variables()) {
-      directional_derivative -= this->damping_factor * this->barrier_parameter() * primal_direction[variable_index];
+
+   double PrimalDualInteriorPointSubproblem::evaluate_subproblem_objective(const Direction& direction) const {
+      const double linear_term = dot(direction.primals, this->objective_gradient);
+      const double quadratic_term = this->hessian_model->hessian->quadratic_product(direction.primals, direction.primals) / 2.;
+      return linear_term + quadratic_term;
    }
-   return directional_derivative;
-}
-
-void PrimalDualInteriorPointSubproblem::update_barrier_parameter(const OptimizationProblem& problem, const Iterate& current_iterate) {
-    const bool barrier_parameter_updated = this->barrier_parameter_update_strategy.update_barrier_parameter(problem, current_iterate);
-    // the barrier parameter may have been changed earlier when entering restoration
-    this->subproblem_definition_changed = this->subproblem_definition_changed || barrier_parameter_updated;
-}
-
-// Section 3.9 in IPOPT paper
-bool PrimalDualInteriorPointSubproblem::is_small_step(const OptimizationProblem& problem, const Iterate& current_iterate, const Direction& direction) const {
-   const VectorExpression relative_direction_size(Range(problem.number_variables), [&](size_t variable_index) {
-      return direction.primals[variable_index] / (1 + std::abs(current_iterate.primals[variable_index]));
-   });
-   static double machine_epsilon = std::numeric_limits<double>::epsilon();
-   return (norm_inf(relative_direction_size) <= this->parameters.small_direction_factor * machine_epsilon);
-}
-
-double PrimalDualInteriorPointSubproblem::evaluate_subproblem_objective(const Direction& direction) const {
-   const double linear_term = dot(direction.primals, this->evaluations.objective_gradient);
-   const double quadratic_term = this->hessian_model->hessian->quadratic_product(direction.primals, direction.primals) / 2.;
-   return linear_term + quadratic_term;
-}
-
-// TODO use a single function for primal and dual fraction-to-boundary rules
-double PrimalDualInteriorPointSubproblem::primal_fraction_to_boundary(const OptimizationProblem& problem, const Vector<double>& current_primals,
-      const Vector<double>& primal_direction, double tau) {
-   double step_length = 1.;
-   for (const size_t variable_index: problem.get_lower_bounded_variables()) {
-      if (primal_direction[variable_index] < 0.) {
-         double distance = -tau * (current_primals[variable_index] - problem.variable_lower_bound(variable_index)) / primal_direction[variable_index];
-         if (0. < distance) {
-            step_length = std::min(step_length, distance);
+
+   // TODO use a single function for primal and dual fraction-to-boundary rules
+   double PrimalDualInteriorPointSubproblem::primal_fraction_to_boundary(const OptimizationProblem& problem, const Vector<double>& current_primals,
+         const Vector<double>& primal_direction, double tau) {
+      double step_length = 1.;
+      for (const size_t variable_index: problem.get_lower_bounded_variables()) {
+         if (primal_direction[variable_index] < 0.) {
+            double distance = -tau * (current_primals[variable_index] - problem.variable_lower_bound(variable_index)) / primal_direction[variable_index];
+            if (0. < distance) {
+               step_length = std::min(step_length, distance);
+            }
          }
       }
-   }
-   for (const size_t variable_index: problem.get_upper_bounded_variables()) {
-      if (0. < primal_direction[variable_index]) {
-         double distance = -tau * (current_primals[variable_index] - problem.variable_upper_bound(variable_index)) / primal_direction[variable_index];
-         if (0. < distance) {
-            step_length = std::min(step_length, distance);
+      for (const size_t variable_index: problem.get_upper_bounded_variables()) {
+         if (0. < primal_direction[variable_index]) {
+            double distance = -tau * (current_primals[variable_index] - problem.variable_upper_bound(variable_index)) / primal_direction[variable_index];
+            if (0. < distance) {
+               step_length = std::min(step_length, distance);
+            }
          }
       }
-   }
-   assert(0. < step_length && step_length <= 1. && "The primal fraction-to-boundary step length is not in (0, 1]");
-   return step_length;
-}
-
-double PrimalDualInteriorPointSubproblem::dual_fraction_to_boundary(const OptimizationProblem& problem, const Multipliers& current_multipliers,
-      Multipliers& direction_multipliers, double tau) {
-   double step_length = 1.;
-   for (const size_t variable_index: problem.get_lower_bounded_variables()) {
-      if (direction_multipliers.lower_bounds[variable_index] < 0.) {
-         double distance = -tau * current_multipliers.lower_bounds[variable_index] / direction_multipliers.lower_bounds[variable_index];
-         if (0. < distance) {
-            step_length = std::min(step_length, distance);
+      assert(0. < step_length && step_length <= 1. && "The primal fraction-to-boundary step length is not in (0, 1]");
+      return step_length;
+   }
+
+   double PrimalDualInteriorPointSubproblem::dual_fraction_to_boundary(const OptimizationProblem& problem, const Multipliers& current_multipliers,
+         Multipliers& direction_multipliers, double tau) {
+      double step_length = 1.;
+      for (const size_t variable_index: problem.get_lower_bounded_variables()) {
+         if (direction_multipliers.lower_bounds[variable_index] < 0.) {
+            double distance = -tau * current_multipliers.lower_bounds[variable_index] / direction_multipliers.lower_bounds[variable_index];
+            if (0. < distance) {
+               step_length = std::min(step_length, distance);
+            }
          }
       }
-   }
-   for (const size_t variable_index: problem.get_upper_bounded_variables()) {
-      if (0. < direction_multipliers.upper_bounds[variable_index]) {
-         double distance = -tau * current_multipliers.upper_bounds[variable_index] / direction_multipliers.upper_bounds[variable_index];
-         if (0. < distance) {
-            step_length = std::min(step_length, distance);
+      for (const size_t variable_index: problem.get_upper_bounded_variables()) {
+         if (0. < direction_multipliers.upper_bounds[variable_index]) {
+            double distance = -tau * current_multipliers.upper_bounds[variable_index] / direction_multipliers.upper_bounds[variable_index];
+            if (0. < distance) {
+               step_length = std::min(step_length, distance);
+            }
          }
       }
+      assert(0. < step_length && step_length <= 1. && "The dual fraction-to-boundary step length is not in (0, 1]");
+      return step_length;
    }
-   assert(0. < step_length && step_length <= 1. && "The dual fraction-to-boundary step length is not in (0, 1]");
-   return step_length;
-}
 
-// generate the right-hand side
-void PrimalDualInteriorPointSubproblem::generate_augmented_rhs(const OptimizationProblem& problem, const Multipliers& current_multipliers) {
-   this->augmented_system.rhs.fill(0.);
+   // generate the right-hand side
+   void PrimalDualInteriorPointSubproblem::assemble_augmented_rhs(const OptimizationProblem& problem, const Multipliers& current_multipliers) {
+      this->augmented_system.rhs.fill(0.);
 
-   // objective gradient
-   for (const auto [variable_index, derivative]: this->evaluations.objective_gradient) {
-      this->augmented_system.rhs[variable_index] -= derivative;
-   }
+      // objective gradient
+      for (const auto [variable_index, derivative]: this->objective_gradient) {
+         this->augmented_system.rhs[variable_index] -= derivative;
+      }
 
-   // constraint: evaluations and gradients
-   for (size_t constraint_index: Range(problem.number_constraints)) {
-      // Lagrangian
-      if (current_multipliers.constraints[constraint_index] != 0.) {
-         for (const auto [variable_index, derivative]: this->evaluations.constraint_jacobian[constraint_index]) {
-            this->augmented_system.rhs[variable_index] += current_multipliers.constraints[constraint_index] * derivative;
+      // constraint: evaluations and gradients
+      for (size_t constraint_index: Range(problem.number_constraints)) {
+         // Lagrangian
+         if (current_multipliers.constraints[constraint_index] != 0.) {
+            for (const auto [variable_index, derivative]: this->constraint_jacobian[constraint_index]) {
+               this->augmented_system.rhs[variable_index] += current_multipliers.constraints[constraint_index] * derivative;
+            }
          }
+         // constraints
+         this->augmented_system.rhs[problem.number_variables + constraint_index] = -this->constraints[constraint_index];
       }
-      // constraints
-      this->augmented_system.rhs[problem.number_variables + constraint_index] = -this->evaluations.constraints[constraint_index];
-   }
-   DEBUG2 << "RHS: "; print_vector(DEBUG2, view(this->augmented_system.rhs, 0, problem.number_variables + problem.number_constraints)); DEBUG << '\n';
-}
-
-void PrimalDualInteriorPointSubproblem::assemble_primal_dual_direction(const OptimizationProblem& problem, const Vector<double>& current_primals,
-      const Multipliers& current_multipliers, Vector<double>& direction_primals, Multipliers& direction_multipliers) {
-   // form the primal-dual direction
-   direction_primals = view(this->augmented_system.solution, 0, problem.number_variables);
-   // retrieve the duals with correct signs (note the minus sign)
-   direction_multipliers.constraints = view(-this->augmented_system.solution, problem.number_variables,
-         problem.number_variables + problem.number_constraints);
-   this->compute_bound_dual_direction(problem, current_primals, current_multipliers, direction_primals, direction_multipliers);
-
-   // "fraction-to-boundary" rule for primal variables and constraints multipliers
-   const double tau = std::max(this->parameters.tau_min, 1. - this->barrier_parameter());
-   const double primal_step_length = PrimalDualInteriorPointSubproblem::primal_fraction_to_boundary(problem, current_primals, direction_primals, tau);
-   const double bound_dual_step_length = PrimalDualInteriorPointSubproblem::dual_fraction_to_boundary(problem, current_multipliers, direction_multipliers, tau);
-   DEBUG << "Fraction-to-boundary rules:\n";
-   DEBUG << "primal step length = " << primal_step_length << '\n';
-   DEBUG << "bound dual step length = " << bound_dual_step_length << "\n\n";
-   // scale the primal-dual variables
-   direction_primals.scale(primal_step_length);
-   direction_multipliers.constraints.scale(primal_step_length);
-   direction_multipliers.lower_bounds.scale(bound_dual_step_length);
-   direction_multipliers.upper_bounds.scale(bound_dual_step_length);
-}
-
-void PrimalDualInteriorPointSubproblem::compute_bound_dual_direction(const OptimizationProblem& problem, const Vector<double>& current_primals,
-      const Multipliers& current_multipliers, const Vector<double>& primal_direction, Multipliers& direction_multipliers) {
-   direction_multipliers.lower_bounds.fill(0.);
-   direction_multipliers.upper_bounds.fill(0.);
-   for (const size_t variable_index: problem.get_lower_bounded_variables()) {
-      const double distance_to_bound = current_primals[variable_index] - problem.variable_lower_bound(variable_index);
-      direction_multipliers.lower_bounds[variable_index] = (this->barrier_parameter() - primal_direction[variable_index] * current_multipliers.lower_bounds[variable_index]) /
-                                            distance_to_bound - current_multipliers.lower_bounds[variable_index];
-      assert(is_finite(direction_multipliers.lower_bounds[variable_index]) && "The displacement lower_delta_z is infinite");
-   }
-   for (const size_t variable_index: problem.get_upper_bounded_variables()) {
-      const double distance_to_bound = current_primals[variable_index] - problem.variable_upper_bound(variable_index);
-      direction_multipliers.upper_bounds[variable_index] = (this->barrier_parameter() - primal_direction[variable_index] * current_multipliers.upper_bounds[variable_index]) /
-                                            distance_to_bound - current_multipliers.upper_bounds[variable_index];
-      assert(is_finite(direction_multipliers.upper_bounds[variable_index]) && "The displacement upper_delta_z is infinite");
-   }
-}
-
-void PrimalDualInteriorPointSubproblem::compute_least_square_multipliers(const OptimizationProblem& problem, Iterate& iterate,
-      Vector<double>& constraint_multipliers) {
-   this->augmented_system.matrix->dimension = problem.number_variables + problem.number_constraints;
-   this->augmented_system.matrix->reset();
-   Preprocessing::compute_least_square_multipliers(problem.model, *this->augmented_system.matrix, this->augmented_system.rhs, *this->linear_solver,
-         iterate, constraint_multipliers, this->least_square_multiplier_max_norm);
-}
-
-void PrimalDualInteriorPointSubproblem::postprocess_iterate(const OptimizationProblem& problem, Iterate& iterate) {
-   // rescale the bound multipliers (Eq. 16 in Ipopt paper)
-   for (const size_t variable_index: problem.get_lower_bounded_variables()) {
-      const double coefficient = this->barrier_parameter() / (iterate.primals[variable_index] - problem.variable_lower_bound(variable_index));
-      const double lb = coefficient / this->parameters.k_sigma;
-      const double ub = coefficient * this->parameters.k_sigma;
-      if (lb <= ub) {
-         const double current_value = iterate.multipliers.lower_bounds[variable_index];
-         iterate.multipliers.lower_bounds[variable_index] = std::max(std::min(iterate.multipliers.lower_bounds[variable_index], ub), lb);
-         if (iterate.multipliers.lower_bounds[variable_index] != current_value) {
-            DEBUG << "Multiplier for lower bound " << variable_index << " rescaled from " << current_value << " to " << iterate.multipliers.lower_bounds[variable_index] << '\n';
-         }
+      DEBUG2 << "RHS: "; print_vector(DEBUG2, view(this->augmented_system.rhs, 0, problem.number_variables + problem.number_constraints)); DEBUG << '\n';
+   }
+
+   void PrimalDualInteriorPointSubproblem::assemble_primal_dual_direction(const OptimizationProblem& problem, const Vector<double>& current_primals,
+         const Multipliers& current_multipliers, Vector<double>& direction_primals, Multipliers& direction_multipliers) {
+      // form the primal-dual direction
+      direction_primals = view(this->augmented_system.solution, 0, problem.number_variables);
+      // retrieve the duals with correct signs (note the minus sign)
+      direction_multipliers.constraints = view(-this->augmented_system.solution, problem.number_variables,
+            problem.number_variables + problem.number_constraints);
+      this->compute_bound_dual_direction(problem, current_primals, current_multipliers, direction_primals, direction_multipliers);
+
+      // determine if the direction is a "small direction" (Section 3.9 of the Ipopt paper) TODO
+      const bool is_small_step = PrimalDualInteriorPointSubproblem::is_small_step(problem, current_primals, direction_primals);
+      if (is_small_step) {
+         DEBUG << "This is a small step\n";
+      }
+
+      // "fraction-to-boundary" rule for primal variables and constraints multipliers
+      const double tau = std::max(this->parameters.tau_min, 1. - this->barrier_parameter());
+      const double primal_step_length = PrimalDualInteriorPointSubproblem::primal_fraction_to_boundary(problem, current_primals, direction_primals, tau);
+      const double bound_dual_step_length = PrimalDualInteriorPointSubproblem::dual_fraction_to_boundary(problem, current_multipliers, direction_multipliers, tau);
+      DEBUG << "Fraction-to-boundary rules:\n";
+      DEBUG << "primal step length = " << primal_step_length << '\n';
+      DEBUG << "bound dual step length = " << bound_dual_step_length << "\n\n";
+      // scale the primal-dual variables
+      direction_primals.scale(primal_step_length);
+      direction_multipliers.constraints.scale(primal_step_length);
+      direction_multipliers.lower_bounds.scale(bound_dual_step_length);
+      direction_multipliers.upper_bounds.scale(bound_dual_step_length);
+   }
+
+   void PrimalDualInteriorPointSubproblem::compute_bound_dual_direction(const OptimizationProblem& problem, const Vector<double>& current_primals,
+         const Multipliers& current_multipliers, const Vector<double>& primal_direction, Multipliers& direction_multipliers) {
+      direction_multipliers.lower_bounds.fill(0.);
+      direction_multipliers.upper_bounds.fill(0.);
+      for (const size_t variable_index: problem.get_lower_bounded_variables()) {
+         const double distance_to_bound = current_primals[variable_index] - problem.variable_lower_bound(variable_index);
+         direction_multipliers.lower_bounds[variable_index] = (this->barrier_parameter() - primal_direction[variable_index] * current_multipliers.lower_bounds[variable_index]) /
+                                                              distance_to_bound - current_multipliers.lower_bounds[variable_index];
+         assert(is_finite(direction_multipliers.lower_bounds[variable_index]) && "The lower bound dual is infinite");
       }
-      else {
-         WARNING << YELLOW << "Barrier subproblem: the bounds are in the wrong order in the lower bound multiplier reset" << RESET << '\n';
+      for (const size_t variable_index: problem.get_upper_bounded_variables()) {
+         const double distance_to_bound = current_primals[variable_index] - problem.variable_upper_bound(variable_index);
+         direction_multipliers.upper_bounds[variable_index] = (this->barrier_parameter() - primal_direction[variable_index] * current_multipliers.upper_bounds[variable_index]) /
+                                                              distance_to_bound - current_multipliers.upper_bounds[variable_index];
+         assert(is_finite(direction_multipliers.upper_bounds[variable_index]) && "The upper bound dual is infinite");
       }
    }
-   for (const size_t variable_index: problem.get_upper_bounded_variables()) {
-      const double coefficient = this->barrier_parameter() / (iterate.primals[variable_index] - problem.variable_upper_bound(variable_index));
-      const double lb = coefficient * this->parameters.k_sigma;
-      const double ub = coefficient / this->parameters.k_sigma;
-      if (lb <= ub) {
-         const double current_value = iterate.multipliers.upper_bounds[variable_index];
-         iterate.multipliers.upper_bounds[variable_index] = std::max(std::min(iterate.multipliers.upper_bounds[variable_index], ub), lb);
-         if (iterate.multipliers.upper_bounds[variable_index] != current_value) {
-            DEBUG << "Multiplier for upper bound " << variable_index << " rescaled from " << current_value << " to " << iterate.multipliers.upper_bounds[variable_index] << '\n';
+
+   void PrimalDualInteriorPointSubproblem::compute_least_square_multipliers(const OptimizationProblem& problem, Iterate& iterate,
+         Vector<double>& constraint_multipliers) {
+      this->augmented_system.matrix->dimension = problem.number_variables + problem.number_constraints;
+      this->augmented_system.matrix->reset();
+      Preprocessing::compute_least_square_multipliers(problem.model, *this->augmented_system.matrix, this->augmented_system.rhs, *this->linear_solver,
+            iterate, constraint_multipliers, this->least_square_multiplier_max_norm);
+   }
+
+   void PrimalDualInteriorPointSubproblem::postprocess_iterate(const OptimizationProblem& problem, Iterate& iterate) {
+      // rescale the bound multipliers (Eq. 16 in Ipopt paper)
+      for (const size_t variable_index: problem.get_lower_bounded_variables()) {
+         const double coefficient = this->barrier_parameter() / (iterate.primals[variable_index] - problem.variable_lower_bound(variable_index));
+         if (is_finite(coefficient)) {
+            const double lb = coefficient / this->parameters.k_sigma;
+            const double ub = coefficient * this->parameters.k_sigma;
+            if (lb <= ub) {
+               const double current_value = iterate.multipliers.lower_bounds[variable_index];
+               iterate.multipliers.lower_bounds[variable_index] = std::max(std::min(iterate.multipliers.lower_bounds[variable_index], ub), lb);
+               if (iterate.multipliers.lower_bounds[variable_index] != current_value) {
+                  DEBUG << "Multiplier for lower bound " << variable_index << " rescaled from " << current_value << " to " << iterate.multipliers.lower_bounds[variable_index] << '\n';
+               }
+            }
+            else {
+               WARNING << YELLOW << "Barrier subproblem: the bounds are in the wrong order in the lower bound multiplier reset" << RESET << '\n';
+            }
          }
+
       }
-      else {
-         WARNING << YELLOW << "Barrier subproblem: the bounds are in the wrong order in the upper bound multiplier reset" << RESET << '\n';
+      for (const size_t variable_index: problem.get_upper_bounded_variables()) {
+         const double coefficient = this->barrier_parameter() / (iterate.primals[variable_index] - problem.variable_upper_bound(variable_index));
+         if (is_finite(coefficient)) {
+            const double lb = coefficient * this->parameters.k_sigma;
+            const double ub = coefficient / this->parameters.k_sigma;
+            if (lb <= ub) {
+               const double current_value = iterate.multipliers.upper_bounds[variable_index];
+               iterate.multipliers.upper_bounds[variable_index] = std::max(std::min(iterate.multipliers.upper_bounds[variable_index], ub), lb);
+               if (iterate.multipliers.upper_bounds[variable_index] != current_value) {
+                  DEBUG << "Multiplier for upper bound " << variable_index << " rescaled from " << current_value << " to " << iterate.multipliers.upper_bounds[variable_index] << '\n';
+               }
+            }
+            else {
+               WARNING << YELLOW << "Barrier subproblem: the bounds are in the wrong order in the upper bound multiplier reset" << RESET << '\n';
+            }
+         }
       }
    }
-}
 
-size_t PrimalDualInteriorPointSubproblem::get_hessian_evaluation_count() const {
-   return this->hessian_model->evaluation_count;
-}
+   size_t PrimalDualInteriorPointSubproblem::get_hessian_evaluation_count() const {
+      return this->hessian_model->evaluation_count;
+   }
 
-void PrimalDualInteriorPointSubproblem::set_initial_point(const Vector<double>& /*point*/) {
-   // do nothing
-}
+   void PrimalDualInteriorPointSubproblem::set_initial_point(const Vector<double>& /*point*/) {
+      // do nothing
+   }
+} // namespace
\ No newline at end of file
diff --git a/uno/ingredients/subproblem/interior_point_methods/PrimalDualInteriorPointSubproblem.hpp b/uno/ingredients/subproblem/interior_point_methods/PrimalDualInteriorPointSubproblem.hpp
index eb7d77c..abf6ba0 100644
--- a/uno/ingredients/subproblem/interior_point_methods/PrimalDualInteriorPointSubproblem.hpp
+++ b/uno/ingredients/subproblem/interior_point_methods/PrimalDualInteriorPointSubproblem.hpp
@@ -10,73 +10,82 @@
 #include "ingredients/subproblem/HessianModel.hpp"
 #include "BarrierParameterUpdateStrategy.hpp"
 
-struct InteriorPointParameters {
-   double tau_min;
-   double k_sigma;
-   double regularization_exponent;
-   double small_direction_factor;
-   double push_variable_to_interior_k1;
-   double push_variable_to_interior_k2;
-};
-
-class PrimalDualInteriorPointSubproblem : public Subproblem {
-public:
-   PrimalDualInteriorPointSubproblem(size_t number_variables, size_t number_constraints, size_t number_jacobian_nonzeros,
-         size_t number_hessian_nonzeros, const Options& options);
-
-   void initialize_statistics(Statistics& statistics, const Options& options) override;
-   [[nodiscard]] bool generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) override;
-   void set_initial_point(const Vector<double>& point) override;
-
-   void initialize_feasibility_problem(const l1RelaxedProblem& problem, Iterate& current_iterate) override;
-   void set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& constraint_index) override;
-   void exit_feasibility_problem(const OptimizationProblem& problem, Iterate& trial_iterate) override;
-
-   void solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
-         Direction& direction, const WarmstartInformation& warmstart_information) override;
-
-   [[nodiscard]] const SymmetricMatrix<double>& get_lagrangian_hessian() const override;
-   void set_auxiliary_measure(const Model& model, Iterate& iterate) override;
-   [[nodiscard]] double compute_predicted_auxiliary_reduction_model(const Model& model, const Iterate& current_iterate,
-         const Vector<double>& primal_direction, double step_length) const override;
-
-   void postprocess_iterate(const OptimizationProblem& problem, Iterate& iterate) override;
-   [[nodiscard]] size_t get_hessian_evaluation_count() const override;
-
-protected:
-   SymmetricIndefiniteLinearSystem<double> augmented_system;
-   const std::unique_ptr<HessianModel> hessian_model; /*!< Strategy to evaluate or approximate the Hessian */
-   const std::unique_ptr<SymmetricIndefiniteLinearSolver<double>> linear_solver;
-
-   BarrierParameterUpdateStrategy barrier_parameter_update_strategy;
-   double previous_barrier_parameter;
-   const double default_multiplier;
-   const InteriorPointParameters parameters;
-   const double least_square_multiplier_max_norm;
-   const double damping_factor; // (Section 3.7 in IPOPT paper)
-
-   bool solving_feasibility_problem{false};
-
-   [[nodiscard]] double barrier_parameter() const;
-   [[nodiscard]] double push_variable_to_interior(double variable_value, double lower_bound, double upper_bound) const;
-   void evaluate_functions(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
-         const WarmstartInformation& warmstart_information);
-   void update_barrier_parameter(const OptimizationProblem& problem, const Iterate& current_iterate);
-   [[nodiscard]] bool is_small_step(const OptimizationProblem& problem, const Iterate& current_iterate, const Direction& direction) const;
-   [[nodiscard]] double evaluate_subproblem_objective(const Direction& direction) const;
-   [[nodiscard]] double compute_barrier_term_directional_derivative(const Model& model, const Iterate& current_iterate,
-         const Vector<double>& primal_direction) const;
-   [[nodiscard]] static double primal_fraction_to_boundary(const OptimizationProblem& problem, const Vector<double>& current_primals,
-         const Vector<double>& primal_direction, double tau);
-   [[nodiscard]] static double dual_fraction_to_boundary(const OptimizationProblem& problem, const Multipliers& current_multipliers,
-         Multipliers& direction_multipliers, double tau);
-   void assemble_augmented_system(Statistics& statistics, const OptimizationProblem& problem, const Multipliers& current_multipliers);
-   void generate_augmented_rhs(const OptimizationProblem& problem, const Multipliers& current_multipliers);
-   void assemble_primal_dual_direction(const OptimizationProblem& problem, const Vector<double>& current_primals, const Multipliers& current_multipliers,
-         Vector<double>& direction_primals, Multipliers& direction_multipliers);
-   void compute_bound_dual_direction(const OptimizationProblem& problem, const Vector<double>& current_primals, const Multipliers& current_multipliers,
-         const Vector<double>& primal_direction, Multipliers& direction_multipliers);
-   void compute_least_square_multipliers(const OptimizationProblem& problem, Iterate& iterate, Vector<double>& constraint_multipliers);
-};
-
-#endif // UNO_INFEASIBLEINTERIORPOINTSUBPROBLEM_H
+namespace uno {
+   // forward reference
+   class PrimalDualResiduals;
+
+   struct InteriorPointParameters {
+      double tau_min;
+      double k_sigma;
+      double regularization_exponent;
+      double small_direction_factor;
+      double push_variable_to_interior_k1;
+      double push_variable_to_interior_k2;
+   };
+
+   class PrimalDualInteriorPointSubproblem : public Subproblem {
+   public:
+      PrimalDualInteriorPointSubproblem(size_t number_variables, size_t number_constraints, size_t number_jacobian_nonzeros,
+            size_t number_hessian_nonzeros, const Options& options);
+
+      void initialize_statistics(Statistics& statistics, const Options& options) override;
+      void generate_initial_iterate(const OptimizationProblem& problem, Iterate& initial_iterate) override;
+      void set_initial_point(const Vector<double>& point) override;
+
+      void initialize_feasibility_problem(const l1RelaxedProblem& problem, Iterate& current_iterate) override;
+      void set_elastic_variable_values(const l1RelaxedProblem& problem, Iterate& constraint_index) override;
+      void exit_feasibility_problem(const OptimizationProblem& problem, Iterate& trial_iterate) override;
+
+      void solve(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate,  const Multipliers& current_multipliers,
+            Direction& direction, const WarmstartInformation& warmstart_information) override;
+
+      [[nodiscard]] const SymmetricMatrix<size_t, double>& get_lagrangian_hessian() const override;
+      void set_auxiliary_measure(const Model& model, Iterate& iterate) override;
+      [[nodiscard]] double compute_predicted_auxiliary_reduction_model(const Model& model, const Iterate& current_iterate,
+            const Vector<double>& primal_direction, double step_length) const override;
+
+      void postprocess_iterate(const OptimizationProblem& problem, Iterate& iterate) override;
+      [[nodiscard]] size_t get_hessian_evaluation_count() const override;
+
+   protected:
+      SparseVector<double> objective_gradient; /*!< Sparse Jacobian of the objective */
+      std::vector<double> constraints; /*!< Constraint values (size \f$m)\f$ */
+      RectangularMatrix<double> constraint_jacobian; /*!< Sparse Jacobian of the constraints */
+      const std::unique_ptr<HessianModel> hessian_model; /*!< Strategy to evaluate or approximate the Hessian */
+
+      SymmetricIndefiniteLinearSystem<double> augmented_system;
+      const std::unique_ptr<SymmetricIndefiniteLinearSolver<size_t, double>> linear_solver;
+
+      BarrierParameterUpdateStrategy barrier_parameter_update_strategy;
+      double previous_barrier_parameter;
+      const double default_multiplier;
+      const InteriorPointParameters parameters;
+      const double least_square_multiplier_max_norm;
+      const double damping_factor; // (Section 3.7 in IPOPT paper)
+
+      bool solving_feasibility_problem{false};
+
+      [[nodiscard]] double barrier_parameter() const;
+      [[nodiscard]] double push_variable_to_interior(double variable_value, double lower_bound, double upper_bound) const;
+      void evaluate_functions(Statistics& statistics, const OptimizationProblem& problem, Iterate& current_iterate, const Multipliers& current_multipliers,
+            const WarmstartInformation& warmstart_information);
+      void update_barrier_parameter(const OptimizationProblem& problem, const Iterate& current_iterate, const PrimalDualResiduals& residuals);
+      [[nodiscard]] bool is_small_step(const OptimizationProblem& problem, const Vector<double>& current_primals, const Vector<double>& direction_primals) const;
+      [[nodiscard]] double evaluate_subproblem_objective(const Direction& direction) const;
+      [[nodiscard]] double compute_barrier_term_directional_derivative(const Model& model, const Iterate& current_iterate,
+            const Vector<double>& primal_direction) const;
+      [[nodiscard]] static double primal_fraction_to_boundary(const OptimizationProblem& problem, const Vector<double>& current_primals,
+            const Vector<double>& primal_direction, double tau);
+      [[nodiscard]] static double dual_fraction_to_boundary(const OptimizationProblem& problem, const Multipliers& current_multipliers,
+            Multipliers& direction_multipliers, double tau);
+      void assemble_augmented_system(Statistics& statistics, const OptimizationProblem& problem, const Multipliers& current_multipliers);
+      void assemble_augmented_rhs(const OptimizationProblem& problem, const Multipliers& current_multipliers);
+      void assemble_primal_dual_direction(const OptimizationProblem& problem, const Vector<double>& current_primals, const Multipliers& current_multipliers,
+            Vector<double>& direction_primals, Multipliers& direction_multipliers);
+      void compute_bound_dual_direction(const OptimizationProblem& problem, const Vector<double>& current_primals, const Multipliers& current_multipliers,
+            const Vector<double>& primal_direction, Multipliers& direction_multipliers);
+      void compute_least_square_multipliers(const OptimizationProblem& problem, Iterate& iterate, Vector<double>& constraint_multipliers);
+   };
+} // namespace
+
+#endif // UNO_INFEASIBLEINTERIORPOINTSUBPROBLEM_H
\ No newline at end of file
diff --git a/uno/interfaces/AMPL/AMPLModel.cpp b/uno/interfaces/AMPL/AMPLModel.cpp
index 0161d45..858bf14 100644
--- a/uno/interfaces/AMPL/AMPLModel.cpp
+++ b/uno/interfaces/AMPL/AMPLModel.cpp
@@ -262,7 +262,7 @@ size_t AMPLModel::compute_hessian_number_nonzeros(double objective_multiplier, c
 }
 
 void AMPLModel::evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
-      SymmetricMatrix<double>& hessian) const {
+      SymmetricMatrix<size_t, double>& hessian) const {
    // register the vector of variables
    (*(this->asl)->p.Xknown)(this->asl, const_cast<double*>(x.data()), nullptr);
 
diff --git a/uno/interfaces/AMPL/AMPLModel.hpp b/uno/interfaces/AMPL/AMPLModel.hpp
index 5eb709f..798341a 100644
--- a/uno/interfaces/AMPL/AMPLModel.hpp
+++ b/uno/interfaces/AMPL/AMPLModel.hpp
@@ -31,7 +31,7 @@ public:
    void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const override;
    void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const override;
    void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
-         SymmetricMatrix<double>& hessian) const override;
+         SymmetricMatrix<size_t, double>& hessian) const override;
 
    [[nodiscard]] double variable_lower_bound(size_t variable_index) const override;
    [[nodiscard]] double variable_upper_bound(size_t variable_index) const override;
diff --git a/uno/linear_algebra/COOSymmetricMatrix.hpp b/uno/linear_algebra/COOSymmetricMatrix.hpp
index 8c98c3d..fc498b8 100644
--- a/uno/linear_algebra/COOSymmetricMatrix.hpp
+++ b/uno/linear_algebra/COOSymmetricMatrix.hpp
@@ -9,134 +9,130 @@
 #include "SymmetricMatrix.hpp"
 #include "tools/Infinity.hpp"
 
-/*
- * Coordinate list
- * https://en.wikipedia.org/wiki/Sparse_matrix#Coordinate_list_(COO)
- */
-template <typename ElementType>
-class COOSymmetricMatrix: public SymmetricMatrix<ElementType> {
-public:
-   COOSymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization);
-
-   void reset() override;
-   void insert(ElementType term, size_t row_index, size_t column_index) override;
-   void finalize_column(size_t /*column_index*/) override { /* do nothing */ }
-   [[nodiscard]] ElementType smallest_diagonal_entry() const override;
-   void set_regularization(const std::function<ElementType(size_t index)>& regularization_function) override;
-
-   void print(std::ostream& stream) const override;
-
-   static COOSymmetricMatrix<ElementType> zero(size_t dimension);
-
-protected:
-   std::vector<size_t> row_indices;
-   std::vector<size_t> column_indices;
-   Vector<ElementType> diagonal_entries;
-
-   void initialize_regularization();
-
-   // iterator functions
-   [[nodiscard]] std::tuple<size_t, size_t, ElementType> dereference_iterator(size_t column_index, size_t nonzero_index) const override;
-   void increment_iterator(size_t& column_index, size_t& nonzero_index) const override;
-};
-
-// implementation
-
-template <typename ElementType>
-COOSymmetricMatrix<ElementType>::COOSymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization):
-      SymmetricMatrix<ElementType>(dimension, original_capacity, use_regularization),
-      diagonal_entries(dimension, ElementType(0)) {
-   this->row_indices.reserve(this->capacity);
-   this->column_indices.reserve(this->capacity);
-
-   // initialize regularization terms
-   if (this->use_regularization) {
-      this->initialize_regularization();
+namespace uno {
+   /*
+    * Coordinate list
+    * https://en.wikipedia.org/wiki/Sparse_matrix#Coordinate_list_(COO)
+    */
+   template <typename IndexType, typename ElementType>
+   class COOSymmetricMatrix : public SymmetricMatrix<IndexType, ElementType> {
+   public:
+      COOSymmetricMatrix(size_t dimension, size_t capacity, bool use_regularization);
+
+      void reset() override;
+      void insert(ElementType term, IndexType row_index, IndexType column_index) override;
+      void finalize_column(IndexType /*column_index*/) override { /* do nothing */ }
+      void set_regularization(const std::function<ElementType(size_t index)>& regularization_function) override;
+
+      void print(std::ostream& stream) const override;
+
+      const IndexType* row_indices_pointer() const {
+         return this->row_indices.data();
+      }
+      IndexType* row_indices_pointer() {
+         return this->row_indices.data();
+      }
+      const IndexType* column_indices_pointer() const {
+         return this->column_indices.data();
+      }
+      IndexType* column_indices_pointer() {
+         return this->column_indices.data();
+      }
+
+      static COOSymmetricMatrix<IndexType, ElementType> zero(size_t dimension);
+
+   protected:
+      std::vector<IndexType> row_indices;
+      std::vector<IndexType> column_indices;
+
+      void initialize_regularization();
+
+      // iterator functions
+      [[nodiscard]] std::tuple<IndexType, IndexType, ElementType> dereference_iterator(size_t column_index, size_t nonzero_index) const override;
+      void increment_iterator(size_t& column_index, size_t& nonzero_index) const override;
+   };
+
+   // implementation
+
+   template <typename IndexType, typename ElementType>
+   COOSymmetricMatrix<IndexType, ElementType>::COOSymmetricMatrix(size_t dimension, size_t capacity, bool use_regularization):
+         SymmetricMatrix<IndexType, ElementType>(dimension, capacity, use_regularization) {
+      this->row_indices.reserve(this->capacity);
+      this->column_indices.reserve(this->capacity);
+
+      // initialize regularization terms
+      if (this->use_regularization) {
+         this->initialize_regularization();
+      }
    }
-}
-
-template <typename ElementType>
-void COOSymmetricMatrix<ElementType>::reset() {
-   // empty the matrix
-   SymmetricMatrix<ElementType>::reset();
-   this->row_indices.clear();
-   this->column_indices.clear();
-   this->diagonal_entries.fill(ElementType(0));
-
-   // initialize regularization terms
-   if (this->use_regularization) {
-      this->initialize_regularization();
+
+   template <typename IndexType, typename ElementType>
+   void COOSymmetricMatrix<IndexType, ElementType>::reset() {
+      // empty the matrix
+      SymmetricMatrix<IndexType, ElementType>::reset();
+      this->row_indices.clear();
+      this->column_indices.clear();
+
+      // initialize regularization terms
+      if (this->use_regularization) {
+         this->initialize_regularization();
+      }
    }
-}
-
-template <typename ElementType>
-void COOSymmetricMatrix<ElementType>::insert(ElementType term, size_t row_index, size_t column_index) {
-   assert(this->number_nonzeros <= this->row_indices.size() && "The COO matrix doesn't have a sufficient capacity");
-   this->entries.push_back(term);
-   this->row_indices.push_back(row_index);
-   this->column_indices.push_back(column_index);
-   this->number_nonzeros++;
-
-   // possibly update diagonal
-   if (row_index == column_index) {
-      this->diagonal_entries[row_index] += term;
+
+   template <typename IndexType, typename ElementType>
+   void COOSymmetricMatrix<IndexType, ElementType>::insert(ElementType term, IndexType row_index, IndexType column_index) {
+      assert(this->number_nonzeros <= this->row_indices.size() && "The COO matrix doesn't have a sufficient capacity");
+      this->entries.push_back(term);
+      this->row_indices.push_back(row_index);
+      this->column_indices.push_back(column_index);
+      this->number_nonzeros++;
    }
-}
 
-template <typename ElementType>
-ElementType COOSymmetricMatrix<ElementType>::smallest_diagonal_entry() const {
-   ElementType smallest_entry = INF<ElementType>;
-   for (size_t row_index: Range(this->dimension)) {
-      smallest_entry = std::min(smallest_entry, this->diagonal_entries[row_index]);
+   template <typename IndexType, typename ElementType>
+   void COOSymmetricMatrix<IndexType, ElementType>::set_regularization(const std::function<ElementType(size_t /*index*/)>& regularization_function) {
+      assert(this->use_regularization && "You are trying to regularize a matrix where regularization was not preallocated.");
+
+      // the regularization terms (that lie at the start of the entries vector) can be directly modified
+      for (size_t row_index: Range(this->dimension)) {
+         const ElementType element = regularization_function(row_index);
+         this->entries[row_index] = element;
+      }
    }
-   return smallest_entry;
-}
-
-template <typename ElementType>
-void COOSymmetricMatrix<ElementType>::set_regularization(const std::function<ElementType(size_t /*index*/)>& regularization_function) {
-   assert(this->use_regularization && "You are trying to regularize a matrix where regularization was not preallocated.");
-
-   // the regularization terms (that lie at the start of the entries vector) can be directly modified
-   for (size_t row_index: Range(this->dimension)) {
-      const ElementType element = regularization_function(row_index);
-      this->entries[row_index] = element;
-      // update diagonal
-      this->diagonal_entries[row_index] += element;
+
+   template <typename IndexType, typename ElementType>
+   void COOSymmetricMatrix<IndexType, ElementType>::print(std::ostream& stream) const {
+      for (const auto[row_index, column_index, element]: *this) {
+         stream << "m(" << row_index << ", " << column_index << ") = " << element << '\n';
+      }
    }
-}
 
-template <typename ElementType>
-void COOSymmetricMatrix<ElementType>::print(std::ostream& stream) const {
-   for (const auto [row_index, column_index, element]: *this) {
-      stream << "m(" << row_index << ", " << column_index << ") = " << element << '\n';
+   template <typename IndexType, typename ElementType>
+   void COOSymmetricMatrix<IndexType, ElementType>::initialize_regularization() {
+      // introduce elements at the start of the entries
+      for (size_t row_index: Range(this->dimension)) {
+         this->insert(ElementType(0), IndexType(row_index), IndexType(row_index));
+      }
    }
-}
 
-template <typename ElementType>
-void COOSymmetricMatrix<ElementType>::initialize_regularization() {
-   // introduce elements at the start of the entries
-   for (size_t row_index: Range(this->dimension)) {
-      this->insert(ElementType(0), row_index, row_index);
+   template <typename IndexType, typename ElementType>
+   std::tuple<IndexType, IndexType, ElementType> COOSymmetricMatrix<IndexType, ElementType>::dereference_iterator(size_t /*column_index*/,
+         size_t nonzero_index) const {
+      return {this->row_indices[nonzero_index], this->column_indices[nonzero_index], this->entries[nonzero_index]};
    }
-}
-
-template <typename ElementType>
-std::tuple<size_t, size_t, ElementType> COOSymmetricMatrix<ElementType>::dereference_iterator(size_t /*column_index*/, size_t nonzero_index) const {
-   return {this->row_indices[nonzero_index], this->column_indices[nonzero_index], this->entries[nonzero_index]};
-}
-
-template <typename ElementType>
-void COOSymmetricMatrix<ElementType>::increment_iterator(size_t& column_index, size_t& nonzero_index) const {
-   nonzero_index++;
-   // if end reached
-   if (nonzero_index == this->number_nonzeros) {
-      column_index = this->dimension;
+
+   template <typename IndexType, typename ElementType>
+   void COOSymmetricMatrix<IndexType, ElementType>::increment_iterator(size_t& column_index, size_t& nonzero_index) const {
+      nonzero_index++;
+      // if end reached
+      if (nonzero_index == this->number_nonzeros) {
+         column_index = this->dimension;
+      }
    }
-}
 
-template <typename ElementType>
-COOSymmetricMatrix<ElementType> COOSymmetricMatrix<ElementType>::zero(size_t dimension) {
-   return COOSymmetricMatrix<ElementType>(dimension, 0, false);
-}
+   template <typename IndexType, typename ElementType>
+   COOSymmetricMatrix<IndexType, ElementType> COOSymmetricMatrix<IndexType, ElementType>::zero(size_t dimension) {
+      return COOSymmetricMatrix<IndexType, ElementType>(dimension, 0, false);
+   }
+} // namespace
 
 #endif // UNO_COOSYMMETRICMATRIX_H
\ No newline at end of file
diff --git a/uno/linear_algebra/CSCSymmetricMatrix.hpp b/uno/linear_algebra/CSCSymmetricMatrix.hpp
index 5c377dd..8e1d344 100644
--- a/uno/linear_algebra/CSCSymmetricMatrix.hpp
+++ b/uno/linear_algebra/CSCSymmetricMatrix.hpp
@@ -10,148 +10,131 @@
 #include "tools/Infinity.hpp"
 #include "symbolic/VectorView.hpp"
 
-/*
+namespace uno {
+   /*
  * Compressed Sparse Column
  * https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_column_(CSC_or_CCS)
  */
 
-template <typename ElementType>
-class CSCSymmetricMatrix : public SymmetricMatrix<ElementType> {
-public:
-   CSCSymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization);
-
-   void reset() override;
-   void insert(ElementType term, size_t row_index, size_t column_index) override;
-   void finalize_column(size_t column_index) override;
-   [[nodiscard]] ElementType smallest_diagonal_entry() const override;
-   void set_regularization(const std::function<ElementType(size_t /*index*/)>& regularization_function) override;
-
-   void print(std::ostream& stream) const override;
-
-   static CSCSymmetricMatrix<ElementType> identity(size_t dimension);
-
-protected:
-   // entries and row_indices have nnz elements
-   // column_starts has dimension+1 elements
-   Vector<size_t> column_starts{};
-   std::vector<size_t> row_indices{};
-   size_t current_column{0};
-   Vector<ElementType> diagonal_entries;
-
-   // iterator functions
-   [[nodiscard]] std::tuple<size_t, size_t, ElementType> dereference_iterator(size_t column_index, size_t nonzero_index) const override;
-   void increment_iterator(size_t& column_index, size_t& nonzero_index) const override;
-};
-
-template <typename ElementType>
-CSCSymmetricMatrix<ElementType>::CSCSymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization):
-      SymmetricMatrix<ElementType>(dimension, original_capacity, use_regularization),
-      column_starts(dimension + 1),
-      diagonal_entries(dimension, ElementType(0)) {
-   this->entries.reserve(this->capacity);
-   this->row_indices.reserve(this->capacity);
-}
-
-template <typename ElementType>
-void CSCSymmetricMatrix<ElementType>::reset() {
-   // empty the matrix
-   SymmetricMatrix<ElementType>::reset();
-   this->row_indices.clear();
-   this->column_starts.fill(0);
-   this->current_column = 0;
-   this->diagonal_entries.fill(ElementType(0));
-}
-
-template <typename ElementType>
-void CSCSymmetricMatrix<ElementType>::insert(ElementType term, size_t row_index, size_t column_index) {
-   assert(column_index == this->current_column && "The previous columns should be finalized");
-
-   this->entries.push_back(term);
-   this->row_indices.push_back(row_index);
-   this->column_starts[column_index + 1]++;
-   this->number_nonzeros++;
-
-   // possibly update diagonal
-   if (row_index == column_index) {
-      this->diagonal_entries[row_index] += term;
+   template <typename IndexType, typename ElementType>
+   class CSCSymmetricMatrix : public SymmetricMatrix<IndexType, ElementType> {
+   public:
+      CSCSymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization);
+
+      void reset() override;
+      void insert(ElementType term, IndexType row_index, IndexType column_index) override;
+      void finalize_column(IndexType column_index) override;
+      void set_regularization(const std::function<ElementType(IndexType /*index*/)>& regularization_function) override;
+
+      void print(std::ostream& stream) const override;
+
+      static CSCSymmetricMatrix<IndexType, ElementType> identity(size_t dimension);
+
+   protected:
+      // entries and row_indices have nnz elements
+      // column_starts has dimension+1 elements
+      Vector<IndexType> column_starts{};
+      std::vector<IndexType> row_indices{};
+      IndexType current_column{0};
+
+      // iterator functions
+      [[nodiscard]] std::tuple<IndexType, IndexType, ElementType> dereference_iterator(IndexType column_index, size_t nonzero_index) const override;
+      void increment_iterator(IndexType& column_index, IndexType& nonzero_index) const override;
+   };
+
+   template <typename IndexType, typename ElementType>
+   CSCSymmetricMatrix<IndexType, ElementType>::CSCSymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization):
+         SymmetricMatrix<IndexType, ElementType>(dimension, original_capacity, use_regularization),
+         column_starts(dimension + 1) {
+      this->entries.reserve(this->capacity);
+      this->row_indices.reserve(this->capacity);
    }
-}
 
-template <typename ElementType>
-void CSCSymmetricMatrix<ElementType>::finalize_column(size_t column_index) {
-   assert(column_index == this->current_column && "You are not finalizing the current column");
-   assert(column_index < this->dimension && "The dimension of the matrix was exceeded");
+   template <typename IndexType, typename ElementType>
+   void CSCSymmetricMatrix<IndexType, ElementType>::reset() {
+      // empty the matrix
+      SymmetricMatrix<IndexType, ElementType>::reset();
+      this->row_indices.clear();
+      this->column_starts.fill(0);
+      this->current_column = 0;
+   }
+
+   template <typename IndexType, typename ElementType>
+   void CSCSymmetricMatrix<IndexType, ElementType>::insert(ElementType term, IndexType row_index, IndexType column_index) {
+      assert(column_index == this->current_column && "The previous columns should be finalized");
 
-   // possibly add regularization
-   if (this->use_regularization) {
-      insert(ElementType(0), column_index, column_index);
+      this->entries.push_back(term);
+      this->row_indices.push_back(row_index);
+      this->column_starts[column_index + 1]++;
+      this->number_nonzeros++;
    }
-   this->current_column++;
 
-   // start the next column at the current start
-   if (column_index < this->dimension - 1) {
-      this->column_starts[column_index + 2] = this->column_starts[column_index + 1];
+   template <typename IndexType, typename ElementType>
+   void CSCSymmetricMatrix<IndexType, ElementType>::finalize_column(IndexType column_index) {
+      assert(column_index == this->current_column && "You are not finalizing the current column");
+      assert(column_index < this->dimension && "The dimension of the matrix was exceeded");
+
+      // possibly add regularization
+      if (this->use_regularization) {
+         insert(ElementType(0), column_index, column_index);
+      }
+      this->current_column++;
+
+      // start the next column at the current start
+      if (column_index < this->dimension - 1) {
+         this->column_starts[column_index + 2] = this->column_starts[column_index + 1];
+      }
    }
-}
 
-template <typename ElementType>
-ElementType CSCSymmetricMatrix<ElementType>::smallest_diagonal_entry() const {
-   ElementType smallest_entry = INF<ElementType>;
-   for (size_t row_index: Range(this->dimension)) {
-      smallest_entry = std::min(smallest_entry, this->diagonal_entries[row_index]);
+   template <typename IndexType, typename ElementType>
+   void CSCSymmetricMatrix<IndexType, ElementType>::set_regularization(const std::function<ElementType(IndexType /*index*/)>& regularization_function) {
+      assert(this->use_regularization && "You are trying to regularize a matrix where regularization was not preallocated.");
+
+      for (size_t row_index: Range(this->dimension)) {
+         // the regularization term is located at the end of the column, that is right before the start of the next column
+         const size_t k = static_cast<size_t>(this->column_starts[row_index + 1] - 1);
+         const ElementType element = regularization_function(row_index);
+         this->entries[k] = element;
+      }
    }
-   return smallest_entry;
-}
-
-template <typename ElementType>
-void CSCSymmetricMatrix<ElementType>::set_regularization(const std::function<ElementType(size_t /*index*/)>& regularization_function) {
-   assert(this->use_regularization && "You are trying to regularize a matrix where regularization was not preallocated.");
-
-   for (size_t row_index: Range(this->dimension)) {
-      // the regularization term is located at the end of the column, that is right before the start of the next column
-      const size_t k = this->column_starts[row_index + 1] - 1;
-      const ElementType element = regularization_function(row_index);
-      this->entries[k] = element;
-      // update diagonal
-      this->diagonal_entries[row_index] += element;
+
+   template <typename IndexType, typename ElementType>
+   std::tuple<IndexType, IndexType, ElementType> CSCSymmetricMatrix<IndexType, ElementType>::dereference_iterator(IndexType column_index,
+         size_t nonzero_index) const {
+      return {this->row_indices[nonzero_index], column_index, this->entries[nonzero_index]};
    }
-}
-
-template <typename ElementType>
-std::tuple<size_t, size_t, ElementType> CSCSymmetricMatrix<ElementType>::dereference_iterator(size_t column_index, size_t nonzero_index) const {
-   return {this->row_indices[nonzero_index], column_index, this->entries[nonzero_index]};
-}
-
-template <typename ElementType>
-void CSCSymmetricMatrix<ElementType>::increment_iterator(size_t& column_index, size_t& nonzero_index) const {
-   if (this->column_starts[column_index] <= nonzero_index && nonzero_index + 1 < this->column_starts[column_index + 1]) {
-      // stay in the column
-      nonzero_index++;
+
+   template <typename IndexType, typename ElementType>
+   void CSCSymmetricMatrix<IndexType, ElementType>::increment_iterator(IndexType& column_index, IndexType& nonzero_index) const {
+      if (this->column_starts[column_index] <= nonzero_index && nonzero_index + 1 < this->column_starts[column_index + 1]) {
+         // stay in the column
+         nonzero_index++;
+      }
+      else {
+         // move on to the next non-empty column
+         do {
+            column_index++;
+         } while (column_index < this->dimension && this->column_starts[column_index] == this->column_starts[column_index + 1]);
+         nonzero_index = this->column_starts[column_index];
+      }
    }
-   else {
-      // move on to the next non-empty column
-      do {
-         column_index++;
-      } while (column_index < this->dimension && this->column_starts[column_index] == this->column_starts[column_index + 1]);
-      nonzero_index = this->column_starts[column_index];
+
+   template <typename IndexType, typename ElementType>
+   void CSCSymmetricMatrix<IndexType, ElementType>::print(std::ostream& stream) const {
+      stream << "W = "; print_vector(stream, view(this->entries, 0, this->number_nonzeros));
+      stream << "with column start: "; print_vector(stream, view(this->column_starts, 0, this->dimension + 1));
+      stream << "and row index: "; print_vector(stream, view(this->row_indices, 0, this->number_nonzeros));
    }
-}
-
-template <typename ElementType>
-void CSCSymmetricMatrix<ElementType>::print(std::ostream& stream) const {
-   stream << "W = "; print_vector(stream, view(this->entries, 0, this->number_nonzeros));
-   stream << "with column start: "; print_vector(stream, view(this->column_starts, 0, this->dimension + 1));
-   stream << "and row index: "; print_vector(stream, view(this->row_indices, 0, this->number_nonzeros));
-}
-
-template <typename ElementType>
-CSCSymmetricMatrix<ElementType> CSCSymmetricMatrix<ElementType>::identity(size_t dimension) {
-   CSCSymmetricMatrix<double> matrix(dimension, dimension, false);
-   for (size_t row_index: Range(dimension)) {
-      matrix.insert(1., row_index, row_index);
-      matrix.finalize_column(row_index);
+
+   template <typename IndexType, typename ElementType>
+   CSCSymmetricMatrix<IndexType, ElementType> CSCSymmetricMatrix<IndexType, ElementType>::identity(size_t dimension) {
+      CSCSymmetricMatrix<IndexType, ElementType> matrix(dimension, dimension, false);
+      for (size_t row_index: Range(dimension)) {
+         matrix.insert(ElementType(1), IndexType(row_index), IndexType(row_index));
+         matrix.finalize_column(row_index);
+      }
+      return matrix;
    }
-   return matrix;
-}
+} // namespace
 
 #endif // UNO_CSCSYMMETRICMATRIX_H
\ No newline at end of file
diff --git a/uno/linear_algebra/Norm.hpp b/uno/linear_algebra/Norm.hpp
index 0933df6..9a3cb3a 100644
--- a/uno/linear_algebra/Norm.hpp
+++ b/uno/linear_algebra/Norm.hpp
@@ -7,138 +7,140 @@
 #include <cmath>
 #include "symbolic/Range.hpp"
 
-// norms of any array with elements of any type
+namespace uno {
+   // norms of any array with elements of any type
+
+   enum class Norm {L1, L2, L2_SQUARED, INF};
+
+   inline Norm norm_from_string(const std::string& norm_string) {
+      if (norm_string == "L1") {
+         return Norm::L1;
+      }
+      else if (norm_string == "L2") {
+         return Norm::L2;
+      }
+      else if (norm_string == "L2_squared") {
+         return Norm::L2_SQUARED;
+      }
+      else if (norm_string == "INF") {
+         return Norm::INF;
+      }
+      throw std::invalid_argument("The norm " + norm_string + " is not known");
+   }
 
-enum class Norm {L1, L2, L2_SQUARED, INF};
+   // generic norm function for iterators that return [key, value] pairs
+   // https://stackoverflow.com/questions/38701475/how-to-overload-function-for-different-iterator-value-types-in-c
+   template <typename KeyValueIterable, typename AccumulationFunction, typename ElementType = typename KeyValueIterable::value_type,
+         typename std::enable_if_t<not std::is_member_function_pointer<decltype(&KeyValueIterable::operator[])>::value, int> = 0>
+   ElementType generic_norm(const KeyValueIterable& x, const AccumulationFunction& accumulation_function) {
+      ElementType result{0};
+      for (const auto [_, element]: x) {
+         accumulation_function(result, element);
+      }
+      return result;
+   }
 
-inline Norm norm_from_string(const std::string& norm_string) {
-   if (norm_string == "L1") {
-      return Norm::L1;
+   // generic norm function for iterators that return the elements
+   template <typename Array, typename AccumulationFunction, typename ElementType = typename Array::value_type>
+   ElementType generic_norm(const Array& x, const AccumulationFunction& accumulation_function) {
+      ElementType result{0};
+      for (size_t index: Range(x.size())) {
+         accumulation_function(result, x[index]);
+      }
+      return result;
    }
-   else if (norm_string == "L2") {
-      return Norm::L2;
+
+   //***********
+   // l1 norm //
+   //***********
+   template <typename ElementType>
+   void norm_1_accumulation(ElementType& result, ElementType element) {
+      result += std::abs(element);
    }
-   else if (norm_string == "L2_squared") {
-      return Norm::L2_SQUARED;
+
+   template <typename Array, typename ElementType = typename Array::value_type>
+   ElementType norm_1(const Array& x) {
+      return generic_norm(x, norm_1_accumulation<ElementType>);
    }
-   else if (norm_string == "INF") {
-      return Norm::INF;
+
+   // l1 norm of several arrays
+   template <typename Array, typename... Arrays, typename ElementType = typename Array::value_type>
+   ElementType norm_1(const Array& x, const Arrays& ... other_arrays) {
+      return norm_1(x) + norm_1(other_arrays...);
    }
-   throw std::invalid_argument("The norm " + norm_string + " is not known");
-}
-
-// generic norm function for iterators that return [key, value] pairs
-// https://stackoverflow.com/questions/38701475/how-to-overload-function-for-different-iterator-value-types-in-c
-template <typename KeyValueIterable, typename AccumulationFunction, typename ElementType = typename KeyValueIterable::value_type,
-      typename std::enable_if_t<not std::is_member_function_pointer<decltype(&KeyValueIterable::operator[])>::value, int> = 0>
-ElementType generic_norm(const KeyValueIterable& x, const AccumulationFunction& accumulation_function) {
-   ElementType result{0};
-   for (const auto [_, element]: x) {
-      accumulation_function(result, element);
+
+   //*******************
+   // l2 squared norm //
+   //*******************
+   template <typename ElementType>
+   void norm_2_squared_accumulation(ElementType& result, ElementType element) {
+      result += element * element;
    }
-   return result;
-}
-
-// generic norm function for iterators that return the elements
-template <typename Array, typename AccumulationFunction, typename ElementType = typename Array::value_type>
-ElementType generic_norm(const Array& x, const AccumulationFunction& accumulation_function) {
-   ElementType result{0};
-   for (size_t index: Range(x.size())) {
-      accumulation_function(result, x[index]);
+
+   template <typename Array, typename ElementType = typename Array::value_type>
+   ElementType norm_2_squared(const Array& x) {
+      return generic_norm(x, norm_2_squared_accumulation<ElementType>);
    }
-   return result;
-}
-
-//***********
-// l1 norm //
-//***********
-template <typename ElementType>
-void norm_1_accumulation(ElementType& result, ElementType element) {
-   result += std::abs(element);
-}
-
-template <typename Array, typename ElementType = typename Array::value_type>
-ElementType norm_1(const Array& x) {
-   return generic_norm(x, norm_1_accumulation<ElementType>);
-}
-
-// l1 norm of several arrays
-template <typename Array, typename... Arrays, typename ElementType = typename Array::value_type>
-ElementType norm_1(const Array& x, const Arrays& ... other_arrays) {
-   return norm_1(x) + norm_1(other_arrays...);
-}
-
-//*******************
-// l2 squared norm //
-//*******************
-template <typename ElementType>
-void norm_2_squared_accumulation(ElementType& result, ElementType element) {
-   result += element * element;
-}
-
-template <typename Array, typename ElementType = typename Array::value_type>
-ElementType norm_2_squared(const Array& x) {
-   return generic_norm(x, norm_2_squared_accumulation<ElementType>);
-}
-
-// l2 squared norm of several arrays
-template <typename Array, typename... Arrays, typename ElementType = typename Array::value_type>
-ElementType norm_2_squared(const Array& x, const Arrays& ... other_arrays) {
-   return norm_2_squared(x) + norm_2_squared(other_arrays...);
-}
-
-//***********
-// l2 norm //
-//***********
-template <typename Array, typename ElementType = typename Array::value_type>
-ElementType norm_2(const Array& x) {
-   return std::sqrt(norm_2_squared(x));
-}
-
-// l2 norm of several arrays
-template <typename Array, typename... Arrays>
-typename Array::value_type norm_2(const Array& x, const Arrays& ... other_arrays) {
-   return std::sqrt(norm_2_squared(x) + norm_2_squared(other_arrays...));
-}
-
-//**************
-// l_inf norm //
-//**************
-template <typename ElementType>
-void norm_inf_accumulation(ElementType& result, ElementType element) {
-   result = std::max(result, std::abs(element));
-}
-
-template <typename Array, typename ElementType = typename Array::value_type>
-ElementType norm_inf(const Array& x) {
-   return generic_norm(x, norm_inf_accumulation<ElementType>);
-}
-
-// inf norm of several arrays
-template <typename Array, typename... Arrays, typename ElementType = typename Array::value_type>
-ElementType norm_inf(const Array& x, const Arrays& ... other_arrays) {
-   return std::max(norm_inf(x), norm_inf(other_arrays...));
-}
-
-//*********************
-// dispatch function //
-//*********************
-// norm of at least one array
-template <typename Array, typename... Arrays, typename ElementType = typename Array::value_type>
-ElementType norm(Norm norm, const Array& x, const Arrays& ... other_arrays) {
-   if (norm == Norm::L1) {
-      return norm_1(x, other_arrays...);
+
+   // l2 squared norm of several arrays
+   template <typename Array, typename... Arrays, typename ElementType = typename Array::value_type>
+   ElementType norm_2_squared(const Array& x, const Arrays& ... other_arrays) {
+      return norm_2_squared(x) + norm_2_squared(other_arrays...);
    }
-   else if (norm == Norm::L2) {
-      return norm_2(x, other_arrays...);
+
+   //***********
+   // l2 norm //
+   //***********
+   template <typename Array, typename ElementType = typename Array::value_type>
+   ElementType norm_2(const Array& x) {
+      return std::sqrt(norm_2_squared(x));
    }
-   else if (norm == Norm::L2_SQUARED) {
-      return norm_2_squared(x, other_arrays...);
+
+   // l2 norm of several arrays
+   template <typename Array, typename... Arrays>
+   typename Array::value_type norm_2(const Array& x, const Arrays& ... other_arrays) {
+      return std::sqrt(norm_2_squared(x) + norm_2_squared(other_arrays...));
+   }
+
+   //**************
+   // l_inf norm //
+   //**************
+   template <typename ElementType>
+   void norm_inf_accumulation(ElementType& result, ElementType element) {
+      result = std::max(result, std::abs(element));
+   }
+
+   template <typename Array, typename ElementType = typename Array::value_type>
+   ElementType norm_inf(const Array& x) {
+      return generic_norm(x, norm_inf_accumulation<ElementType>);
+   }
+
+   // inf norm of several arrays
+   template <typename Array, typename... Arrays, typename ElementType = typename Array::value_type>
+   ElementType norm_inf(const Array& x, const Arrays& ... other_arrays) {
+      return std::max(norm_inf(x), norm_inf(other_arrays...));
    }
-   else if (norm == Norm::INF) {
-      return norm_inf(x, other_arrays...);
+
+   //*********************
+   // dispatch function //
+   //*********************
+   // norm of at least one array
+   template <typename Array, typename... Arrays, typename ElementType = typename Array::value_type>
+   ElementType norm(Norm norm, const Array& x, const Arrays& ... other_arrays) {
+      if (norm == Norm::L1) {
+         return norm_1(x, other_arrays...);
+      }
+      else if (norm == Norm::L2) {
+         return norm_2(x, other_arrays...);
+      }
+      else if (norm == Norm::L2_SQUARED) {
+         return norm_2_squared(x, other_arrays...);
+      }
+      else if (norm == Norm::INF) {
+         return norm_inf(x, other_arrays...);
+      }
+      throw std::invalid_argument("The norm is not known");
    }
-   throw std::invalid_argument("The norm is not known");
-}
+} // namespace
 
-#endif // UNO_NORM_H
\ No newline at end of file
+#endif // UNO_NORM_H
diff --git a/uno/linear_algebra/RectangularMatrix.hpp b/uno/linear_algebra/RectangularMatrix.hpp
index 1f62251..789b21c 100644
--- a/uno/linear_algebra/RectangularMatrix.hpp
+++ b/uno/linear_algebra/RectangularMatrix.hpp
@@ -7,37 +7,39 @@
 #include <vector>
 #include "SparseVector.hpp"
 
-// TODO use more appropriate sparse representation
-//template <typename ElementType>
-//using RectangularMatrix = std::vector<SparseVector<ElementType>>;
-
-template <typename ElementType>
-class RectangularMatrix {
-public:
-   using value_type = ElementType;
-
-   RectangularMatrix(size_t number_rows, size_t number_columns): matrix(number_rows) {
-      for (auto& row: this->matrix) {
-         row.reserve(number_columns);
+namespace uno {
+   // TODO use more appropriate sparse representation
+   //template <typename ElementType>
+   //using RectangularMatrix = std::vector<SparseVector<ElementType>>;
+
+   template <typename ElementType>
+   class RectangularMatrix {
+   public:
+      using value_type = ElementType;
+
+      RectangularMatrix(size_t number_rows, size_t number_columns): matrix(number_rows) {
+         for (auto& row: this->matrix) {
+            row.reserve(number_columns);
+         }
       }
-   }
 
-   SparseVector<ElementType>& operator[](size_t row_index) {
-      return this->matrix[row_index];
-   }
+      SparseVector<ElementType>& operator[](size_t row_index) {
+         return this->matrix[row_index];
+      }
 
-   const SparseVector<ElementType>& operator[](size_t row_index) const {
-      return this->matrix[row_index];
-   }
+      const SparseVector<ElementType>& operator[](size_t row_index) const {
+         return this->matrix[row_index];
+      }
 
-   void clear() {
-      for (auto& row: this->matrix) {
-         row.clear();
+      void clear() {
+         for (auto& row: this->matrix) {
+            row.clear();
+         }
       }
-   }
 
-protected:
-   std::vector<SparseVector<ElementType>> matrix;
-};
+   protected:
+      std::vector<SparseVector<ElementType>> matrix;
+   };
+} // namespace
 
-#endif // UNO_RECTANGULARMATRIX_H
\ No newline at end of file
+#endif // UNO_RECTANGULARMATRIX_H
diff --git a/uno/linear_algebra/SparseVector.hpp b/uno/linear_algebra/SparseVector.hpp
index d11bf0a..db0b79a 100644
--- a/uno/linear_algebra/SparseVector.hpp
+++ b/uno/linear_algebra/SparseVector.hpp
@@ -10,142 +10,144 @@
 #include "symbolic/Range.hpp"
 #include "symbolic/Collection.hpp"
 
-// SparseVector is a sparse vector that uses contiguous memory. It contains:
-// - a vector of indices of type size_t
-// - a vector of values of type ElementType
-// the indices are neither unique nor sorted
-template <typename ElementType>
-class SparseVector {
-public:
-   class iterator {
+namespace uno {
+   // SparseVector is a sparse vector that uses contiguous memory. It contains:
+   // - a vector of indices of type size_t
+   // - a vector of values of type ElementType
+   // the indices are neither unique nor sorted
+   template <typename ElementType>
+   class SparseVector {
    public:
-      using value_type = std::pair<size_t, ElementType>;
+      class iterator {
+      public:
+         using value_type = std::pair<size_t, ElementType>;
 
-      iterator(const SparseVector& vector, size_t index): vector(vector), index(index) { }
+         iterator(const SparseVector& vector, size_t index): vector(vector), index(index) { }
 
-      value_type operator*() const {
-         return {this->vector.indices[this->index], this->vector.values[this->index]};
-      }
+         value_type operator*() const {
+            return {this->vector.indices[this->index], this->vector.values[this->index]};
+         }
 
-      iterator& operator++() {
-         this->index++;
-         return *this;
-      }
+         iterator& operator++() {
+            this->index++;
+            return *this;
+         }
 
-      friend bool operator!=(const iterator& a, const iterator& b) {
-         return &a.vector != &b.vector || a.index != b.index;
-      }
+         friend bool operator!=(const iterator& a, const iterator& b) {
+            return &a.vector != &b.vector || a.index != b.index;
+         }
+
+      protected:
+         const SparseVector& vector;
+         size_t index;
+      };
+
+      using value_type = ElementType;
+
+      explicit SparseVector(size_t capacity = 0);
+
+      [[nodiscard]] size_t size() const;
+      void reserve(size_t capacity);
+
+      void insert(size_t index, ElementType value);
+      void transform(const std::function<ElementType(ElementType)>& f);
+      void clear();
+      [[nodiscard]] bool is_empty() const;
+
+      [[nodiscard]] iterator begin() const { return iterator(*this, 0); }
+      [[nodiscard]] iterator end() const { return iterator(*this, this->number_nonzeros); }
+
+      template <typename U>
+      friend std::ostream& operator<<(std::ostream& stream, const SparseVector<U>& x);
 
    protected:
-      const SparseVector& vector;
-      size_t index;
+      std::vector<size_t> indices{};
+      std::vector<ElementType> values{};
+      size_t number_nonzeros{0};
    };
 
-   using value_type = ElementType;
-
-   explicit SparseVector(size_t capacity = 0);
-
-   [[nodiscard]] size_t size() const;
-   void reserve(size_t capacity);
-
-   void insert(size_t index, ElementType value);
-   void transform(const std::function<ElementType(ElementType)>& f);
-   void clear();
-   [[nodiscard]] bool is_empty() const;
-
-   [[nodiscard]] iterator begin() const { return iterator(*this, 0); }
-   [[nodiscard]] iterator end() const { return iterator(*this, this->number_nonzeros); }
-
-   template <typename U>
-   friend std::ostream& operator<<(std::ostream& stream, const SparseVector<U>& x);
-
-protected:
-   std::vector<size_t> indices{};
-   std::vector<ElementType> values{};
-   size_t number_nonzeros{0};
-};
-
-// SparseVector methods
-template <typename ElementType>
-SparseVector<ElementType>::SparseVector(size_t capacity) {
-   this->reserve(capacity);
-}
-
-template <typename ElementType>
-size_t SparseVector<ElementType>::size() const {
-   return this->number_nonzeros;
-}
-
-template <typename ElementType>
-void SparseVector<ElementType>::reserve(size_t capacity) {
-   this->indices.reserve(capacity);
-   this->values.reserve(capacity);
-}
-
-template <typename ElementType>
-void SparseVector<ElementType>::insert(size_t index, ElementType value) {
-   this->indices.push_back(index);
-   this->values.push_back(value);
-   this->number_nonzeros++;
-}
-
-template <typename ElementType>
-void SparseVector<ElementType>::clear() {
-   this->indices.clear();
-   this->values.clear();
-   this->number_nonzeros = 0;
-}
-
-template <typename ElementType>
-bool SparseVector<ElementType>::is_empty() const {
-   return (this->number_nonzeros == 0);
-}
-
-template <typename ElementType>
-void SparseVector<ElementType>::transform(const std::function<ElementType (ElementType)>& f) {
-   for (size_t index: Range(this->number_nonzeros)) {
-      this->values[index] = f(this->values[index]);
+   // SparseVector methods
+   template <typename ElementType>
+   SparseVector<ElementType>::SparseVector(size_t capacity) {
+      this->reserve(capacity);
+   }
+
+   template <typename ElementType>
+   size_t SparseVector<ElementType>::size() const {
+      return this->number_nonzeros;
+   }
+
+   template <typename ElementType>
+   void SparseVector<ElementType>::reserve(size_t capacity) {
+      this->indices.reserve(capacity);
+      this->values.reserve(capacity);
    }
-}
 
-template <typename ElementType>
-std::ostream& operator<<(std::ostream& stream, const SparseVector<ElementType>& x) {
-   stream << "sparse vector with " << x.size() << " nonzeros\n";
-   for (const auto [index, element]: x) {
-      stream << "index " << index << ", value " << element << '\n';
+   template <typename ElementType>
+   void SparseVector<ElementType>::insert(size_t index, ElementType value) {
+      this->indices.push_back(index);
+      this->values.push_back(value);
+      this->number_nonzeros++;
    }
-   return stream;
-}
 
-// free functions
+   template <typename ElementType>
+   void SparseVector<ElementType>::clear() {
+      this->indices.clear();
+      this->values.clear();
+      this->number_nonzeros = 0;
+   }
+
+   template <typename ElementType>
+   bool SparseVector<ElementType>::is_empty() const {
+      return (this->number_nonzeros == 0);
+   }
+
+   template <typename ElementType>
+   void SparseVector<ElementType>::transform(const std::function<ElementType (ElementType)>& f) {
+      for (size_t index: Range(this->number_nonzeros)) {
+         this->values[index] = f(this->values[index]);
+      }
+   }
+
+   template <typename ElementType>
+   std::ostream& operator<<(std::ostream& stream, const SparseVector<ElementType>& x) {
+      stream << "sparse vector with " << x.size() << " nonzeros\n";
+      for (const auto [index, element]: x) {
+         stream << "index " << index << ", value " << element << '\n';
+      }
+      return stream;
+   }
 
-template <typename ElementType>
-ElementType norm_inf(const SparseVector<ElementType>& x) {
-   ElementType norm = ElementType(0);
-   for (const auto [_, element]: x) {
-      norm = std::max(norm, std::abs(element));
+   // free functions
+
+   template <typename ElementType>
+   ElementType norm_inf(const SparseVector<ElementType>& x) {
+      ElementType norm = ElementType(0);
+      for (const auto [_, element]: x) {
+         norm = std::max(norm, std::abs(element));
+      }
+      return norm;
    }
-   return norm;
-}
 
-template <typename Vector, typename ElementType>
-ElementType dot(const Vector& x, const SparseVector<ElementType>& y) {
-   static_assert(std::is_same_v<typename Vector::value_type, ElementType>);
+   template <typename Vector, typename ElementType>
+   ElementType dot(const Vector& x, const SparseVector<ElementType>& y) {
+      static_assert(std::is_same_v<typename Vector::value_type, ElementType>);
+
+      ElementType dot_product = ElementType(0);
+      for (const auto [index, y_element]: y) {
+         assert(index < x.size() && "Vector.dot: the sparse vector y is larger than the dense vector x");
+         dot_product += x[index] * y_element;
+      }
+      return dot_product;
+   }
 
-   ElementType dot_product = ElementType(0);
-   for (const auto [index, y_element]: y) {
-      assert(index < x.size() && "Vector.dot: the sparse vector y is larger than the dense vector x");
-      dot_product += x[index] * y_element;
+   // precondition: factor != 0
+   template <typename ElementType>
+   void scale(SparseVector<ElementType>& x, ElementType factor) {
+      x.transform([=](ElementType element) {
+         return factor * element;
+      });
    }
-   return dot_product;
-}
-
-// precondition: factor != 0
-template <typename ElementType>
-void scale(SparseVector<ElementType>& x, ElementType factor) {
-   x.transform([=](ElementType element) {
-      return factor * element;
-   });
-}
+} // namespace
 
 #endif // UNO_SPARSEVECTOR_H
diff --git a/uno/linear_algebra/SymmetricIndefiniteLinearSystem.hpp b/uno/linear_algebra/SymmetricIndefiniteLinearSystem.hpp
index e49a6f7..0ff6316 100644
--- a/uno/linear_algebra/SymmetricIndefiniteLinearSystem.hpp
+++ b/uno/linear_algebra/SymmetricIndefiniteLinearSystem.hpp
@@ -13,183 +13,187 @@
 #include "tools/Options.hpp"
 #include "tools/Statistics.hpp"
 
-struct UnstableRegularization : public std::exception {
+namespace uno {
+   struct UnstableRegularization : public std::exception {
 
-   [[nodiscard]] const char* what() const noexcept override {
-      return "The inertia correction got unstable (delta_w > threshold)";
-   }
-};
-
-template <typename ElementType>
-class SymmetricIndefiniteLinearSystem {
-public:
-   std::unique_ptr<SymmetricMatrix<ElementType>> matrix;
-   Vector<ElementType> rhs{};
-   Vector<ElementType> solution{};
-
-   SymmetricIndefiniteLinearSystem(const std::string& sparse_format, size_t dimension, size_t number_non_zeros, bool use_regularization,
-         const Options& options);
-   void assemble_matrix(const SymmetricMatrix<double>& hessian, const RectangularMatrix<double>& constraint_jacobian,
-         size_t number_variables, size_t number_constraints);
-   void factorize_matrix(const Model& model, SymmetricIndefiniteLinearSolver<ElementType>& linear_solver);
-   void regularize_matrix(Statistics& statistics, const Model& model, SymmetricIndefiniteLinearSolver<ElementType>& linear_solver, size_t size_primal_block,
-         size_t size_dual_block, ElementType dual_regularization_parameter);
-   void solve(SymmetricIndefiniteLinearSolver<ElementType>& linear_solver);
-   // [[nodiscard]] T get_primal_regularization() const;
-
-protected:
-   size_t number_factorizations{0};
-   ElementType primal_regularization{0.};
-   ElementType dual_regularization{0.};
-   ElementType previous_primal_regularization{0.};
-   const ElementType regularization_failure_threshold;
-   const ElementType primal_regularization_initial_factor;
-   const ElementType dual_regularization_fraction;
-   const ElementType primal_regularization_lb;
-   const ElementType primal_regularization_decrease_factor;
-   const ElementType primal_regularization_fast_increase_factor;
-   const ElementType primal_regularization_slow_increase_factor;
-   const size_t threshold_unsuccessful_attempts;
-};
-
-template <typename ElementType>
-SymmetricIndefiniteLinearSystem<ElementType>::SymmetricIndefiniteLinearSystem(const std::string& sparse_format, size_t dimension,
-      size_t number_non_zeros, bool use_regularization, const Options& options):
-      matrix(SymmetricMatrixFactory<ElementType>::create(sparse_format, dimension, number_non_zeros, use_regularization)),
-      rhs(dimension),
-      solution(dimension),
-      regularization_failure_threshold(ElementType(options.get_double("regularization_failure_threshold"))),
-      primal_regularization_initial_factor(ElementType(options.get_double("primal_regularization_initial_factor"))),
-      dual_regularization_fraction(ElementType(options.get_double("dual_regularization_fraction"))),
-      primal_regularization_lb(ElementType(options.get_double("primal_regularization_lb"))),
-      primal_regularization_decrease_factor(ElementType(options.get_double("primal_regularization_decrease_factor"))),
-      primal_regularization_fast_increase_factor(ElementType(options.get_double("primal_regularization_fast_increase_factor"))),
-      primal_regularization_slow_increase_factor(ElementType(options.get_double("primal_regularization_slow_increase_factor"))),
-      threshold_unsuccessful_attempts(options.get_unsigned_int("threshold_unsuccessful_attempts")) {
-}
-
-template <typename ElementType>
-void SymmetricIndefiniteLinearSystem<ElementType>::assemble_matrix(const SymmetricMatrix<double>& hessian, const RectangularMatrix<double>& constraint_jacobian,
-      size_t number_variables, size_t number_constraints) {
-   this->matrix->dimension = number_variables + number_constraints;
-   this->matrix->reset();
-   // copy the Lagrangian Hessian in the top left block
-   //size_t current_column = 0;
-   for (const auto [row_index, column_index, element]: hessian) {
-      // finalize all empty columns
-      /*for (size_t column: Range(current_column, column_index)) {
-         this->matrix->finalize_column(column);
-         current_column++;
-      }*/
-      this->matrix->insert(element, row_index, column_index);
+      [[nodiscard]] const char* what() const noexcept override {
+         return "The inertia correction got unstable (delta_w > threshold)";
+      }
+   };
+
+   template <typename ElementType>
+   class SymmetricIndefiniteLinearSystem {
+   public:
+      std::unique_ptr<SymmetricMatrix<size_t, ElementType>> matrix;
+      Vector<ElementType> rhs{};
+      Vector<ElementType> solution{};
+
+      SymmetricIndefiniteLinearSystem(const std::string& sparse_format, size_t dimension, size_t number_non_zeros, bool use_regularization,
+            const Options& options);
+      void assemble_matrix(const SymmetricMatrix<size_t, double>& hessian, const RectangularMatrix<double>& constraint_jacobian,
+            size_t number_variables, size_t number_constraints);
+      void factorize_matrix(const Model& model, SymmetricIndefiniteLinearSolver<size_t, ElementType>& linear_solver);
+      void regularize_matrix(Statistics& statistics, const Model& model, SymmetricIndefiniteLinearSolver<size_t, ElementType>& linear_solver,
+            size_t size_primal_block, size_t size_dual_block, ElementType dual_regularization_parameter);
+      void solve(SymmetricIndefiniteLinearSolver<size_t, ElementType>& linear_solver);
+      // [[nodiscard]] T get_primal_regularization() const;
+
+   protected:
+      size_t number_factorizations{0};
+      ElementType primal_regularization{0.};
+      ElementType dual_regularization{0.};
+      ElementType previous_primal_regularization{0.};
+      const ElementType regularization_failure_threshold;
+      const ElementType primal_regularization_initial_factor;
+      const ElementType dual_regularization_fraction;
+      const ElementType primal_regularization_lb;
+      const ElementType primal_regularization_decrease_factor;
+      const ElementType primal_regularization_fast_increase_factor;
+      const ElementType primal_regularization_slow_increase_factor;
+      const size_t threshold_unsuccessful_attempts;
+   };
+
+   template <typename ElementType>
+   SymmetricIndefiniteLinearSystem<ElementType>::SymmetricIndefiniteLinearSystem(const std::string& sparse_format, size_t dimension,
+         size_t number_non_zeros, bool use_regularization, const Options& options):
+         matrix(SymmetricMatrixFactory<size_t, ElementType>::create(sparse_format, dimension, number_non_zeros, use_regularization)),
+         rhs(dimension),
+         solution(dimension),
+         regularization_failure_threshold(ElementType(options.get_double("regularization_failure_threshold"))),
+         primal_regularization_initial_factor(ElementType(options.get_double("primal_regularization_initial_factor"))),
+         dual_regularization_fraction(ElementType(options.get_double("dual_regularization_fraction"))),
+         primal_regularization_lb(ElementType(options.get_double("primal_regularization_lb"))),
+         primal_regularization_decrease_factor(ElementType(options.get_double("primal_regularization_decrease_factor"))),
+         primal_regularization_fast_increase_factor(ElementType(options.get_double("primal_regularization_fast_increase_factor"))),
+         primal_regularization_slow_increase_factor(ElementType(options.get_double("primal_regularization_slow_increase_factor"))),
+         threshold_unsuccessful_attempts(options.get_unsigned_int("threshold_unsuccessful_attempts")) {
    }
 
-   // Jacobian of general constraints
-   for (size_t column_index: Range(number_constraints)) {
-      for (const auto [row_index, derivative]: constraint_jacobian[column_index]) {
-         this->matrix->insert(derivative, row_index, number_variables + column_index);
+   template <typename ElementType>
+   void SymmetricIndefiniteLinearSystem<ElementType>::assemble_matrix(const SymmetricMatrix<size_t, double>& hessian,
+         const RectangularMatrix<double>& constraint_jacobian, size_t number_variables, size_t number_constraints) {
+      this->matrix->dimension = number_variables + number_constraints;
+      this->matrix->reset();
+      // copy the Lagrangian Hessian in the top left block
+      //size_t current_column = 0;
+      for (const auto[row_index, column_index, element]: hessian) {
+         // finalize all empty columns
+         /*for (size_t column: Range(current_column, column_index)) {
+            this->matrix->finalize_column(column);
+            current_column++;
+         }*/
+         this->matrix->insert(element, row_index, column_index);
+      }
+
+      // Jacobian of general constraints
+      for (size_t column_index: Range(number_constraints)) {
+         for (const auto[row_index, derivative]: constraint_jacobian[column_index]) {
+            this->matrix->insert(derivative, row_index, number_variables + column_index);
+         }
+         this->matrix->finalize_column(column_index);
       }
-      this->matrix->finalize_column(column_index);
-   }
-}
-
-template <typename ElementType>
-void SymmetricIndefiniteLinearSystem<ElementType>::factorize_matrix(const Model& model, SymmetricIndefiniteLinearSolver<ElementType>& linear_solver) {
-   // compute the symbolic factorization only when:
-   // the problem has a non-constant augmented system (ie is not an LP or a QP) or it is the first factorization
-   if (true || this->number_factorizations == 0 || not model.fixed_hessian_sparsity) {
-      linear_solver.do_symbolic_factorization(*this->matrix);
-   }
-   linear_solver.do_numerical_factorization(*this->matrix);
-   this->number_factorizations++;
-}
-
-template <typename ElementType>
-void SymmetricIndefiniteLinearSystem<ElementType>::regularize_matrix(Statistics& statistics, const Model& model,
-      SymmetricIndefiniteLinearSolver<ElementType>& linear_solver, size_t size_primal_block, size_t size_dual_block,
-      ElementType dual_regularization_parameter) {
-   DEBUG2 << "Original matrix\n" << *this->matrix << '\n';
-   this->primal_regularization = ElementType(0.);
-   this->dual_regularization = ElementType(0.);
-   size_t number_attempts = 1;
-   DEBUG << "Testing factorization with regularization factors (" << this->primal_regularization << ", " << this->dual_regularization << ")\n";
-
-   if (not linear_solver.matrix_is_singular() && linear_solver.number_negative_eigenvalues() == size_dual_block) {
-      DEBUG << "Inertia is good\n";
-      statistics.set("regularization", this->primal_regularization);
-      return;
-   }
-   auto[number_pos_eigenvalues, number_neg_eigenvalues, number_zero_eigenvalues] = linear_solver.get_inertia();
-   DEBUG << "Expected inertia (" << size_primal_block << ", " << size_dual_block << ", 0), ";
-   DEBUG << "got (" << number_pos_eigenvalues << ", " << number_neg_eigenvalues << ", " << number_zero_eigenvalues << ")\n";
-   DEBUG << "Number of attempts: " << number_attempts << "\n\n";
-
-   // set the constraint regularization coefficient
-   if (linear_solver.matrix_is_singular()) {
-      DEBUG << "Matrix is singular\n";
-      this->dual_regularization = this->dual_regularization_fraction * dual_regularization_parameter;
-   }
-   // set the Hessian regularization coefficient
-   if (this->previous_primal_regularization == 0.) {
-      this->primal_regularization = this->primal_regularization_initial_factor;
-   }
-   else {
-      this->primal_regularization = std::max(this->primal_regularization_lb, this->previous_primal_regularization / this->primal_regularization_decrease_factor);
    }
 
-   // regularize the augmented matrix
-   this->matrix->set_regularization([=](size_t row_index) {
-      return (row_index < size_primal_block) ? this->primal_regularization : -this->dual_regularization;
-   });
+   template <typename ElementType>
+   void SymmetricIndefiniteLinearSystem<ElementType>::factorize_matrix(const Model& model,
+         SymmetricIndefiniteLinearSolver<size_t, ElementType>& linear_solver) {
+      // compute the symbolic factorization only when:
+      // the problem has a non-constant augmented system (ie is not an LP or a QP) or it is the first factorization
+      if (true || this->number_factorizations == 0 || not model.fixed_hessian_sparsity) {
+         linear_solver.do_symbolic_factorization(*this->matrix);
+      }
+      linear_solver.do_numerical_factorization(*this->matrix);
+      this->number_factorizations++;
+   }
 
-   bool good_inertia = false;
-   while (not good_inertia) {
+   template <typename ElementType>
+   void SymmetricIndefiniteLinearSystem<ElementType>::regularize_matrix(Statistics& statistics, const Model& model,
+         SymmetricIndefiniteLinearSolver<size_t, ElementType>& linear_solver, size_t size_primal_block, size_t size_dual_block,
+         ElementType dual_regularization_parameter) {
+      DEBUG2 << "Original matrix\n" << *this->matrix << '\n';
+      this->primal_regularization = ElementType(0.);
+      this->dual_regularization = ElementType(0.);
+      size_t number_attempts = 1;
       DEBUG << "Testing factorization with regularization factors (" << this->primal_regularization << ", " << this->dual_regularization << ")\n";
-      DEBUG2 << *this->matrix << '\n';
-      this->factorize_matrix(model, linear_solver);
-      number_attempts++;
 
       if (not linear_solver.matrix_is_singular() && linear_solver.number_negative_eigenvalues() == size_dual_block) {
-         good_inertia = true;
-         DEBUG << "Factorization was a success\n";
-         this->previous_primal_regularization = this->primal_regularization;
+         DEBUG << "Inertia is good\n";
+         statistics.set("regularization", this->primal_regularization);
+         return;
+      }
+      auto[number_pos_eigenvalues, number_neg_eigenvalues, number_zero_eigenvalues] = linear_solver.get_inertia();
+      DEBUG << "Expected inertia (" << size_primal_block << ", " << size_dual_block << ", 0), ";
+      DEBUG << "got (" << number_pos_eigenvalues << ", " << number_neg_eigenvalues << ", " << number_zero_eigenvalues << ")\n";
+      DEBUG << "Number of attempts: " << number_attempts << "\n\n";
+
+      // set the constraint regularization coefficient
+      if (linear_solver.matrix_is_singular()) {
+         DEBUG << "Matrix is singular\n";
+         this->dual_regularization = this->dual_regularization_fraction * dual_regularization_parameter;
+      }
+      // set the Hessian regularization coefficient
+      if (this->previous_primal_regularization == 0.) {
+         this->primal_regularization = this->primal_regularization_initial_factor;
       }
       else {
-         auto[number_pos_eigenvalues, number_neg_eigenvalues, number_zero_eigenvalues] = linear_solver.get_inertia();
-         DEBUG << "Expected inertia (" << size_primal_block << ", " << size_dual_block << ", 0), ";
-         DEBUG << "got (" << number_pos_eigenvalues << ", " << number_neg_eigenvalues << ", " << number_zero_eigenvalues << ")\n";
-         DEBUG << "Number of attempts: " << number_attempts << "\n";
-         if (this->previous_primal_regularization == 0. || this->threshold_unsuccessful_attempts < number_attempts) {
-            this->primal_regularization *= this->primal_regularization_fast_increase_factor;
-         }
-         else {
-            this->primal_regularization *= this->primal_regularization_slow_increase_factor;
-         }
+         this->primal_regularization = std::max(this->primal_regularization_lb,
+               this->previous_primal_regularization / this->primal_regularization_decrease_factor);
+      }
 
-         if (this->primal_regularization <= this->regularization_failure_threshold) {
-            // regularize the augmented matrix
-            this->matrix->set_regularization([=](size_t row_index) {
-               return (row_index < size_primal_block) ? this->primal_regularization : -this->dual_regularization;
-            });
+      // regularize the augmented matrix
+      this->matrix->set_regularization([=](size_t row_index) {
+         return (row_index < size_primal_block) ? this->primal_regularization : -this->dual_regularization;
+      });
+
+      bool good_inertia = false;
+      while (not good_inertia) {
+         DEBUG << "Testing factorization with regularization factors (" << this->primal_regularization << ", " << this->dual_regularization << ")\n";
+         DEBUG2 << *this->matrix << '\n';
+         this->factorize_matrix(model, linear_solver);
+         number_attempts++;
+
+         if (not linear_solver.matrix_is_singular() && linear_solver.number_negative_eigenvalues() == size_dual_block) {
+            good_inertia = true;
+            DEBUG << "Factorization was a success\n";
+            this->previous_primal_regularization = this->primal_regularization;
          }
          else {
-            throw UnstableRegularization();
+            auto[number_pos_eigenvalues, number_neg_eigenvalues, number_zero_eigenvalues] = linear_solver.get_inertia();
+            DEBUG << "Expected inertia (" << size_primal_block << ", " << size_dual_block << ", 0), ";
+            DEBUG << "got (" << number_pos_eigenvalues << ", " << number_neg_eigenvalues << ", " << number_zero_eigenvalues << ")\n";
+            DEBUG << "Number of attempts: " << number_attempts << "\n";
+            if (this->previous_primal_regularization == 0. || this->threshold_unsuccessful_attempts < number_attempts) {
+               this->primal_regularization *= this->primal_regularization_fast_increase_factor;
+            }
+            else {
+               this->primal_regularization *= this->primal_regularization_slow_increase_factor;
+            }
+
+            if (this->primal_regularization <= this->regularization_failure_threshold) {
+               // regularize the augmented matrix
+               this->matrix->set_regularization([=](size_t row_index) {
+                  return (row_index < size_primal_block) ? this->primal_regularization : -this->dual_regularization;
+               });
+            }
+            else {
+               throw UnstableRegularization();
+            }
          }
       }
+      statistics.set("regularization", this->primal_regularization);
+   }
+
+   template <typename ElementType>
+   void SymmetricIndefiniteLinearSystem<ElementType>::solve(SymmetricIndefiniteLinearSolver<size_t, ElementType>& linear_solver) {
+      linear_solver.solve_indefinite_system(*this->matrix, this->rhs, this->solution);
    }
-   statistics.set("regularization", this->primal_regularization);
-}
-
-template <typename ElementType>
-void SymmetricIndefiniteLinearSystem<ElementType>::solve(SymmetricIndefiniteLinearSolver<ElementType>& linear_solver) {
-   linear_solver.solve_indefinite_system(*this->matrix, this->rhs, this->solution);
-}
-
-/*
-template <typename ElementType>
-ElementType SymmetricIndefiniteLinearSystem<ElementType>::get_primal_regularization() const {
-   return this->primal_regularization;
-}
-*/
-
-#endif // UNO_SYMMETRICINDEFINITELINEARSYSTEM_H
+
+   /*
+   template <typename ElementType>
+   ElementType SymmetricIndefiniteLinearSystem<ElementType>::get_primal_regularization() const {
+      return this->primal_regularization;
+   }
+   */
+} // namespace
+
+#endif // UNO_SYMMETRICINDEFINITELINEARSYSTEM_H
\ No newline at end of file
diff --git a/uno/linear_algebra/SymmetricMatrix.hpp b/uno/linear_algebra/SymmetricMatrix.hpp
index 53c2e54..9a0ad91 100644
--- a/uno/linear_algebra/SymmetricMatrix.hpp
+++ b/uno/linear_algebra/SymmetricMatrix.hpp
@@ -9,122 +9,145 @@
 #include <cassert>
 #include "Vector.hpp"
 #include "SparseVector.hpp"
+#include "tools/Infinity.hpp"
 
-// abstract class
-template <typename ElementType>
-class SymmetricMatrix {
-public:
-   class iterator {
+namespace uno {
+   // abstract class
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrix {
    public:
-      iterator(const SymmetricMatrix<ElementType>& matrix, size_t column_index, size_t nonzero_index):
-         matrix(matrix), column_index(column_index), nonzero_index(nonzero_index) { }
-
-      [[nodiscard]] std::tuple<size_t, size_t, ElementType> operator*() const {
-         return this->matrix.dereference_iterator(this->column_index, this->nonzero_index);
+      class iterator {
+      public:
+         iterator(const SymmetricMatrix<IndexType, ElementType>& matrix, size_t column_index, size_t nonzero_index) :
+               matrix(matrix), column_index(column_index), nonzero_index(nonzero_index) {
+         }
+
+         [[nodiscard]] std::tuple<IndexType, IndexType, ElementType> operator*() const {
+            return this->matrix.dereference_iterator(this->column_index, this->nonzero_index);
+         }
+
+         iterator& operator++() {
+            this->matrix.increment_iterator(this->column_index, this->nonzero_index);
+            return *this;
+         }
+
+         friend bool operator!=(const iterator& a, const iterator& b) {
+            return &a.matrix != &b.matrix || a.column_index != b.column_index || a.nonzero_index != b.nonzero_index;
+         }
+
+      protected:
+         const SymmetricMatrix<IndexType, ElementType>& matrix;
+         size_t column_index;
+         size_t nonzero_index;
+      };
+
+      using value_type = ElementType;
+
+      size_t dimension;
+      size_t number_nonzeros{0};
+      size_t capacity;
+
+      SymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization);
+      virtual ~SymmetricMatrix() = default;
+
+      virtual void reset();
+      template <typename Vector1, typename Vector2>
+      ElementType quadratic_product(const Vector1& x, const Vector2& y) const;
+
+      // build the matrix incrementally
+      virtual void insert(ElementType term, IndexType row_index, IndexType column_index) = 0;
+      // this method will be used by the CSCSymmetricMatrix subclass
+      virtual void finalize_column(IndexType column_index) = 0;
+      [[nodiscard]] ElementType smallest_diagonal_entry(size_t max_dimension) const;
+      virtual void set_regularization(const std::function<ElementType(size_t /*index*/)>& regularization_function) = 0;
+
+      [[nodiscard]] iterator begin() const {
+         return iterator(*this, 0, 0);
       }
-
-      iterator& operator++() {
-         this->matrix.increment_iterator(this->column_index, this->nonzero_index);
-         return *this;
+      [[nodiscard]] iterator end() const {
+         return iterator(*this, this->dimension, this->number_nonzeros);
       }
 
-      friend bool operator!=(const iterator& a, const iterator& b) {
-         return &a.matrix != &b.matrix || a.column_index != b.column_index || a.nonzero_index != b.nonzero_index;
+      [[nodiscard]] const ElementType* data_pointer() const noexcept {
+         return this->entries.data();
       }
+      [[nodiscard]] ElementType* data_pointer() noexcept {
+         return this->entries.data();
+      }
+
+      virtual void print(std::ostream& stream) const = 0;
+      template <typename Index, typename Element>
+      friend std::ostream& operator<<(std::ostream& stream, const SymmetricMatrix<Index, Element>& matrix);
 
    protected:
-      const SymmetricMatrix<ElementType>& matrix;
-      size_t column_index;
-      size_t nonzero_index;
+      std::vector<ElementType> entries{};
+      // regularization
+      const bool use_regularization;
+
+      // virtual iterator functions
+      [[nodiscard]] virtual std::tuple<IndexType, IndexType, ElementType> dereference_iterator(size_t column_index, size_t nonzero_index) const = 0;
+      virtual void increment_iterator(size_t& column_index, size_t& nonzero_index) const = 0;
    };
 
-   using value_type = ElementType;
+   // implementation
 
-   size_t dimension;
-   size_t number_nonzeros{0};
-   size_t capacity;
+   template <typename IndexType, typename ElementType>
+   SymmetricMatrix<IndexType, ElementType>::SymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization) :
+         dimension(dimension),
+         // if regularization is used, allocate the necessary space
+         capacity(original_capacity + (use_regularization ? dimension : 0)),
+         use_regularization(use_regularization) {
+      this->entries.reserve(this->capacity);
+   }
 
-   SymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization);
-   virtual ~SymmetricMatrix() = default;
+   template <typename IndexType, typename ElementType>
+   void SymmetricMatrix<IndexType, ElementType>::reset() {
+      this->number_nonzeros = 0;
+      this->entries.clear();
+   }
 
-   virtual void reset();
-   template <typename Vector1, typename Vector2>
-   ElementType quadratic_product(const Vector1& x, const Vector2& y) const;
-
-   // build the matrix incrementally
-   virtual void insert(ElementType term, size_t row_index, size_t column_index) = 0;
-   // this method will be used by the CSCSymmetricMatrix subclass
-   virtual void finalize_column(size_t column_index) = 0;
-   [[nodiscard]] virtual ElementType smallest_diagonal_entry() const = 0;
-   virtual void set_regularization(const std::function<ElementType(size_t /*index*/)>& regularization_function) = 0;
-
-   [[nodiscard]] iterator begin() const { return iterator(*this, 0, 0); }
-   [[nodiscard]] iterator end() const { return iterator(*this, this->dimension, this->number_nonzeros); }
-
-   [[nodiscard]] const ElementType* data_raw_pointer() const;
-
-   virtual void print(std::ostream& stream) const = 0;
-   template <typename U>
-   friend std::ostream& operator<<(std::ostream& stream, const SymmetricMatrix<U>& matrix);
-
-protected:
-   std::vector<ElementType> entries{};
-   // regularization
-   const bool use_regularization;
-
-   // virtual iterator functions
-   [[nodiscard]] virtual std::tuple<size_t, size_t, ElementType> dereference_iterator(size_t column_index, size_t nonzero_index) const = 0;
-   virtual void increment_iterator(size_t& column_index, size_t& nonzero_index) const = 0;
-};
-
-// implementation
-
-template <typename ElementType>
-SymmetricMatrix<ElementType>::SymmetricMatrix(size_t dimension, size_t original_capacity, bool use_regularization) :
-      dimension(dimension),
-      // if regularization is used, allocate the necessary space
-      capacity(original_capacity + (use_regularization ? dimension : 0)),
-      use_regularization(use_regularization) {
-   this->entries.reserve(this->capacity);
-}
-
-template <typename ElementType>
-void SymmetricMatrix<ElementType>::reset() {
-   this->number_nonzeros = 0;
-   this->entries.clear();
-}
-
-template <typename ElementType>
-template <typename Vector1, typename Vector2>
-ElementType SymmetricMatrix<ElementType>::quadratic_product(const Vector1& x, const Vector2& y) const {
-   static_assert(std::is_same_v<typename Vector1::value_type, ElementType>);
-   static_assert(std::is_same_v<typename Vector2::value_type, ElementType>);
-   assert(x.size() == y.size() && "SymmetricMatrix::quadratic_product: the two vectors x and y do not have the same size");
-
-   ElementType result = ElementType(0);
-   for (const auto [row_index, column_index, element]: *this) {
-      if (row_index == column_index) {
-         // diagonal term
-         result += element * x[row_index] * y[row_index];
+   template <typename IndexType, typename ElementType>
+   // TODO fix. We need to scan through all the columns
+   ElementType SymmetricMatrix<IndexType, ElementType>::smallest_diagonal_entry(size_t max_dimension) const {
+      ElementType smallest_entry = INF<ElementType>;
+      for (const auto[row_index, column_index, element]: *this) {
+         if (row_index == column_index && row_index < max_dimension) {
+            smallest_entry = std::min(smallest_entry, element);
+         }
       }
-      else {
-         // off-diagonal term
-         result += element * (x[row_index] * y[column_index] + x[column_index] * y[row_index]);
+      if (smallest_entry == INF<ElementType>) {
+         smallest_entry = ElementType(0);
       }
+      return smallest_entry;
    }
-   return result;
-}
-
-template <typename ElementType>
-const ElementType* SymmetricMatrix<ElementType>::data_raw_pointer() const {
-   return this->entries.data();
-}
-
-template <typename ElementType>
-std::ostream& operator<<(std::ostream& stream, const SymmetricMatrix<ElementType>& matrix) {
-   stream << "Dimension: " << matrix.dimension << ", number of nonzeros: " << matrix.number_nonzeros << '\n';
-   matrix.print(stream);
-   return stream;
-}
-
-#endif // UNO_SYMMETRICMATRIX_H
+
+   template <typename IndexType, typename ElementType>
+   template <typename Vector1, typename Vector2>
+   ElementType SymmetricMatrix<IndexType, ElementType>::quadratic_product(const Vector1& x, const Vector2& y) const {
+      static_assert(std::is_same_v<typename Vector1::value_type, ElementType>);
+      static_assert(std::is_same_v<typename Vector2::value_type, ElementType>);
+      assert(x.size() == y.size() && "SymmetricMatrix::quadratic_product: the two vectors x and y do not have the same size");
+
+      ElementType result = ElementType(0);
+      for (const auto[row_index, column_index, element]: *this) {
+         if (row_index == column_index) {
+            // diagonal term
+            result += element * x[row_index] * y[row_index];
+         }
+         else {
+            // off-diagonal term
+            result += element * (x[row_index] * y[column_index] + x[column_index] * y[row_index]);
+         }
+      }
+      return result;
+   }
+
+   template <typename Index, typename Element>
+   std::ostream& operator<<(std::ostream& stream, const SymmetricMatrix<Index, Element>& matrix) {
+      stream << "Dimension: " << matrix.dimension << ", number of nonzeros: " << matrix.number_nonzeros << '\n';
+      matrix.print(stream);
+      return stream;
+   }
+} // namespace
+
+#endif // UNO_SYMMETRICMATRIX_H
\ No newline at end of file
diff --git a/uno/linear_algebra/SymmetricMatrixFactory.hpp b/uno/linear_algebra/SymmetricMatrixFactory.hpp
index 1adec2d..5db763b 100644
--- a/uno/linear_algebra/SymmetricMatrixFactory.hpp
+++ b/uno/linear_algebra/SymmetricMatrixFactory.hpp
@@ -8,23 +8,25 @@
 #include "COOSymmetricMatrix.hpp"
 #include "CSCSymmetricMatrix.hpp"
 
-template <typename ElementType>
-class SymmetricMatrixFactory {
-public:
-   static std::unique_ptr<SymmetricMatrix<ElementType>> create(const std::string& symmetric_matrix_type, size_t dimension, size_t capacity,
-         bool use_regularization);
-};
+namespace uno {
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrixFactory {
+   public:
+      static std::unique_ptr<SymmetricMatrix<IndexType, ElementType>> create(const std::string& symmetric_matrix_type, size_t dimension,
+            size_t capacity, bool use_regularization);
+   };
 
-template <typename ElementType>
-std::unique_ptr<SymmetricMatrix<ElementType>> SymmetricMatrixFactory<ElementType>::create(const std::string& symmetric_matrix_type, size_t dimension,
-      size_t capacity, bool use_regularization) {
-   if (symmetric_matrix_type == "COO") {
-      return std::make_unique<COOSymmetricMatrix<ElementType>>(dimension, capacity, use_regularization);
+   template <typename IndexType, typename ElementType>
+   std::unique_ptr<SymmetricMatrix<IndexType, ElementType>> SymmetricMatrixFactory<IndexType, ElementType>::create(const std::string& symmetric_matrix_type,
+         size_t dimension, size_t capacity, bool use_regularization) {
+      if (symmetric_matrix_type == "COO") {
+         return std::make_unique<COOSymmetricMatrix<IndexType, ElementType>>(dimension, capacity, use_regularization);
+      }
+      else if (symmetric_matrix_type == "CSC") {
+         return std::make_unique<CSCSymmetricMatrix<IndexType, ElementType>>(dimension, capacity, use_regularization);
+      }
+      throw std::invalid_argument("Symmetric matrix type " + symmetric_matrix_type + " unknown");
    }
-   else if (symmetric_matrix_type == "CSC") {
-      return std::make_unique<CSCSymmetricMatrix<ElementType>>(dimension, capacity, use_regularization);
-   }
-   throw std::invalid_argument("Symmetric matrix type " + symmetric_matrix_type + " unknown");
-}
+} // namespace
 
-#endif // UNO_SYMMETRICMATRIXFACTORY_H
+#endif // UNO_SYMMETRICMATRIXFACTORY_H
\ No newline at end of file
diff --git a/uno/linear_algebra/Vector.hpp b/uno/linear_algebra/Vector.hpp
index a7595ff..79b8c36 100644
--- a/uno/linear_algebra/Vector.hpp
+++ b/uno/linear_algebra/Vector.hpp
@@ -10,108 +10,120 @@
 #include <initializer_list>
 #include "symbolic/Range.hpp"
 
-template <typename ElementType>
-class Vector {
-public:
-   using value_type = ElementType;
-   // iterators
-   using iterator = typename std::vector<ElementType>::iterator;
-   using const_iterator = typename std::vector<ElementType>::const_iterator;
-
-   // constructors and destructor
-   explicit Vector(size_t capacity = 0): vector(capacity) { }
-   explicit Vector(size_t capacity, ElementType value): vector(capacity, value) { }
-   Vector(std::initializer_list<ElementType> initializer_list): vector(initializer_list) { }
-   ~Vector() = default;
-
-   // copy assignment operator
-   template <typename Expression>
-   Vector& operator=(const Expression& expression) {
-      static_assert(std::is_same_v<typename Expression::value_type, ElementType>);
-      for (size_t index = 0; index < this->size(); index++) {
-         this->vector[index] = expression[index];
+namespace uno {
+   template <typename ElementType>
+   class Vector {
+   public:
+      using value_type = ElementType;
+      // iterators
+      using iterator = typename std::vector<ElementType>::iterator;
+      using const_iterator = typename std::vector<ElementType>::const_iterator;
+
+      // constructors and destructor
+      explicit Vector(size_t capacity = 0): vector(capacity) { }
+      explicit Vector(size_t capacity, ElementType value): vector(capacity, value) { }
+      Vector(std::initializer_list<ElementType> initializer_list): vector(initializer_list) { }
+      Vector(const Vector<ElementType>& other) noexcept : vector(other.vector) { }
+      Vector(Vector<ElementType>&& other) noexcept : vector(std::move(other.vector)) { }
+      ~Vector() = default;
+
+      // copy assignment operator
+      Vector<ElementType>& operator=(const Vector<ElementType>& other) {
+         if (&other != this) {
+            this->vector = other.vector;
+         }
+         return *this;
       }
-      return *this;
-   }
 
-   // move assignment operator
-   Vector& operator=(std::vector<ElementType>&& other) {
-      if (&other != this) {
-         this->vector = std::move(other.vector);
+      // move assignment operator
+      Vector<ElementType>& operator=(Vector<ElementType>&& other) noexcept {
+         if (&other != this) {
+            this->vector = std::move(other.vector);
+         }
+         return *this;
       }
-      return *this;
-   }
-
-   // random access
-   ElementType& operator[](size_t index) { return this->vector[index]; }
-   const ElementType& operator[](size_t index) const { return this->vector[index]; }
 
-   // size
-   [[nodiscard]] size_t size() const { return this->vector.size(); }
-   [[nodiscard]] bool empty() const { return (this->size() == 0); }
-   void resize(size_t new_size) { this->vector.resize(new_size); }
+      // assignment operator from some expression
+      template <typename Expression>
+      Vector<ElementType>& operator=(const Expression& expression) {
+         static_assert(std::is_same_v<typename Expression::value_type, ElementType>);
+         for (size_t index = 0; index < this->size(); index++) {
+            this->vector[index] = expression[index];
+         }
+         return *this;
+      }
 
-   // iterators
-   iterator begin() noexcept { return this->vector.begin(); }
-   iterator end() noexcept { return this->vector.end(); }
-   const_iterator begin() const noexcept { return this->vector.cbegin(); }
-   const_iterator end() const noexcept { return this->vector.cend(); }
+      // sum operator
+      template <typename Expression>
+      Vector<ElementType>& operator+=(const Expression& expression) {
+         for (size_t index = 0; index < this->size(); index++) {
+            this->vector[index] += expression[index];
+         }
+         return *this;
+      }
 
-   void fill(ElementType value) {
-      for (size_t index = 0; index < this->size(); index++) {
-         this->vector[index] = value;
+      // random access
+      ElementType& operator[](size_t index) { return this->vector[index]; }
+      const ElementType& operator[](size_t index) const { return this->vector[index]; }
+
+      // size
+      [[nodiscard]] size_t size() const { return this->vector.size(); }
+      [[nodiscard]] bool empty() const { return (this->size() == 0); }
+      void resize(size_t new_size) { this->vector.resize(new_size); }
+
+      // iterators
+      iterator begin() noexcept { return this->vector.begin(); }
+      iterator end() noexcept { return this->vector.end(); }
+      const_iterator begin() const noexcept { return this->vector.cbegin(); }
+      const_iterator end() const noexcept { return this->vector.cend(); }
+
+      void fill(ElementType value) {
+         for (size_t index = 0; index < this->size(); index++) {
+            this->vector[index] = value;
+         }
       }
-   }
 
-   void scale(ElementType factor) {
-      for (size_t index = 0; index < this->size(); index++) {
-         this->vector[index] *= factor;
+      void scale(ElementType factor) {
+         for (size_t index = 0; index < this->size(); index++) {
+            this->vector[index] *= factor;
+         }
       }
-   }
 
-   ElementType* data() { return this->vector.data(); }
-   const ElementType* data() const { return this->vector.data(); }
+      ElementType* data() { return this->vector.data(); }
+      const ElementType* data() const { return this->vector.data(); }
 
-   // sum operator
-   template <typename Expression>
-   Vector& operator+=(const Expression& expression) {
-      for (size_t index = 0; index < this->size(); index++) {
-         this->vector[index] += expression[index];
+      void print(std::ostream& stream) const {
+         for (const ElementType& element: *this) {
+            stream << element << ' ';
+         }
       }
-      return *this;
-   }
 
-   void print(std::ostream& stream) const {
-      for (const ElementType& element: *this) {
-         stream << element << ' ';
+   protected:
+      std::vector<ElementType> vector;
+   };
+
+   // use && to allow temporaries (such as std::cout or logger DEBUG, WARNING, etc)
+   template <typename Array, typename Stream>
+   void print_vector(Stream&& stream, const Array& x) {
+      for (size_t index: Range(x.size())) {
+         stream << x[index] << " ";
       }
+      stream << '\n';
    }
 
-protected:
-   std::vector<ElementType> vector;
-};
-
-// use && to allow temporaries (such as std::cout or logger DEBUG, WARNING, etc)
-template <typename Array, typename Stream>
-void print_vector(Stream&& stream, const Array& x) {
-   for (size_t index: Range(x.size())) {
-      stream << x[index] << " ";
+   template <typename ElementType>
+   std::ostream& operator<<(std::ostream& stream, const Vector<ElementType>& vector) {
+      vector.print(stream);
+      return stream;
    }
-   stream << '\n';
-}
-
-template <typename ElementType>
-std::ostream& operator<<(std::ostream& stream, const Vector<ElementType>& vector) {
-   vector.print(stream);
-   return stream;
-}
-
-// subtract operator
-template <typename ResultExpression, typename Expression>
-void operator-=(ResultExpression&& result, const Expression& expression) {
-   for (size_t index = 0; index < result.size(); index++) {
-      result[index] -= expression[index];
+
+   // subtract operator
+   template <typename ResultExpression, typename Expression>
+   void operator-=(ResultExpression&& result, const Expression& expression) {
+      for (size_t index = 0; index < result.size(); index++) {
+         result[index] -= expression[index];
+      }
    }
-}
+} // namespace
 
 #endif // UNO_VECTOR_H
diff --git a/uno/model/BoundRelaxedModel.hpp b/uno/model/BoundRelaxedModel.hpp
index 13e0436..8118602 100644
--- a/uno/model/BoundRelaxedModel.hpp
+++ b/uno/model/BoundRelaxedModel.hpp
@@ -8,75 +8,77 @@
 #include "optimization/Iterate.hpp"
 #include "tools/Options.hpp"
 
-class BoundRelaxedModel: public Model {
-public:
-   BoundRelaxedModel(std::unique_ptr<Model> original_model, const Options& options);
+namespace uno {
+   class BoundRelaxedModel: public Model {
+   public:
+      BoundRelaxedModel(std::unique_ptr<Model> original_model, const Options& options);
 
-   [[nodiscard]] double evaluate_objective(const Vector<double>& x) const override { return this->model->evaluate_objective(x); }
-   void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const override {
-      this->model->evaluate_objective_gradient(x, gradient);
-   }
-   void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const override {
-      this->model->evaluate_constraints(x, constraints);
-   }
-   void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const override {
-      this->model->evaluate_constraint_gradient(x, constraint_index, gradient);
-   }
-   void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const override {
-      this->model->evaluate_constraint_jacobian(x, constraint_jacobian);
-   }
-   void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
-         SymmetricMatrix<double>& hessian) const override {
-      this->model->evaluate_lagrangian_hessian(x, objective_multiplier, multipliers, hessian);
-   }
+      [[nodiscard]] double evaluate_objective(const Vector<double>& x) const override { return this->model->evaluate_objective(x); }
+      void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const override {
+         this->model->evaluate_objective_gradient(x, gradient);
+      }
+      void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const override {
+         this->model->evaluate_constraints(x, constraints);
+      }
+      void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const override {
+         this->model->evaluate_constraint_gradient(x, constraint_index, gradient);
+      }
+      void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const override {
+         this->model->evaluate_constraint_jacobian(x, constraint_jacobian);
+      }
+      void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
+            SymmetricMatrix<size_t, double>& hessian) const override {
+         this->model->evaluate_lagrangian_hessian(x, objective_multiplier, multipliers, hessian);
+      }
 
-   // only these two functions are redefined
-   [[nodiscard]] double variable_lower_bound(size_t variable_index) const override;
-   [[nodiscard]] double variable_upper_bound(size_t variable_index) const override;
-   [[nodiscard]] BoundType get_variable_bound_type(size_t variable_index) const override { return this->model->get_variable_bound_type(variable_index); }
-   [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override { return this->model->get_lower_bounded_variables(); }
-   [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override { return this->model->get_upper_bounded_variables(); }
-   [[nodiscard]] const SparseVector<size_t>& get_slacks() const override { return this->model->get_slacks(); }
-   [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override { return this->model->get_single_lower_bounded_variables(); }
-   [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override { return this->model->get_single_upper_bounded_variables(); }
+      // only these two functions are redefined
+      [[nodiscard]] double variable_lower_bound(size_t variable_index) const override;
+      [[nodiscard]] double variable_upper_bound(size_t variable_index) const override;
+      [[nodiscard]] BoundType get_variable_bound_type(size_t variable_index) const override { return this->model->get_variable_bound_type(variable_index); }
+      [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override { return this->model->get_lower_bounded_variables(); }
+      [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override { return this->model->get_upper_bounded_variables(); }
+      [[nodiscard]] const SparseVector<size_t>& get_slacks() const override { return this->model->get_slacks(); }
+      [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override { return this->model->get_single_lower_bounded_variables(); }
+      [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override { return this->model->get_single_upper_bounded_variables(); }
 
-   [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override { return this->model->constraint_lower_bound(constraint_index); }
-   [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override { return this->model->constraint_upper_bound(constraint_index); }
-   [[nodiscard]] FunctionType get_constraint_type(size_t constraint_index) const override { return this->model->get_constraint_type(constraint_index); }
-   [[nodiscard]] BoundType get_constraint_bound_type(size_t constraint_index) const override { return this->model->get_constraint_bound_type(constraint_index); }
-   [[nodiscard]] const Collection<size_t>& get_equality_constraints() const override { return this->model->get_equality_constraints(); }
-   [[nodiscard]] const Collection<size_t>& get_inequality_constraints() const override { return this->model->get_inequality_constraints(); }
-   [[nodiscard]] const std::vector<size_t>& get_linear_constraints() const override { return this->model->get_linear_constraints(); }
+      [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override { return this->model->constraint_lower_bound(constraint_index); }
+      [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override { return this->model->constraint_upper_bound(constraint_index); }
+      [[nodiscard]] FunctionType get_constraint_type(size_t constraint_index) const override { return this->model->get_constraint_type(constraint_index); }
+      [[nodiscard]] BoundType get_constraint_bound_type(size_t constraint_index) const override { return this->model->get_constraint_bound_type(constraint_index); }
+      [[nodiscard]] const Collection<size_t>& get_equality_constraints() const override { return this->model->get_equality_constraints(); }
+      [[nodiscard]] const Collection<size_t>& get_inequality_constraints() const override { return this->model->get_inequality_constraints(); }
+      [[nodiscard]] const std::vector<size_t>& get_linear_constraints() const override { return this->model->get_linear_constraints(); }
 
-   void initial_primal_point(Vector<double>& x) const override { this->model->initial_primal_point(x); }
-   void initial_dual_point(Vector<double>& multipliers) const override { this->model->initial_dual_point(multipliers); }
-   void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const override {
-      this->model->postprocess_solution(iterate, termination_status);
-   }
+      void initial_primal_point(Vector<double>& x) const override { this->model->initial_primal_point(x); }
+      void initial_dual_point(Vector<double>& multipliers) const override { this->model->initial_dual_point(multipliers); }
+      void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const override {
+         this->model->postprocess_solution(iterate, termination_status);
+      }
 
-   [[nodiscard]] size_t number_objective_gradient_nonzeros() const override { return this->model->number_objective_gradient_nonzeros(); }
-   [[nodiscard]] size_t number_jacobian_nonzeros() const override { return this->model->number_jacobian_nonzeros(); }
-   [[nodiscard]] size_t number_hessian_nonzeros() const override { return this->model->number_hessian_nonzeros(); }
+      [[nodiscard]] size_t number_objective_gradient_nonzeros() const override { return this->model->number_objective_gradient_nonzeros(); }
+      [[nodiscard]] size_t number_jacobian_nonzeros() const override { return this->model->number_jacobian_nonzeros(); }
+      [[nodiscard]] size_t number_hessian_nonzeros() const override { return this->model->number_hessian_nonzeros(); }
 
-private:
-   const std::unique_ptr<Model> model;
-   const double relaxation_factor;
-};
+   private:
+      const std::unique_ptr<Model> model;
+      const double relaxation_factor;
+   };
 
-inline BoundRelaxedModel::BoundRelaxedModel(std::unique_ptr<Model> original_model, const Options& options):
-      Model(original_model->name + "_boundrelaxed", original_model->number_variables, original_model->number_constraints, original_model->objective_sign),
-      model(std::move(original_model)),
-      relaxation_factor(options.get_double("tolerance")) {
-}
+   inline BoundRelaxedModel::BoundRelaxedModel(std::unique_ptr<Model> original_model, const Options& options):
+         Model(original_model->name + "_boundrelaxed", original_model->number_variables, original_model->number_constraints, original_model->objective_sign),
+         model(std::move(original_model)),
+         relaxation_factor(options.get_double("tolerance")) {
+   }
 
-inline double BoundRelaxedModel::variable_lower_bound(size_t variable_index) const {
-   const double lower_bound = this->model->variable_lower_bound(variable_index);
-   return lower_bound - this->relaxation_factor * std::max(1., std::abs(lower_bound));
-}
+   inline double BoundRelaxedModel::variable_lower_bound(size_t variable_index) const {
+      const double lower_bound = this->model->variable_lower_bound(variable_index);
+      return lower_bound - this->relaxation_factor * std::max(1., std::abs(lower_bound));
+   }
 
-inline double BoundRelaxedModel::variable_upper_bound(size_t variable_index) const {
-   const double upper_bound = this->model->variable_upper_bound(variable_index);
-   return upper_bound + this->relaxation_factor * std::max(1., std::abs(upper_bound));
-}
+   inline double BoundRelaxedModel::variable_upper_bound(size_t variable_index) const {
+      const double upper_bound = this->model->variable_upper_bound(variable_index);
+      return upper_bound + this->relaxation_factor * std::max(1., std::abs(upper_bound));
+   }
+} // namespace
 
-#endif // UNO_BOUNDRELAXEDMODEL_H
+#endif // UNO_BOUNDRELAXEDMODEL_H
\ No newline at end of file
diff --git a/uno/model/HomogeneousEqualityConstrainedModel.hpp b/uno/model/HomogeneousEqualityConstrainedModel.hpp
index 645961d..1ab55b6 100644
--- a/uno/model/HomogeneousEqualityConstrainedModel.hpp
+++ b/uno/model/HomogeneousEqualityConstrainedModel.hpp
@@ -12,193 +12,195 @@
 #include "symbolic/Range.hpp"
 #include "tools/Infinity.hpp"
 
-// generate an equality-constrained model by:
-// - introducing slacks in inequality constraints
-// - subtracting the (possibly nonzero) RHS of equality constraints
-// all constraints are of the form "c(x) = 0"
-class HomogeneousEqualityConstrainedModel: public Model {
-public:
-   explicit HomogeneousEqualityConstrainedModel(std::unique_ptr<Model> original_model);
-
-   [[nodiscard]] double evaluate_objective(const Vector<double>& x) const override { return this->model->evaluate_objective(x); }
-   void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const override { this->model->evaluate_objective_gradient(x, gradient); }
-   void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const override;
-   void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const override;
-   void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const override;
-   void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
-         SymmetricMatrix<double>& hessian) const override;
-
-   [[nodiscard]] double variable_lower_bound(size_t variable_index) const override;
-   [[nodiscard]] double variable_upper_bound(size_t variable_index) const override;
-   [[nodiscard]] BoundType get_variable_bound_type(size_t variable_index) const override;
-   [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override { return this->lower_bounded_variables; }
-   [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override { return this->upper_bounded_variables; }
-   [[nodiscard]] const SparseVector<size_t>& get_slacks() const override { return this->slacks; }
-   [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override { return this->single_lower_bounded_variables; }
-   [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override { return this->single_upper_bounded_variables; }
-
-   [[nodiscard]] double constraint_lower_bound(size_t /*constraint_index*/) const override { return 0.; } // c(x) = 0
-   [[nodiscard]] double constraint_upper_bound(size_t /*constraint_index*/) const override { return 0.; }
-   [[nodiscard]] FunctionType get_constraint_type(size_t constraint_index) const override { return this->model->get_constraint_type(constraint_index); }
-   [[nodiscard]] BoundType get_constraint_bound_type(size_t /*constraint_index*/) const override { return EQUAL_BOUNDS; }
-   [[nodiscard]] const Collection<size_t>& get_equality_constraints() const override { return this->equality_constraints; }
-   [[nodiscard]] const Collection<size_t>& get_inequality_constraints() const override { return this->inequality_constraints; }
-   [[nodiscard]] const std::vector<size_t>& get_linear_constraints() const override { return this->model->get_linear_constraints(); }
-
-   void initial_primal_point(Vector<double>& x) const override;
-   void initial_dual_point(Vector<double>& multipliers) const override { this->model->initial_dual_point(multipliers); }
-   void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const override;
-
-   [[nodiscard]] size_t number_objective_gradient_nonzeros() const override { return this->model->number_objective_gradient_nonzeros(); }
-   [[nodiscard]] size_t number_jacobian_nonzeros() const override { return this->model->number_jacobian_nonzeros() + this->slacks.size(); }
-   [[nodiscard]] size_t number_hessian_nonzeros() const override { return this->model->number_hessian_nonzeros(); }
-
-protected:
-   const std::unique_ptr<Model> model;
-   std::vector<size_t> constraint_index_of_inequality_index;
-   std::vector<size_t> slack_index_of_constraint_index;
-
-   ForwardRange equality_constraints;
-   ForwardRange inequality_constraints;
-   SparseVector<size_t> slacks;
-   std::vector<size_t> lower_bounded_slacks;
-   std::vector<size_t> upper_bounded_slacks;
-   std::vector<size_t> single_lower_bounded_slacks;
-   std::vector<size_t> single_upper_bounded_slacks;
-   Concatenation<const Collection<size_t>&, CollectionAdapter<std::vector<size_t>&>> lower_bounded_variables;
-   Concatenation<const Collection<size_t>&, CollectionAdapter<std::vector<size_t>&>> upper_bounded_variables;
-   Concatenation<const Collection<size_t>&, CollectionAdapter<std::vector<size_t>&>> single_lower_bounded_variables;
-   Concatenation<const Collection<size_t>&, CollectionAdapter<std::vector<size_t>&>> single_upper_bounded_variables;
-};
-
-// Transform the problem into an equality-constrained problem with constraints c(x) = 0. This implies:
-// - inequality constraints get a slack
-// - equality constraints are shifted by their RHS
-inline HomogeneousEqualityConstrainedModel::HomogeneousEqualityConstrainedModel(std::unique_ptr<Model> original_model):
-      Model(original_model->name + "_equalityconstrained", original_model->number_variables + original_model->get_inequality_constraints().size(),
-            original_model->number_constraints, original_model->objective_sign),
-      // transfer ownership of the pointer
-      model(std::move(original_model)),
-      constraint_index_of_inequality_index(this->model->get_inequality_constraints().size()),
-      slack_index_of_constraint_index(this->model->number_constraints),
-      // all constraints are equality constraints
-      equality_constraints(Range(this->number_constraints)),
-      inequality_constraints(Range(0)),
-      slacks(this->model->get_inequality_constraints().size()),
-      lower_bounded_slacks(this->slacks.size()),
-      upper_bounded_slacks(this->slacks.size()),
-      lower_bounded_variables(concatenate(this->model->get_lower_bounded_variables(), CollectionAdapter(this->lower_bounded_slacks))),
-      upper_bounded_variables(concatenate(this->model->get_upper_bounded_variables(), CollectionAdapter(this->upper_bounded_slacks))),
-      single_lower_bounded_variables(concatenate(this->model->get_single_lower_bounded_variables(), CollectionAdapter(this->single_lower_bounded_slacks))),
-      single_upper_bounded_variables(concatenate(this->model->get_single_upper_bounded_variables(), CollectionAdapter(this->single_upper_bounded_slacks))){
-   // register the inequality constraint of each slack
-   size_t inequality_index = 0;
-   for (const size_t constraint_index: this->model->get_inequality_constraints()) {
-      const size_t slack_variable_index = this->model->number_variables + inequality_index;
-      this->constraint_index_of_inequality_index[inequality_index] = constraint_index;
-      this->slack_index_of_constraint_index[constraint_index] = slack_variable_index;
-      this->slacks.insert(constraint_index, slack_variable_index);
-      if (is_finite(this->model->constraint_lower_bound(constraint_index))) {
-         this->lower_bounded_slacks.push_back(slack_variable_index);
-         if (not is_finite(this->model->constraint_upper_bound(constraint_index))) {
-            this->single_lower_bounded_slacks.push_back(slack_variable_index);
+namespace uno {
+   // generate an equality-constrained model by:
+   // - introducing slacks in inequality constraints
+   // - subtracting the (possibly nonzero) RHS of equality constraints
+   // all constraints are of the form "c(x) = 0"
+   class HomogeneousEqualityConstrainedModel: public Model {
+   public:
+      explicit HomogeneousEqualityConstrainedModel(std::unique_ptr<Model> original_model);
+
+      [[nodiscard]] double evaluate_objective(const Vector<double>& x) const override { return this->model->evaluate_objective(x); }
+      void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const override { this->model->evaluate_objective_gradient(x, gradient); }
+      void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const override;
+      void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const override;
+      void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const override;
+      void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
+            SymmetricMatrix<size_t, double>& hessian) const override;
+
+      [[nodiscard]] double variable_lower_bound(size_t variable_index) const override;
+      [[nodiscard]] double variable_upper_bound(size_t variable_index) const override;
+      [[nodiscard]] BoundType get_variable_bound_type(size_t variable_index) const override;
+      [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override { return this->lower_bounded_variables; }
+      [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override { return this->upper_bounded_variables; }
+      [[nodiscard]] const SparseVector<size_t>& get_slacks() const override { return this->slacks; }
+      [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override { return this->single_lower_bounded_variables; }
+      [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override { return this->single_upper_bounded_variables; }
+
+      [[nodiscard]] double constraint_lower_bound(size_t /*constraint_index*/) const override { return 0.; } // c(x) = 0
+      [[nodiscard]] double constraint_upper_bound(size_t /*constraint_index*/) const override { return 0.; }
+      [[nodiscard]] FunctionType get_constraint_type(size_t constraint_index) const override { return this->model->get_constraint_type(constraint_index); }
+      [[nodiscard]] BoundType get_constraint_bound_type(size_t /*constraint_index*/) const override { return EQUAL_BOUNDS; }
+      [[nodiscard]] const Collection<size_t>& get_equality_constraints() const override { return this->equality_constraints; }
+      [[nodiscard]] const Collection<size_t>& get_inequality_constraints() const override { return this->inequality_constraints; }
+      [[nodiscard]] const std::vector<size_t>& get_linear_constraints() const override { return this->model->get_linear_constraints(); }
+
+      void initial_primal_point(Vector<double>& x) const override;
+      void initial_dual_point(Vector<double>& multipliers) const override { this->model->initial_dual_point(multipliers); }
+      void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const override;
+
+      [[nodiscard]] size_t number_objective_gradient_nonzeros() const override { return this->model->number_objective_gradient_nonzeros(); }
+      [[nodiscard]] size_t number_jacobian_nonzeros() const override { return this->model->number_jacobian_nonzeros() + this->slacks.size(); }
+      [[nodiscard]] size_t number_hessian_nonzeros() const override { return this->model->number_hessian_nonzeros(); }
+
+   protected:
+      const std::unique_ptr<Model> model;
+      std::vector<size_t> constraint_index_of_inequality_index;
+      std::vector<size_t> slack_index_of_constraint_index;
+
+      ForwardRange equality_constraints;
+      ForwardRange inequality_constraints;
+      SparseVector<size_t> slacks;
+      std::vector<size_t> lower_bounded_slacks;
+      std::vector<size_t> upper_bounded_slacks;
+      std::vector<size_t> single_lower_bounded_slacks;
+      std::vector<size_t> single_upper_bounded_slacks;
+      Concatenation<const Collection<size_t>&, CollectionAdapter<std::vector<size_t>&>> lower_bounded_variables;
+      Concatenation<const Collection<size_t>&, CollectionAdapter<std::vector<size_t>&>> upper_bounded_variables;
+      Concatenation<const Collection<size_t>&, CollectionAdapter<std::vector<size_t>&>> single_lower_bounded_variables;
+      Concatenation<const Collection<size_t>&, CollectionAdapter<std::vector<size_t>&>> single_upper_bounded_variables;
+   };
+
+   // Transform the problem into an equality-constrained problem with constraints c(x) = 0. This implies:
+   // - inequality constraints get a slack
+   // - equality constraints are shifted by their RHS
+   inline HomogeneousEqualityConstrainedModel::HomogeneousEqualityConstrainedModel(std::unique_ptr<Model> original_model):
+         Model(original_model->name + "_equalityconstrained", original_model->number_variables + original_model->get_inequality_constraints().size(),
+               original_model->number_constraints, original_model->objective_sign),
+         // transfer ownership of the pointer
+         model(std::move(original_model)),
+         constraint_index_of_inequality_index(this->model->get_inequality_constraints().size()),
+         slack_index_of_constraint_index(this->model->number_constraints),
+         // all constraints are equality constraints
+         equality_constraints(Range(this->number_constraints)),
+         inequality_constraints(Range(0)),
+         slacks(this->model->get_inequality_constraints().size()),
+         lower_bounded_slacks(this->slacks.size()),
+         upper_bounded_slacks(this->slacks.size()),
+         lower_bounded_variables(concatenate(this->model->get_lower_bounded_variables(), CollectionAdapter(this->lower_bounded_slacks))),
+         upper_bounded_variables(concatenate(this->model->get_upper_bounded_variables(), CollectionAdapter(this->upper_bounded_slacks))),
+         single_lower_bounded_variables(concatenate(this->model->get_single_lower_bounded_variables(), CollectionAdapter(this->single_lower_bounded_slacks))),
+         single_upper_bounded_variables(concatenate(this->model->get_single_upper_bounded_variables(), CollectionAdapter(this->single_upper_bounded_slacks))){
+      // register the inequality constraint of each slack
+      size_t inequality_index = 0;
+      for (const size_t constraint_index: this->model->get_inequality_constraints()) {
+         const size_t slack_variable_index = this->model->number_variables + inequality_index;
+         this->constraint_index_of_inequality_index[inequality_index] = constraint_index;
+         this->slack_index_of_constraint_index[constraint_index] = slack_variable_index;
+         this->slacks.insert(constraint_index, slack_variable_index);
+         if (is_finite(this->model->constraint_lower_bound(constraint_index))) {
+            this->lower_bounded_slacks.push_back(slack_variable_index);
+            if (not is_finite(this->model->constraint_upper_bound(constraint_index))) {
+               this->single_lower_bounded_slacks.push_back(slack_variable_index);
+            }
          }
-      }
-      if (is_finite(this->model->constraint_upper_bound(constraint_index))) {
-         this->upper_bounded_slacks.push_back(slack_variable_index);
-         if (not is_finite(this->model->constraint_lower_bound(constraint_index))) {
-            this->single_upper_bounded_slacks.push_back(slack_variable_index);
+         if (is_finite(this->model->constraint_upper_bound(constraint_index))) {
+            this->upper_bounded_slacks.push_back(slack_variable_index);
+            if (not is_finite(this->model->constraint_lower_bound(constraint_index))) {
+               this->single_upper_bounded_slacks.push_back(slack_variable_index);
+            }
          }
+         inequality_index++;
       }
-      inequality_index++;
    }
-}
 
-inline void HomogeneousEqualityConstrainedModel::evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const {
-   this->model->evaluate_constraints(x, constraints);
-   // inequality constraints: add the slacks
-   for (const auto [constraint_index, slack_index]: this->get_slacks()) {
-      constraints[constraint_index] -= x[slack_index];
-   }
+   inline void HomogeneousEqualityConstrainedModel::evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const {
+      this->model->evaluate_constraints(x, constraints);
+      // inequality constraints: add the slacks
+      for (const auto [constraint_index, slack_index]: this->get_slacks()) {
+         constraints[constraint_index] -= x[slack_index];
+      }
 
-   // equality constraints: make sure they are homogeneous (c(x) = 0)
-   for (const size_t constraint_index: this->model->get_equality_constraints()) {
-      constraints[constraint_index] -= this->model->constraint_lower_bound(constraint_index);
-   }
-}
-
-inline void HomogeneousEqualityConstrainedModel::evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index,
-      SparseVector<double>& gradient) const {
-   this->model->evaluate_constraint_gradient(x, constraint_index, gradient);
-   // if the original constraint is an inequality, add the slack contribution
-   if (this->model->get_constraint_bound_type(constraint_index) != EQUAL_BOUNDS) {
-      const size_t slack_variable_index = this->slack_index_of_constraint_index[constraint_index];
-      gradient.insert(slack_variable_index, -1.);
+      // equality constraints: make sure they are homogeneous (c(x) = 0)
+      for (const size_t constraint_index: this->model->get_equality_constraints()) {
+         constraints[constraint_index] -= this->model->constraint_lower_bound(constraint_index);
+      }
    }
-}
 
-inline void HomogeneousEqualityConstrainedModel::evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const {
-   this->model->evaluate_constraint_jacobian(x, constraint_jacobian);
-   // add the slack contributions
-   for (const auto [constraint_index, slack_index]: this->get_slacks()) {
-      constraint_jacobian[constraint_index].insert(slack_index, -1.);
-   }
-}
-
-inline void HomogeneousEqualityConstrainedModel::evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier,
-      const Vector<double>& multipliers, SymmetricMatrix<double>& hessian) const {
-   this->model->evaluate_lagrangian_hessian(x, objective_multiplier, multipliers, hessian);
-   // extend the dimension of the Hessian by finalizing the remaining columns (note: the slacks do not enter the Hessian)
-   for (size_t constraint_index: Range(this->model->number_variables, this->number_variables)) {
-      hessian.finalize_column(constraint_index);
+   inline void HomogeneousEqualityConstrainedModel::evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index,
+         SparseVector<double>& gradient) const {
+      this->model->evaluate_constraint_gradient(x, constraint_index, gradient);
+      // if the original constraint is an inequality, add the slack contribution
+      if (this->model->get_constraint_bound_type(constraint_index) != EQUAL_BOUNDS) {
+         const size_t slack_variable_index = this->slack_index_of_constraint_index[constraint_index];
+         gradient.insert(slack_variable_index, -1.);
+      }
    }
-}
 
-inline double HomogeneousEqualityConstrainedModel::variable_lower_bound(size_t variable_index) const {
-   if (variable_index < this->model->number_variables) { // original variable
-      return this->model->variable_lower_bound(variable_index);
-   }
-   else { // slack variable
-      const size_t slack_index = variable_index - this->model->number_variables;
-      const size_t constraint_index = this->constraint_index_of_inequality_index[slack_index];
-      return this->model->constraint_lower_bound(constraint_index);
+   inline void HomogeneousEqualityConstrainedModel::evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const {
+      this->model->evaluate_constraint_jacobian(x, constraint_jacobian);
+      // add the slack contributions
+      for (const auto [constraint_index, slack_index]: this->get_slacks()) {
+         constraint_jacobian[constraint_index].insert(slack_index, -1.);
+      }
    }
-}
 
-inline double HomogeneousEqualityConstrainedModel::variable_upper_bound(size_t variable_index) const {
-   if (variable_index < this->model->number_variables) { // original variable
-      return this->model->variable_upper_bound(variable_index);
+   inline void HomogeneousEqualityConstrainedModel::evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier,
+         const Vector<double>& multipliers, SymmetricMatrix<size_t, double>& hessian) const {
+      this->model->evaluate_lagrangian_hessian(x, objective_multiplier, multipliers, hessian);
+      // extend the dimension of the Hessian by finalizing the remaining columns (note: the slacks do not enter the Hessian)
+      for (size_t constraint_index: Range(this->model->number_variables, this->number_variables)) {
+         hessian.finalize_column(constraint_index);
+      }
    }
-   else { // slack variable
-      const size_t inequality_index = variable_index - this->model->number_variables;
-      const size_t constraint_index = this->constraint_index_of_inequality_index[inequality_index];
-      return this->model->constraint_upper_bound(constraint_index);
+
+   inline double HomogeneousEqualityConstrainedModel::variable_lower_bound(size_t variable_index) const {
+      if (variable_index < this->model->number_variables) { // original variable
+         return this->model->variable_lower_bound(variable_index);
+      }
+      else { // slack variable
+         const size_t slack_index = variable_index - this->model->number_variables;
+         const size_t constraint_index = this->constraint_index_of_inequality_index[slack_index];
+         return this->model->constraint_lower_bound(constraint_index);
+      }
    }
-}
 
-inline BoundType HomogeneousEqualityConstrainedModel::get_variable_bound_type(size_t variable_index) const {
-   if (variable_index < this->model->number_variables) { // original variable
-      return this->model->get_variable_bound_type(variable_index);
+   inline double HomogeneousEqualityConstrainedModel::variable_upper_bound(size_t variable_index) const {
+      if (variable_index < this->model->number_variables) { // original variable
+         return this->model->variable_upper_bound(variable_index);
+      }
+      else { // slack variable
+         const size_t inequality_index = variable_index - this->model->number_variables;
+         const size_t constraint_index = this->constraint_index_of_inequality_index[inequality_index];
+         return this->model->constraint_upper_bound(constraint_index);
+      }
    }
-   else { // slack variable
-      const size_t inequality_index = variable_index - this->model->number_variables;
-      const size_t constraint_index = this->constraint_index_of_inequality_index[inequality_index];
-      return this->model->get_constraint_bound_type(constraint_index);
+
+   inline BoundType HomogeneousEqualityConstrainedModel::get_variable_bound_type(size_t variable_index) const {
+      if (variable_index < this->model->number_variables) { // original variable
+         return this->model->get_variable_bound_type(variable_index);
+      }
+      else { // slack variable
+         const size_t inequality_index = variable_index - this->model->number_variables;
+         const size_t constraint_index = this->constraint_index_of_inequality_index[inequality_index];
+         return this->model->get_constraint_bound_type(constraint_index);
+      }
    }
-}
 
-inline void HomogeneousEqualityConstrainedModel::initial_primal_point(Vector<double>& x) const {
-   this->model->initial_primal_point(x);
-   // set the slacks
-   for (const auto [_, slack_index]: this->get_slacks()) {
-      x[slack_index] = 0.;
+   inline void HomogeneousEqualityConstrainedModel::initial_primal_point(Vector<double>& x) const {
+      this->model->initial_primal_point(x);
+      // set the slacks
+      for (const auto [_, slack_index]: this->get_slacks()) {
+         x[slack_index] = 0.;
+      }
    }
-}
 
-inline void HomogeneousEqualityConstrainedModel::postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const {
-   this->model->postprocess_solution(iterate, termination_status);
-   // discard the slacks
-   iterate.number_variables = this->model->number_variables;
-}
+   inline void HomogeneousEqualityConstrainedModel::postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const {
+      this->model->postprocess_solution(iterate, termination_status);
+      // discard the slacks
+      iterate.number_variables = this->model->number_variables;
+   }
+} // namespace
 
-#endif // UNO_HOMOGENEOUSEQUALITYCONSTRAINEDMODEL_H
+#endif // UNO_HOMOGENEOUSEQUALITYCONSTRAINEDMODEL_H
\ No newline at end of file
diff --git a/uno/model/Model.cpp b/uno/model/Model.cpp
index 93b9276..1a16040 100644
--- a/uno/model/Model.cpp
+++ b/uno/model/Model.cpp
@@ -8,24 +8,26 @@
 #include "symbolic/VectorExpression.hpp"
 #include "linear_algebra/Vector.hpp"
 
-// abstract Problem class
-Model::Model(std::string name, size_t number_variables, size_t number_constraints, double objective_sign) :
-      name(std::move(name)), number_variables(number_variables), number_constraints(number_constraints), objective_sign(objective_sign) {
-}
+namespace uno {
+   // abstract Problem class
+   Model::Model(std::string name, size_t number_variables, size_t number_constraints, double objective_sign) :
+         name(std::move(name)), number_variables(number_variables), number_constraints(number_constraints), objective_sign(objective_sign) {
+   }
 
-void Model::project_onto_variable_bounds(Vector<double>& x) const {
-   for (size_t variable_index: Range(this->number_variables)) {
-      x[variable_index] = std::max(std::min(x[variable_index], this->variable_upper_bound(variable_index)), this->variable_lower_bound(variable_index));
+   void Model::project_onto_variable_bounds(Vector<double>& x) const {
+      for (size_t variable_index: Range(this->number_variables)) {
+         x[variable_index] = std::max(std::min(x[variable_index], this->variable_upper_bound(variable_index)), this->variable_lower_bound(variable_index));
+      }
    }
-}
 
-bool Model::is_constrained() const {
-   return (0 < this->number_constraints);
-}
+   bool Model::is_constrained() const {
+      return (0 < this->number_constraints);
+   }
 
-// individual constraint violation
-double Model::constraint_violation(double constraint_value, size_t constraint_index) const {
-   const double lower_bound_violation = std::max(0., this->constraint_lower_bound(constraint_index) - constraint_value);
-   const double upper_bound_violation = std::max(0., constraint_value - this->constraint_upper_bound(constraint_index));
-   return std::max(lower_bound_violation, upper_bound_violation);
-}
\ No newline at end of file
+   // individual constraint violation
+   double Model::constraint_violation(double constraint_value, size_t constraint_index) const {
+      const double lower_bound_violation = std::max(0., this->constraint_lower_bound(constraint_index) - constraint_value);
+      const double upper_bound_violation = std::max(0., constraint_value - this->constraint_upper_bound(constraint_index));
+      return std::max(lower_bound_violation, upper_bound_violation);
+   }
+} // namespace
diff --git a/uno/model/Model.hpp b/uno/model/Model.hpp
index d985f57..838447a 100644
--- a/uno/model/Model.hpp
+++ b/uno/model/Model.hpp
@@ -11,91 +11,93 @@
 #include "optimization/TerminationStatus.hpp"
 #include "symbolic/VectorExpression.hpp"
 
-// forward declarations
-template <typename ElementType>
-class Collection;
-template <typename ElementType>
-class RectangularMatrix;
-template <typename ElementType>
-class SparseVector;
-template <typename ElementType>
-class SymmetricMatrix;
-
-enum FunctionType {LINEAR, NONLINEAR};
-enum BoundType {EQUAL_BOUNDS, BOUNDED_LOWER, BOUNDED_UPPER, BOUNDED_BOTH_SIDES, UNBOUNDED};
-
-// forward declaration
-class Iterate;
-
-/*! \class Problem
- * \brief Optimization problem
- *
- *  Description of an optimization problem
- */
-class Model {
-public:
-   Model(std::string name, size_t number_variables, size_t number_constraints, double objective_sign);
-   virtual ~Model() = default;
-
-   const std::string name;
-   const size_t number_variables; /*!< Number of variables */
-   const size_t number_constraints; /*!< Number of constraints */
-   const double objective_sign; /*!< Sign of the objective function (1: minimization, -1: maximization) */
-
-   // Hessian
-   const bool fixed_hessian_sparsity{true};
-
-   [[nodiscard]] virtual double evaluate_objective(const Vector<double>& x) const = 0;
-   virtual void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const = 0;
-   virtual void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const = 0;
-   virtual void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const = 0;
-   virtual void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const = 0;
-   virtual void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
-         SymmetricMatrix<double>& hessian) const = 0;
-
-   // purely virtual functions
-   [[nodiscard]] virtual double variable_lower_bound(size_t variable_index) const = 0;
-   [[nodiscard]] virtual double variable_upper_bound(size_t variable_index) const = 0;
-   [[nodiscard]] virtual BoundType get_variable_bound_type(size_t variable_index) const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_lower_bounded_variables() const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_upper_bounded_variables() const = 0;
-   [[nodiscard]] virtual const SparseVector<size_t>& get_slacks() const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_single_lower_bounded_variables() const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_single_upper_bounded_variables() const = 0;
-
-   [[nodiscard]] virtual double constraint_lower_bound(size_t constraint_index) const = 0;
-   [[nodiscard]] virtual double constraint_upper_bound(size_t constraint_index) const = 0;
-   [[nodiscard]] virtual FunctionType get_constraint_type(size_t constraint_index) const = 0;
-   [[nodiscard]] virtual BoundType get_constraint_bound_type(size_t constraint_index) const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_equality_constraints() const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_inequality_constraints() const = 0;
-   [[nodiscard]] virtual const std::vector<size_t>& get_linear_constraints() const = 0;
-
-   virtual void initial_primal_point(Vector<double>& x) const = 0;
-   virtual void initial_dual_point(Vector<double>& multipliers) const = 0;
-   virtual void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const = 0;
-
-   [[nodiscard]] virtual size_t number_objective_gradient_nonzeros() const = 0;
-   [[nodiscard]] virtual size_t number_jacobian_nonzeros() const = 0;
-   [[nodiscard]] virtual size_t number_hessian_nonzeros() const = 0;
-
-   // auxiliary functions
-   void project_onto_variable_bounds(Vector<double>& x) const;
-   [[nodiscard]] bool is_constrained() const;
-
-   // constraint violation
-   [[nodiscard]] virtual double constraint_violation(double constraint_value, size_t constraint_index) const;
+namespace uno {
+   // forward declarations
+   template <typename ElementType>
+   class Collection;
+   template <typename ElementType>
+   class RectangularMatrix;
+   template <typename ElementType>
+   class SparseVector;
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrix;
+
+   enum FunctionType {LINEAR, NONLINEAR};
+   enum BoundType {EQUAL_BOUNDS, BOUNDED_LOWER, BOUNDED_UPPER, BOUNDED_BOTH_SIDES, UNBOUNDED};
+
+   // forward declaration
+   class Iterate;
+
+   /*! \class Problem
+    * \brief Optimization problem
+    *
+    *  Description of an optimization problem
+    */
+   class Model {
+   public:
+      Model(std::string name, size_t number_variables, size_t number_constraints, double objective_sign);
+      virtual ~Model() = default;
+
+      const std::string name;
+      const size_t number_variables; /*!< Number of variables */
+      const size_t number_constraints; /*!< Number of constraints */
+      const double objective_sign; /*!< Sign of the objective function (1: minimization, -1: maximization) */
+
+      // Hessian
+      const bool fixed_hessian_sparsity{true};
+
+      [[nodiscard]] virtual double evaluate_objective(const Vector<double>& x) const = 0;
+      virtual void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const = 0;
+      virtual void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const = 0;
+      virtual void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const = 0;
+      virtual void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const = 0;
+      virtual void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
+            SymmetricMatrix<size_t, double>& hessian) const = 0;
+
+      // purely virtual functions
+      [[nodiscard]] virtual double variable_lower_bound(size_t variable_index) const = 0;
+      [[nodiscard]] virtual double variable_upper_bound(size_t variable_index) const = 0;
+      [[nodiscard]] virtual BoundType get_variable_bound_type(size_t variable_index) const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_lower_bounded_variables() const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_upper_bounded_variables() const = 0;
+      [[nodiscard]] virtual const SparseVector<size_t>& get_slacks() const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_single_lower_bounded_variables() const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_single_upper_bounded_variables() const = 0;
+
+      [[nodiscard]] virtual double constraint_lower_bound(size_t constraint_index) const = 0;
+      [[nodiscard]] virtual double constraint_upper_bound(size_t constraint_index) const = 0;
+      [[nodiscard]] virtual FunctionType get_constraint_type(size_t constraint_index) const = 0;
+      [[nodiscard]] virtual BoundType get_constraint_bound_type(size_t constraint_index) const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_equality_constraints() const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_inequality_constraints() const = 0;
+      [[nodiscard]] virtual const std::vector<size_t>& get_linear_constraints() const = 0;
+
+      virtual void initial_primal_point(Vector<double>& x) const = 0;
+      virtual void initial_dual_point(Vector<double>& multipliers) const = 0;
+      virtual void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const = 0;
+
+      [[nodiscard]] virtual size_t number_objective_gradient_nonzeros() const = 0;
+      [[nodiscard]] virtual size_t number_jacobian_nonzeros() const = 0;
+      [[nodiscard]] virtual size_t number_hessian_nonzeros() const = 0;
+
+      // auxiliary functions
+      void project_onto_variable_bounds(Vector<double>& x) const;
+      [[nodiscard]] bool is_constrained() const;
+
+      // constraint violation
+      [[nodiscard]] virtual double constraint_violation(double constraint_value, size_t constraint_index) const;
+      template <typename Array>
+      double constraint_violation(const Array& constraints, Norm residual_norm) const;
+   };
+
+   // compute ||c||
    template <typename Array>
-   double constraint_violation(const Array& constraints, Norm residual_norm) const;
-};
-
-// compute ||c||
-template <typename Array>
-double Model::constraint_violation(const Array& constraints, Norm residual_norm) const {
-   const VectorExpression constraint_violation(Range(constraints.size()), [&](size_t constraint_index) {
-      return this->constraint_violation(constraints[constraint_index], constraint_index);
-   });
-   return norm(residual_norm, constraint_violation);
-}
-
-#endif // UNO_MODEL_H
+   double Model::constraint_violation(const Array& constraints, Norm residual_norm) const {
+      const VectorExpression constraint_violation(Range(constraints.size()), [&](size_t constraint_index) {
+         return this->constraint_violation(constraints[constraint_index], constraint_index);
+      });
+      return norm(residual_norm, constraint_violation);
+   }
+} // namespace
+
+#endif // UNO_MODEL_H
\ No newline at end of file
diff --git a/uno/model/ModelFactory.cpp b/uno/model/ModelFactory.cpp
index 2abb6cf..b8b1e7c 100644
--- a/uno/model/ModelFactory.cpp
+++ b/uno/model/ModelFactory.cpp
@@ -8,21 +8,23 @@
 #include "optimization/Iterate.hpp"
 #include "tools/Options.hpp"
 
-// note: ownership of the pointer is transferred
-std::unique_ptr<Model> ModelFactory::reformulate(std::unique_ptr<Model> model, Iterate& initial_iterate, const Options& options) {
-   // optional: scale the problem using the evaluations at the first iterate
-   // TODO create scaling *after* generating the IPM initial iterate
-   if (options.get_string("scale_functions") == "yes") {
-      model = std::make_unique<ScaledModel>(std::move(model), initial_iterate, options);
-   }
+namespace uno {
+   // note: ownership of the pointer is transferred
+   std::unique_ptr<Model> ModelFactory::reformulate(std::unique_ptr<Model> model, Iterate& initial_iterate, const Options& options) {
+      // optional: scale the problem using the evaluations at the first iterate
+      // TODO create scaling *after* generating the IPM initial iterate
+      if (options.get_string("scale_functions") == "yes") {
+         model = std::make_unique<ScaledModel>(std::move(model), initial_iterate, options);
+      }
 
-   if (options.get_string("subproblem") == "primal_dual_interior_point") {
-      // if an equality-constrained problem is required (e.g. interior points or AL), reformulate the model with slacks
-      model = std::make_unique<HomogeneousEqualityConstrainedModel>(std::move(model));
-      // slightly relax the bound constraints
-      model = std::make_unique<BoundRelaxedModel>(std::move(model), options);
-      // add the slacks to the initial iterate
-      initial_iterate.set_number_variables(model->number_variables);
+      if (options.get_string("subproblem") == "primal_dual_interior_point") {
+         // if an equality-constrained problem is required (e.g. interior points or AL), reformulate the model with slacks
+         model = std::make_unique<HomogeneousEqualityConstrainedModel>(std::move(model));
+         // slightly relax the bound constraints
+         model = std::make_unique<BoundRelaxedModel>(std::move(model), options);
+         // add the slacks to the initial iterate
+         initial_iterate.set_number_variables(model->number_variables);
+      }
+      return model;
    }
-   return model;
-}
+} // namespace
diff --git a/uno/model/ModelFactory.hpp b/uno/model/ModelFactory.hpp
index eb94187..171d443 100644
--- a/uno/model/ModelFactory.hpp
+++ b/uno/model/ModelFactory.hpp
@@ -7,13 +7,15 @@
 #include <memory>
 #include "Model.hpp"
 
-// forward declarations
-class Iterate;
-class Options;
+namespace uno {
+   // forward declarations
+   class Iterate;
+   class Options;
 
-class ModelFactory {
-public:
-   static std::unique_ptr<Model> reformulate(std::unique_ptr<Model> model, Iterate& initial_iterate, const Options& options);
-};
+   class ModelFactory {
+   public:
+      static std::unique_ptr<Model> reformulate(std::unique_ptr<Model> model, Iterate& initial_iterate, const Options& options);
+   };
+} // namespace
 
 #endif // UNO_MODELFACTORY_H
diff --git a/uno/model/ScaledModel.hpp b/uno/model/ScaledModel.hpp
index 469a788..0673afe 100644
--- a/uno/model/ScaledModel.hpp
+++ b/uno/model/ScaledModel.hpp
@@ -9,140 +9,142 @@
 #include "preprocessing/Scaling.hpp"
 #include "tools/Options.hpp"
 
-class ScaledModel: public Model {
-public:
-   ScaledModel(std::unique_ptr<Model> original_model, Iterate& initial_iterate, const Options& options);
-
-   [[nodiscard]] double evaluate_objective(const Vector<double>& x) const override;
-   void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const override;
-   void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const override;
-   void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const override;
-   void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const override;
-   void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
-         SymmetricMatrix<double>& hessian) const override;
-
-   [[nodiscard]] double variable_lower_bound(size_t variable_index) const override { return this->model->variable_lower_bound(variable_index); }
-   [[nodiscard]] double variable_upper_bound(size_t variable_index) const override { return this->model->variable_upper_bound(variable_index); }
-   [[nodiscard]] BoundType get_variable_bound_type(size_t variable_index) const override { return this->model->get_variable_bound_type(variable_index); }
-   [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override { return this->model->get_lower_bounded_variables(); }
-   [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override { return this->model->get_upper_bounded_variables(); }
-   [[nodiscard]] const SparseVector<size_t>& get_slacks() const override { return this->model->get_slacks(); }
-   [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override { return this->model->get_single_lower_bounded_variables(); }
-   [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override { return this->model->get_single_upper_bounded_variables(); }
-
-   [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override;
-   [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override;
-   [[nodiscard]] FunctionType get_constraint_type(size_t constraint_index) const override { return this->model->get_constraint_type(constraint_index); }
-   [[nodiscard]] BoundType get_constraint_bound_type(size_t constraint_index) const override { return this->model->get_constraint_bound_type(constraint_index); }
-   [[nodiscard]] const Collection<size_t>& get_equality_constraints() const override { return this->model->get_equality_constraints(); }
-   [[nodiscard]] const Collection<size_t>& get_inequality_constraints() const override { return this->model->get_inequality_constraints(); }
-   [[nodiscard]] const std::vector<size_t>& get_linear_constraints() const override { return this->model->get_linear_constraints(); }
-
-   void initial_primal_point(Vector<double>& x) const override { this->model->initial_primal_point(x); }
-   void initial_dual_point(Vector<double>& multipliers) const override { this->model->initial_dual_point(multipliers); }
-   void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const override;
-
-   [[nodiscard]] size_t number_objective_gradient_nonzeros() const override { return this->model->number_objective_gradient_nonzeros(); }
-   [[nodiscard]] size_t number_jacobian_nonzeros() const override { return this->model->number_jacobian_nonzeros(); }
-   [[nodiscard]] size_t number_hessian_nonzeros() const override { return this->model->number_hessian_nonzeros(); }
-
-private:
-   const std::unique_ptr<Model> model;
-   Scaling scaling;
-};
-
-inline ScaledModel::ScaledModel(std::unique_ptr<Model> original_model, Iterate& initial_iterate, const Options& options):
-      Model(original_model->name + "_scaled", original_model->number_variables, original_model->number_constraints, original_model->objective_sign),
-      model(std::move(original_model)),
-      scaling(this->model->number_constraints, options.get_double("function_scaling_threshold")) {
-   if (options.get_bool("scale_functions")) {
-      // evaluate the gradients at the current point
-      initial_iterate.evaluate_objective_gradient(*this->model);
-      initial_iterate.evaluate_constraint_jacobian(*this->model);
-      this->scaling.compute(initial_iterate.evaluations.objective_gradient, initial_iterate.evaluations.constraint_jacobian);
-      // scale the gradients
-      scale(initial_iterate.evaluations.objective_gradient, this->scaling.get_objective_scaling());
-      for (size_t constraint_index: Range(this->model->number_constraints)) {
-         scale(initial_iterate.evaluations.constraint_jacobian[constraint_index], this->scaling.get_constraint_scaling(constraint_index));
+namespace uno {
+   class ScaledModel: public Model {
+   public:
+      ScaledModel(std::unique_ptr<Model> original_model, Iterate& initial_iterate, const Options& options);
+
+      [[nodiscard]] double evaluate_objective(const Vector<double>& x) const override;
+      void evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const override;
+      void evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const override;
+      void evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const override;
+      void evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const override;
+      void evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
+            SymmetricMatrix<size_t, double>& hessian) const override;
+
+      [[nodiscard]] double variable_lower_bound(size_t variable_index) const override { return this->model->variable_lower_bound(variable_index); }
+      [[nodiscard]] double variable_upper_bound(size_t variable_index) const override { return this->model->variable_upper_bound(variable_index); }
+      [[nodiscard]] BoundType get_variable_bound_type(size_t variable_index) const override { return this->model->get_variable_bound_type(variable_index); }
+      [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override { return this->model->get_lower_bounded_variables(); }
+      [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override { return this->model->get_upper_bounded_variables(); }
+      [[nodiscard]] const SparseVector<size_t>& get_slacks() const override { return this->model->get_slacks(); }
+      [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override { return this->model->get_single_lower_bounded_variables(); }
+      [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override { return this->model->get_single_upper_bounded_variables(); }
+
+      [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override;
+      [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override;
+      [[nodiscard]] FunctionType get_constraint_type(size_t constraint_index) const override { return this->model->get_constraint_type(constraint_index); }
+      [[nodiscard]] BoundType get_constraint_bound_type(size_t constraint_index) const override { return this->model->get_constraint_bound_type(constraint_index); }
+      [[nodiscard]] const Collection<size_t>& get_equality_constraints() const override { return this->model->get_equality_constraints(); }
+      [[nodiscard]] const Collection<size_t>& get_inequality_constraints() const override { return this->model->get_inequality_constraints(); }
+      [[nodiscard]] const std::vector<size_t>& get_linear_constraints() const override { return this->model->get_linear_constraints(); }
+
+      void initial_primal_point(Vector<double>& x) const override { this->model->initial_primal_point(x); }
+      void initial_dual_point(Vector<double>& multipliers) const override { this->model->initial_dual_point(multipliers); }
+      void postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const override;
+
+      [[nodiscard]] size_t number_objective_gradient_nonzeros() const override { return this->model->number_objective_gradient_nonzeros(); }
+      [[nodiscard]] size_t number_jacobian_nonzeros() const override { return this->model->number_jacobian_nonzeros(); }
+      [[nodiscard]] size_t number_hessian_nonzeros() const override { return this->model->number_hessian_nonzeros(); }
+
+   private:
+      const std::unique_ptr<Model> model;
+      Scaling scaling;
+   };
+
+   inline ScaledModel::ScaledModel(std::unique_ptr<Model> original_model, Iterate& initial_iterate, const Options& options):
+         Model(original_model->name + "_scaled", original_model->number_variables, original_model->number_constraints, original_model->objective_sign),
+         model(std::move(original_model)),
+         scaling(this->model->number_constraints, options.get_double("function_scaling_threshold")) {
+      if (options.get_bool("scale_functions")) {
+         // evaluate the gradients at the current point
+         initial_iterate.evaluate_objective_gradient(*this->model);
+         initial_iterate.evaluate_constraint_jacobian(*this->model);
+         this->scaling.compute(initial_iterate.evaluations.objective_gradient, initial_iterate.evaluations.constraint_jacobian);
+         // scale the gradients
+         scale(initial_iterate.evaluations.objective_gradient, this->scaling.get_objective_scaling());
+         for (size_t constraint_index: Range(this->model->number_constraints)) {
+            scale(initial_iterate.evaluations.constraint_jacobian[constraint_index], this->scaling.get_constraint_scaling(constraint_index));
+         }
+         // since the definition of the constraints changed, reset the evaluation flags
+         initial_iterate.is_objective_gradient_computed = false;
+         initial_iterate.is_constraint_jacobian_computed = false;
+      }
+      // check the scaling factors
+      assert(0 < this->scaling.get_objective_scaling() && "Objective scaling failed.");
+      for ([[maybe_unused]] size_t constraint_index: Range(this->number_constraints)) {
+         assert(0 < this->scaling.get_constraint_scaling(constraint_index) && "Constraint scaling failed.");
       }
    }
-   // check the scaling factors
-   assert(0 < this->scaling.get_objective_scaling() && "Objective scaling failed.");
-   for ([[maybe_unused]] size_t constraint_index: Range(this->number_constraints)) {
-      assert(0 < this->scaling.get_constraint_scaling(constraint_index) && "Constraint scaling failed.");
-   }
-}
-
-inline double ScaledModel::evaluate_objective(const Vector<double>& x) const {
-   const double objective = this->model->evaluate_objective(x);
-   return this->scaling.get_objective_scaling()*objective;
-}
-
-inline void ScaledModel::evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const {
-   this->model->evaluate_objective_gradient(x, gradient);
-   scale(gradient, this->scaling.get_objective_scaling());
-}
-
-inline void ScaledModel::evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const {
-   this->model->evaluate_constraints(x, constraints);
-   for (size_t constraint_index: Range(this->number_constraints)) {
-      constraints[constraint_index] *= this->scaling.get_constraint_scaling(constraint_index);
-   }
-}
 
-inline void ScaledModel::evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const {
-   this->model->evaluate_constraint_gradient(x, constraint_index, gradient);
-   scale(gradient, this->scaling.get_constraint_scaling(constraint_index));
-}
+   inline double ScaledModel::evaluate_objective(const Vector<double>& x) const {
+      const double objective = this->model->evaluate_objective(x);
+      return this->scaling.get_objective_scaling()*objective;
+   }
 
-inline void ScaledModel::evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const {
-   this->model->evaluate_constraint_jacobian(x, constraint_jacobian);
-   for (size_t constraint_index: Range(this->number_constraints)) {
-      scale(constraint_jacobian[constraint_index], this->scaling.get_constraint_scaling(constraint_index));
+   inline void ScaledModel::evaluate_objective_gradient(const Vector<double>& x, SparseVector<double>& gradient) const {
+      this->model->evaluate_objective_gradient(x, gradient);
+      scale(gradient, this->scaling.get_objective_scaling());
    }
-}
-
-inline void ScaledModel::evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
-      SymmetricMatrix<double>& hessian) const {
-   // scale the objective and constraint multipliers
-   const double scaled_objective_multiplier = objective_multiplier*this->scaling.get_objective_scaling();
-   // TODO preallocate this vector
-   // TODO check if the multipliers should be scaled
-   static Vector<double> scaled_multipliers(this->number_constraints);
-   for (size_t constraint_index: Range(this->number_constraints)) {
-      scaled_multipliers[constraint_index] = scaling.get_constraint_scaling(constraint_index) * multipliers[constraint_index];
+
+   inline void ScaledModel::evaluate_constraints(const Vector<double>& x, std::vector<double>& constraints) const {
+      this->model->evaluate_constraints(x, constraints);
+      for (size_t constraint_index: Range(this->number_constraints)) {
+         constraints[constraint_index] *= this->scaling.get_constraint_scaling(constraint_index);
+      }
    }
-   this->model->evaluate_lagrangian_hessian(x, scaled_objective_multiplier, scaled_multipliers, hessian);
-}
 
-inline double ScaledModel::constraint_lower_bound(size_t constraint_index) const {
-   const double lb = this->model->constraint_lower_bound(constraint_index);
-   return this->scaling.get_constraint_scaling(constraint_index)*lb;
-}
+   inline void ScaledModel::evaluate_constraint_gradient(const Vector<double>& x, size_t constraint_index, SparseVector<double>& gradient) const {
+      this->model->evaluate_constraint_gradient(x, constraint_index, gradient);
+      scale(gradient, this->scaling.get_constraint_scaling(constraint_index));
+   }
 
-inline double ScaledModel::constraint_upper_bound(size_t constraint_index) const {
-   const double ub = this->model->constraint_upper_bound(constraint_index);
-   return this->scaling.get_constraint_scaling(constraint_index)*ub;
-}
+   inline void ScaledModel::evaluate_constraint_jacobian(const Vector<double>& x, RectangularMatrix<double>& constraint_jacobian) const {
+      this->model->evaluate_constraint_jacobian(x, constraint_jacobian);
+      for (size_t constraint_index: Range(this->number_constraints)) {
+         scale(constraint_jacobian[constraint_index], this->scaling.get_constraint_scaling(constraint_index));
+      }
+   }
 
-inline void ScaledModel::postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const {
-   this->model->postprocess_solution(iterate, termination_status);
+   inline void ScaledModel::evaluate_lagrangian_hessian(const Vector<double>& x, double objective_multiplier, const Vector<double>& multipliers,
+         SymmetricMatrix<size_t, double>& hessian) const {
+      // scale the objective and constraint multipliers
+      const double scaled_objective_multiplier = objective_multiplier*this->scaling.get_objective_scaling();
+      // TODO preallocate this vector
+      static Vector<double> scaled_multipliers(this->number_constraints);
+      for (size_t constraint_index: Range(this->number_constraints)) {
+         scaled_multipliers[constraint_index] = this->scaling.get_constraint_scaling(constraint_index) * multipliers[constraint_index];
+      }
+      this->model->evaluate_lagrangian_hessian(x, scaled_objective_multiplier, scaled_multipliers, hessian);
+   }
 
-   // unscale the objective value
-   if (iterate.is_objective_computed) {
-      iterate.evaluations.objective /= this->scaling.get_objective_scaling();
+   inline double ScaledModel::constraint_lower_bound(size_t constraint_index) const {
+      return this->scaling.get_constraint_scaling(constraint_index) * this->model->constraint_lower_bound(constraint_index);
    }
 
-   // unscale the constraint multipliers
-   for (size_t constraint_index: Range(iterate.number_constraints)) {
-      iterate.multipliers.constraints[constraint_index] *= this->scaling.get_constraint_scaling(constraint_index) / this->scaling.get_objective_scaling();
+   inline double ScaledModel::constraint_upper_bound(size_t constraint_index) const {
+      return this->scaling.get_constraint_scaling(constraint_index) * this->model->constraint_upper_bound(constraint_index);
    }
 
-   // unscale the bound multipliers
-   for (size_t variable_index: Range(iterate.number_variables)) {
-      iterate.multipliers.lower_bounds[variable_index] /= this->scaling.get_objective_scaling();
-      iterate.multipliers.upper_bounds[variable_index] /= this->scaling.get_objective_scaling();
+   inline void ScaledModel::postprocess_solution(Iterate& iterate, TerminationStatus termination_status) const {
+      this->model->postprocess_solution(iterate, termination_status);
+
+      // unscale the objective value
+      if (iterate.is_objective_computed) {
+         iterate.evaluations.objective /= this->scaling.get_objective_scaling();
+      }
+
+      // unscale the constraint multipliers
+      for (size_t constraint_index: Range(iterate.number_constraints)) {
+         iterate.multipliers.constraints[constraint_index] *= this->scaling.get_constraint_scaling(constraint_index) / this->scaling.get_objective_scaling();
+      }
+
+      // unscale the bound multipliers
+      for (size_t variable_index: Range(iterate.number_variables)) {
+         iterate.multipliers.lower_bounds[variable_index] /= this->scaling.get_objective_scaling();
+         iterate.multipliers.upper_bounds[variable_index] /= this->scaling.get_objective_scaling();
+      }
    }
-}
+} // namespace
 
-#endif // UNO_SCALEDMODEL_H
+#endif // UNO_SCALEDMODEL_H
\ No newline at end of file
diff --git a/uno/optimization/EvaluationErrors.hpp b/uno/optimization/EvaluationErrors.hpp
index 43c250e..e83189c 100644
--- a/uno/optimization/EvaluationErrors.hpp
+++ b/uno/optimization/EvaluationErrors.hpp
@@ -4,20 +4,22 @@
 #ifndef UNO_EVALUATIONERRORS_H
 #define UNO_EVALUATIONERRORS_H
 
-struct EvaluationError : public std::exception {
-   [[nodiscard]] const char* what() const noexcept override = 0;
-};
+namespace uno {
+   struct EvaluationError : public std::exception {
+      [[nodiscard]] const char* what() const noexcept override = 0;
+   };
 
-struct GradientEvaluationError : EvaluationError {
-   [[nodiscard]] const char* what() const noexcept override {
-      return "A numerical error was encountered while evaluating a gradient\n";
-   }
-};
+   struct GradientEvaluationError : EvaluationError {
+      [[nodiscard]] const char* what() const noexcept override {
+         return "A numerical error was encountered while evaluating a gradient\n";
+      }
+   };
 
-struct FunctionEvaluationError : EvaluationError {
-   [[nodiscard]] const char* what() const noexcept override {
-      return "A numerical error was encountered while evaluating a function\n";
-   }
-};
+   struct FunctionEvaluationError : EvaluationError {
+      [[nodiscard]] const char* what() const noexcept override {
+         return "A numerical error was encountered while evaluating a function\n";
+      }
+   };
+} // namespace
 
 #endif // UNO_EVALUATIONERRORS_H
diff --git a/uno/optimization/Evaluations.hpp b/uno/optimization/Evaluations.hpp
index 948d661..2b01d5c 100644
--- a/uno/optimization/Evaluations.hpp
+++ b/uno/optimization/Evaluations.hpp
@@ -9,17 +9,19 @@
 #include "linear_algebra/SparseVector.hpp"
 #include "tools/Infinity.hpp"
 
-struct Evaluations {
-   double objective{INF<double>}; /*!< Objective value */
-   std::vector<double> constraints; /*!< Constraint values (size \f$m)\f$ */
-   SparseVector<double> objective_gradient; /*!< Sparse Jacobian of the objective */
-   RectangularMatrix<double> constraint_jacobian; /*!< Sparse Jacobian of the constraints */
+namespace uno {
+   struct Evaluations {
+      double objective{INF<double>}; /*!< Objective value */
+      std::vector<double> constraints; /*!< Constraint values (size \f$m)\f$ */
+      SparseVector<double> objective_gradient; /*!< Sparse Jacobian of the objective */
+      RectangularMatrix<double> constraint_jacobian; /*!< Sparse Jacobian of the constraints */
 
-   Evaluations(size_t number_variables, size_t number_constraints):
-         constraints(number_constraints),
-         objective_gradient(number_variables),
-         constraint_jacobian(number_constraints, number_variables) {
-   }
-};
+      Evaluations(size_t number_variables, size_t number_constraints):
+            constraints(number_constraints),
+            objective_gradient(number_variables),
+            constraint_jacobian(number_constraints, number_variables) {
+      }
+   };
+} // namespace
 
 #endif // UNO_EVALUATIONS_H
diff --git a/uno/optimization/Iterate.cpp b/uno/optimization/Iterate.cpp
index 5e6ca44..1ed9f7a 100644
--- a/uno/optimization/Iterate.cpp
+++ b/uno/optimization/Iterate.cpp
@@ -8,100 +8,103 @@
 #include "optimization/EvaluationErrors.hpp"
 #include "tools/Logger.hpp"
 
-size_t Iterate::number_eval_objective = 0;
-size_t Iterate::number_eval_constraints = 0;
-size_t Iterate::number_eval_objective_gradient = 0;
-size_t Iterate::number_eval_jacobian = 0;
+namespace uno {
+   size_t Iterate::number_eval_objective = 0;
+   size_t Iterate::number_eval_constraints = 0;
+   size_t Iterate::number_eval_objective_gradient = 0;
+   size_t Iterate::number_eval_jacobian = 0;
 
-Iterate::Iterate(size_t number_variables, size_t number_constraints) :
-      number_variables(number_variables), number_constraints(number_constraints),
-      primals(number_variables), multipliers(number_variables, number_constraints), feasibility_multipliers(number_variables, number_constraints),
-      evaluations(number_variables, number_constraints),
-      lagrangian_gradient(number_variables) {
-}
+   Iterate::Iterate(size_t number_variables, size_t number_constraints) :
+         number_variables(number_variables), number_constraints(number_constraints),
+         primals(number_variables), multipliers(number_variables, number_constraints), feasibility_multipliers(number_variables, number_constraints),
+         evaluations(number_variables, number_constraints), residuals(number_variables), feasibility_residuals(number_variables) {
+   }
 
-void Iterate::evaluate_objective(const Model& model) {
-   if (not this->is_objective_computed) {
-      // evaluate the objective
-      this->evaluations.objective = model.evaluate_objective(this->primals);
-      Iterate::number_eval_objective++;
-      if (not is_finite(this->evaluations.objective)) {
-         throw FunctionEvaluationError();
+   void Iterate::evaluate_objective(const Model& model) {
+      if (not this->is_objective_computed) {
+         // evaluate the objective
+         this->evaluations.objective = model.evaluate_objective(this->primals);
+         Iterate::number_eval_objective++;
+         if (not is_finite(this->evaluations.objective)) {
+            throw FunctionEvaluationError();
+         }
+         this->is_objective_computed = true;
       }
-      this->is_objective_computed = true;
    }
-}
 
-void Iterate::evaluate_constraints(const Model& model) {
-   if (not this->are_constraints_computed) {
-      if (model.is_constrained()) {
-         // evaluate the constraints
-         model.evaluate_constraints(this->primals, this->evaluations.constraints);
-         Iterate::number_eval_constraints++;
-         // check finiteness
-         if (std::any_of(this->evaluations.constraints.cbegin(), this->evaluations.constraints.cend(), [](double constraint_j) {
-            return not is_finite(constraint_j);
-         })) {
-            throw FunctionEvaluationError();
+   void Iterate::evaluate_constraints(const Model& model) {
+      if (not this->are_constraints_computed) {
+         if (model.is_constrained()) {
+            // evaluate the constraints
+            model.evaluate_constraints(this->primals, this->evaluations.constraints);
+            Iterate::number_eval_constraints++;
+            // check finiteness
+            if (std::any_of(this->evaluations.constraints.cbegin(), this->evaluations.constraints.cend(), [](double constraint_j) {
+               return not is_finite(constraint_j);
+            })) {
+               throw FunctionEvaluationError();
+            }
          }
+         this->are_constraints_computed = true;
       }
-      this->are_constraints_computed = true;
    }
-}
 
-void Iterate::evaluate_objective_gradient(const Model& model) {
-   if (not this->is_objective_gradient_computed) {
-      this->evaluations.objective_gradient.clear();
-      // evaluate the objective gradient
-      model.evaluate_objective_gradient(this->primals, this->evaluations.objective_gradient);
-      this->is_objective_gradient_computed = true;
-      Iterate::number_eval_objective_gradient++;
+   void Iterate::evaluate_objective_gradient(const Model& model) {
+      if (not this->is_objective_gradient_computed) {
+         this->evaluations.objective_gradient.clear();
+         // evaluate the objective gradient
+         model.evaluate_objective_gradient(this->primals, this->evaluations.objective_gradient);
+         this->is_objective_gradient_computed = true;
+         Iterate::number_eval_objective_gradient++;
+      }
    }
-}
 
-void Iterate::evaluate_constraint_jacobian(const Model& model) {
-   if (not this->is_constraint_jacobian_computed) {
-      this->evaluations.constraint_jacobian.clear();
-      if (model.is_constrained()) {
-         model.evaluate_constraint_jacobian(this->primals, this->evaluations.constraint_jacobian);
-         Iterate::number_eval_jacobian++;
+   void Iterate::evaluate_constraint_jacobian(const Model& model) {
+      if (not this->is_constraint_jacobian_computed) {
+         this->evaluations.constraint_jacobian.clear();
+         if (model.is_constrained()) {
+            model.evaluate_constraint_jacobian(this->primals, this->evaluations.constraint_jacobian);
+            Iterate::number_eval_jacobian++;
+         }
+         this->is_constraint_jacobian_computed = true;
       }
-      this->is_constraint_jacobian_computed = true;
    }
-}
 
-void Iterate::set_number_variables(size_t new_number_variables) {
-   this->number_variables = new_number_variables;
-   this->primals.resize(new_number_variables);
-   this->multipliers.lower_bounds.resize(new_number_variables);
-   this->multipliers.upper_bounds.resize(new_number_variables);
-   this->feasibility_multipliers.lower_bounds.resize(new_number_variables);
-   this->feasibility_multipliers.upper_bounds.resize(new_number_variables);
-   this->evaluations.objective_gradient.reserve(new_number_variables);
-   this->lagrangian_gradient.resize(new_number_variables);
-}
+   void Iterate::set_number_variables(size_t new_number_variables) {
+      this->number_variables = new_number_variables;
+      this->primals.resize(new_number_variables);
+      this->multipliers.lower_bounds.resize(new_number_variables);
+      this->multipliers.upper_bounds.resize(new_number_variables);
+      this->feasibility_multipliers.lower_bounds.resize(new_number_variables);
+      this->feasibility_multipliers.upper_bounds.resize(new_number_variables);
+      this->evaluations.objective_gradient.reserve(new_number_variables);
+      this->residuals.lagrangian_gradient.resize(new_number_variables);
+      this->feasibility_residuals.lagrangian_gradient.resize(new_number_variables);
+   }
+
+   std::ostream& operator<<(std::ostream& stream, const Iterate& iterate) {
+      stream << "Primal variables: " << iterate.primals << '\n';
+      stream << "            ┌ Constraint: " << iterate.multipliers.constraints << '\n';
+      stream << "Multipliers │ Lower bound: " << iterate.multipliers.lower_bounds << '\n';
+      stream << "            â”” Upper bound: " << iterate.multipliers.upper_bounds << '\n';
+      stream << "                        ┌ Constraint: " << iterate.feasibility_multipliers.constraints << '\n';
+      stream << "Feasibility multipliers │ Lower bound: " << iterate.feasibility_multipliers.lower_bounds << '\n';
+      stream << "                        â”” Upper bound: " << iterate.feasibility_multipliers.upper_bounds << '\n';
+      stream << "Objective value: " << iterate.evaluations.objective << '\n';
 
-std::ostream& operator<<(std::ostream& stream, const Iterate& iterate) {
-   stream << "Primal variables: " << iterate.primals << '\n';
-   stream << "            ┌ Constraint: " << iterate.multipliers.constraints << '\n';
-   stream << "Multipliers │ Lower bound: " << iterate.multipliers.lower_bounds << '\n';
-   stream << "            â”” Upper bound: " << iterate.multipliers.upper_bounds << '\n';
-   stream << "                        ┌ Constraint: " << iterate.feasibility_multipliers.constraints << '\n';
-   stream << "Feasibility multipliers │ Lower bound: " << iterate.feasibility_multipliers.lower_bounds << '\n';
-   stream << "                        â”” Upper bound: " << iterate.feasibility_multipliers.upper_bounds << '\n';
-   stream << "Objective value: " << iterate.evaluations.objective << '\n';
+      stream << "          ┌ Stationarity: " << iterate.residuals.stationarity << '\n';
+      stream << "Residuals │ Primal feasibility: " << iterate.residuals.primal_feasibility << '\n';
+      stream << "          │ Complementarity: " << iterate.residuals.complementarity << '\n';
+      stream << "          â”” Lagrangian gradient: " << iterate.residuals.lagrangian_gradient;
+      stream << "Feasibility residuals ┌ Stationarity: " << iterate.feasibility_residuals.stationarity << '\n';
+      stream << "                      │ Complementarity: " << iterate.feasibility_residuals.complementarity << '\n';
+      stream << "                      â”” Lagrangian gradient: " << iterate.feasibility_residuals.lagrangian_gradient;
 
-   stream << "          ┌ Stationarity: " << iterate.residuals.KKT_stationarity << '\n';
-   stream << "          │ FJ stationarity: " << iterate.residuals.FJ_stationarity << '\n';
-   stream << "Residuals │ Feasibility stationarity: " << iterate.residuals.feasibility_stationarity << '\n';
-   stream << "          │ Constraint violation: " << iterate.residuals.infeasibility << '\n';
-   stream << "          │ Complementarity: " << iterate.residuals.complementarity << '\n';
-   stream << "          â”” Feasibility complementarity: " << iterate.residuals.feasibility_complementarity << '\n';
+      stream << "                  ┌ Infeasibility: " << iterate.progress.infeasibility << '\n';
+      stream << "Progress measures │ Optimality: " << iterate.progress.objective(1.) << '\n';
+      stream << "                  â”” Auxiliary terms: " << iterate.progress.auxiliary << '\n';
 
-   stream << "                  ┌ Infeasibility: " << iterate.progress.infeasibility << '\n';
-   stream << "Progress measures │ Optimality: " << iterate.progress.objective(1.) << '\n';
-   stream << "                  â”” Auxiliary terms: " << iterate.progress.auxiliary << '\n';
 
-   stream << "Lagrangian gradient: " << iterate.lagrangian_gradient;
-   return stream;
-}
\ No newline at end of file
+      return stream;
+   }
+} // namespace
\ No newline at end of file
diff --git a/uno/optimization/Iterate.hpp b/uno/optimization/Iterate.hpp
index 0802ade..54b9f2a 100644
--- a/uno/optimization/Iterate.hpp
+++ b/uno/optimization/Iterate.hpp
@@ -11,50 +11,54 @@
 #include "optimization/Multipliers.hpp"
 #include "optimization/PrimalDualResiduals.hpp"
 
-// forward declaration
-class Model;
-
-class Iterate {
-public:
-   Iterate(size_t number_variables, size_t number_constraints);
-
-   size_t number_variables;
-   size_t number_constraints;
-   Vector<double> primals;
-   Multipliers multipliers; /*!< \f$\mathbb{R}^n\f$ Lagrange multipliers/dual variables */
-   Multipliers feasibility_multipliers; /*!< \f$\mathbb{R}^n\f$ Lagrange multipliers/dual variables */
-   double objective_multiplier{1.};
-
-   // evaluations
-   Evaluations evaluations;
-   static size_t number_eval_objective;
-   static size_t number_eval_constraints;
-   static size_t number_eval_objective_gradient;
-   static size_t number_eval_jacobian;
-   // lazy evaluation flags
-   bool is_objective_computed{false};
-   bool are_constraints_computed{false};
-   bool is_objective_gradient_computed{false}; /*!< Flag that indicates if the objective gradient has already been computed */
-   bool is_constraint_jacobian_computed{false}; /*!< Flag that indicates if the constraint Jacobian has already been computed */
-
-   // primal-dual residuals
-   PrimalDualResiduals residuals{};
-   LagrangianGradient<double> lagrangian_gradient;
-
-   // measures of progress (infeasibility, objective, auxiliary)
-   ProgressMeasures progress{INF<double>, {}, INF<double>};
-
-   // status
-   TerminationStatus status{TerminationStatus::NOT_OPTIMAL};
-
-   void evaluate_objective(const Model& model);
-   void evaluate_constraints(const Model& model);
-   void evaluate_objective_gradient(const Model& model);
-   void evaluate_constraint_jacobian(const Model& model);
-
-   void set_number_variables(size_t number_variables);
-
-   friend std::ostream& operator<<(std::ostream& stream, const Iterate& iterate);
-};
+namespace uno {
+   // forward declaration
+   class Model;
+
+   class Iterate {
+   public:
+      Iterate(size_t number_variables, size_t number_constraints);
+      Iterate(Iterate&& other) noexcept = default;
+      Iterate& operator=(Iterate&& other) noexcept = default;
+
+      size_t number_variables;
+      size_t number_constraints;
+      Vector<double> primals;
+      Multipliers multipliers; /*!< \f$\mathbb{R}^n\f$ Lagrange multipliers/dual variables */
+      Multipliers feasibility_multipliers; /*!< \f$\mathbb{R}^n\f$ Lagrange multipliers/dual variables */
+      double objective_multiplier{1.};
+
+      // evaluations
+      Evaluations evaluations;
+      static size_t number_eval_objective;
+      static size_t number_eval_constraints;
+      static size_t number_eval_objective_gradient;
+      static size_t number_eval_jacobian;
+      // lazy evaluation flags
+      bool is_objective_computed{false};
+      bool are_constraints_computed{false};
+      bool is_objective_gradient_computed{false}; /*!< Flag that indicates if the objective gradient has already been computed */
+      bool is_constraint_jacobian_computed{false}; /*!< Flag that indicates if the constraint Jacobian has already been computed */
+
+      // primal-dual residuals
+      PrimalDualResiduals residuals;
+      PrimalDualResiduals feasibility_residuals;
+
+      // measures of progress (infeasibility, objective, auxiliary)
+      ProgressMeasures progress{INF<double>, {}, INF<double>};
+
+      // status
+      TerminationStatus status{TerminationStatus::NOT_OPTIMAL};
+
+      void evaluate_objective(const Model& model);
+      void evaluate_constraints(const Model& model);
+      void evaluate_objective_gradient(const Model& model);
+      void evaluate_constraint_jacobian(const Model& model);
+
+      void set_number_variables(size_t number_variables);
+
+      friend std::ostream& operator<<(std::ostream& stream, const Iterate& iterate);
+   };
+} // namespace
 
 #endif // UNO_ITERATE_H
diff --git a/uno/optimization/LagrangianGradient.hpp b/uno/optimization/LagrangianGradient.hpp
index 6e09130..45af70c 100644
--- a/uno/optimization/LagrangianGradient.hpp
+++ b/uno/optimization/LagrangianGradient.hpp
@@ -7,55 +7,57 @@
 #include <iostream>
 #include "linear_algebra/Vector.hpp"
 
-// Gradient of the Lagrangian
-// Keep the objective and constraint contributions separate. This helps:
-// - computing the KKT and FJ stationarity conditions
-// - forming quasi-Newton matrices with an objective multiplier
-template <typename ElementType>
-class LagrangianGradient {
-public:
-   Vector<ElementType> objective_contribution{};
-   Vector<ElementType> constraints_contribution{};
-
-   using value_type = ElementType;
-
-   explicit LagrangianGradient(size_t number_variables);
-   [[nodiscard]] size_t size() const;
-   [[nodiscard]] ElementType operator[](size_t variable_index) const;
-   void resize(size_t number_variables);
-};
-
-template <typename ElementType>
-LagrangianGradient<ElementType>::LagrangianGradient(size_t number_variables) :
-      objective_contribution(number_variables),
-      constraints_contribution(number_variables) {
-}
-
-template <typename ElementType>
-size_t LagrangianGradient<ElementType>::size() const {
-   return this->objective_contribution.size();
-}
-
-// access i-th element
-template <typename ElementType>
-ElementType LagrangianGradient<ElementType>::operator[](size_t variable_index) const {
-   return this->objective_contribution[variable_index] + this->constraints_contribution[variable_index];
-}
-
-template <typename ElementType>
-void LagrangianGradient<ElementType>::resize(size_t number_variables) {
-   this->objective_contribution.resize(number_variables);
-   this->constraints_contribution.resize(number_variables);
-}
-
-template <typename ElementType>
-std::ostream& operator<<(std::ostream& stream, const LagrangianGradient<ElementType>& gradient) {
-   for (size_t variable_index: Range(gradient.constraints_contribution.size())) {
-      stream << gradient[variable_index] << ' ';
+namespace uno {
+   // Gradient of the Lagrangian
+   // Keep the objective and constraint contributions separate. This helps:
+   // - computing the KKT and FJ stationarity conditions
+   // - forming quasi-Newton matrices with an objective multiplier
+   template <typename ElementType>
+   class LagrangianGradient {
+   public:
+      Vector<ElementType> objective_contribution{};
+      Vector<ElementType> constraints_contribution{};
+
+      using value_type = ElementType;
+
+      explicit LagrangianGradient(size_t number_variables);
+      [[nodiscard]] size_t size() const;
+      [[nodiscard]] ElementType operator[](size_t variable_index) const;
+      void resize(size_t number_variables);
+   };
+
+   template <typename ElementType>
+   LagrangianGradient<ElementType>::LagrangianGradient(size_t number_variables) :
+         objective_contribution(number_variables),
+         constraints_contribution(number_variables) {
+   }
+
+   template <typename ElementType>
+   size_t LagrangianGradient<ElementType>::size() const {
+      return this->objective_contribution.size();
+   }
+
+   // access i-th element
+   template <typename ElementType>
+   ElementType LagrangianGradient<ElementType>::operator[](size_t variable_index) const {
+      return this->objective_contribution[variable_index] + this->constraints_contribution[variable_index];
+   }
+
+   template <typename ElementType>
+   void LagrangianGradient<ElementType>::resize(size_t number_variables) {
+      this->objective_contribution.resize(number_variables);
+      this->constraints_contribution.resize(number_variables);
+   }
+
+   template <typename ElementType>
+   std::ostream& operator<<(std::ostream& stream, const LagrangianGradient<ElementType>& gradient) {
+      for (size_t variable_index: Range(gradient.constraints_contribution.size())) {
+         stream << gradient[variable_index] << ' ';
+      }
+      stream << '\n';
+      return stream;
    }
-   stream << '\n';
-   return stream;
-}
+} // namespace
 
 #endif // UNO_LAGRANGIANGRADIENT_H
 
diff --git a/uno/optimization/Multipliers.hpp b/uno/optimization/Multipliers.hpp
index 9413872..5ce11d2 100644
--- a/uno/optimization/Multipliers.hpp
+++ b/uno/optimization/Multipliers.hpp
@@ -6,40 +6,46 @@
 
 #include "linear_algebra/Vector.hpp"
 
-struct Multipliers {
-   Vector<double> lower_bounds{}; /*!< Multipliers of the lower bound constraints */
-   Vector<double> upper_bounds{}; /*!< Multipliers of the lower bound constraints */
-   Vector<double> constraints{}; /*!< Multipliers of the general constraints */
-
-   Multipliers(size_t number_variables, size_t number_constraints);
-   void reset();
-   [[nodiscard]] bool not_all_zero(size_t number_variables, double tolerance) const;
-};
-
-inline Multipliers::Multipliers(size_t number_variables, size_t number_constraints) : lower_bounds(number_variables),
-      upper_bounds(number_variables), constraints(number_constraints) {
-}
-
-inline void Multipliers::reset() {
-   this->constraints.fill(0.);
-   this->lower_bounds.fill(0.);
-   this->upper_bounds.fill(0.);
-}
-
-inline bool Multipliers::not_all_zero(size_t number_variables, double tolerance) const {
-   // constraint multipliers
-   for (double multiplier_j: this->constraints) {
-      if (tolerance < std::abs(multiplier_j)) {
-         return true;
-      }
+namespace uno {
+   struct Multipliers {
+      Vector<double> lower_bounds{}; /*!< Multipliers of the lower bound constraints */
+      Vector<double> upper_bounds{}; /*!< Multipliers of the lower bound constraints */
+      Vector<double> constraints{}; /*!< Multipliers of the general constraints */
+
+      Multipliers(size_t number_variables, size_t number_constraints);
+      Multipliers(Multipliers&& other) noexcept = default;
+      Multipliers& operator=(const Multipliers& other) noexcept = default;
+      Multipliers& operator=(Multipliers&& other) noexcept = default;
+
+      void reset();
+      [[nodiscard]] bool not_all_zero(size_t number_variables, double tolerance) const;
+   };
+
+   inline Multipliers::Multipliers(size_t number_variables, size_t number_constraints) : lower_bounds(number_variables),
+         upper_bounds(number_variables), constraints(number_constraints) {
+   }
+
+   inline void Multipliers::reset() {
+      this->constraints.fill(0.);
+      this->lower_bounds.fill(0.);
+      this->upper_bounds.fill(0.);
    }
-   // bound multipliers
-   for (size_t variable_index: Range(number_variables)) {
-      if (tolerance < std::abs(this->lower_bounds[variable_index] + this->upper_bounds[variable_index])) {
-         return true;
+
+   inline bool Multipliers::not_all_zero(size_t number_variables, double tolerance) const {
+      // constraint multipliers
+      for (double multiplier_j: this->constraints) {
+         if (tolerance < std::abs(multiplier_j)) {
+            return true;
+         }
+      }
+      // bound multipliers
+      for (size_t variable_index: Range(number_variables)) {
+         if (tolerance < std::abs(this->lower_bounds[variable_index] + this->upper_bounds[variable_index])) {
+            return true;
+         }
       }
+      return false;
    }
-   return false;
-}
+} // namespace
 
 #endif // UNO_MULTIPLIERS_H
diff --git a/uno/optimization/PrimalDualResiduals.hpp b/uno/optimization/PrimalDualResiduals.hpp
index 7be5a13..bca2589 100644
--- a/uno/optimization/PrimalDualResiduals.hpp
+++ b/uno/optimization/PrimalDualResiduals.hpp
@@ -6,16 +6,20 @@
 
 #include "tools/Infinity.hpp"
 
-class PrimalDualResiduals {
-public:
-   double KKT_stationarity{INF<double>};
-   double FJ_stationarity{INF<double>};
-   double feasibility_stationarity{INF<double>};
-   double infeasibility{INF<double>};
-   double complementarity{INF<double>};
-   double feasibility_complementarity{INF<double>};
-   double stationarity_scaling{INF<double>};
-   double complementarity_scaling{INF<double>};
-};
+namespace uno {
+   class PrimalDualResiduals {
+   public:
+      explicit PrimalDualResiduals(size_t number_variables): lagrangian_gradient(number_variables) { }
 
-#endif // UNO_PRIMALDUALRESIDUALS_H
\ No newline at end of file
+      double stationarity{INF<double>};
+      double primal_feasibility{INF<double>};
+      double complementarity{INF<double>};
+
+      double stationarity_scaling{INF<double>};
+      double complementarity_scaling{INF<double>};
+
+      LagrangianGradient<double> lagrangian_gradient;
+   };
+} // namespace
+
+#endif // UNO_PRIMALDUALRESIDUALS_H
diff --git a/uno/optimization/Result.cpp b/uno/optimization/Result.cpp
index bdca1ec..417476e 100644
--- a/uno/optimization/Result.cpp
+++ b/uno/optimization/Result.cpp
@@ -6,58 +6,66 @@
 #include "TerminationStatus.hpp"
 #include "symbolic/VectorView.hpp"
 
-void Result::print(bool print_primal_dual_solution) const {
-   std::cout << "Status:\t\t\t\t\t";
-   if (this->solution.status == TerminationStatus::FEASIBLE_KKT_POINT) {
-      std::cout << "Converged with feasible KKT point\n";
-   }
-   else if (this->solution.status == TerminationStatus::FEASIBLE_FJ_POINT) {
-      std::cout << "Converged with feasible FJ point\n";
-   }
-   else if (this->solution.status == TerminationStatus::INFEASIBLE_STATIONARY_POINT) {
-      std::cout << "Converged with infeasible stationary point\n";
-   }
-   else if (this->solution.status == TerminationStatus::FEASIBLE_SMALL_STEP) {
-      std::cout << "Terminated with feasible small step\n";
-   }
-   else if (this->solution.status == TerminationStatus::INFEASIBLE_SMALL_STEP) {
-      std::cout << "Failed with infeasible small step\n";
-   }
-   else if (this->solution.status == TerminationStatus::UNBOUNDED) {
-      std::cout << "Terminated with unbounded problem\n";
-   }
-   else {
-      std::cout << "Failed with suboptimal point\n";
-   }
+namespace uno {
+   void Result::print(bool print_primal_dual_solution) const {
+      std::cout << "Status:\t\t\t\t\t";
+      if (this->solution.status == TerminationStatus::FEASIBLE_KKT_POINT) {
+         std::cout << "Converged with feasible KKT point\n";
+      }
+      else if (this->solution.status == TerminationStatus::FEASIBLE_FJ_POINT) {
+         std::cout << "Converged with feasible FJ point\n";
+      }
+      else if (this->solution.status == TerminationStatus::INFEASIBLE_STATIONARY_POINT) {
+         std::cout << "Converged with infeasible stationary point\n";
+      }
+      else if (this->solution.status == TerminationStatus::FEASIBLE_SMALL_STEP) {
+         std::cout << "Terminated with feasible small step\n";
+      }
+      else if (this->solution.status == TerminationStatus::INFEASIBLE_SMALL_STEP) {
+         std::cout << "Failed with infeasible small step\n";
+      }
+      else if (this->solution.status == TerminationStatus::UNBOUNDED) {
+         std::cout << "Terminated with unbounded problem\n";
+      }
+      else {
+         std::cout << "Failed with suboptimal point\n";
+      }
+
+      std::cout << "Objective value:\t\t\t" << std::defaultfloat << std::setprecision(7) << this->solution.evaluations.objective << '\n';
 
-   std::cout << "Objective value:\t\t\t" << std::defaultfloat << std::setprecision(7) << this->solution.evaluations.objective << '\n';
+      std::cout << "┌ Stationarity residual:\t\t" << this->solution.residuals.stationarity << '\n';
+      std::cout << "│ Primal feasibility residual:\t\t" << this->solution.residuals.primal_feasibility << '\n';
+      std::cout << "â”” Complementarity residual:\t\t" << this->solution.residuals.complementarity << '\n';
 
-   std::cout << "┌ Stationarity residual:\t\t" << this->solution.residuals.KKT_stationarity << '\n';
-   std::cout << "│ Feasibility stationarity residual:\t" << this->solution.residuals.feasibility_stationarity << '\n';
-   std::cout << "│ Constraint violation:\t\t\t" << this->solution.residuals.infeasibility << '\n';
-   std::cout << "│ Complementarity residual:\t\t" << this->solution.residuals.complementarity << '\n';
-   std::cout << "â”” Feasibility complementarity residual:\t" << this->solution.residuals.feasibility_complementarity << '\n';
+      std::cout << "┌ Feasibility stationarity residual:\t" << this->solution.residuals.stationarity << '\n';
+      std::cout << "â”” Feasibility complementarity residual:\t" << this->solution.residuals.complementarity << '\n';
 
-   std::cout << "┌ Infeasibility measure:\t\t" << this->solution.progress.infeasibility << '\n';
-   std::cout << "│ Objective measure:\t\t\t" << this->solution.progress.objective(1.) << '\n';
-   std::cout << "â”” Auxiliary measure:\t\t\t" << this->solution.progress.auxiliary << '\n';
+      std::cout << "┌ Infeasibility measure:\t\t" << this->solution.progress.infeasibility << '\n';
+      std::cout << "│ Objective measure:\t\t\t" << this->solution.progress.objective(1.) << '\n';
+      std::cout << "â”” Auxiliary measure:\t\t\t" << this->solution.progress.auxiliary << '\n';
 
-   if (print_primal_dual_solution) {
-      std::cout << "Primal solution:\t\t\t"; print_vector(std::cout, view(this->solution.primals, 0, this->number_variables));
-      if (not this->solution.multipliers.constraints.empty()) {
-         std::cout << "Constraint multipliers:\t\t\t"; print_vector(std::cout, this->solution.multipliers.constraints);
+      if (print_primal_dual_solution) {
+         std::cout << "Primal solution:\t\t\t"; print_vector(std::cout, view(this->solution.primals, 0, this->number_variables));
+         std::cout << "┌ Constraint multipliers:\t\t"; print_vector(std::cout, this->solution.multipliers.constraints);
+         std::cout << "│ Lower bound multipliers:\t\t"; print_vector(std::cout, view(this->solution.multipliers.lower_bounds, 0,
+               this->number_variables));
+         std::cout << "â”” Upper bound multipliers:\t\t"; print_vector(std::cout, view(this->solution.multipliers.upper_bounds, 0,
+               this->number_variables));
+         std::cout << "┌ Constraint feasibility multipliers:\t"; print_vector(std::cout, this->solution.feasibility_multipliers.constraints);
+         std::cout << "│ Lower bound feasibility multipliers:\t"; print_vector(std::cout, view(this->solution.feasibility_multipliers.lower_bounds, 0,
+               this->number_variables));
+         std::cout << "â”” Upper bound feasibility multipliers:\t"; print_vector(std::cout, view(this->solution.feasibility_multipliers.upper_bounds, 0,
+               this->number_variables));
+         std::cout << "Objective multiplier:\t\t\t" << this->solution.objective_multiplier << '\n';
       }
-      std::cout << "Lower bound multipliers:\t\t"; print_vector(std::cout, view(this->solution.multipliers.lower_bounds, 0, this->number_variables));
-      std::cout << "Upper bound multipliers:\t\t"; print_vector(std::cout, view(this->solution.multipliers.upper_bounds, 0, this->number_variables));
-      std::cout << "Objective multiplier:\t\t\t" << this->solution.objective_multiplier << '\n';
-   }
 
-   std::cout << "CPU time:\t\t\t\t" << this->cpu_time << "s\n";
-   std::cout << "Iterations:\t\t\t\t"	 << this->iteration << '\n';
-   std::cout << "Objective evaluations:\t\t\t" << this->objective_evaluations << '\n';
-   std::cout << "Constraints evaluations:\t\t" << this->constraint_evaluations << '\n';
-   std::cout << "Objective gradient evaluations:\t\t" << this->objective_gradient_evaluations << '\n';
-   std::cout << "Jacobian evaluations:\t\t\t" << this->jacobian_evaluations << '\n';
-   std::cout << "Hessian evaluations:\t\t\t" << this->hessian_evaluations << '\n';
-   std::cout << "Number of subproblems solved:\t\t" << this->number_subproblems_solved << '\n';
-}
+      std::cout << "CPU time:\t\t\t\t" << this->cpu_time << "s\n";
+      std::cout << "Iterations:\t\t\t\t"	 << this->iteration << '\n';
+      std::cout << "Objective evaluations:\t\t\t" << this->objective_evaluations << '\n';
+      std::cout << "Constraints evaluations:\t\t" << this->constraint_evaluations << '\n';
+      std::cout << "Objective gradient evaluations:\t\t" << this->objective_gradient_evaluations << '\n';
+      std::cout << "Jacobian evaluations:\t\t\t" << this->jacobian_evaluations << '\n';
+      std::cout << "Hessian evaluations:\t\t\t" << this->hessian_evaluations << '\n';
+      std::cout << "Number of subproblems solved:\t\t" << this->number_subproblems_solved << '\n';
+   }
+} // namespace
diff --git a/uno/optimization/Result.hpp b/uno/optimization/Result.hpp
index 4ec7788..2daf17e 100644
--- a/uno/optimization/Result.hpp
+++ b/uno/optimization/Result.hpp
@@ -6,22 +6,24 @@
 
 #include "Iterate.hpp"
 
-struct Result {
-   Result() = delete;
+namespace uno {
+   struct Result {
+      Result() = delete;
 
-   Iterate solution;
-   size_t number_variables;
-   size_t number_constraints;
-   size_t iteration;
-   double cpu_time;
-   size_t objective_evaluations;
-   size_t constraint_evaluations;
-   size_t objective_gradient_evaluations;
-   size_t jacobian_evaluations;
-   size_t hessian_evaluations;
-   size_t number_subproblems_solved;
+      Iterate solution;
+      size_t number_variables;
+      size_t number_constraints;
+      size_t iteration;
+      double cpu_time;
+      size_t objective_evaluations;
+      size_t constraint_evaluations;
+      size_t objective_gradient_evaluations;
+      size_t jacobian_evaluations;
+      size_t hessian_evaluations;
+      size_t number_subproblems_solved;
 
-   void print(bool print_primal_dual_solution) const;
-};
+      void print(bool print_primal_dual_solution) const;
+   };
+} // namespace
 
-#endif // UNO_RESULT_H
\ No newline at end of file
+#endif // UNO_RESULT_H
diff --git a/uno/optimization/TerminationStatus.hpp b/uno/optimization/TerminationStatus.hpp
index 0632224..4afb8f8 100644
--- a/uno/optimization/TerminationStatus.hpp
+++ b/uno/optimization/TerminationStatus.hpp
@@ -4,14 +4,16 @@
 #ifndef UNO_TERMINATIONSTATUS_H
 #define UNO_TERMINATIONSTATUS_H
 
-enum class TerminationStatus {
-   NOT_OPTIMAL = 0,
-   FEASIBLE_KKT_POINT, /* feasible stationary point */
-   FEASIBLE_FJ_POINT, /* stationary point without constraint qualification */
-   INFEASIBLE_STATIONARY_POINT, /* infeasible stationary point of constraint violation */
-   FEASIBLE_SMALL_STEP,
-   INFEASIBLE_SMALL_STEP,
-   UNBOUNDED
-};
+namespace uno {
+   enum class TerminationStatus {
+      NOT_OPTIMAL = 0,
+      FEASIBLE_KKT_POINT, /* feasible stationary point */
+      FEASIBLE_FJ_POINT, /* stationary point without constraint qualification */
+      INFEASIBLE_STATIONARY_POINT, /* infeasible stationary point of constraint violation */
+      FEASIBLE_SMALL_STEP,
+      INFEASIBLE_SMALL_STEP,
+      UNBOUNDED
+   };
+} // namespace
 
 #endif // UNO_TERMINATIONSTATUS_H
diff --git a/uno/optimization/WarmstartInformation.hpp b/uno/optimization/WarmstartInformation.hpp
index 21dd640..ca2f891 100644
--- a/uno/optimization/WarmstartInformation.hpp
+++ b/uno/optimization/WarmstartInformation.hpp
@@ -4,58 +4,60 @@
 #ifndef UNO_WARMSTARTINFORMATION_H
 #define UNO_WARMSTARTINFORMATION_H
 
-struct WarmstartInformation {
-   bool objective_changed{false};
-   bool constraints_changed{false};
-   bool constraint_bounds_changed{false};
-   bool variable_bounds_changed{false};
-   bool problem_changed{false};
-
-   void display() const;
-   void set_cold_start();
-   void set_hot_start();
-   void only_objective_changed();
-   void only_variable_bounds_changed();
-};
-
-inline void WarmstartInformation::display() const {
-   std::cout << "Objective: " << std::boolalpha << this->objective_changed << '\n';
-   std::cout << "Constraints: " << std::boolalpha << this->constraints_changed << '\n';
-   std::cout << "Constraint bounds: " << std::boolalpha << this->constraint_bounds_changed << '\n';
-   std::cout << "Variable bounds: " << std::boolalpha << this->variable_bounds_changed << '\n';
-   std::cout << "Problem: " << std::boolalpha << this->problem_changed << '\n';
-}
-
-inline void WarmstartInformation::set_cold_start() {
-   this->objective_changed = true;
-   this->constraints_changed = true;
-   this->constraint_bounds_changed = true;
-   this->variable_bounds_changed = true;
-   this->problem_changed = true;
-}
-
-inline void WarmstartInformation::set_hot_start() {
-   this->objective_changed = true;
-   this->constraints_changed = true;
-   this->constraint_bounds_changed = true;
-   this->variable_bounds_changed = true;
-   this->problem_changed = false;
-}
-
-inline void WarmstartInformation::only_objective_changed() {
-   this->objective_changed = true;
-   this->constraints_changed = false;
-   this->constraint_bounds_changed = false;
-   this->variable_bounds_changed = false;
-   this->problem_changed = false;
-}
-
-inline void WarmstartInformation::only_variable_bounds_changed() {
-   this->objective_changed = false;
-   this->constraints_changed = false;
-   this->constraint_bounds_changed = false;
-   this->variable_bounds_changed = true;
-   this->problem_changed = false;
-}
+namespace uno {
+   struct WarmstartInformation {
+      bool objective_changed{false};
+      bool constraints_changed{false};
+      bool constraint_bounds_changed{false};
+      bool variable_bounds_changed{false};
+      bool problem_changed{false};
+
+      void display() const;
+      void set_cold_start();
+      void set_hot_start();
+      void only_objective_changed();
+      void only_variable_bounds_changed();
+   };
+
+   inline void WarmstartInformation::display() const {
+      std::cout << "Objective: " << std::boolalpha << this->objective_changed << '\n';
+      std::cout << "Constraints: " << std::boolalpha << this->constraints_changed << '\n';
+      std::cout << "Constraint bounds: " << std::boolalpha << this->constraint_bounds_changed << '\n';
+      std::cout << "Variable bounds: " << std::boolalpha << this->variable_bounds_changed << '\n';
+      std::cout << "Problem: " << std::boolalpha << this->problem_changed << '\n';
+   }
+
+   inline void WarmstartInformation::set_cold_start() {
+      this->objective_changed = true;
+      this->constraints_changed = true;
+      this->constraint_bounds_changed = true;
+      this->variable_bounds_changed = true;
+      this->problem_changed = true;
+   }
+
+   inline void WarmstartInformation::set_hot_start() {
+      this->objective_changed = true;
+      this->constraints_changed = true;
+      this->constraint_bounds_changed = true;
+      this->variable_bounds_changed = true;
+      this->problem_changed = false;
+   }
+
+   inline void WarmstartInformation::only_objective_changed() {
+      this->objective_changed = true;
+      this->constraints_changed = false;
+      this->constraint_bounds_changed = false;
+      this->variable_bounds_changed = false;
+      this->problem_changed = false;
+   }
+
+   inline void WarmstartInformation::only_variable_bounds_changed() {
+      this->objective_changed = false;
+      this->constraints_changed = false;
+      this->constraint_bounds_changed = false;
+      this->variable_bounds_changed = true;
+      this->problem_changed = false;
+   }
+} // namespace
 
 #endif // UNO_WARMSTARTINFORMATION_H
diff --git a/uno/preprocessing/Preprocessing.cpp b/uno/preprocessing/Preprocessing.cpp
index 3601154..1725734 100644
--- a/uno/preprocessing/Preprocessing.cpp
+++ b/uno/preprocessing/Preprocessing.cpp
@@ -12,126 +12,135 @@
 #include "solvers/QP/QPSolver.hpp"
 #include "symbolic/VectorView.hpp"
 
-// compute a least-square approximation of the multipliers by solving a linear system
-void Preprocessing::compute_least_square_multipliers(const Model& model, SymmetricMatrix<double>& matrix, Vector<double>& rhs,
-      SymmetricIndefiniteLinearSolver<double>& linear_solver, Iterate& current_iterate, Vector<double>& multipliers, double multiplier_max_norm) {
-   current_iterate.evaluate_objective_gradient(model);
-   current_iterate.evaluate_constraint_jacobian(model);
+namespace uno {
+   // compute a least-square approximation of the multipliers by solving a linear system
+   void Preprocessing::compute_least_square_multipliers(const Model& model, SymmetricMatrix<size_t, double>& matrix, Vector<double>& rhs,
+         SymmetricIndefiniteLinearSolver<size_t, double>& linear_solver, Iterate& current_iterate, Vector<double>& multipliers,
+         double multiplier_max_norm) {
+      current_iterate.evaluate_objective_gradient(model);
+      current_iterate.evaluate_constraint_jacobian(model);
+      DEBUG << "Computing least-square multipliers\n";
+      DEBUG2 << "Current primals: " << current_iterate.primals << '\n';
 
-   /* build the symmetric matrix */
-   matrix.reset();
-   // identity block
-   for (size_t variable_index: Range(model.number_variables)) {
-      matrix.insert(1., variable_index, variable_index);
-      matrix.finalize_column(variable_index);
-   }
-   // Jacobian of general constraints
-   for (size_t constraint_index: Range(model.number_constraints)) {
-      for (const auto [variable_index, derivative]: current_iterate.evaluations.constraint_jacobian[constraint_index]) {
-         matrix.insert(derivative, variable_index, model.number_variables + constraint_index);
+      /* generate the right-hand side */
+      rhs.fill(0.);
+      // objective gradient
+      for (const auto [variable_index, derivative]: current_iterate.evaluations.objective_gradient) {
+         rhs[variable_index] += model.objective_sign * derivative;
       }
-      matrix.finalize_column(model.number_variables + constraint_index);
-   }
-   DEBUG2 << "Matrix for least-square multipliers:\n" << matrix << '\n';
+      // variable bound constraints
+      for (size_t variable_index: Range(model.number_variables)) {
+         rhs[variable_index] -= current_iterate.multipliers.lower_bounds[variable_index] + current_iterate.multipliers.upper_bounds[variable_index];
+      }
+      DEBUG2 << "RHS for least-square multipliers: "; print_vector(DEBUG2, view(rhs, 0, model.number_variables + model.number_constraints));
 
-   /* generate the right-hand side */
-   rhs.fill(0.);
-   // objective gradient
-   for (const auto [variable_index, derivative]: current_iterate.evaluations.objective_gradient) {
-      rhs[variable_index] += model.objective_sign * derivative;
-   }
-   // variable bound constraints
-   for (size_t variable_index: Range(model.number_variables)) {
-      rhs[variable_index] -= current_iterate.multipliers.lower_bounds[variable_index] + current_iterate.multipliers.upper_bounds[variable_index];
-   }
-   DEBUG2 << "RHS for least-square multipliers: "; print_vector(DEBUG2, view(rhs, 0, matrix.dimension));
-   
-   /* solve the system */
-   Vector<double> solution(matrix.dimension);
-   linear_solver.factorize(matrix);
-   linear_solver.solve_indefinite_system(matrix, rhs, solution);
-   DEBUG2 << "Solution: "; print_vector(DEBUG2, view(solution, 0, matrix.dimension));
+      // if the residuals on the RHS are all 0, the least-square multipliers are all 0
+      if (norm_inf(view(rhs, 0, model.number_variables + model.number_constraints)) == 0.) {
+         multipliers.fill(0.);
+         DEBUG << "Least-square multipliers are all 0.\n";
+         return;
+      }
 
-   // if least-square multipliers too big, discard them. Otherwise, keep them
-   if (norm_inf(view(solution, model.number_variables, model.number_variables + model.number_constraints)) <= multiplier_max_norm) {
+      /* build the symmetric matrix */
+      matrix.reset();
+      // identity block
+      for (size_t variable_index: Range(model.number_variables)) {
+         matrix.insert(1., variable_index, variable_index);
+         matrix.finalize_column(variable_index);
+      }
+      // Jacobian of general constraints
       for (size_t constraint_index: Range(model.number_constraints)) {
-         multipliers[constraint_index] = solution[model.number_variables + constraint_index];
+         for (const auto [variable_index, derivative]: current_iterate.evaluations.constraint_jacobian[constraint_index]) {
+            matrix.insert(derivative, variable_index, model.number_variables + constraint_index);
+         }
+         matrix.finalize_column(model.number_variables + constraint_index);
       }
-   }
-   else {
-      DEBUG << "Ignoring the least-square multipliers\n";
-   }
-   DEBUG << '\n';
-}
+      DEBUG2 << "Matrix for least-square multipliers:\n" << matrix << '\n';
+
+      /* solve the system */
+      Vector<double> solution(matrix.dimension);
+      linear_solver.factorize(matrix);
+      linear_solver.solve_indefinite_system(matrix, rhs, solution);
 
-size_t count_infeasible_linear_constraints(const Model& model, const std::vector<double>& constraint_values) {
-   size_t infeasible_linear_constraints = 0;
-   for (size_t constraint_index: model.get_linear_constraints()) {
-      if (constraint_values[constraint_index] < model.constraint_lower_bound(constraint_index) ||
-            model.constraint_upper_bound(constraint_index) < constraint_values[constraint_index]) {
-         infeasible_linear_constraints++;
+      // if least-square multipliers too big, discard them. Otherwise, keep them
+      const auto trial_multipliers = view(solution, model.number_variables, model.number_variables + model.number_constraints);
+      DEBUG2 << "Trial multipliers: "; print_vector(DEBUG2, trial_multipliers);
+      if (norm_inf(trial_multipliers) <= multiplier_max_norm) {
+         multipliers = trial_multipliers;
+      }
+      else {
+         DEBUG << "Ignoring the least-square multipliers\n";
       }
+      DEBUG << '\n';
    }
-   return infeasible_linear_constraints;
-}
 
-bool Preprocessing::enforce_linear_constraints(const Model& model, Vector<double>& x, Multipliers& multipliers, QPSolver& qp_solver) {
-   const auto& linear_constraints = model.get_linear_constraints();
-   INFO << "Preprocessing phase: the problem has " << linear_constraints.size() << " linear constraints\n";
-   if (not linear_constraints.empty()) {
-      // evaluate the constraints
-      std::vector<double> constraints(model.number_constraints);
-      model.evaluate_constraints(x, constraints);
-      const size_t infeasible_linear_constraints = count_infeasible_linear_constraints(model, constraints);
-      INFO << "There are " << infeasible_linear_constraints << " infeasible linear constraints at the initial point\n";
-      if (0 < infeasible_linear_constraints) {
-         // Hessian
-         const CSCSymmetricMatrix<double> hessian = CSCSymmetricMatrix<double>::identity(model.number_variables);
-         // constraint Jacobian
-         RectangularMatrix<double> constraint_jacobian(linear_constraints.size(), model.number_variables);
-         for (size_t linear_constraint_index: Range(linear_constraints.size())) {
-            const size_t constraint_index = linear_constraints[linear_constraint_index];
-            model.evaluate_constraint_gradient(x, constraint_index, constraint_jacobian[linear_constraint_index]);
-         }
-         // variable bounds
-         std::vector<double> variables_lower_bounds(model.number_variables);
-         std::vector<double> variables_upper_bounds(model.number_variables);
-         for (size_t variable_index: Range(model.number_variables)) {
-            variables_lower_bounds[variable_index] = model.variable_lower_bound(variable_index) - x[variable_index];
-            variables_upper_bounds[variable_index] = model.variable_upper_bound(variable_index) - x[variable_index];
-         }
-         // constraint bounds
-         std::vector<double> constraints_lower_bounds(linear_constraints.size());
-         std::vector<double> constraints_upper_bounds(linear_constraints.size());
-         for (size_t linear_constraint_index: Range(linear_constraints.size())) {
-            const size_t constraint_index = linear_constraints[linear_constraint_index];
-            constraints_lower_bounds[linear_constraint_index] = model.constraint_lower_bound(constraint_index) - constraints[constraint_index];
-            constraints_upper_bounds[linear_constraint_index] = model.constraint_upper_bound(constraint_index) - constraints[constraint_index];
+   size_t count_infeasible_linear_constraints(const Model& model, const std::vector<double>& constraint_values) {
+      size_t infeasible_linear_constraints = 0;
+      for (size_t constraint_index: model.get_linear_constraints()) {
+         if (constraint_values[constraint_index] < model.constraint_lower_bound(constraint_index) ||
+             model.constraint_upper_bound(constraint_index) < constraint_values[constraint_index]) {
+            infeasible_linear_constraints++;
          }
+      }
+      return infeasible_linear_constraints;
+   }
 
-         // solve the strictly convex QP
-         Vector<double> d0(model.number_variables); // = 0
-         SparseVector<double> linear_objective; // empty
-         WarmstartInformation warmstart_information{true, true, true, true};
-         Direction direction(model.number_variables, model.number_constraints);
-         qp_solver.solve_QP(model.number_variables, linear_constraints.size(), variables_lower_bounds, variables_upper_bounds, constraints_lower_bounds,
-               constraints_upper_bounds, linear_objective, constraint_jacobian, hessian, d0, direction, warmstart_information);
-         if (direction.status == SubproblemStatus::INFEASIBLE) {
-            INFO << "Linear constraints cannot be satisfied.\n";
-            return false;
-         }
+   void Preprocessing::enforce_linear_constraints(const Model& model, Vector<double>& x, Multipliers& multipliers, QPSolver& qp_solver) {
+      const auto& linear_constraints = model.get_linear_constraints();
+      INFO << "Preprocessing phase: the problem has " << linear_constraints.size() << " linear constraints\n";
+      if (not linear_constraints.empty()) {
+         // evaluate the constraints
+         std::vector<double> constraints(model.number_constraints);
+         model.evaluate_constraints(x, constraints);
+         const size_t infeasible_linear_constraints = count_infeasible_linear_constraints(model, constraints);
+         INFO << "There are " << infeasible_linear_constraints << " infeasible linear constraints at the initial point\n";
+         if (0 < infeasible_linear_constraints) {
+            // Hessian
+            const CSCSymmetricMatrix<size_t, double> hessian = CSCSymmetricMatrix<size_t, double>::identity(model.number_variables);
+            // constraint Jacobian
+            RectangularMatrix<double> constraint_jacobian(linear_constraints.size(), model.number_variables);
+            for (size_t linear_constraint_index: Range(linear_constraints.size())) {
+               const size_t constraint_index = linear_constraints[linear_constraint_index];
+               model.evaluate_constraint_gradient(x, constraint_index, constraint_jacobian[linear_constraint_index]);
+            }
+            // variable bounds
+            std::vector<double> variables_lower_bounds(model.number_variables);
+            std::vector<double> variables_upper_bounds(model.number_variables);
+            for (size_t variable_index: Range(model.number_variables)) {
+               variables_lower_bounds[variable_index] = model.variable_lower_bound(variable_index) - x[variable_index];
+               variables_upper_bounds[variable_index] = model.variable_upper_bound(variable_index) - x[variable_index];
+            }
+            // constraint bounds
+            std::vector<double> constraints_lower_bounds(linear_constraints.size());
+            std::vector<double> constraints_upper_bounds(linear_constraints.size());
+            for (size_t linear_constraint_index: Range(linear_constraints.size())) {
+               const size_t constraint_index = linear_constraints[linear_constraint_index];
+               constraints_lower_bounds[linear_constraint_index] = model.constraint_lower_bound(constraint_index) - constraints[constraint_index];
+               constraints_upper_bounds[linear_constraint_index] = model.constraint_upper_bound(constraint_index) - constraints[constraint_index];
+            }
+
+            // solve the strictly convex QP
+            Vector<double> d0(model.number_variables); // = 0
+            SparseVector<double> linear_objective; // empty
+            WarmstartInformation warmstart_information{true, true, true, true};
+            Direction direction(model.number_variables, model.number_constraints);
+            qp_solver.solve_QP(model.number_variables, linear_constraints.size(), variables_lower_bounds, variables_upper_bounds, constraints_lower_bounds,
+                  constraints_upper_bounds, linear_objective, constraint_jacobian, hessian, d0, direction, warmstart_information);
+            if (direction.status == SubproblemStatus::INFEASIBLE) {
+               throw std::runtime_error("Linear constraints cannot be satisfied at the initial point.");
+            }
 
-         // take the step
-         x += direction.primals;
-         multipliers.lower_bounds += direction.multipliers.lower_bounds;
-         multipliers.upper_bounds += direction.multipliers.upper_bounds;
-         for (size_t linear_constraint_index: Range(linear_constraints.size())) {
-            const size_t constraint_index = linear_constraints[linear_constraint_index];
-            multipliers.constraints[constraint_index] += direction.multipliers.constraints[linear_constraint_index];
+            // take the step
+            x += direction.primals;
+            multipliers.lower_bounds += direction.multipliers.lower_bounds;
+            multipliers.upper_bounds += direction.multipliers.upper_bounds;
+            for (size_t linear_constraint_index: Range(linear_constraints.size())) {
+               const size_t constraint_index = linear_constraints[linear_constraint_index];
+               multipliers.constraints[constraint_index] += direction.multipliers.constraints[linear_constraint_index];
+            }
+            DEBUG3 << "Linear feasible initial point: "; print_vector(DEBUG3, x);
+            DEBUG3 << '\n';
          }
-         DEBUG3 << "Linear feasible initial point: "; print_vector(DEBUG3, x);
-         std::cout << '\n';
       }
    }
-   return true;
-}
+} // namespace
\ No newline at end of file
diff --git a/uno/preprocessing/Preprocessing.hpp b/uno/preprocessing/Preprocessing.hpp
index 53c7844..3002ada 100644
--- a/uno/preprocessing/Preprocessing.hpp
+++ b/uno/preprocessing/Preprocessing.hpp
@@ -6,24 +6,26 @@
 
 #include <vector>
 
-// forward declarations
-class Iterate;
-class Model;
-class Multipliers;
-class QPSolver;
-template <typename ElementType>
-class SymmetricIndefiniteLinearSolver;
-template <typename ElementType>
-class SymmetricMatrix;
-template <typename ElementType>
-class Vector;
+namespace uno {
+   // forward declarations
+   class Iterate;
+   class Model;
+   struct Multipliers;
+   class QPSolver;
+   template <typename IndexType, typename ElementType>
+   class SymmetricIndefiniteLinearSolver;
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrix;
+   template <typename ElementType>
+   class Vector;
 
-class Preprocessing {
-public:
-   static void compute_least_square_multipliers(const Model& model, SymmetricMatrix<double>& matrix, Vector<double>& rhs,
-         SymmetricIndefiniteLinearSolver<double>& linear_solver, Iterate& current_iterate, Vector<double>& multipliers,
-         double multiplier_max_norm);
-   [[nodiscard]] static bool enforce_linear_constraints(const Model& model, Vector<double>& x, Multipliers& multipliers, QPSolver& qp_solver);
-};
+   class Preprocessing {
+   public:
+      static void compute_least_square_multipliers(const Model& model, SymmetricMatrix<size_t, double>& matrix, Vector<double>& rhs,
+            SymmetricIndefiniteLinearSolver<size_t, double>& linear_solver, Iterate& current_iterate, Vector<double>& multipliers,
+            double multiplier_max_norm);
+      static void enforce_linear_constraints(const Model& model, Vector<double>& x, Multipliers& multipliers, QPSolver& qp_solver);
+   };
+} // namespace
 
-#endif //UNO_PREPROCESSING_H
+#endif //UNO_PREPROCESSING_H
\ No newline at end of file
diff --git a/uno/preprocessing/Scaling.cpp b/uno/preprocessing/Scaling.cpp
index 3fc734d..283a46b 100644
--- a/uno/preprocessing/Scaling.cpp
+++ b/uno/preprocessing/Scaling.cpp
@@ -7,26 +7,28 @@
 #include "linear_algebra/SparseVector.hpp"
 #include "linear_algebra/Vector.hpp"
 
-Scaling::Scaling(size_t number_constraints, double gradient_threshold):
-      gradient_threshold(gradient_threshold), objective_scaling(1.), constraint_scaling(number_constraints, 1.) {
-}
+namespace uno {
+   Scaling::Scaling(size_t number_constraints, double gradient_threshold):
+         gradient_threshold(gradient_threshold), objective_scaling(1.), constraint_scaling(number_constraints, 1.) {
+   }
 
-void Scaling::compute(const SparseVector<double>& objective_gradient, const RectangularMatrix<double>& constraint_jacobian) {
-   // objective
-   this->objective_scaling = std::min(1., this->gradient_threshold / norm_inf(objective_gradient));
+   void Scaling::compute(const SparseVector<double>& objective_gradient, const RectangularMatrix<double>& constraint_jacobian) {
+      // objective
+      this->objective_scaling = std::min(1., this->gradient_threshold / norm_inf(objective_gradient));
 
-   // constraints
-   for (size_t constraint_index: Range(this->constraint_scaling.size())) {
-      this->constraint_scaling[constraint_index] = std::min(1., this->gradient_threshold / norm_inf(constraint_jacobian[constraint_index]));
+      // constraints
+      for (size_t constraint_index: Range(this->constraint_scaling.size())) {
+         this->constraint_scaling[constraint_index] = std::min(1., this->gradient_threshold / norm_inf(constraint_jacobian[constraint_index]));
+      }
+      DEBUG2 << "Objective scaling: " << this->objective_scaling << '\n';
+      DEBUG2 << "Constraint scaling: "; print_vector(DEBUG2, this->constraint_scaling);
    }
-   DEBUG2 << "Objective scaling: " << this->objective_scaling << '\n';
-   DEBUG2 << "Constraint scaling: "; print_vector(DEBUG2, this->constraint_scaling);
-}
 
-double Scaling::get_objective_scaling() const {
-   return this->objective_scaling;
-}
+   double Scaling::get_objective_scaling() const {
+      return this->objective_scaling;
+   }
 
-double Scaling::get_constraint_scaling(size_t constraint_index) const {
-   return this->constraint_scaling[constraint_index];
-}
\ No newline at end of file
+   double Scaling::get_constraint_scaling(size_t constraint_index) const {
+      return this->constraint_scaling[constraint_index];
+   }
+} // namespace
diff --git a/uno/preprocessing/Scaling.hpp b/uno/preprocessing/Scaling.hpp
index 0907c76..852f9b8 100644
--- a/uno/preprocessing/Scaling.hpp
+++ b/uno/preprocessing/Scaling.hpp
@@ -7,23 +7,25 @@
 #include <vector>
 #include <cstddef>
 
-// forward declarations
-template <typename ElementType>
-class RectangularMatrix;
-template <typename ElementType>
-class SparseVector;
+namespace uno {
+   // forward declarations
+   template <typename ElementType>
+   class RectangularMatrix;
+   template <typename ElementType>
+   class SparseVector;
 
-class Scaling {
-public:
-   Scaling(size_t number_constraints, double gradient_threshold);
-   void compute(const SparseVector<double>& objective_gradient, const RectangularMatrix<double>& constraint_jacobian);
-   [[nodiscard]] double get_objective_scaling() const;
-   [[nodiscard]] double get_constraint_scaling(size_t constraint_index) const;
+   class Scaling {
+   public:
+      Scaling(size_t number_constraints, double gradient_threshold);
+      void compute(const SparseVector<double>& objective_gradient, const RectangularMatrix<double>& constraint_jacobian);
+      [[nodiscard]] double get_objective_scaling() const;
+      [[nodiscard]] double get_constraint_scaling(size_t constraint_index) const;
 
-protected:
-   const double gradient_threshold;
-   double objective_scaling;
-   std::vector<double> constraint_scaling;
-};
+   protected:
+      const double gradient_threshold;
+      double objective_scaling;
+      std::vector<double> constraint_scaling;
+   };
+} // namespace
 
-#endif // UNO_SCALING_H
\ No newline at end of file
+#endif // UNO_SCALING_H
diff --git a/uno/reformulation/ElasticVariables.hpp b/uno/reformulation/ElasticVariables.hpp
new file mode 100644
index 0000000..dbf2833
--- /dev/null
+++ b/uno/reformulation/ElasticVariables.hpp
@@ -0,0 +1,66 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_ELASTICVARIABLES_H
+#define UNO_ELASTICVARIABLES_H
+
+namespace uno {
+   struct ElasticVariablesSizes {
+      size_t positive{0};
+      size_t negative{0};
+   };
+
+   class ElasticVariables {
+   public:
+      SparseVector<size_t> positive{};
+      SparseVector<size_t> negative{};
+
+      ElasticVariables(size_t number_positive_variables, size_t number_negative_variables);
+      [[nodiscard]] size_t size() const { return this->positive.size() + this->negative.size(); }
+
+      static ElasticVariables generate(const Model& model);
+
+   protected:
+      static ElasticVariablesSizes count(const Model& model);
+   };
+
+   inline ElasticVariables::ElasticVariables(size_t number_positive_variables, size_t number_negative_variables):
+      positive(number_positive_variables), negative(number_negative_variables) { }
+
+   inline ElasticVariables ElasticVariables::generate(const Model& model) {
+      const ElasticVariablesSizes sizes = ElasticVariables::count(model);
+      ElasticVariables elastic_variables(sizes.positive, sizes.negative);
+
+      // generate elastic variables to relax the constraints
+      size_t elastic_index = model.number_variables;
+      for (size_t constraint_index: Range(model.number_constraints)) {
+         if (is_finite(model.constraint_upper_bound(constraint_index))) {
+            // nonnegative variable p that captures the positive part of the constraint violation
+            elastic_variables.positive.insert(constraint_index, elastic_index);
+            elastic_index++;
+         }
+         if (is_finite(model.constraint_lower_bound(constraint_index))) {
+            // nonpositive variable n that captures the negative part of the constraint violation
+            elastic_variables.negative.insert(constraint_index, elastic_index);
+            elastic_index++;
+         }
+      }
+      return elastic_variables;
+   }
+
+   inline ElasticVariablesSizes ElasticVariables::count(const Model& model) {
+      ElasticVariablesSizes number_elastic_variables;
+      // if the subproblem uses slack variables, the bounds of the constraints are [0, 0]
+      for (size_t constraint_index: Range(model.number_constraints)) {
+         if (is_finite(model.constraint_upper_bound(constraint_index))) {
+            number_elastic_variables.positive++;
+         }
+         if (is_finite(model.constraint_lower_bound(constraint_index))) {
+            number_elastic_variables.negative++;
+         }
+      }
+      return number_elastic_variables;
+   }
+} // namespace
+
+#endif // UNO_ELASTICVARIABLES_H
diff --git a/uno/reformulation/OptimalityProblem.hpp b/uno/reformulation/OptimalityProblem.hpp
index b390758..967f1f7 100644
--- a/uno/reformulation/OptimalityProblem.hpp
+++ b/uno/reformulation/OptimalityProblem.hpp
@@ -9,92 +9,87 @@
 #include "optimization/LagrangianGradient.hpp"
 #include "symbolic/Expression.hpp"
 
-class OptimalityProblem: public OptimizationProblem {
-public:
-   explicit OptimalityProblem(const Model& model);
-
-   [[nodiscard]] double get_objective_multiplier() const override { return 1.; }
-   void evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const override;
-   void evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const override;
-   void evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const override;
-   void evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers, SymmetricMatrix<double>& hessian) const override;
-
-   [[nodiscard]] double variable_lower_bound(size_t variable_index) const override { return this->model.variable_lower_bound(variable_index); }
-   [[nodiscard]] double variable_upper_bound(size_t variable_index) const override { return this->model.variable_upper_bound(variable_index); }
-   [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override { return this->model.get_lower_bounded_variables(); }
-   [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override { return this->model.get_upper_bounded_variables(); }
-   [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override { return this->model.get_single_lower_bounded_variables(); }
-   [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override { return this->model.get_single_upper_bounded_variables(); }
-
-   [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override { return this->model.constraint_lower_bound(constraint_index); }
-   [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override { return this->model.constraint_upper_bound(constraint_index); }
-
-   [[nodiscard]] size_t number_objective_gradient_nonzeros() const override { return this->model.number_objective_gradient_nonzeros(); }
-   [[nodiscard]] size_t number_jacobian_nonzeros() const override { return this->model.number_jacobian_nonzeros(); }
-   [[nodiscard]] size_t number_hessian_nonzeros() const override { return this->model.number_hessian_nonzeros(); }
-
-   [[nodiscard]] double stationarity_error(const LagrangianGradient<double>& lagrangian_gradient, double objective_multiplier,
-         Norm residual_norm) const override;
-   [[nodiscard]] double complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
-         const Multipliers& multipliers, Norm residual_norm) const override;
-};
-
-inline OptimalityProblem::OptimalityProblem(const Model& model): OptimizationProblem(model, model.number_variables, model.number_constraints) {
-}
-
-inline void OptimalityProblem::evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const {
-   iterate.evaluate_objective_gradient(this->model);
-   // TODO change this
-   objective_gradient = iterate.evaluations.objective_gradient;
-}
-
-inline void OptimalityProblem::evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const {
-   iterate.evaluate_constraints(this->model);
-   constraints = iterate.evaluations.constraints;
-}
-
-inline void OptimalityProblem::evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const {
-   iterate.evaluate_constraint_jacobian(this->model);
-   // TODO change this
-   constraint_jacobian = iterate.evaluations.constraint_jacobian;
-}
-
-inline void OptimalityProblem::evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers,
-      SymmetricMatrix<double>& hessian) const {
-   this->model.evaluate_lagrangian_hessian(x, this->get_objective_multiplier(), multipliers, hessian);
-}
-
-inline double OptimalityProblem::stationarity_error(const LagrangianGradient<double>& lagrangian_gradient, double objective_multiplier,
-      Norm residual_norm) const {
-   // norm of the scaled Lagrangian gradient
-   const auto scaled_lagrangian = objective_multiplier * lagrangian_gradient.objective_contribution + lagrangian_gradient.constraints_contribution;
-   return norm(residual_norm, scaled_lagrangian);
-}
-
-inline double OptimalityProblem::complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
-      const Multipliers& multipliers, Norm residual_norm) const {
-   // bound constraints
-   const VectorExpression variable_complementarity(Range(this->model.number_variables), [&](size_t variable_index) {
-      if (0. < multipliers.lower_bounds[variable_index]) {
-         return multipliers.lower_bounds[variable_index] * (primals[variable_index] - this->model.variable_lower_bound(variable_index));
-      }
-      if (multipliers.upper_bounds[variable_index] < 0.) {
-         return multipliers.upper_bounds[variable_index] * (primals[variable_index] - this->model.variable_upper_bound(variable_index));
-      }
-      return 0.;
-   });
-
-   // inequality constraints
-   const VectorExpression constraint_complementarity(this->model.get_inequality_constraints(), [&](size_t constraint_index) {
-      if (0. < multipliers.constraints[constraint_index]) { // lower bound
-         return multipliers.constraints[constraint_index] * (constraints[constraint_index] - this->model.constraint_lower_bound(constraint_index));
-      }
-      else if (multipliers.constraints[constraint_index] < 0.) { // upper bound
-         return multipliers.constraints[constraint_index] * (constraints[constraint_index] - this->model.constraint_upper_bound(constraint_index));
-      }
-      return 0.;
-   });
-   return norm(residual_norm, variable_complementarity, constraint_complementarity);
-}
-
-#endif // UNO_OPTIMALITYPROBLEM_H
+namespace uno {
+   class OptimalityProblem: public OptimizationProblem {
+   public:
+      explicit OptimalityProblem(const Model& model);
+
+      [[nodiscard]] double get_objective_multiplier() const override { return 1.; }
+      void evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const override;
+      void evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const override;
+      void evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const override;
+      void evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers, SymmetricMatrix<size_t, double>& hessian) const override;
+
+      [[nodiscard]] double variable_lower_bound(size_t variable_index) const override { return this->model.variable_lower_bound(variable_index); }
+      [[nodiscard]] double variable_upper_bound(size_t variable_index) const override { return this->model.variable_upper_bound(variable_index); }
+      [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override { return this->model.get_lower_bounded_variables(); }
+      [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override { return this->model.get_upper_bounded_variables(); }
+      [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override { return this->model.get_single_lower_bounded_variables(); }
+      [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override { return this->model.get_single_upper_bounded_variables(); }
+
+      [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override { return this->model.constraint_lower_bound(constraint_index); }
+      [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override { return this->model.constraint_upper_bound(constraint_index); }
+
+      [[nodiscard]] size_t number_objective_gradient_nonzeros() const override { return this->model.number_objective_gradient_nonzeros(); }
+      [[nodiscard]] size_t number_jacobian_nonzeros() const override { return this->model.number_jacobian_nonzeros(); }
+      [[nodiscard]] size_t number_hessian_nonzeros() const override { return this->model.number_hessian_nonzeros(); }
+
+      [[nodiscard]] double complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
+            const Multipliers& multipliers, double shift_value, Norm residual_norm) const override;
+   };
+
+   inline OptimalityProblem::OptimalityProblem(const Model& model): OptimizationProblem(model, model.number_variables, model.number_constraints) {
+   }
+
+   inline void OptimalityProblem::evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const {
+      iterate.evaluate_objective_gradient(this->model);
+      // TODO change this
+      objective_gradient = iterate.evaluations.objective_gradient;
+   }
+
+   inline void OptimalityProblem::evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const {
+      iterate.evaluate_constraints(this->model);
+      constraints = iterate.evaluations.constraints;
+   }
+
+   inline void OptimalityProblem::evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const {
+      iterate.evaluate_constraint_jacobian(this->model);
+      // TODO change this
+      constraint_jacobian = iterate.evaluations.constraint_jacobian;
+   }
+
+   inline void OptimalityProblem::evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers,
+         SymmetricMatrix<size_t, double>& hessian) const {
+      this->model.evaluate_lagrangian_hessian(x, this->get_objective_multiplier(), multipliers, hessian);
+   }
+
+   inline double OptimalityProblem::complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
+         const Multipliers& multipliers, double shift_value, Norm residual_norm) const {
+      // bound constraints
+      const VectorExpression variable_complementarity(Range(this->model.number_variables), [&](size_t variable_index) {
+         if (0. < multipliers.lower_bounds[variable_index]) {
+            return multipliers.lower_bounds[variable_index] * (primals[variable_index] - this->model.variable_lower_bound(variable_index)) - shift_value;
+         }
+         if (multipliers.upper_bounds[variable_index] < 0.) {
+            return multipliers.upper_bounds[variable_index] * (primals[variable_index] - this->model.variable_upper_bound(variable_index)) - shift_value;
+         }
+         return 0.;
+      });
+
+      // inequality constraints
+      const VectorExpression constraint_complementarity(this->model.get_inequality_constraints(), [&](size_t constraint_index) {
+         if (0. < multipliers.constraints[constraint_index]) { // lower bound
+            return multipliers.constraints[constraint_index] * (constraints[constraint_index] - this->model.constraint_lower_bound(constraint_index)) -
+                   shift_value;
+         }
+         else if (multipliers.constraints[constraint_index] < 0.) { // upper bound
+            return multipliers.constraints[constraint_index] * (constraints[constraint_index] - this->model.constraint_upper_bound(constraint_index)) -
+                   shift_value;
+         }
+         return 0.;
+      });
+      return norm(residual_norm, variable_complementarity, constraint_complementarity);
+   }
+} // namespace
+
+#endif // UNO_OPTIMALITYPROBLEM_H
\ No newline at end of file
diff --git a/uno/reformulation/OptimizationProblem.hpp b/uno/reformulation/OptimizationProblem.hpp
index c42d641..808b1c7 100644
--- a/uno/reformulation/OptimizationProblem.hpp
+++ b/uno/reformulation/OptimizationProblem.hpp
@@ -7,74 +7,83 @@
 #include <vector>
 #include "linear_algebra/Norm.hpp"
 #include "model/Model.hpp"
+#include "optimization/LagrangianGradient.hpp"
+#include "symbolic/Expression.hpp"
 
-// forward declarations
-template <typename ElementType>
-class Collection;
-class Iterate;
-template <typename ElementType>
-class LagrangianGradient;
-class Multipliers;
-template <typename ElementType>
-class RectangularMatrix;
-template <typename ElementType>
-class SparseVector;
-template <typename ElementType>
-class SymmetricMatrix;
-
-class OptimizationProblem {
-public:
-   OptimizationProblem(const Model& model, size_t number_variables, size_t number_constraints);
-   virtual ~OptimizationProblem() = default;
-
-   const Model& model;
-   const size_t number_variables; /*!< Number of variables */
-   const size_t number_constraints; /*!< Number of constraints */
-
-   [[nodiscard]] bool is_constrained() const;
-   [[nodiscard]] bool has_inequality_constraints() const;
-
-   // function evaluations
-   [[nodiscard]] virtual double get_objective_multiplier() const = 0;
-   virtual void evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const = 0;
-   virtual void evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const = 0;
-   virtual void evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const = 0;
-   virtual void evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers, SymmetricMatrix<double>& hessian) const = 0;
-
-   [[nodiscard]] size_t get_number_original_variables() const;
-   [[nodiscard]] virtual double variable_lower_bound(size_t variable_index) const = 0;
-   [[nodiscard]] virtual double variable_upper_bound(size_t variable_index) const = 0;
-   [[nodiscard]] virtual double constraint_lower_bound(size_t constraint_index) const = 0;
-   [[nodiscard]] virtual double constraint_upper_bound(size_t constraint_index) const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_lower_bounded_variables() const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_upper_bounded_variables() const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_single_lower_bounded_variables() const = 0;
-   [[nodiscard]] virtual const Collection<size_t>& get_single_upper_bounded_variables() const = 0;
-
-   [[nodiscard]] virtual size_t number_objective_gradient_nonzeros() const = 0;
-   [[nodiscard]] virtual size_t number_jacobian_nonzeros() const = 0;
-   [[nodiscard]] virtual size_t number_hessian_nonzeros() const = 0;
-
-   [[nodiscard]] virtual double stationarity_error(const LagrangianGradient<double>& lagrangian_gradient, double objective_multiplier,
-         Norm residual_norm) const = 0;
-   [[nodiscard]] virtual double complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
-         const Multipliers& multipliers, Norm residual_norm) const = 0;
-};
-
-inline OptimizationProblem::OptimizationProblem(const Model& model, size_t number_variables, size_t number_constraints):
-      model(model), number_variables(number_variables), number_constraints(number_constraints) {
-}
-
-inline bool OptimizationProblem::is_constrained() const {
-   return (0 < this->number_constraints);
-}
-
-inline bool OptimizationProblem::has_inequality_constraints() const {
-   return (not this->model.get_inequality_constraints().is_empty());
-}
-
-inline size_t OptimizationProblem::get_number_original_variables() const {
-   return this->model.number_variables;
-}
-
-#endif // UNO_OPTIMIZATIONPROBLEM_H
+namespace uno {
+   // forward declarations
+   template <typename ElementType>
+   class Collection;
+   class Iterate;
+   struct Multipliers;
+   template <typename ElementType>
+   class RectangularMatrix;
+   template <typename ElementType>
+   class SparseVector;
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrix;
+
+   class OptimizationProblem {
+   public:
+      OptimizationProblem(const Model& model, size_t number_variables, size_t number_constraints);
+      virtual ~OptimizationProblem() = default;
+
+      const Model& model;
+      const size_t number_variables; /*!< Number of variables */
+      const size_t number_constraints; /*!< Number of constraints */
+
+      [[nodiscard]] bool is_constrained() const;
+      [[nodiscard]] bool has_inequality_constraints() const;
+
+      // function evaluations
+      [[nodiscard]] virtual double get_objective_multiplier() const = 0;
+      virtual void evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const = 0;
+      virtual void evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const = 0;
+      virtual void evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const = 0;
+      virtual void evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers, SymmetricMatrix<size_t, double>& hessian) const = 0;
+
+      [[nodiscard]] size_t get_number_original_variables() const;
+      [[nodiscard]] virtual double variable_lower_bound(size_t variable_index) const = 0;
+      [[nodiscard]] virtual double variable_upper_bound(size_t variable_index) const = 0;
+      [[nodiscard]] virtual double constraint_lower_bound(size_t constraint_index) const = 0;
+      [[nodiscard]] virtual double constraint_upper_bound(size_t constraint_index) const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_lower_bounded_variables() const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_upper_bounded_variables() const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_single_lower_bounded_variables() const = 0;
+      [[nodiscard]] virtual const Collection<size_t>& get_single_upper_bounded_variables() const = 0;
+
+      [[nodiscard]] virtual size_t number_objective_gradient_nonzeros() const = 0;
+      [[nodiscard]] virtual size_t number_jacobian_nonzeros() const = 0;
+      [[nodiscard]] virtual size_t number_hessian_nonzeros() const = 0;
+
+      [[nodiscard]] static double stationarity_error(const LagrangianGradient<double>& lagrangian_gradient, double objective_multiplier,
+            Norm residual_norm);
+      [[nodiscard]] virtual double complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
+            const Multipliers& multipliers, double shift_value, Norm residual_norm) const = 0;
+   };
+
+   inline OptimizationProblem::OptimizationProblem(const Model& model, size_t number_variables, size_t number_constraints):
+         model(model), number_variables(number_variables), number_constraints(number_constraints) {
+   }
+
+   inline bool OptimizationProblem::is_constrained() const {
+      return (0 < this->number_constraints);
+   }
+
+   inline bool OptimizationProblem::has_inequality_constraints() const {
+      return (not this->model.get_inequality_constraints().is_empty());
+   }
+
+   inline size_t OptimizationProblem::get_number_original_variables() const {
+      return this->model.number_variables;
+   }
+
+   inline double OptimizationProblem::stationarity_error(const LagrangianGradient<double>& lagrangian_gradient, double objective_multiplier,
+         Norm residual_norm) {
+      // norm of the scaled Lagrangian gradient
+      const auto scaled_lagrangian = objective_multiplier * lagrangian_gradient.objective_contribution + lagrangian_gradient.constraints_contribution;
+      return norm(residual_norm, scaled_lagrangian);
+   }
+} // namespace
+
+#endif // UNO_OPTIMIZATIONPROBLEM_H
\ No newline at end of file
diff --git a/uno/reformulation/l1RelaxedProblem.hpp b/uno/reformulation/l1RelaxedProblem.hpp
index 56d7eb4..17b3e45 100644
--- a/uno/reformulation/l1RelaxedProblem.hpp
+++ b/uno/reformulation/l1RelaxedProblem.hpp
@@ -5,6 +5,7 @@
 #define UNO_L1RELAXEDPROBLEM_H
 
 #include "OptimizationProblem.hpp"
+#include "ElasticVariables.hpp"
 #include "linear_algebra/SymmetricMatrix.hpp"
 #include "model/Model.hpp"
 #include "optimization/Iterate.hpp"
@@ -15,283 +16,247 @@
 #include "symbolic/Range.hpp"
 #include "tools/Infinity.hpp"
 
-struct ElasticVariables {
-   SparseVector<size_t> positive;
-   SparseVector<size_t> negative;
-   explicit ElasticVariables(size_t capacity): positive(capacity), negative(capacity) {}
-   [[nodiscard]] size_t size() const { return this->positive.size() + this->negative.size(); }
-};
-
-class l1RelaxedProblem: public OptimizationProblem {
-public:
-   l1RelaxedProblem(const Model& model, double objective_multiplier, double constraint_violation_coefficient);
-
-   [[nodiscard]] double get_objective_multiplier() const override;
-   void evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const override;
-   void evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const override;
-   void evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const override;
-   void evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers, SymmetricMatrix<double>& hessian) const override;
-
-   [[nodiscard]] double variable_lower_bound(size_t variable_index) const override;
-   [[nodiscard]] double variable_upper_bound(size_t variable_index) const override;
-   [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override;
-   [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override;
-   [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override;
-   [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override;
-
-   [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override;
-   [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override;
-
-   [[nodiscard]] size_t number_objective_gradient_nonzeros() const override;
-   [[nodiscard]] size_t number_jacobian_nonzeros() const override;
-   [[nodiscard]] size_t number_hessian_nonzeros() const override;
-
-   [[nodiscard]] double stationarity_error(const LagrangianGradient<double>& lagrangian_gradient, double objective_multiplier,
-         Norm residual_norm) const override;
-   [[nodiscard]] double complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
-         const Multipliers& multipliers, Norm residual_norm) const override;
-
-   // parameterization
-   void set_objective_multiplier(double new_objective_multiplier);
-
-   void set_elastic_variable_values(Iterate& iterate, const std::function<void(Iterate&, size_t, size_t, double)>& elastic_setting_function) const;
-
-protected:
-   double objective_multiplier;
-   const double constraint_violation_coefficient;
-   ElasticVariables elastic_variables;
-   const Concatenation<const Collection<size_t>&, ForwardRange> lower_bounded_variables; // model variables + elastic variables
-   const Concatenation<const Collection<size_t>&, ForwardRange> single_lower_bounded_variables; // model variables + elastic variables
-
-   [[nodiscard]] static size_t count_elastic_variables(const Model& model);
-   void generate_elastic_variables();
-};
-
-inline l1RelaxedProblem::l1RelaxedProblem(const Model& model, double objective_multiplier, double constraint_violation_coefficient):
-      OptimizationProblem(model, model.number_variables + l1RelaxedProblem::count_elastic_variables(model), model.number_constraints),
-      objective_multiplier(objective_multiplier),
-      constraint_violation_coefficient(constraint_violation_coefficient),
-      elastic_variables(this->number_constraints),
-      // lower bounded variables are the model variables + the elastic variables
-      lower_bounded_variables(concatenate(this->model.get_lower_bounded_variables(), Range(model.number_variables,
-            model.number_variables + this->elastic_variables.size()))),
-      single_lower_bounded_variables(concatenate(this->model.get_single_lower_bounded_variables(),
-            Range(model.number_variables, model.number_variables + this->elastic_variables.size()))) {
-   this->generate_elastic_variables();
-}
-
-inline double l1RelaxedProblem::get_objective_multiplier() const {
-   return this->objective_multiplier;
-}
-
-inline void l1RelaxedProblem::evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const {
-   // scale nabla f(x) by rho
-   if (this->objective_multiplier != 0.) {
-      iterate.evaluate_objective_gradient(this->model);
-      // TODO change this
-      objective_gradient = iterate.evaluations.objective_gradient;
-      scale(objective_gradient, this->objective_multiplier);
-   }
-   else {
-      objective_gradient.clear();
+namespace uno {
+   class l1RelaxedProblem: public OptimizationProblem {
+   public:
+      l1RelaxedProblem(const Model& model, double objective_multiplier, double constraint_violation_coefficient);
+
+      [[nodiscard]] double get_objective_multiplier() const override;
+      void evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const override;
+      void evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const override;
+      void evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const override;
+      void evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers, SymmetricMatrix<size_t, double>& hessian) const override;
+
+      [[nodiscard]] double variable_lower_bound(size_t variable_index) const override;
+      [[nodiscard]] double variable_upper_bound(size_t variable_index) const override;
+      [[nodiscard]] const Collection<size_t>& get_lower_bounded_variables() const override;
+      [[nodiscard]] const Collection<size_t>& get_upper_bounded_variables() const override;
+      [[nodiscard]] const Collection<size_t>& get_single_lower_bounded_variables() const override;
+      [[nodiscard]] const Collection<size_t>& get_single_upper_bounded_variables() const override;
+
+      [[nodiscard]] double constraint_lower_bound(size_t constraint_index) const override;
+      [[nodiscard]] double constraint_upper_bound(size_t constraint_index) const override;
+
+      [[nodiscard]] size_t number_objective_gradient_nonzeros() const override;
+      [[nodiscard]] size_t number_jacobian_nonzeros() const override;
+      [[nodiscard]] size_t number_hessian_nonzeros() const override;
+
+      [[nodiscard]] double complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
+            const Multipliers& multipliers, double shift_value, Norm residual_norm) const override;
+
+      // parameterization
+      void set_objective_multiplier(double new_objective_multiplier);
+
+      void set_elastic_variable_values(Iterate& iterate, const std::function<void(Iterate&, size_t, size_t, double)>& elastic_setting_function) const;
+
+   protected:
+      double objective_multiplier;
+      const double constraint_violation_coefficient;
+      ElasticVariables elastic_variables;
+      const Concatenation<const Collection<size_t>&, ForwardRange> lower_bounded_variables; // model variables + elastic variables
+      const Concatenation<const Collection<size_t>&, ForwardRange> single_lower_bounded_variables; // model variables + elastic variables
+
+      // delegating constructor
+      l1RelaxedProblem(const Model& model, ElasticVariables&& elastic_variables, double objective_multiplier, double constraint_violation_coefficient);
+   };
+
+   inline l1RelaxedProblem::l1RelaxedProblem(const Model& model, double objective_multiplier, double constraint_violation_coefficient):
+   // call delegating constructor
+         l1RelaxedProblem(model, ElasticVariables::generate(model), objective_multiplier, constraint_violation_coefficient) {
    }
 
-   // elastic contribution
-   for (const auto [_, elastic_index]: this->elastic_variables.positive) {
-      objective_gradient.insert(elastic_index, this->constraint_violation_coefficient);
-   }
-   for (const auto [_, elastic_index]: this->elastic_variables.negative) {
-      objective_gradient.insert(elastic_index, this->constraint_violation_coefficient);
-   }
-}
-
-inline void l1RelaxedProblem::evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const {
-   iterate.evaluate_constraints(this->model);
-   constraints = iterate.evaluations.constraints;
-   // add the contribution of the elastics
-   for (const auto [constraint_index, elastic_index]: this->elastic_variables.positive) {
-      constraints[constraint_index] -= iterate.primals[elastic_index];
-   }
-   for (const auto [constraint_index, elastic_index]: this->elastic_variables.negative) {
-      constraints[constraint_index] += iterate.primals[elastic_index];
-   }
-}
-
-inline void l1RelaxedProblem::evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const {
-   iterate.evaluate_constraint_jacobian(this->model);
-   // TODO change this
-   constraint_jacobian = iterate.evaluations.constraint_jacobian;
-   // add the contribution of the elastics
-   for (const auto [constraint_index, elastic_index]: this->elastic_variables.positive) {
-      constraint_jacobian[constraint_index].insert(elastic_index, -1.);
+   // private delegating constructor
+   inline l1RelaxedProblem::l1RelaxedProblem(const Model& model, ElasticVariables&& elastic_variables, double objective_multiplier,
+         double constraint_violation_coefficient):
+         OptimizationProblem(model, model.number_variables + elastic_variables.size(), model.number_constraints),
+         objective_multiplier(objective_multiplier),
+         constraint_violation_coefficient(constraint_violation_coefficient),
+         elastic_variables(std::forward<ElasticVariables>(elastic_variables)),
+         // lower bounded variables are the model variables + the elastic variables
+         lower_bounded_variables(concatenate(this->model.get_lower_bounded_variables(), Range(model.number_variables,
+               model.number_variables + this->elastic_variables.size()))),
+         single_lower_bounded_variables(concatenate(this->model.get_single_lower_bounded_variables(),
+               Range(model.number_variables, model.number_variables + this->elastic_variables.size()))) {
    }
-   for (const auto [constraint_index, elastic_index]: this->elastic_variables.negative) {
-      constraint_jacobian[constraint_index].insert(elastic_index, 1.);
-   }
-}
-
-inline void l1RelaxedProblem::evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers,
-      SymmetricMatrix<double>& hessian) const {
-   this->model.evaluate_lagrangian_hessian(x, this->objective_multiplier, multipliers, hessian);
 
-   // extend the dimension of the Hessian by finalizing the remaining columns (note: the elastics do not enter the Hessian)
-   for (size_t constraint_index: Range(this->model.number_variables, this->number_variables)) {
-      hessian.finalize_column(constraint_index);
+   inline double l1RelaxedProblem::get_objective_multiplier() const {
+      return this->objective_multiplier;
    }
-}
-
-inline double l1RelaxedProblem::stationarity_error(const LagrangianGradient<double>& lagrangian_gradient, double objective_multiplier,
-      Norm residual_norm) const {
-   // norm of the scaled Lagrangian gradient
-   const auto scaled_lagrangian = objective_multiplier * lagrangian_gradient.objective_contribution + lagrangian_gradient.constraints_contribution;
-   return norm(residual_norm, scaled_lagrangian);
-}
-
-// complementary slackness error: expression for violated constraints depends on the definition of the relaxed problem
-inline double l1RelaxedProblem::complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
-      const Multipliers& multipliers, Norm residual_norm) const {
-   // complementarity for variable bounds
-   const VectorExpression variable_complementarity(Range(this->model.number_variables), [&](size_t variable_index) {
-      if (0. < multipliers.lower_bounds[variable_index]) {
-         return multipliers.lower_bounds[variable_index] * (primals[variable_index] - this->variable_lower_bound(variable_index));
+
+   inline void l1RelaxedProblem::evaluate_objective_gradient(Iterate& iterate, SparseVector<double>& objective_gradient) const {
+      // scale nabla f(x) by rho
+      if (this->objective_multiplier != 0.) {
+         iterate.evaluate_objective_gradient(this->model);
+         // TODO change this
+         objective_gradient = iterate.evaluations.objective_gradient;
+         scale(objective_gradient, this->objective_multiplier);
       }
-      if (multipliers.upper_bounds[variable_index] < 0.) {
-         return multipliers.upper_bounds[variable_index] * (primals[variable_index] - this->variable_upper_bound(variable_index));
+      else {
+         objective_gradient.clear();
       }
-      return 0.;
-   });
-
-   // complementarity for constraint bounds
-   const VectorExpression constraint_complementarity(Range(constraints.size()), [&](size_t constraint_index) {
-      // violated constraints
-      if (constraints[constraint_index] < this->constraint_lower_bound(constraint_index)) { // lower violated
-         return (this->constraint_violation_coefficient - multipliers.constraints[constraint_index]) * (constraints[constraint_index] -
-               this->constraint_lower_bound(constraint_index));
+
+      // elastic contribution
+      for (const auto [_, elastic_index]: this->elastic_variables.positive) {
+         objective_gradient.insert(elastic_index, this->constraint_violation_coefficient);
       }
-      else if (this->constraint_upper_bound(constraint_index) < constraints[constraint_index]) { // upper violated
-         return (this->constraint_violation_coefficient + multipliers.constraints[constraint_index]) * (constraints[constraint_index] -
-               this->constraint_upper_bound(constraint_index));
+      for (const auto [_, elastic_index]: this->elastic_variables.negative) {
+         objective_gradient.insert(elastic_index, this->constraint_violation_coefficient);
       }
-      // satisfied constraints
-      else if (0. < multipliers.constraints[constraint_index]) { // lower bound
-         return multipliers.constraints[constraint_index] * (constraints[constraint_index] - this->constraint_lower_bound(constraint_index));
+   }
+
+   inline void l1RelaxedProblem::evaluate_constraints(Iterate& iterate, std::vector<double>& constraints) const {
+      iterate.evaluate_constraints(this->model);
+      constraints = iterate.evaluations.constraints;
+      // add the contribution of the elastics
+      for (const auto [constraint_index, elastic_index]: this->elastic_variables.positive) {
+         constraints[constraint_index] -= iterate.primals[elastic_index];
       }
-      else if (multipliers.constraints[constraint_index] < 0.) { // upper bound
-         return multipliers.constraints[constraint_index] * (constraints[constraint_index] - this->constraint_upper_bound(constraint_index));
+      for (const auto [constraint_index, elastic_index]: this->elastic_variables.negative) {
+         constraints[constraint_index] += iterate.primals[elastic_index];
       }
-      return 0.;
-   });
-   return norm(residual_norm, variable_complementarity, constraint_complementarity);
-}
-
-inline double l1RelaxedProblem::variable_lower_bound(size_t variable_index) const {
-   if (variable_index < this->model.number_variables) { // model variable
-      return this->model.variable_lower_bound(variable_index);
    }
-   else { // elastic variable in [0, +inf[
-      return 0.;
+
+   inline void l1RelaxedProblem::evaluate_constraint_jacobian(Iterate& iterate, RectangularMatrix<double>& constraint_jacobian) const {
+      iterate.evaluate_constraint_jacobian(this->model);
+      // TODO change this
+      constraint_jacobian = iterate.evaluations.constraint_jacobian;
+      // add the contribution of the elastics
+      for (const auto [constraint_index, elastic_index]: this->elastic_variables.positive) {
+         constraint_jacobian[constraint_index].insert(elastic_index, -1.);
+      }
+      for (const auto [constraint_index, elastic_index]: this->elastic_variables.negative) {
+         constraint_jacobian[constraint_index].insert(elastic_index, 1.);
+      }
    }
-}
 
-inline double l1RelaxedProblem::variable_upper_bound(size_t variable_index) const {
-   if (variable_index < this->model.number_variables) { // model variable
-      return this->model.variable_upper_bound(variable_index);
+   inline void l1RelaxedProblem::evaluate_lagrangian_hessian(const Vector<double>& x, const Vector<double>& multipliers,
+         SymmetricMatrix<size_t, double>& hessian) const {
+      this->model.evaluate_lagrangian_hessian(x, this->objective_multiplier, multipliers, hessian);
+
+      // extend the dimension of the Hessian by finalizing the remaining columns (note: the elastics do not enter the Hessian)
+      for (size_t constraint_index: Range(this->model.number_variables, this->number_variables)) {
+         hessian.finalize_column(constraint_index);
+      }
    }
-   else { // elastic variable in [0, +inf[
-      return INF<double>;
+
+   // complementary slackness error: expression for violated constraints depends on the definition of the relaxed problem
+   inline double l1RelaxedProblem::complementarity_error(const Vector<double>& primals, const std::vector<double>& constraints,
+         const Multipliers& multipliers, double shift_value, Norm residual_norm) const {
+      // bound constraints
+      const VectorExpression bounds_complementarity(Range(this->model.number_variables), [&](size_t variable_index) {
+         if (0. < multipliers.lower_bounds[variable_index]) {
+            return multipliers.lower_bounds[variable_index] * (primals[variable_index] - this->variable_lower_bound(variable_index)) - shift_value;
+         }
+         if (multipliers.upper_bounds[variable_index] < 0.) {
+            return multipliers.upper_bounds[variable_index] * (primals[variable_index] - this->variable_upper_bound(variable_index)) - shift_value;
+         }
+         return 0.;
+      });
+
+      // general constraints
+      const VectorExpression constraints_complementarity(Range(constraints.size()), [&](size_t constraint_index) {
+         // lower violated
+         if (constraints[constraint_index] < this->constraint_lower_bound(constraint_index)) {
+            return (1. - multipliers.constraints[constraint_index]/this->constraint_violation_coefficient) * (constraints[constraint_index] -
+                                                                                                              this->constraint_lower_bound(constraint_index)) - shift_value/this->constraint_violation_coefficient;
+         }
+            // upper violated
+         else if (this->constraint_upper_bound(constraint_index) < constraints[constraint_index]) {
+            return (1. + multipliers.constraints[constraint_index]/this->constraint_violation_coefficient) * (constraints[constraint_index] -
+                                                                                                              this->constraint_upper_bound(constraint_index)) - shift_value/this->constraint_violation_coefficient;
+         }
+            // inequality
+         else if (this->model.get_constraint_bound_type(constraint_index) != EQUAL_BOUNDS) {
+            if (0. < multipliers.constraints[constraint_index]) { // lower bound
+               return multipliers.constraints[constraint_index] * (constraints[constraint_index] - this->constraint_lower_bound(constraint_index)) - shift_value;
+            }
+            else if (multipliers.constraints[constraint_index] < 0.) { // upper bound
+               return multipliers.constraints[constraint_index] * (constraints[constraint_index] - this->constraint_upper_bound(constraint_index)) - shift_value;
+            }
+         }
+         return 0.;
+      });
+      return norm(residual_norm, bounds_complementarity, constraints_complementarity);
    }
-}
 
-inline double l1RelaxedProblem::constraint_lower_bound(size_t constraint_index) const {
-   return this->model.constraint_lower_bound(constraint_index);
-}
+   inline double l1RelaxedProblem::variable_lower_bound(size_t variable_index) const {
+      if (variable_index < this->model.number_variables) { // model variable
+         return this->model.variable_lower_bound(variable_index);
+      }
+      else { // elastic variable in [0, +inf[
+         return 0.;
+      }
+   }
 
-inline double l1RelaxedProblem::constraint_upper_bound(size_t constraint_index) const {
-   return this->model.constraint_upper_bound(constraint_index);
-}
+   inline double l1RelaxedProblem::variable_upper_bound(size_t variable_index) const {
+      if (variable_index < this->model.number_variables) { // model variable
+         return this->model.variable_upper_bound(variable_index);
+      }
+      else { // elastic variable in [0, +inf[
+         return INF<double>;
+      }
+   }
 
-inline const Collection<size_t>& l1RelaxedProblem::get_lower_bounded_variables() const {
-   return this->lower_bounded_variables;
-}
+   inline double l1RelaxedProblem::constraint_lower_bound(size_t constraint_index) const {
+      return this->model.constraint_lower_bound(constraint_index);
+   }
 
-inline const Collection<size_t>& l1RelaxedProblem::get_upper_bounded_variables() const {
-   // same set as the model
-   return this->model.get_upper_bounded_variables();
-}
+   inline double l1RelaxedProblem::constraint_upper_bound(size_t constraint_index) const {
+      return this->model.constraint_upper_bound(constraint_index);
+   }
 
-inline const Collection<size_t>& l1RelaxedProblem::get_single_lower_bounded_variables() const {
-   return this->single_lower_bounded_variables;
-}
+   inline const Collection<size_t>& l1RelaxedProblem::get_lower_bounded_variables() const {
+      return this->lower_bounded_variables;
+   }
 
-inline const Collection<size_t>& l1RelaxedProblem::get_single_upper_bounded_variables() const {
-   // same set as the model
-   return this->model.get_single_upper_bounded_variables();
-}
+   inline const Collection<size_t>& l1RelaxedProblem::get_upper_bounded_variables() const {
+      // same set as the model
+      return this->model.get_upper_bounded_variables();
+   }
 
-inline size_t l1RelaxedProblem::number_objective_gradient_nonzeros() const {
-   // elastic contribution
-   size_t number_nonzeros = this->elastic_variables.size();
+   inline const Collection<size_t>& l1RelaxedProblem::get_single_lower_bounded_variables() const {
+      return this->single_lower_bounded_variables;
+   }
 
-   // objective contribution
-   if (this->objective_multiplier != 0.) {
-      number_nonzeros += this->model.number_objective_gradient_nonzeros();
+   inline const Collection<size_t>& l1RelaxedProblem::get_single_upper_bounded_variables() const {
+      // same set as the model
+      return this->model.get_single_upper_bounded_variables();
    }
-   return number_nonzeros;
-}
-
-inline size_t l1RelaxedProblem::number_jacobian_nonzeros() const {
-   return this->model.number_jacobian_nonzeros() + this->elastic_variables.size();
-}
-
-inline size_t l1RelaxedProblem::number_hessian_nonzeros() const {
-   return this->model.number_hessian_nonzeros();
-}
-
-inline void l1RelaxedProblem::set_objective_multiplier(double new_objective_multiplier) {
-   assert(0. <= new_objective_multiplier && "The objective multiplier should be non-negative");
-   this->objective_multiplier = new_objective_multiplier;
-}
-
-inline size_t l1RelaxedProblem::count_elastic_variables(const Model& model) {
-   size_t number_elastic_variables = 0;
-   // if the subproblem uses slack variables, the bounds of the constraints are [0, 0]
-   for (size_t constraint_index: Range(model.number_constraints)) {
-      if (is_finite(model.constraint_lower_bound(constraint_index))) {
-         number_elastic_variables++;
-      }
-      if (is_finite(model.constraint_upper_bound(constraint_index))) {
-         number_elastic_variables++;
+
+   inline size_t l1RelaxedProblem::number_objective_gradient_nonzeros() const {
+      // elastic contribution
+      size_t number_nonzeros = this->elastic_variables.size();
+
+      // objective contribution
+      if (this->objective_multiplier != 0.) {
+         number_nonzeros += this->model.number_objective_gradient_nonzeros();
       }
+      return number_nonzeros;
    }
-   return number_elastic_variables;
-}
-
-inline void l1RelaxedProblem::generate_elastic_variables() {
-   // generate elastic variables to relax the constraints
-   size_t elastic_index = this->model.number_variables;
-   for (size_t constraint_index: Range(this->model.number_constraints)) {
-      if (is_finite(this->model.constraint_upper_bound(constraint_index))) {
-         // nonnegative variable p that captures the positive part of the constraint violation
-         this->elastic_variables.positive.insert(constraint_index, elastic_index);
-         elastic_index++;
-      }
-      if (is_finite(this->model.constraint_lower_bound(constraint_index))) {
-         // nonpositive variable n that captures the negative part of the constraint violation
-         this->elastic_variables.negative.insert(constraint_index, elastic_index);
-         elastic_index++;
-      }
+
+   inline size_t l1RelaxedProblem::number_jacobian_nonzeros() const {
+      return this->model.number_jacobian_nonzeros() + this->elastic_variables.size();
    }
-}
 
-inline void l1RelaxedProblem::set_elastic_variable_values(Iterate& iterate, const std::function<void(Iterate&, size_t, size_t, double)>&
-      elastic_setting_function) const {
-   iterate.set_number_variables(this->number_variables);
-   for (const auto [constraint_index, elastic_index]: this->elastic_variables.positive) {
-      elastic_setting_function(iterate, constraint_index, elastic_index, -1.);
+   inline size_t l1RelaxedProblem::number_hessian_nonzeros() const {
+      return this->model.number_hessian_nonzeros();
    }
-   for (const auto [constraint_index, elastic_index]: this->elastic_variables.negative) {
-      elastic_setting_function(iterate, constraint_index, elastic_index, 1.);
+
+   inline void l1RelaxedProblem::set_objective_multiplier(double new_objective_multiplier) {
+      assert(0. <= new_objective_multiplier && "The objective multiplier should be non-negative");
+      this->objective_multiplier = new_objective_multiplier;
+   }
+
+   inline void l1RelaxedProblem::set_elastic_variable_values(Iterate& iterate, const std::function<void(Iterate&, size_t, size_t, double)>&
+   elastic_setting_function) const {
+      iterate.set_number_variables(this->number_variables);
+      for (const auto [constraint_index, elastic_index]: this->elastic_variables.positive) {
+         elastic_setting_function(iterate, constraint_index, elastic_index, -1.);
+      }
+      for (const auto [constraint_index, elastic_index]: this->elastic_variables.negative) {
+         elastic_setting_function(iterate, constraint_index, elastic_index, 1.);
+      }
    }
-}
+} // namespace
 
-#endif // UNO_L1RELAXEDPROBLEM_H
+#endif // UNO_L1RELAXEDPROBLEM_H
\ No newline at end of file
diff --git a/uno/solvers/LP/LPSolver.hpp b/uno/solvers/LP/LPSolver.hpp
index 9191439..e40b055 100644
--- a/uno/solvers/LP/LPSolver.hpp
+++ b/uno/solvers/LP/LPSolver.hpp
@@ -6,30 +6,32 @@
 
 #include <vector>
 
-// forward declarations
-class Direction;
-template <typename ElementType>
-class RectangularMatrix;
-template <typename ElementType>
-class SparseVector;
-template <typename ElementType>
-class Vector;
-class WarmstartInformation;
+namespace uno {
+   // forward declarations
+   class Direction;
+   template <typename ElementType>
+   class RectangularMatrix;
+   template <typename ElementType>
+   class SparseVector;
+   template <typename ElementType>
+   class Vector;
+   struct WarmstartInformation;
 
-/*! \class LPSolver
- * \brief LP solver
- *
- */
-class LPSolver {
-public:
-   LPSolver() = default;
-   virtual ~LPSolver() = default;
+   /*! \class LPSolver
+    * \brief LP solver
+    *
+    */
+   class LPSolver {
+   public:
+      LPSolver() = default;
+      virtual ~LPSolver() = default;
 
-   virtual void solve_LP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-         const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
-         const WarmstartInformation& warmstart_information) = 0;
-};
+      virtual void solve_LP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+            const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+            const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+            const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
+            const WarmstartInformation& warmstart_information) = 0;
+   };
+} // namespace
 
 #endif // UNO_LPSOLVER_H
diff --git a/uno/solvers/LP/LPSolverFactory.hpp b/uno/solvers/LP/LPSolverFactory.hpp
index 3acc2e7..ea9914c 100644
--- a/uno/solvers/LP/LPSolverFactory.hpp
+++ b/uno/solvers/LP/LPSolverFactory.hpp
@@ -11,18 +11,21 @@
 #include "solvers/QP/BQPDSolver.hpp"
 #endif
 
-class LPSolverFactory {
-public:
-   static std::unique_ptr<LPSolver> create(const std::string& LP_solver_name, size_t number_variables, size_t number_constraints,
-         size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros, const Options& options) {
-#ifdef HAS_BQPD
-      if (LP_solver_name == "BQPD") {
-         return std::make_unique<BQPDSolver>(number_variables, number_constraints, number_objective_gradient_nonzeros,
-               number_jacobian_nonzeros, 0, BQPDProblemType::LP, options);
+namespace uno {
+   class LPSolverFactory {
+   public:
+      static std::unique_ptr<LPSolver> create([[maybe_unused]] const std::string& LP_solver_name, [[maybe_unused]] size_t number_variables,
+            [[maybe_unused]] size_t number_constraints, [[maybe_unused]] size_t number_objective_gradient_nonzeros,
+            [[maybe_unused]] size_t number_jacobian_nonzeros, [[maybe_unused]] const Options& options) {
+   #ifdef HAS_BQPD
+         if (LP_solver_name == "BQPD") {
+            return std::make_unique<BQPDSolver>(number_variables, number_constraints, number_objective_gradient_nonzeros,
+                  number_jacobian_nonzeros, 0, BQPDProblemType::LP, options);
+         }
+   #endif
+         throw std::invalid_argument("LP solver not found");
       }
-#endif
-      throw std::invalid_argument("LP solver not found");
-   }
-};
+   };
+} // namespace
 
 #endif // UNO_LPSOLVERFACTORY_H
diff --git a/uno/solvers/QP/BQPDSolver.cpp b/uno/solvers/QP/BQPDSolver.cpp
index 959cc2b..5cfa3fb 100644
--- a/uno/solvers/QP/BQPDSolver.cpp
+++ b/uno/solvers/QP/BQPDSolver.cpp
@@ -13,9 +13,10 @@
 #include "tools/Logger.hpp"
 #include "tools/Options.hpp"
 
+namespace uno {
 #define BIG 1e30
 
-extern "C" {
+   extern "C" {
    // fortran common block used in bqpd/bqpd.f
    extern struct {
       int kk, ll, kkk, lll, mxws, mxlws;
@@ -30,278 +31,279 @@ extern "C" {
    bqpd_(const int* n, const int* m, int* k, int* kmax, double* a, int* la, double* x, double* bl, double* bu, double* f, double* fmin, double* g,
          double* r, double* w, double* e, int* ls, double* alp, int* lp, int* mlp, int* peq, double* ws, int* lws, const int* mode, int* ifail,
          int* info, int* iprint, int* nout);
-}
+   }
 
-// preallocate a bunch of stuff
-BQPDSolver::BQPDSolver(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros,
+   // preallocate a bunch of stuff
+   BQPDSolver::BQPDSolver(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros,
          size_t number_hessian_nonzeros, BQPDProblemType problem_type, const Options& options):
-      QPSolver(), number_hessian_nonzeros(number_hessian_nonzeros),
-      lb(number_variables + number_constraints),
-      ub(number_variables + number_constraints),
-      jacobian(number_jacobian_nonzeros + number_objective_gradient_nonzeros), // Jacobian + objective gradient
-      jacobian_sparsity(number_jacobian_nonzeros + number_objective_gradient_nonzeros + number_constraints + 3),
-      kmax(problem_type == BQPDProblemType::QP ? options.get_int("BQPD_kmax") : 0), alp(this->mlp), lp(this->mlp),
-      active_set(number_variables + number_constraints),
-      w(number_variables + number_constraints), gradient_solution(number_variables), residuals(number_variables + number_constraints),
-      e(number_variables + number_constraints),
-      size_hessian_sparsity(problem_type == BQPDProblemType::QP ? number_hessian_nonzeros + number_variables + 3 : 0),
-      size_hessian_workspace(number_hessian_nonzeros + this->kmax * (this->kmax + 9) / 2 + 2 * number_variables + number_constraints + this->mxwk0),
-      size_hessian_sparsity_workspace(this->size_hessian_sparsity + this->kmax + this->mxiwk0),
-      hessian_values(this->size_hessian_workspace),
-      hessian_sparsity(this->size_hessian_sparsity_workspace),
-      current_hessian_indices(number_variables),
-      print_subproblem(options.get_bool("BQPD_print_subproblem")) {
-   // default active set
-   for (size_t variable_index: Range(number_variables + number_constraints)) {
-      this->active_set[variable_index] = static_cast<int>(variable_index) + this->fortran_shift;
+         QPSolver(), number_hessian_nonzeros(number_hessian_nonzeros),
+         lb(number_variables + number_constraints),
+         ub(number_variables + number_constraints),
+         jacobian(number_jacobian_nonzeros + number_objective_gradient_nonzeros), // Jacobian + objective gradient
+         jacobian_sparsity(number_jacobian_nonzeros + number_objective_gradient_nonzeros + number_constraints + 3),
+         kmax(problem_type == BQPDProblemType::QP ? options.get_int("BQPD_kmax") : 0), alp(static_cast<size_t>(this->mlp)),
+         lp(static_cast<size_t>(this->mlp)),
+         active_set(number_variables + number_constraints),
+         w(number_variables + number_constraints), gradient_solution(number_variables), residuals(number_variables + number_constraints),
+         e(number_variables + number_constraints),
+         size_hessian_sparsity(problem_type == BQPDProblemType::QP ? number_hessian_nonzeros + number_variables + 3 : 0),
+         size_hessian_workspace(number_hessian_nonzeros + static_cast<size_t>(this->kmax * (this->kmax + 9) / 2) + 2 * number_variables +
+                                number_constraints + this->mxwk0),
+         size_hessian_sparsity_workspace(this->size_hessian_sparsity + static_cast<size_t>(this->kmax) + this->mxiwk0),
+         hessian_values(this->size_hessian_workspace),
+         hessian_sparsity(this->size_hessian_sparsity_workspace),
+         current_hessian_indices(number_variables),
+         print_subproblem(options.get_bool("BQPD_print_subproblem")) {
+      // default active set
+      for (size_t variable_index: Range(number_variables + number_constraints)) {
+         this->active_set[variable_index] = static_cast<int>(variable_index) + this->fortran_shift;
+      }
    }
-}
 
-void BQPDSolver::solve_QP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-      const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-      const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-      const RectangularMatrix<double>& constraint_jacobian, const SymmetricMatrix<double>& hessian, const Vector<double>& initial_point,
-      Direction& direction, const WarmstartInformation& warmstart_information) {
-   if (warmstart_information.objective_changed || warmstart_information.constraints_changed) {
-      this->save_hessian_to_local_format(hessian);
-   }
-   if (this->print_subproblem) {
-      DEBUG << "QP:\n";
-      DEBUG << "Hessian: " << hessian;
+   void BQPDSolver::solve_QP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+         const RectangularMatrix<double>& constraint_jacobian, const SymmetricMatrix<size_t, double>& hessian, const Vector<double>& initial_point,
+         Direction& direction, const WarmstartInformation& warmstart_information) {
+      if (warmstart_information.objective_changed || warmstart_information.constraints_changed) {
+         this->save_hessian_to_local_format(hessian);
+      }
+      if (this->print_subproblem) {
+         DEBUG << "QP:\n";
+         DEBUG << "Hessian: " << hessian;
+      }
+      this->solve_subproblem(number_variables, number_constraints, variables_lower_bounds, variables_upper_bounds, constraints_lower_bounds,
+            constraints_upper_bounds, linear_objective, constraint_jacobian, initial_point, direction, warmstart_information);
    }
-   this->solve_subproblem(number_variables, number_constraints, variables_lower_bounds, variables_upper_bounds, constraints_lower_bounds,
-         constraints_upper_bounds, linear_objective, constraint_jacobian, initial_point, direction, warmstart_information);
-}
 
-void BQPDSolver::solve_LP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-      const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-      const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-      const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
-      const WarmstartInformation& warmstart_information) {
-   if (this->print_subproblem) {
-      DEBUG << "LP:\n";
+   void BQPDSolver::solve_LP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+         const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
+         const WarmstartInformation& warmstart_information) {
+      if (this->print_subproblem) {
+         DEBUG << "LP:\n";
+      }
+      this->solve_subproblem(number_variables, number_constraints, variables_lower_bounds, variables_upper_bounds, constraints_lower_bounds,
+            constraints_upper_bounds, linear_objective, constraint_jacobian, initial_point, direction, warmstart_information);
    }
-   this->solve_subproblem(number_variables, number_constraints, variables_lower_bounds, variables_upper_bounds, constraints_lower_bounds,
-         constraints_upper_bounds, linear_objective, constraint_jacobian, initial_point, direction, warmstart_information);
-}
 
-void BQPDSolver::solve_subproblem(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-      const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-      const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-      const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
-      const WarmstartInformation& warmstart_information) {
-   // initialize wsc_ common block (Hessian & workspace for BQPD)
-   // setting the common block here ensures that several instances of BQPD can run simultaneously
-   wsc_.kk = static_cast<int>(this->number_hessian_nonzeros);
-   wsc_.ll = static_cast<int>(this->size_hessian_sparsity);
-   wsc_.mxws = static_cast<int>(this->size_hessian_workspace);
-   wsc_.mxlws = static_cast<int>(this->size_hessian_sparsity_workspace);
-   kktalphac_.alpha = 0; // inertia control
+   void BQPDSolver::solve_subproblem(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+         const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
+         const WarmstartInformation& warmstart_information) {
+      // initialize wsc_ common block (Hessian & workspace for BQPD)
+      // setting the common block here ensures that several instances of BQPD can run simultaneously
+      wsc_.kk = static_cast<int>(this->number_hessian_nonzeros);
+      wsc_.ll = static_cast<int>(this->size_hessian_sparsity);
+      wsc_.mxws = static_cast<int>(this->size_hessian_workspace);
+      wsc_.mxlws = static_cast<int>(this->size_hessian_sparsity_workspace);
+      kktalphac_.alpha = 0; // inertia control
 
-   if (this->print_subproblem) {
-      DEBUG << "objective gradient: " << linear_objective;
-      for (size_t constraint_index: Range(number_constraints)) {
-         DEBUG << "gradient c" << constraint_index << ": " << constraint_jacobian[constraint_index];
-      }
-      for (size_t variable_index: Range(number_variables)) {
-         DEBUG << "d_x" << variable_index << " in [" << variables_lower_bounds[variable_index] << ", " << variables_upper_bounds[variable_index] << "]\n";
-      }
-      for (size_t constraint_index: Range(number_constraints)) {
-         DEBUG << "linearized c" << constraint_index << " in [" << constraints_lower_bounds[constraint_index] << ", " << constraints_upper_bounds[constraint_index] << "]\n";
+      if (this->print_subproblem) {
+         DEBUG << "objective gradient: " << linear_objective;
+         for (size_t constraint_index: Range(number_constraints)) {
+            DEBUG << "gradient c" << constraint_index << ": " << constraint_jacobian[constraint_index];
+         }
+         for (size_t variable_index: Range(number_variables)) {
+            DEBUG << "d_x" << variable_index << " in [" << variables_lower_bounds[variable_index] << ", " << variables_upper_bounds[variable_index] << "]\n";
+         }
+         for (size_t constraint_index: Range(number_constraints)) {
+            DEBUG << "linearized c" << constraint_index << " in [" << constraints_lower_bounds[constraint_index] << ", " << constraints_upper_bounds[constraint_index] << "]\n";
+         }
       }
-   }
 
-   // Jacobian (objective and constraints)
-   if (warmstart_information.objective_changed || warmstart_information.constraints_changed) {
-      this->save_gradients_to_local_format(number_constraints, linear_objective, constraint_jacobian);
-   }
+      // Jacobian (objective and constraints)
+      if (warmstart_information.objective_changed || warmstart_information.constraints_changed) {
+         this->save_gradients_to_local_format(number_constraints, linear_objective, constraint_jacobian);
+      }
 
-   // set variable bounds
-   if (warmstart_information.variable_bounds_changed) {
-      for (size_t variable_index: Range(number_variables)) {
-         this->lb[variable_index] = (variables_lower_bounds[variable_index] == -INF<double>) ? -BIG : variables_lower_bounds[variable_index];
-         this->ub[variable_index] = (variables_upper_bounds[variable_index] == INF<double>) ? BIG : variables_upper_bounds[variable_index];
+      // set variable bounds
+      if (warmstart_information.variable_bounds_changed) {
+         for (size_t variable_index: Range(number_variables)) {
+            this->lb[variable_index] = (variables_lower_bounds[variable_index] == -INF<double>) ? -BIG : variables_lower_bounds[variable_index];
+            this->ub[variable_index] = (variables_upper_bounds[variable_index] == INF<double>) ? BIG : variables_upper_bounds[variable_index];
+         }
       }
-   }
-   // set constraint bounds
-   if (warmstart_information.constraint_bounds_changed) {
-      for (size_t constraint_index: Range(number_constraints)) {
-         this->lb[number_variables + constraint_index] = (constraints_lower_bounds[constraint_index] == -INF<double>) ? -BIG : constraints_lower_bounds[constraint_index];
-         this->ub[number_variables + constraint_index] = (constraints_upper_bounds[constraint_index] == INF<double>) ? BIG : constraints_upper_bounds[constraint_index];
+      // set constraint bounds
+      if (warmstart_information.constraint_bounds_changed) {
+         for (size_t constraint_index: Range(number_constraints)) {
+            this->lb[number_variables + constraint_index] = (constraints_lower_bounds[constraint_index] == -INF<double>) ? -BIG : constraints_lower_bounds[constraint_index];
+            this->ub[number_variables + constraint_index] = (constraints_upper_bounds[constraint_index] == INF<double>) ? BIG : constraints_upper_bounds[constraint_index];
+         }
       }
-   }
 
-   direction.primals = initial_point;
-   const int n = static_cast<int>(number_variables);
-   const int m = static_cast<int>(number_constraints);
+      direction.primals = initial_point;
+      const int n = static_cast<int>(number_variables);
+      const int m = static_cast<int>(number_constraints);
 
-   const BQPDMode mode = this->determine_mode(warmstart_information);
-   const int mode_integer = static_cast<int>(mode);
+      const BQPDMode mode = this->determine_mode(warmstart_information);
+      const int mode_integer = static_cast<int>(mode);
 
-   // solve the LP/QP
-   bqpd_(&n, &m, &this->k, &this->kmax, this->jacobian.data(), this->jacobian_sparsity.data(), direction.primals.data(), this->lb.data(),
-         this->ub.data(), &direction.subproblem_objective, &this->fmin, this->gradient_solution.data(), this->residuals.data(), this->w.data(),
-         this->e.data(), this->active_set.data(), this->alp.data(), this->lp.data(), &this->mlp, &this->peq_solution, this->hessian_values.data(),
-         this->hessian_sparsity.data(), &mode_integer, &this->ifail, this->info.data(), &this->iprint, &this->nout);
-   const BQPDStatus bqpd_status = BQPDSolver::bqpd_status_from_int(this->ifail);
-   direction.status = BQPDSolver::status_from_bqpd_status(bqpd_status);
-   this->number_calls++;
+      // solve the LP/QP
+      bqpd_(&n, &m, &this->k, &this->kmax, this->jacobian.data(), this->jacobian_sparsity.data(), direction.primals.data(), this->lb.data(),
+            this->ub.data(), &direction.subproblem_objective, &this->fmin, this->gradient_solution.data(), this->residuals.data(), this->w.data(),
+            this->e.data(), this->active_set.data(), this->alp.data(), this->lp.data(), &this->mlp, &this->peq_solution, this->hessian_values.data(),
+            this->hessian_sparsity.data(), &mode_integer, &this->ifail, this->info.data(), &this->iprint, &this->nout);
+      const BQPDStatus bqpd_status = BQPDSolver::bqpd_status_from_int(this->ifail);
+      direction.status = BQPDSolver::status_from_bqpd_status(bqpd_status);
+      this->number_calls++;
 
-   // project solution into bounds
-   for (size_t variable_index: Range(number_variables)) {
-      direction.primals[variable_index] = std::min(std::max(direction.primals[variable_index], variables_lower_bounds[variable_index]),
-            variables_upper_bounds[variable_index]);
+      // project solution into bounds
+      for (size_t variable_index: Range(number_variables)) {
+         direction.primals[variable_index] = std::min(std::max(direction.primals[variable_index], variables_lower_bounds[variable_index]),
+               variables_upper_bounds[variable_index]);
+      }
+      this->categorize_constraints(number_variables, direction);
    }
-   this->categorize_constraints(number_variables, direction);
-}
 
-BQPDMode BQPDSolver::determine_mode(const WarmstartInformation& warmstart_information) const {
-   BQPDMode mode = (this->number_calls == 0) ? BQPDMode::ACTIVE_SET_EQUALITIES : BQPDMode::USER_DEFINED;
-   // if problem changed, use cold start
-   if (warmstart_information.problem_changed) {
-      mode = BQPDMode::ACTIVE_SET_EQUALITIES;
-   }
-   // if only the variable bounds changed, reuse the active set estimate and the Jacobian information
-   else if (warmstart_information.variable_bounds_changed && not warmstart_information.objective_changed &&
-         not warmstart_information.constraints_changed && not warmstart_information.constraint_bounds_changed) {
-      mode = BQPDMode::UNCHANGED_ACTIVE_SET_AND_JACOBIAN;
+   BQPDMode BQPDSolver::determine_mode(const WarmstartInformation& warmstart_information) const {
+      BQPDMode mode = (this->number_calls == 0) ? BQPDMode::ACTIVE_SET_EQUALITIES : BQPDMode::USER_DEFINED;
+      // if problem changed, use cold start
+      if (warmstart_information.problem_changed) {
+         mode = BQPDMode::ACTIVE_SET_EQUALITIES;
+      }
+         // if only the variable bounds changed, reuse the active set estimate and the Jacobian information
+      else if (warmstart_information.variable_bounds_changed && not warmstart_information.objective_changed &&
+               not warmstart_information.constraints_changed && not warmstart_information.constraint_bounds_changed) {
+         mode = BQPDMode::UNCHANGED_ACTIVE_SET_AND_JACOBIAN;
+      }
+      return mode;
    }
-   return mode;
-}
 
-// save Hessian (in arbitrary format) to a "weak" CSC format: compressed columns but row indices are not sorted, nor unique
-void BQPDSolver::save_hessian_to_local_format(const SymmetricMatrix<double>& hessian) {
-   const size_t header_size = 1;
-   // pointers withing the single array
-   int* row_indices = &this->hessian_sparsity[header_size];
-   int* column_starts = &this->hessian_sparsity[header_size + hessian.number_nonzeros];
-   // header
-   this->hessian_sparsity[0] = static_cast<int>(hessian.number_nonzeros + 1);
-   // count the elements in each column
-   for (size_t column_index: Range(hessian.dimension + 1)) {
-      column_starts[column_index] = 0;
-   }
-   for (const auto [row_index, column_index, element]: hessian) {
-      column_starts[column_index + 1]++;
-   }
-   // carry over the column starts
-   for (size_t column_index: Range(1, hessian.dimension + 1)) {
-      column_starts[column_index] += column_starts[column_index - 1];
-      column_starts[column_index - 1] += this->fortran_shift;
-   }
-   column_starts[hessian.dimension] += this->fortran_shift;
-   // copy the entries
-   //std::vector<int> current_indices(hessian.dimension);
-   this->current_hessian_indices.fill(0);
-   for (const auto [row_index, column_index, element]: hessian) {
-      const size_t index = static_cast<size_t>(column_starts[column_index] + this->current_hessian_indices[column_index] - this->fortran_shift);
-      assert(index <= static_cast<size_t>(column_starts[column_index + 1]) &&
-             "BQPD: error in converting the Hessian matrix to the local format. Try setting the sparse format to CSC");
-      this->hessian_values[index] = element;
-      row_indices[index] = static_cast<int>(row_index) + this->fortran_shift;
-      this->current_hessian_indices[column_index]++;
+   // save Hessian (in arbitrary format) to a "weak" CSC format: compressed columns but row indices are not sorted, nor unique
+   void BQPDSolver::save_hessian_to_local_format(const SymmetricMatrix<size_t, double>& hessian) {
+      const size_t header_size = 1;
+      // pointers withing the single array
+      int* row_indices = &this->hessian_sparsity[header_size];
+      int* column_starts = &this->hessian_sparsity[header_size + hessian.number_nonzeros];
+      // header
+      this->hessian_sparsity[0] = static_cast<int>(hessian.number_nonzeros + 1);
+      // count the elements in each column
+      for (size_t column_index: Range(hessian.dimension + 1)) {
+         column_starts[column_index] = 0;
+      }
+      for (const auto [row_index, column_index, element]: hessian) {
+         column_starts[column_index + 1]++;
+      }
+      // carry over the column starts
+      for (size_t column_index: Range(1, hessian.dimension + 1)) {
+         column_starts[column_index] += column_starts[column_index - 1];
+         column_starts[column_index - 1] += this->fortran_shift;
+      }
+      column_starts[hessian.dimension] += this->fortran_shift;
+      // copy the entries
+      //std::vector<int> current_indices(hessian.dimension);
+      this->current_hessian_indices.fill(0);
+      for (const auto [row_index, column_index, element]: hessian) {
+         const size_t index = static_cast<size_t>(column_starts[column_index] + this->current_hessian_indices[column_index] - this->fortran_shift);
+         assert(index <= static_cast<size_t>(column_starts[column_index + 1]) &&
+                "BQPD: error in converting the Hessian matrix to the local format. Try setting the sparse format to CSC");
+         this->hessian_values[index] = element;
+         row_indices[index] = static_cast<int>(row_index) + this->fortran_shift;
+         this->current_hessian_indices[column_index]++;
+      }
    }
-}
 
-void BQPDSolver::save_gradients_to_local_format(size_t number_constraints, const SparseVector<double>& linear_objective,
-      const RectangularMatrix<double>& constraint_jacobian) {
-   size_t current_index = 0;
-   for (const auto [variable_index, derivative]: linear_objective) {
-      this->jacobian[current_index] = derivative;
-      this->jacobian_sparsity[current_index + 1] = static_cast<int>(variable_index) + this->fortran_shift;
-      current_index++;
-   }
-   for (size_t constraint_index: Range(number_constraints)) {
-      for (const auto [variable_index, derivative]: constraint_jacobian[constraint_index]) {
+   void BQPDSolver::save_gradients_to_local_format(size_t number_constraints, const SparseVector<double>& linear_objective,
+         const RectangularMatrix<double>& constraint_jacobian) {
+      size_t current_index = 0;
+      for (const auto [variable_index, derivative]: linear_objective) {
          this->jacobian[current_index] = derivative;
          this->jacobian_sparsity[current_index + 1] = static_cast<int>(variable_index) + this->fortran_shift;
          current_index++;
       }
-   }
-   current_index++;
-   this->jacobian_sparsity[0] = static_cast<int>(current_index);
-   // header
-   size_t size = 1;
-   this->jacobian_sparsity[current_index] = static_cast<int>(size);
-   current_index++;
-   size += linear_objective.size();
-   this->jacobian_sparsity[current_index] = static_cast<int>(size);
-   current_index++;
-   for (size_t constraint_index: Range(number_constraints)) {
-      size += constraint_jacobian[constraint_index].size();
+      for (size_t constraint_index: Range(number_constraints)) {
+         for (const auto [variable_index, derivative]: constraint_jacobian[constraint_index]) {
+            this->jacobian[current_index] = derivative;
+            this->jacobian_sparsity[current_index + 1] = static_cast<int>(variable_index) + this->fortran_shift;
+            current_index++;
+         }
+      }
+      current_index++;
+      this->jacobian_sparsity[0] = static_cast<int>(current_index);
+      // header
+      size_t size = 1;
       this->jacobian_sparsity[current_index] = static_cast<int>(size);
       current_index++;
+      size += linear_objective.size();
+      this->jacobian_sparsity[current_index] = static_cast<int>(size);
+      current_index++;
+      for (size_t constraint_index: Range(number_constraints)) {
+         size += constraint_jacobian[constraint_index].size();
+         this->jacobian_sparsity[current_index] = static_cast<int>(size);
+         current_index++;
+      }
    }
-}
 
-void BQPDSolver::categorize_constraints(size_t number_variables, Direction& direction) {
-   direction.multipliers.reset();
+   void BQPDSolver::categorize_constraints(size_t number_variables, Direction& direction) {
+      direction.multipliers.reset();
 
-   // active constraints
-   for (size_t active_constraint_index: Range(number_variables - static_cast<size_t>(this->k))) {
-      const size_t index = static_cast<size_t>(std::abs(this->active_set[active_constraint_index]) - this->fortran_shift);
+      // active constraints
+      for (size_t active_constraint_index: Range(number_variables - static_cast<size_t>(this->k))) {
+         const size_t index = static_cast<size_t>(std::abs(this->active_set[active_constraint_index]) - this->fortran_shift);
 
-      if (index < number_variables) {
-         // bound constraint
-         if (0 <= this->active_set[active_constraint_index]) { // lower bound active
-            direction.multipliers.lower_bounds[index] = this->residuals[index];
-            direction.active_set.bounds.at_lower_bound.push_back(index);
-         }
-         else { // upper bound active */
-            direction.multipliers.upper_bounds[index] = -this->residuals[index];
-            direction.active_set.bounds.at_upper_bound.push_back(index);
-         }
-      }
-      else {
-         // general constraint
-         size_t constraint_index = index - number_variables;
-         if (0 <= this->active_set[active_constraint_index]) { // lower bound active
-            direction.multipliers.constraints[constraint_index] = this->residuals[index];
-            direction.active_set.constraints.at_lower_bound.push_back(constraint_index);
+         if (index < number_variables) {
+            // bound constraint
+            if (0 <= this->active_set[active_constraint_index]) { // lower bound active
+               direction.multipliers.lower_bounds[index] = this->residuals[index];
+               direction.active_bounds.at_lower_bound.push_back(index);
+            }
+            else { // upper bound active */
+               direction.multipliers.upper_bounds[index] = -this->residuals[index];
+               direction.active_bounds.at_upper_bound.push_back(index);
+            }
          }
-         else { // upper bound active
-            direction.multipliers.constraints[constraint_index] = -this->residuals[index];
-            direction.active_set.constraints.at_upper_bound.push_back(constraint_index);
+         else {
+            // general constraint
+            size_t constraint_index = index - number_variables;
+            if (0 <= this->active_set[active_constraint_index]) { // lower bound active
+               direction.multipliers.constraints[constraint_index] = this->residuals[index];
+            }
+            else { // upper bound active
+               direction.multipliers.constraints[constraint_index] = -this->residuals[index];
+            }
          }
       }
    }
-}
 
-BQPDStatus BQPDSolver::bqpd_status_from_int(int ifail) {
-   assert(0 <= ifail && ifail <= 9 && "BQPDSolver.bqpd_status_from_int: ifail does not belong to [0, 9]");
-   return static_cast<BQPDStatus>(ifail);
-}
+   BQPDStatus BQPDSolver::bqpd_status_from_int(int ifail) {
+      assert(0 <= ifail && ifail <= 9 && "BQPDSolver.bqpd_status_from_int: ifail does not belong to [0, 9]");
+      return static_cast<BQPDStatus>(ifail);
+   }
 
-SubproblemStatus BQPDSolver::status_from_bqpd_status(BQPDStatus bqpd_status) {
-   switch (bqpd_status) {
-      case BQPDStatus::OPTIMAL:
-         return SubproblemStatus::OPTIMAL;
-      case BQPDStatus::UNBOUNDED_PROBLEM:
-         return SubproblemStatus::UNBOUNDED_PROBLEM;
-      case BQPDStatus::BOUND_INCONSISTENCY:
-         DEBUG << YELLOW << "BQPD error: bound inconsistency\n" << RESET;
-         return SubproblemStatus::INFEASIBLE;
-      case BQPDStatus::INFEASIBLE:
-         return SubproblemStatus::INFEASIBLE;
-      // errors
-      case BQPDStatus::INCORRECT_PARAMETER:
-         DEBUG << YELLOW << "BQPD error: incorrect parameter\n" << RESET;
-         return SubproblemStatus::ERROR;
-      case BQPDStatus::LP_INSUFFICIENT_SPACE:
-         DEBUG << YELLOW << "BQPD error: LP insufficient space\n" << RESET;
-         return SubproblemStatus::ERROR;
-      case BQPDStatus::HESSIAN_INSUFFICIENT_SPACE:
-         DEBUG << YELLOW << "BQPD kmax too small, continue anyway\n" << RESET;
-         return SubproblemStatus::ERROR;
-      case BQPDStatus::SPARSE_INSUFFICIENT_SPACE:
-         DEBUG << YELLOW << "BQPD error: sparse insufficient space\n" << RESET;
-         return SubproblemStatus::ERROR;
-      case BQPDStatus::MAX_RESTARTS_REACHED:
-         DEBUG << YELLOW << "BQPD max restarts reached\n" << RESET;
-         return SubproblemStatus::ERROR;
-      case BQPDStatus::UNDEFINED:
-         DEBUG << YELLOW << "BQPD error: undefined\n" << RESET;
-         return SubproblemStatus::ERROR;
+   SubproblemStatus BQPDSolver::status_from_bqpd_status(BQPDStatus bqpd_status) {
+      switch (bqpd_status) {
+         case BQPDStatus::OPTIMAL:
+            return SubproblemStatus::OPTIMAL;
+         case BQPDStatus::UNBOUNDED_PROBLEM:
+            return SubproblemStatus::UNBOUNDED_PROBLEM;
+         case BQPDStatus::BOUND_INCONSISTENCY:
+            DEBUG << YELLOW << "BQPD error: bound inconsistency\n" << RESET;
+            return SubproblemStatus::ERROR;
+         case BQPDStatus::INFEASIBLE:
+            return SubproblemStatus::INFEASIBLE;
+            // errors
+         case BQPDStatus::INCORRECT_PARAMETER:
+            DEBUG << YELLOW << "BQPD error: incorrect parameter\n" << RESET;
+            return SubproblemStatus::ERROR;
+         case BQPDStatus::LP_INSUFFICIENT_SPACE:
+            DEBUG << YELLOW << "BQPD error: LP insufficient space\n" << RESET;
+            return SubproblemStatus::ERROR;
+         case BQPDStatus::HESSIAN_INSUFFICIENT_SPACE:
+            DEBUG << YELLOW << "BQPD kmax too small, continue anyway\n" << RESET;
+            return SubproblemStatus::ERROR;
+         case BQPDStatus::SPARSE_INSUFFICIENT_SPACE:
+            DEBUG << YELLOW << "BQPD error: sparse insufficient space\n" << RESET;
+            return SubproblemStatus::ERROR;
+         case BQPDStatus::MAX_RESTARTS_REACHED:
+            DEBUG << YELLOW << "BQPD max restarts reached\n" << RESET;
+            return SubproblemStatus::ERROR;
+         case BQPDStatus::UNDEFINED:
+            DEBUG << YELLOW << "BQPD error: undefined\n" << RESET;
+            return SubproblemStatus::ERROR;
+      }
+      throw std::invalid_argument("The BQPD ifail is not consistent with the Uno status values");
    }
-   throw std::invalid_argument("The BQPD ifail is not consistent with the Uno status values");
-}
\ No newline at end of file
+} // namespace
\ No newline at end of file
diff --git a/uno/solvers/QP/BQPDSolver.hpp b/uno/solvers/QP/BQPDSolver.hpp
index 9bb224c..8c5f9bd 100644
--- a/uno/solvers/QP/BQPDSolver.hpp
+++ b/uno/solvers/QP/BQPDSolver.hpp
@@ -9,91 +9,93 @@
 #include "linear_algebra/Vector.hpp"
 #include "QPSolver.hpp"
 
-// forward declaration
-class Options;
+namespace uno {
+   // forward declaration
+   class Options;
 
-// see bqpd.f
-enum class BQPDStatus {
-   OPTIMAL = 0,
-   UNBOUNDED_PROBLEM = 1,
-   BOUND_INCONSISTENCY = 2,
-   INFEASIBLE = 3,
-   INCORRECT_PARAMETER = 4,
-   LP_INSUFFICIENT_SPACE = 5,
-   HESSIAN_INSUFFICIENT_SPACE = 6,
-   SPARSE_INSUFFICIENT_SPACE = 7,
-   MAX_RESTARTS_REACHED = 8,
-   UNDEFINED = 9
-};
+   // see bqpd.f
+   enum class BQPDStatus {
+      OPTIMAL = 0,
+      UNBOUNDED_PROBLEM = 1,
+      BOUND_INCONSISTENCY = 2,
+      INFEASIBLE = 3,
+      INCORRECT_PARAMETER = 4,
+      LP_INSUFFICIENT_SPACE = 5,
+      HESSIAN_INSUFFICIENT_SPACE = 6,
+      SPARSE_INSUFFICIENT_SPACE = 7,
+      MAX_RESTARTS_REACHED = 8,
+      UNDEFINED = 9
+   };
 
-enum BQPDMode {
-   COLD_START = 0,
-   ACTIVE_SET_EQUALITIES = 1, // cold start
-   USER_DEFINED = 2, // hot start
-   UNCHANGED_ACTIVE_SET = 3,
-   UNCHANGED_ACTIVE_SET_AND_JACOBIAN = 4,
-   UNCHANGED_ACTIVE_SET_AND_REDUCED_HESSIAN = 5,
-   UNCHANGED_ACTIVE_SET_AND_JACOBIAN_AND_REDUCED_HESSIAN = 6, // warm start
-};
+   enum BQPDMode {
+      COLD_START = 0,
+      ACTIVE_SET_EQUALITIES = 1, // cold start
+      USER_DEFINED = 2, // hot start
+      UNCHANGED_ACTIVE_SET = 3,
+      UNCHANGED_ACTIVE_SET_AND_JACOBIAN = 4,
+      UNCHANGED_ACTIVE_SET_AND_REDUCED_HESSIAN = 5,
+      UNCHANGED_ACTIVE_SET_AND_JACOBIAN_AND_REDUCED_HESSIAN = 6, // warm start
+   };
 
-enum class BQPDProblemType {LP, QP};
+   enum class BQPDProblemType {LP, QP};
 
-class BQPDSolver : public QPSolver {
-public:
-   BQPDSolver(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros,
-         size_t number_hessian_nonzeros, BQPDProblemType problem_type, const Options& options);
+   class BQPDSolver : public QPSolver {
+   public:
+      BQPDSolver(size_t number_variables, size_t number_constraints, size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros,
+            size_t number_hessian_nonzeros, BQPDProblemType problem_type, const Options& options);
 
-   void solve_LP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-         const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
-         const WarmstartInformation& warmstart_information) override;
+      void solve_LP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+            const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+            const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+            const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
+            const WarmstartInformation& warmstart_information) override;
 
-   void solve_QP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-         const RectangularMatrix<double>& constraint_jacobian, const SymmetricMatrix<double>& hessian, const Vector<double>& initial_point,
-         Direction& direction, const WarmstartInformation& warmstart_information) override;
+      void solve_QP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+            const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+            const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+            const RectangularMatrix<double>& constraint_jacobian, const SymmetricMatrix<size_t, double>& hessian, const Vector<double>& initial_point,
+            Direction& direction, const WarmstartInformation& warmstart_information) override;
 
-private:
-   const size_t number_hessian_nonzeros;
-   std::vector<double> lb{}, ub{}; // lower and upper bounds of variables and constraints
+   private:
+      const size_t number_hessian_nonzeros;
+      std::vector<double> lb{}, ub{}; // lower and upper bounds of variables and constraints
 
-   std::vector<double> jacobian{};
-   std::vector<int> jacobian_sparsity{};
-   int kmax{0}, mlp{1000};
-   size_t mxwk0{2000000}, mxiwk0{500000};
-   std::array<int, 100> info{};
-   std::vector<double> alp{};
-   std::vector<int> lp{}, active_set{};
-   std::vector<double> w{}, gradient_solution{}, residuals{}, e{};
-   size_t size_hessian_sparsity{};
-   size_t size_hessian_workspace{};
-   size_t size_hessian_sparsity_workspace{};
-   std::vector<double> hessian_values{};
-   std::vector<int> hessian_sparsity{};
-   int k{0};
-   int iprint{0}, nout{6};
-   double fmin{-1e20};
-   int peq_solution{0}, ifail{0};
-   const int fortran_shift{1};
-   Vector<int> current_hessian_indices{};
+      std::vector<double> jacobian{};
+      std::vector<int> jacobian_sparsity{};
+      int kmax{0}, mlp{1000};
+      size_t mxwk0{2000000}, mxiwk0{500000};
+      std::array<int, 100> info{};
+      std::vector<double> alp{};
+      std::vector<int> lp{}, active_set{};
+      std::vector<double> w{}, gradient_solution{}, residuals{}, e{};
+      size_t size_hessian_sparsity{};
+      size_t size_hessian_workspace{};
+      size_t size_hessian_sparsity_workspace{};
+      std::vector<double> hessian_values{};
+      std::vector<int> hessian_sparsity{};
+      int k{0};
+      int iprint{0}, nout{6};
+      double fmin{-1e20};
+      int peq_solution{0}, ifail{0};
+      const int fortran_shift{1};
+      Vector<int> current_hessian_indices{};
 
-   size_t number_calls{0};
-   const bool print_subproblem;
+      size_t number_calls{0};
+      const bool print_subproblem;
 
-   void solve_subproblem(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-         const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
-         const WarmstartInformation& warmstart_information);
-   void categorize_constraints(size_t number_variables, Direction& direction);
-   void save_hessian_to_local_format(const SymmetricMatrix<double>& hessian);
-   void save_gradients_to_local_format(size_t number_constraints, const SparseVector<double>& linear_objective,
-         const RectangularMatrix<double>& constraint_jacobian);
-   [[nodiscard]] BQPDMode determine_mode(const WarmstartInformation& warmstart_information) const;
-   static BQPDStatus bqpd_status_from_int(int ifail);
-   static SubproblemStatus status_from_bqpd_status(BQPDStatus bqpd_status);
-};
+      void solve_subproblem(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+            const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+            const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+            const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
+            const WarmstartInformation& warmstart_information);
+      void categorize_constraints(size_t number_variables, Direction& direction);
+      void save_hessian_to_local_format(const SymmetricMatrix<size_t, double>& hessian);
+      void save_gradients_to_local_format(size_t number_constraints, const SparseVector<double>& linear_objective,
+            const RectangularMatrix<double>& constraint_jacobian);
+      [[nodiscard]] BQPDMode determine_mode(const WarmstartInformation& warmstart_information) const;
+      static BQPDStatus bqpd_status_from_int(int ifail);
+      static SubproblemStatus status_from_bqpd_status(BQPDStatus bqpd_status);
+   };
+} // namespace
 
-#endif // UNO_BQPDSOLVER_H
+#endif // UNO_BQPDSOLVER_H
\ No newline at end of file
diff --git a/uno/solvers/QP/QPSolver.hpp b/uno/solvers/QP/QPSolver.hpp
index 5bbc8e1..1af8d86 100644
--- a/uno/solvers/QP/QPSolver.hpp
+++ b/uno/solvers/QP/QPSolver.hpp
@@ -7,39 +7,41 @@
 #include <vector>
 #include "solvers/LP/LPSolver.hpp"
 
-// forward declarations
-class Direction;
-template <typename ElementType>
-class RectangularMatrix;
-template <typename ElementType>
-class SparseVector;
-template <typename ElementType>
-class SymmetricMatrix;
-class WarmstartInformation;
-
-/*! \class QPSolver
- * \brief QP solver
- *
- */
-class QPSolver : public LPSolver {
-public:
-   QPSolver();
-   ~QPSolver() override = default;
-
-   void solve_LP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-         const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
-         const WarmstartInformation& warmstart_information) override = 0;
-
-   virtual void solve_QP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
-         const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
-         const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
-         const RectangularMatrix<double>& constraint_jacobian, const SymmetricMatrix<double>& hessian, const Vector<double>& initial_point,
-         Direction& direction, const WarmstartInformation& warmstart_information) = 0;
-};
-
-inline QPSolver::QPSolver(): LPSolver() {
-}
-
-#endif // UNO_QPSOLVER_H
+namespace uno {
+   // forward declarations
+   class Direction;
+   template <typename ElementType>
+   class RectangularMatrix;
+   template <typename ElementType>
+   class SparseVector;
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrix;
+   struct WarmstartInformation;
+
+   /*! \class QPSolver
+    * \brief QP solver
+    *
+    */
+   class QPSolver : public LPSolver {
+   public:
+      QPSolver();
+      ~QPSolver() override = default;
+
+      void solve_LP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+            const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+            const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+            const RectangularMatrix<double>& constraint_jacobian, const Vector<double>& initial_point, Direction& direction,
+            const WarmstartInformation& warmstart_information) override = 0;
+
+      virtual void solve_QP(size_t number_variables, size_t number_constraints, const std::vector<double>& variables_lower_bounds,
+            const std::vector<double>& variables_upper_bounds, const std::vector<double>& constraints_lower_bounds,
+            const std::vector<double>& constraints_upper_bounds, const SparseVector<double>& linear_objective,
+            const RectangularMatrix<double>& constraint_jacobian, const SymmetricMatrix<size_t, double>& hessian, const Vector<double>& initial_point,
+            Direction& direction, const WarmstartInformation& warmstart_information) = 0;
+   };
+
+   inline QPSolver::QPSolver(): LPSolver() {
+   }
+} // namespace
+
+#endif // UNO_QPSOLVER_H
\ No newline at end of file
diff --git a/uno/solvers/QP/QPSolverFactory.hpp b/uno/solvers/QP/QPSolverFactory.hpp
index 9bbb155..ee04d40 100644
--- a/uno/solvers/QP/QPSolverFactory.hpp
+++ b/uno/solvers/QP/QPSolverFactory.hpp
@@ -11,28 +11,31 @@
 #include "BQPDSolver.hpp"
 #endif
 
-class QPSolverFactory {
-public:
-   // create a QP solver
-   static std::unique_ptr<QPSolver> create(const std::string& QP_solver_name, size_t number_variables, size_t number_constraints,
-         size_t number_objective_gradient_nonzeros, size_t number_jacobian_nonzeros, size_t number_hessian_nonzeros, const Options& options) {
-#ifdef HAS_BQPD
-      if (QP_solver_name == "BQPD") {
-         return std::make_unique<BQPDSolver>(number_variables, number_constraints, number_objective_gradient_nonzeros, number_jacobian_nonzeros,
-            number_hessian_nonzeros, BQPDProblemType::QP, options);
+namespace uno {
+   class QPSolverFactory {
+   public:
+      // create a QP solver
+      static std::unique_ptr<QPSolver> create([[maybe_unused]] const std::string& QP_solver_name, [[maybe_unused]] size_t number_variables,
+            [[maybe_unused]] size_t number_constraints, [[maybe_unused]] size_t number_objective_gradient_nonzeros,
+            [[maybe_unused]] size_t number_jacobian_nonzeros, [[maybe_unused]] size_t number_hessian_nonzeros, [[maybe_unused]] const Options& options) {
+   #ifdef HAS_BQPD
+         if (QP_solver_name == "BQPD") {
+            return std::make_unique<BQPDSolver>(number_variables, number_constraints, number_objective_gradient_nonzeros, number_jacobian_nonzeros,
+               number_hessian_nonzeros, BQPDProblemType::QP, options);
+         }
+   #endif
+         throw std::invalid_argument("QP solver name is unknown");
       }
-#endif
-      throw std::invalid_argument("QP solver name is unknown");
-   }
 
-   // return the list of available QP solvers
-   static std::vector<std::string> available_solvers() {
-      std::vector<std::string> solvers{};
-      #ifdef HAS_BQPD
-      solvers.emplace_back("BQPD");
-      #endif
-      return solvers;
-   }
-};
+      // return the list of available QP solvers
+      static std::vector<std::string> available_solvers() {
+         std::vector<std::string> solvers{};
+         #ifdef HAS_BQPD
+         solvers.emplace_back("BQPD");
+         #endif
+         return solvers;
+      }
+   };
+} // namespace
 
 #endif // UNO_QPSOLVERFACTORY_H
diff --git a/uno/solvers/linear/MA57Solver.cpp b/uno/solvers/linear/MA57Solver.cpp
index ea75d4b..ad608d1 100644
--- a/uno/solvers/linear/MA57Solver.cpp
+++ b/uno/solvers/linear/MA57Solver.cpp
@@ -7,164 +7,166 @@
 #include "linear_algebra/SymmetricMatrix.hpp"
 #include "linear_algebra/Vector.hpp"
 
-extern "C" {
-// MA57
-// default values of controlling parameters
-void ma57id_(double cntl[], int icntl[]);
-// symbolic factorization
-void ma57ad_(const int* n, const int* ne, const int irn[], const int jcn[], const int* lkeep, int keep[], int iwork[], int icntl[], int info[], double
-rinfo[]);
-// numerical factorization
-void ma57bd_(const int* n, int* ne, const double a[], /* out */ double fact[], const int* lfact, /* out */ int ifact[], const int* lifact,
-      const int* lkeep, const int keep[], int iwork[], int icntl[], double cntl[], /* out */ int info[], /* out */ double rinfo[]);
-// linear system solve without iterative refinement
-void ma57cd_(const int* job, const int* n, double fact[], int* lfact, int ifact[], int* lifact, const int* nrhs, double rhs[], const int* lrhs, double
-work[], int* lwork, int iwork[], int icntl[], int info[]);
-// linear system solve with iterative refinement
-void ma57dd_(const int* job, const int* n, int* ne, const double a[], const int irn[], const int jcn[], double fact[], int* lfact, int ifact[], int*
-lifact, const double rhs[], double x[], double resid[], double work[], int iwork[], int icntl[],
-      double cntl[], int info[], double rinfo[]);
-}
-
-MA57Solver::MA57Solver(size_t dimension, size_t number_nonzeros) : SymmetricIndefiniteLinearSolver<double>(dimension),
-   lkeep(static_cast<int>(5 * dimension + number_nonzeros + std::max(dimension, number_nonzeros) + 42)),
-   keep(static_cast<size_t>(lkeep)),
-   iwork(5 * dimension),
-   lwork(static_cast<int>(1.2 * static_cast<double>(dimension))),
-   work(static_cast<size_t>(this->lwork)), residuals(dimension) {
-   this->row_indices.reserve(number_nonzeros);
-   this->column_indices.reserve(number_nonzeros);
-   // set the default values of the controlling parameters
-   ma57id_(this->cntl.data(), this->icntl.data());
-   // suppress warning messages
-   this->icntl[4] = 0;
-   // iterative refinement enabled
-   this->icntl[8] = 1;
-}
-
-void MA57Solver::factorize(const SymmetricMatrix<double>& matrix) {
-   // general factorization method: symbolic factorization and numerical factorization
-   this->do_symbolic_factorization(matrix);
-   this->do_numerical_factorization(matrix);
-}
+namespace uno {
+   extern "C" {
+   // MA57
+   // default values of controlling parameters
+   void ma57id_(double cntl[], int icntl[]);
+   // symbolic factorization
+   void ma57ad_(const int* n, const int* ne, const int irn[], const int jcn[], const int* lkeep, int keep[], int iwork[], int icntl[], int info[], double
+   rinfo[]);
+   // numerical factorization
+   void ma57bd_(const int* n, int* ne, const double a[], /* out */ double fact[], const int* lfact, /* out */ int ifact[], const int* lifact,
+         const int* lkeep, const int keep[], int iwork[], int icntl[], double cntl[], /* out */ int info[], /* out */ double rinfo[]);
+   // linear system solve without iterative refinement
+   void ma57cd_(const int* job, const int* n, double fact[], int* lfact, int ifact[], int* lifact, const int* nrhs, double rhs[], const int* lrhs, double
+   work[], int* lwork, int iwork[], int icntl[], int info[]);
+   // linear system solve with iterative refinement
+   void ma57dd_(const int* job, const int* n, int* ne, const double a[], const int irn[], const int jcn[], double fact[], int* lfact, int ifact[], int*
+   lifact, const double rhs[], double x[], double resid[], double work[], int iwork[], int icntl[],
+         double cntl[], int info[], double rinfo[]);
+   }
 
-void MA57Solver::do_symbolic_factorization(const SymmetricMatrix<double>& matrix) {
-   assert(matrix.dimension <= this->dimension && "MA57Solver: the dimension of the matrix is larger than the preallocated size");
-   assert(matrix.number_nonzeros <= this->row_indices.capacity() &&
-      "MA57Solver: the number of nonzeros of the matrix is larger than the preallocated size");
+   MA57Solver::MA57Solver(size_t dimension, size_t number_nonzeros) : SymmetricIndefiniteLinearSolver<size_t, double>(dimension),
+         lkeep(static_cast<int>(5 * dimension + number_nonzeros + std::max(dimension, number_nonzeros) + 42)),
+         keep(static_cast<size_t>(lkeep)),
+         iwork(5 * dimension),
+         lwork(static_cast<int>(1.2 * static_cast<double>(dimension))),
+         work(static_cast<size_t>(this->lwork)), residuals(dimension) {
+      this->row_indices.reserve(number_nonzeros);
+      this->column_indices.reserve(number_nonzeros);
+      // set the default values of the controlling parameters
+      ma57id_(this->cntl.data(), this->icntl.data());
+      // suppress warning messages
+      this->icntl[4] = 0;
+      // iterative refinement enabled
+      this->icntl[8] = 1;
+   }
 
-   // build the internal matrix representation
-   this->save_matrix_to_local_format(matrix);
+   // general factorization method: symbolic factorization and numerical factorization
+   void MA57Solver::factorize(const SymmetricMatrix<size_t, double>& matrix) {
+      this->do_symbolic_factorization(matrix);
+      this->do_numerical_factorization(matrix);
+   }
 
-   const int n = static_cast<int>(matrix.dimension);
-   const int nnz = static_cast<int>(matrix.number_nonzeros);
+   void MA57Solver::do_symbolic_factorization(const SymmetricMatrix<size_t, double>& matrix) {
+      assert(matrix.dimension <= this->dimension && "MA57Solver: the dimension of the matrix is larger than the preallocated size");
+      assert(matrix.number_nonzeros <= this->row_indices.capacity() &&
+             "MA57Solver: the number of nonzeros of the matrix is larger than the preallocated size");
+
+      // build the internal matrix representation
+      this->save_sparsity_pattern_internally(matrix);
+
+      const int n = static_cast<int>(matrix.dimension);
+      const int nnz = static_cast<int>(matrix.number_nonzeros);
+
+      // symbolic factorization
+      ma57ad_(/* const */ &n,
+            /* const */ &nnz,
+            /* const */ this->row_indices.data(),
+            /* const */ this->column_indices.data(),
+            /* const */ &this->lkeep,
+            /* const */ this->keep.data(),
+            /* out */ this->iwork.data(),
+            /* const */ this->icntl.data(),
+            /* out */ this->info.data(),
+            /* out */ this->rinfo.data());
+
+      assert(0 <= info[0] && "MA57: the symbolic factorization failed");
+      if (0 < info[0]) {
+         WARNING << "MA57 has issued a warning: info(1) = " << info[0] << '\n';
+      }
+
+      // get LFACT and LIFACT and resize FACT and IFACT (no effect if resized to <= size)
+      int lfact = 2 * this->info[8];
+      int lifact = 2 * this->info[9];
+      this->fact.resize(static_cast<size_t>(lfact));
+      this->ifact.resize(static_cast<size_t>(lifact));
+
+      // store the sizes of the symbolic factorization
+      this->factorization = {n, nnz, lfact, lifact};
+   }
 
-   // symbolic factorization
-   ma57ad_(/* const */ &n,
-         /* const */ &nnz,
-         /* const */ this->row_indices.data(),
-         /* const */ this->column_indices.data(),
-         /* const */ &this->lkeep,
-         /* const */ this->keep.data(),
-         /* out */ this->iwork.data(),
-         /* const */ this->icntl.data(),
-         /* out */ this->info.data(),
-         /* out */ this->rinfo.data());
-
-   assert(0 <= info[0] && "MA57: the symbolic factorization failed");
-   if (0 < info[0]) {
-      WARNING << "MA57 has issued a warning: info(1) = " << info[0] << '\n';
+   void MA57Solver::do_numerical_factorization(const SymmetricMatrix<size_t, double>& matrix) {
+      assert(matrix.dimension <= this->dimension && "MA57Solver: the dimension of the matrix is larger than the preallocated size");
+      assert(this->factorization.nnz == static_cast<int>(matrix.number_nonzeros) && "MA57Solver: the numbers of nonzeros do not match");
+
+      const int n = static_cast<int>(matrix.dimension);
+      int nnz = static_cast<int>(matrix.number_nonzeros);
+
+      // numerical factorization
+      ma57bd_(&n,
+            &nnz,
+            /* const */ matrix.data_pointer(),
+            /* out */ this->fact.data(),
+            /* const */ &this->factorization.lfact,
+            /* out */ this->ifact.data(),
+            /* const */ &this->factorization.lifact,
+            /* const */ &this->lkeep,
+            /* const */ this->keep.data(), this->iwork.data(), this->icntl.data(), this->cntl.data(),
+            /* out */ this->info.data(),
+            /* out */ this->rinfo.data());
    }
 
-   // get LFACT and LIFACT and resize FACT and IFACT (no effect if resized to <= size)
-   int lfact = 2 * this->info[8];
-   int lifact = 2 * this->info[9];
-   this->fact.resize(static_cast<size_t>(lfact));
-   this->ifact.resize(static_cast<size_t>(lifact));
+   void MA57Solver::solve_indefinite_system(const SymmetricMatrix<size_t, double>& matrix, const Vector<double>& rhs, Vector<double>& result) {
+      // solve
+      const int n = static_cast<int>(matrix.dimension);
+      int nnz = static_cast<int>(matrix.number_nonzeros);
+      const int lrhs = n; // integer, length of rhs
+
+      // solve the linear system
+      if (this->use_iterative_refinement) {
+         ma57dd_(&this->job, &n, &nnz, matrix.data_pointer(), this->row_indices.data(), this->column_indices.data(),
+               this->fact.data(), &this->factorization.lfact, this->ifact.data(), &this->factorization.lifact,
+               rhs.data(), result.data(), this->residuals.data(), this->work.data(), this->iwork.data(), this->icntl.data(),
+               this->cntl.data(), this->info.data(), this->rinfo.data());
+      }
+      else {
+         // copy rhs into result (overwritten by MA57)
+         result = rhs;
+
+         ma57cd_(&this->job, &n, this->fact.data(), &this->factorization.lfact, this->ifact.data(),
+               &this->factorization.lifact, &this->nrhs, result.data(), &lrhs, this->work.data(), &this->lwork, this->iwork.data(),
+               this->icntl.data(), this->info.data());
+      }
+   }
 
-   // store the sizes of the symbolic factorization
-   this->factorization = {n, nnz, lfact, lifact};
-}
+   std::tuple<size_t, size_t, size_t> MA57Solver::get_inertia() const {
+      // rank = number_positive_eigenvalues + number_negative_eigenvalues
+      // n = rank + number_zero_eigenvalues
+      const size_t rank = this->rank();
+      const size_t number_negative_eigenvalues = this->number_negative_eigenvalues();
+      const size_t number_positive_eigenvalues = rank - number_negative_eigenvalues;
+      const size_t number_zero_eigenvalues = static_cast<size_t>(this->factorization.n) - rank;
+      return std::make_tuple(number_positive_eigenvalues, number_negative_eigenvalues, number_zero_eigenvalues);
+   }
 
-void MA57Solver::do_numerical_factorization(const SymmetricMatrix<double>& matrix) {
-   assert(matrix.dimension <= this->dimension && "MA57Solver: the dimension of the matrix is larger than the preallocated size");
-   assert(this->factorization.nnz == static_cast<int>(matrix.number_nonzeros) && "MA57Solver: the numbers of nonzeros do not match");
+   size_t MA57Solver::number_negative_eigenvalues() const {
+      return static_cast<size_t>(this->info[23]);
+   }
 
-   const int n = static_cast<int>(matrix.dimension);
-   int nnz = static_cast<int>(matrix.number_nonzeros);
+   /*
+   bool MA57Solver::matrix_is_positive_definite() const {
+      // positive definite = non-singular and no negative eigenvalues
+      return not this->matrix_is_singular() && this->number_negative_eigenvalues() == 0;
+   }
+   */
 
-   // numerical factorization
-   ma57bd_(&n,
-         &nnz,
-         /* const */ matrix.data_raw_pointer(),
-         /* out */ this->fact.data(),
-         /* const */ &this->factorization.lfact,
-         /* out */ this->ifact.data(),
-         /* const */ &this->factorization.lifact,
-         /* const */ &this->lkeep,
-         /* const */ this->keep.data(), this->iwork.data(), this->icntl.data(), this->cntl.data(),
-         /* out */ this->info.data(),
-         /* out */ this->rinfo.data());
-}
-
-void MA57Solver::solve_indefinite_system(const SymmetricMatrix<double>& matrix, const Vector<double>& rhs, Vector<double>& result) {
-   // solve
-   const int n = static_cast<int>(matrix.dimension);
-   int nnz = static_cast<int>(matrix.number_nonzeros);
-   const int lrhs = n; // integer, length of rhs
-
-   // solve the linear system
-   if (this->use_iterative_refinement) {
-      ma57dd_(&this->job, &n, &nnz, matrix.data_raw_pointer(), this->row_indices.data(), this->column_indices.data(),
-            this->fact.data(), &this->factorization.lfact, this->ifact.data(), &this->factorization.lifact,
-            rhs.data(), result.data(), this->residuals.data(), this->work.data(), this->iwork.data(), this->icntl.data(),
-            this->cntl.data(), this->info.data(), this->rinfo.data());
+   bool MA57Solver::matrix_is_singular() const {
+      return (this->info[0] == 4);
    }
-   else {
-      // copy rhs into result (overwritten by MA57)
-      result = rhs;
 
-      ma57cd_(&this->job, &n, this->fact.data(), &this->factorization.lfact, this->ifact.data(),
-            &this->factorization.lifact, &this->nrhs, result.data(), &lrhs, this->work.data(), &this->lwork, this->iwork.data(),
-            this->icntl.data(), this->info.data());
+   size_t MA57Solver::rank() const {
+      return static_cast<size_t>(this->info[24]);
    }
-}
-
-std::tuple<size_t, size_t, size_t> MA57Solver::get_inertia() const {
-   // rank = number_positive_eigenvalues + number_negative_eigenvalues
-   // n = rank + number_zero_eigenvalues
-   const size_t rank = this->rank();
-   const size_t number_negative_eigenvalues = this->number_negative_eigenvalues();
-   const size_t number_positive_eigenvalues = rank - number_negative_eigenvalues;
-   const size_t number_zero_eigenvalues = static_cast<size_t>(this->factorization.n) - rank;
-   return std::make_tuple(number_positive_eigenvalues, number_negative_eigenvalues, number_zero_eigenvalues);
-}
-
-size_t MA57Solver::number_negative_eigenvalues() const {
-   return static_cast<size_t>(this->info[23]);
-}
-
-/*
-bool MA57Solver::matrix_is_positive_definite() const {
-   // positive definite = non-singular and no negative eigenvalues
-   return not this->matrix_is_singular() && this->number_negative_eigenvalues() == 0;
-}
-*/
-
-bool MA57Solver::matrix_is_singular() const {
-   return (this->info[0] == 4);
-}
-
-size_t MA57Solver::rank() const {
-   return static_cast<size_t>(this->info[24]);
-}
-
-void MA57Solver::save_matrix_to_local_format(const SymmetricMatrix<double>& matrix) {
-   // build the internal matrix representation
-   this->row_indices.clear();
-   this->column_indices.clear();
-   for (const auto [row_index, column_index, element]: matrix) {
-      this->row_indices.push_back(static_cast<int>(row_index + this->fortran_shift));
-      this->column_indices.push_back(static_cast<int>(column_index + this->fortran_shift));
+
+   void MA57Solver::save_sparsity_pattern_internally(const SymmetricMatrix<size_t, double>& matrix) {
+      // build the internal matrix representation
+      this->row_indices.clear();
+      this->column_indices.clear();
+      for (const auto [row_index, column_index, element]: matrix) {
+         this->row_indices.push_back(static_cast<int>(row_index + this->fortran_shift));
+         this->column_indices.push_back(static_cast<int>(column_index + this->fortran_shift));
+      }
    }
-}
+} // namespace
\ No newline at end of file
diff --git a/uno/solvers/linear/MA57Solver.hpp b/uno/solvers/linear/MA57Solver.hpp
index 1723a58..b9f4c6f 100644
--- a/uno/solvers/linear/MA57Solver.hpp
+++ b/uno/solvers/linear/MA57Solver.hpp
@@ -8,69 +8,71 @@
 #include <vector>
 #include "SymmetricIndefiniteLinearSolver.hpp"
 
-// forward declaration
-template <typename ElementType>
-class Vector;
+namespace uno {
+   // forward declaration
+   template <typename ElementType>
+   class Vector;
 
-struct MA57Factorization {
-   int n{};
-   int nnz{};
-   int lfact{};
-   int lifact{};
+   struct MA57Factorization {
+      int n{};
+      int nnz{};
+      int lfact{};
+      int lifact{};
 
-   MA57Factorization() = default;
-};
+      MA57Factorization() = default;
+   };
 
-/*! \class MA57Solver
- * \brief Interface for MA57
- * see https://github.com/YimingYAN/linSolve
- *
- *  Interface to the symmetric indefinite linear solver MA57
- */
-class MA57Solver : public SymmetricIndefiniteLinearSolver<double> {
-public:
-   MA57Solver(size_t dimension, size_t number_nonzeros);
-   ~MA57Solver() override = default;
+   /*! \class MA57Solver
+    * \brief Interface for MA57
+    * see https://github.com/YimingYAN/linSolve
+    *
+    *  Interface to the symmetric indefinite linear solver MA57
+    */
+   class MA57Solver : public SymmetricIndefiniteLinearSolver<size_t, double> {
+   public:
+      MA57Solver(size_t dimension, size_t number_nonzeros);
+      ~MA57Solver() override = default;
 
-   void factorize(const SymmetricMatrix<double>& matrix) override;
-   void do_symbolic_factorization(const SymmetricMatrix<double>& matrix) override;
-   void do_numerical_factorization(const SymmetricMatrix<double>& matrix) override;
-   void solve_indefinite_system(const SymmetricMatrix<double>& matrix, const Vector<double>& rhs, Vector<double>& result) override;
+      void factorize(const SymmetricMatrix<size_t, double>& matrix) override;
+      void do_symbolic_factorization(const SymmetricMatrix<size_t, double>& matrix) override;
+      void do_numerical_factorization(const SymmetricMatrix<size_t, double>& matrix) override;
+      void solve_indefinite_system(const SymmetricMatrix<size_t, double>& matrix, const Vector<double>& rhs, Vector<double>& result) override;
 
-   [[nodiscard]] std::tuple<size_t, size_t, size_t> get_inertia() const override;
-   [[nodiscard]] size_t number_negative_eigenvalues() const override;
-   // [[nodiscard]] bool matrix_is_positive_definite() const override;
-   [[nodiscard]] bool matrix_is_singular() const override;
-   [[nodiscard]] size_t rank() const override;
+      [[nodiscard]] std::tuple<size_t, size_t, size_t> get_inertia() const override;
+      [[nodiscard]] size_t number_negative_eigenvalues() const override;
+      // [[nodiscard]] bool matrix_is_positive_definite() const override;
+      [[nodiscard]] bool matrix_is_singular() const override;
+      [[nodiscard]] size_t rank() const override;
 
-private:
-   // internal matrix representation
-   std::vector<int> row_indices;
-   std::vector<int> column_indices;
+   private:
+      // internal matrix representation
+      std::vector<int> row_indices;
+      std::vector<int> column_indices;
 
-   // factorization
-   MA57Factorization factorization{};
-   std::vector<double> fact{0}; // do not initialize, resize at every iteration
-   std::vector<int> ifact{0}; // do not initialize, resize at every iteration
-   const int lkeep;
-   std::vector<int> keep{};
-   std::vector<int> iwork{};
-   int lwork;
-   std::vector<double> work{};
+      // factorization
+      MA57Factorization factorization{};
+      std::vector<double> fact{0}; // do not initialize, resize at every iteration
+      std::vector<int> ifact{0}; // do not initialize, resize at every iteration
+      const int lkeep;
+      std::vector<int> keep{};
+      std::vector<int> iwork{};
+      int lwork;
+      std::vector<double> work{};
 
-   // for ma57id_ (default values of controlling parameters)
-   std::array<double, 5> cntl{};
-   std::array<int, 20> icntl{};
-   std::array<double, 20> rinfo{};
-   std::array<int, 40> info{};
+      // for ma57id_ (default values of controlling parameters)
+      std::array<double, 5> cntl{};
+      std::array<int, 20> icntl{};
+      std::array<double, 20> rinfo{};
+      std::array<int, 40> info{};
 
-   const int nrhs{1}; // number of right hand side being solved
-   const int job{1};
-   std::vector<double> residuals;
-   const size_t fortran_shift{1};
+      const int nrhs{1}; // number of right hand side being solved
+      const int job{1};
+      std::vector<double> residuals;
+      const size_t fortran_shift{1};
 
-   bool use_iterative_refinement{false};
-   void save_matrix_to_local_format(const SymmetricMatrix<double>& row_index);
-};
+      bool use_iterative_refinement{false};
+      void save_sparsity_pattern_internally(const SymmetricMatrix<size_t, double>& matrix);
+   };
+} // namespace
 
-#endif // UNO_MA57SOLVER_H
+#endif // UNO_MA57SOLVER_H
\ No newline at end of file
diff --git a/uno/solvers/linear/MUMPSSolver.cpp b/uno/solvers/linear/MUMPSSolver.cpp
new file mode 100644
index 0000000..662412d
--- /dev/null
+++ b/uno/solvers/linear/MUMPSSolver.cpp
@@ -0,0 +1,96 @@
+#include "MUMPSSolver.hpp"
+#if defined(HAS_MPI) && defined(MUMPS_PARALLEL)
+#include "mpi.h"
+#endif
+
+#define USE_COMM_WORLD -987654
+
+namespace uno {
+   MUMPSSolver::MUMPSSolver(size_t dimension, size_t number_nonzeros) : SymmetricIndefiniteLinearSolver<size_t, double>(dimension),
+         COO_matrix(dimension, number_nonzeros, false) {
+      this->mumps_structure.sym = MUMPSSolver::GENERAL_SYMMETRIC;
+#if defined(HAS_MPI) && defined(MUMPS_PARALLEL)
+      // TODO load number of processes from option file
+      this->mumps_structure.par = 1;
+#endif
+#ifdef MUMPS_SEQUENTIAL
+      this->mumps_structure.par = 1;
+#endif
+      this->mumps_structure.job = MUMPSSolver::JOB_INIT;
+      this->mumps_structure.comm_fortran = USE_COMM_WORLD;
+      dmumps_c(&this->mumps_structure);
+      // control parameters
+      this->mumps_structure.icntl[0] = -1;
+      this->mumps_structure.icntl[1] = -1;
+      this->mumps_structure.icntl[2] = -1;
+      this->mumps_structure.icntl[3] = 0;
+   }
+
+   MUMPSSolver::~MUMPSSolver() {
+      this->mumps_structure.job = MUMPSSolver::JOB_END;
+      dmumps_c(&this->mumps_structure);
+   }
+
+   void MUMPSSolver::factorize(const SymmetricMatrix<size_t, double>& matrix) {
+      // general factorization method: symbolic factorization and numerical factorization
+      this->do_symbolic_factorization(matrix);
+      this->do_numerical_factorization(matrix);
+   }
+
+   void MUMPSSolver::do_symbolic_factorization(const SymmetricMatrix<size_t, double>& matrix) {
+      this->save_matrix_to_local_format(matrix);
+      this->mumps_structure.n = static_cast<int>(matrix.dimension);
+      this->mumps_structure.nnz = static_cast<int>(matrix.number_nonzeros);
+      this->mumps_structure.job = MUMPSSolver::JOB_ANALYSIS;
+      // connect the local COO matrix with the pointers in the structure
+      this->mumps_structure.irn = this->COO_matrix.row_indices_pointer();
+      this->mumps_structure.jcn = this->COO_matrix.column_indices_pointer();
+      dmumps_c(&this->mumps_structure);
+   }
+
+   void MUMPSSolver::do_numerical_factorization(const SymmetricMatrix<size_t, double>& /*matrix*/) {
+      this->mumps_structure.job = MUMPSSolver::JOB_FACTORIZATION;
+      this->mumps_structure.a = this->COO_matrix.data_pointer();
+      dmumps_c(&this->mumps_structure);
+   }
+
+   void MUMPSSolver::solve_indefinite_system(const SymmetricMatrix<size_t, double>& /*matrix*/, const Vector<double>& rhs, Vector<double>& result) {
+      result = rhs;
+      this->mumps_structure.rhs = result.data();
+      this->mumps_structure.job = MUMPSSolver::JOB_SOLVE;
+      dmumps_c(&this->mumps_structure);
+   }
+
+   std::tuple<size_t, size_t, size_t> MUMPSSolver::get_inertia() const {
+      const size_t number_negative_eigenvalues = this->number_negative_eigenvalues();
+      const size_t number_zero_eigenvalues = this->number_zero_eigenvalues();
+      const size_t number_positive_eigenvalues = this->dimension - (number_negative_eigenvalues + number_zero_eigenvalues);
+      return std::make_tuple(number_positive_eigenvalues, number_negative_eigenvalues, number_zero_eigenvalues);
+   }
+
+   size_t MUMPSSolver::number_negative_eigenvalues() const {
+      // INFOG(12)
+      return static_cast<size_t>(this->mumps_structure.infog[11]);
+   }
+
+   size_t MUMPSSolver::number_zero_eigenvalues() const {
+      // INFOG(28)
+      return static_cast<size_t>(this->mumps_structure.infog[27]);
+   }
+
+   bool MUMPSSolver::matrix_is_singular() const {
+      return (this->number_zero_eigenvalues() > 0);
+   }
+
+   size_t MUMPSSolver::rank() const {
+      return this->dimension - this->number_zero_eigenvalues();
+   }
+
+   void MUMPSSolver::save_matrix_to_local_format(const SymmetricMatrix<size_t, double>& matrix) {
+      // build the internal matrix representation
+      this->COO_matrix.reset();
+      for (const auto[row_index, column_index, element]: matrix) {
+         this->COO_matrix.insert(element, static_cast<int>(row_index + this->fortran_shift), static_cast<int>(column_index + this->fortran_shift));
+      }
+   }
+} // namespace
\ No newline at end of file
diff --git a/uno/solvers/linear/MUMPSSolver.hpp b/uno/solvers/linear/MUMPSSolver.hpp
new file mode 100644
index 0000000..f933bf8
--- /dev/null
+++ b/uno/solvers/linear/MUMPSSolver.hpp
@@ -0,0 +1,47 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#ifndef UNO_MUMPSSOLVER_H
+#define UNO_MUMPSSOLVER_H
+
+#include "SymmetricIndefiniteLinearSolver.hpp"
+#include "linear_algebra/COOSymmetricMatrix.hpp"
+#include "dmumps_c.h"
+
+namespace uno {
+   class MUMPSSolver : public SymmetricIndefiniteLinearSolver<size_t, double> {
+   public:
+      explicit MUMPSSolver(size_t dimension, size_t number_nonzeros);
+      ~MUMPSSolver() override;
+
+      void factorize(const SymmetricMatrix<size_t, double>& matrix) override;
+      void do_symbolic_factorization(const SymmetricMatrix<size_t, double>& matrix) override;
+      void do_numerical_factorization(const SymmetricMatrix<size_t, double>& matrix) override;
+      void solve_indefinite_system(const SymmetricMatrix<size_t, double>& matrix, const Vector<double>& rhs,
+            Vector<double>& result) override;
+
+      [[nodiscard]] std::tuple<size_t, size_t, size_t> get_inertia() const override;
+      [[nodiscard]] size_t number_negative_eigenvalues() const override;
+      [[nodiscard]] size_t number_zero_eigenvalues() const;
+      // [[nodiscard]] bool matrix_is_positive_definite() const override;
+      [[nodiscard]] bool matrix_is_singular() const override;
+      [[nodiscard]] size_t rank() const override;
+
+   protected:
+      DMUMPS_STRUC_C mumps_structure{};
+      COOSymmetricMatrix<int, double> COO_matrix;
+
+      static const int JOB_INIT = -1;
+      static const int JOB_END = -2;
+      static const int JOB_ANALYSIS = 1;
+      static const int JOB_FACTORIZATION = 2;
+      static const int JOB_SOLVE = 3;
+
+      static const int GENERAL_SYMMETRIC = 2;
+
+      const size_t fortran_shift{1};
+      void save_matrix_to_local_format(const SymmetricMatrix<size_t, double>& row_index);
+   };
+} // namespace
+
+#endif // UNO_MUMPSSOLVER_H
diff --git a/uno/solvers/linear/SymmetricIndefiniteLinearSolver.hpp b/uno/solvers/linear/SymmetricIndefiniteLinearSolver.hpp
index 7a3153c..91ddd71 100644
--- a/uno/solvers/linear/SymmetricIndefiniteLinearSolver.hpp
+++ b/uno/solvers/linear/SymmetricIndefiniteLinearSolver.hpp
@@ -4,31 +4,39 @@
 #ifndef UNO_SYMMETRICINDEFINITELINEARSOLVER_H
 #define UNO_SYMMETRICINDEFINITELINEARSOLVER_H
 
-template <typename ElementType>
-class SymmetricMatrix;
-template <typename ElementType>
-class Vector;
-
-template <typename ElementType>
-class SymmetricIndefiniteLinearSolver {
-public:
-   explicit SymmetricIndefiniteLinearSolver(size_t dimension): dimension(dimension) {};
-   virtual ~SymmetricIndefiniteLinearSolver() = default;
-
-   virtual void factorize(const SymmetricMatrix<ElementType>& matrix) = 0;
-   virtual void do_symbolic_factorization(const SymmetricMatrix<ElementType>& matrix) = 0;
-   virtual void do_numerical_factorization(const SymmetricMatrix<ElementType>& matrix) = 0;
-   virtual void solve_indefinite_system(const SymmetricMatrix<ElementType>& matrix, const Vector<ElementType>& rhs,
-         Vector<ElementType>& result) = 0;
-
-   [[nodiscard]] virtual std::tuple<size_t, size_t, size_t> get_inertia() const = 0;
-   [[nodiscard]] virtual size_t number_negative_eigenvalues() const = 0;
-   // [[nodiscard]] virtual bool matrix_is_positive_definite() const = 0;
-   [[nodiscard]] virtual bool matrix_is_singular() const = 0;
-   [[nodiscard]] virtual size_t rank() const = 0;
-
-protected:
-   const size_t dimension;
-};
-
-#endif // UNO_SYMMETRICINDEFINITELINEARSOLVER_H
+#include <cstddef>
+#include <tuple>
+
+namespace uno {
+   // forward declarations
+   template <typename IndexType, typename ElementType>
+   class SymmetricMatrix;
+
+   template <typename ElementType>
+   class Vector;
+
+   template <typename IndexType, typename ElementType>
+   class SymmetricIndefiniteLinearSolver {
+   public:
+      explicit SymmetricIndefiniteLinearSolver(size_t dimension) : dimension(dimension) {
+      };
+      virtual ~SymmetricIndefiniteLinearSolver() = default;
+
+      virtual void factorize(const SymmetricMatrix<IndexType, ElementType>& matrix) = 0;
+      virtual void do_symbolic_factorization(const SymmetricMatrix<IndexType, ElementType>& matrix) = 0;
+      virtual void do_numerical_factorization(const SymmetricMatrix<IndexType, ElementType>& matrix) = 0;
+      virtual void solve_indefinite_system(const SymmetricMatrix<IndexType, ElementType>& matrix, const Vector<ElementType>& rhs,
+            Vector<ElementType>& result) = 0;
+
+      [[nodiscard]] virtual std::tuple<size_t, size_t, size_t> get_inertia() const = 0;
+      [[nodiscard]] virtual size_t number_negative_eigenvalues() const = 0;
+      // [[nodiscard]] virtual bool matrix_is_positive_definite() const = 0;
+      [[nodiscard]] virtual bool matrix_is_singular() const = 0;
+      [[nodiscard]] virtual size_t rank() const = 0;
+
+   protected:
+      const size_t dimension;
+   };
+} // namespace
+
+#endif // UNO_SYMMETRICINDEFINITELINEARSOLVER_H
\ No newline at end of file
diff --git a/uno/solvers/linear/SymmetricIndefiniteLinearSolverFactory.hpp b/uno/solvers/linear/SymmetricIndefiniteLinearSolverFactory.hpp
index 45dcdc0..a2b0cec 100644
--- a/uno/solvers/linear/SymmetricIndefiniteLinearSolverFactory.hpp
+++ b/uno/solvers/linear/SymmetricIndefiniteLinearSolverFactory.hpp
@@ -11,25 +11,37 @@
 #include "MA57Solver.hpp"
 #endif
 
-class SymmetricIndefiniteLinearSolverFactory {
-public:
-   static std::unique_ptr<SymmetricIndefiniteLinearSolver<double>> create(const std::string& linear_solver_name, size_t dimension, size_t number_nonzeros) {
+#ifdef HAS_MUMPS
+#include "MUMPSSolver.hpp"
+#endif
+
+namespace uno {
+   class SymmetricIndefiniteLinearSolverFactory {
+   public:
+      static std::unique_ptr<SymmetricIndefiniteLinearSolver<size_t, double>> create([[maybe_unused]] const std::string& linear_solver_name,
+            [[maybe_unused]] size_t dimension, [[maybe_unused]] size_t number_nonzeros) {
 #ifdef HAS_MA57
-      if (linear_solver_name == "MA57") {
-         return std::make_unique<MA57Solver>(dimension, number_nonzeros);
-      }
+         if (linear_solver_name == "MA57") {
+            return std::make_unique<MA57Solver>(dimension, number_nonzeros);
+         }
 #endif
-      throw std::invalid_argument("Linear solver name is unknown");
-   }
+#ifdef HAS_MUMPS
+         if (linear_solver_name == "MUMPS") {
+            return std::make_unique<MUMPSSolver>(dimension, number_nonzeros);
+         }
+#endif
+         throw std::invalid_argument("Linear solver name is unknown");
+      }
 
-   // return the list of available QP solvers
-   static std::vector<std::string> available_solvers() {
-      std::vector<std::string> solvers{};
-      #ifdef HAS_MA57
-      solvers.emplace_back("MA57");
-      #endif
-      return solvers;
-   }
-};
+      // return the list of available QP solvers
+      static std::vector<std::string> available_solvers() {
+         std::vector<std::string> solvers{};
+         #ifdef HAS_MA57
+         solvers.emplace_back("MA57");
+         #endif
+         return solvers;
+      }
+   };
+} // namespace
 
 #endif // UNO_LINEARSOLVERFACTORY_H
diff --git a/uno/symbolic/Collection.hpp b/uno/symbolic/Collection.hpp
index 607b045..1e1ab27 100644
--- a/uno/symbolic/Collection.hpp
+++ b/uno/symbolic/Collection.hpp
@@ -6,53 +6,55 @@
 
 #include <functional>
 
-// iterable collection
-template <typename ElementType>
-class Collection {
-public:
-   // https://internalpointers.com/post/writing-custom-iterators-modern-cpp
-   class iterator {
+namespace uno {
+   // iterable collection
+   template <typename ElementType>
+   class Collection {
    public:
-      using value_type = ElementType;
+      // https://internalpointers.com/post/writing-custom-iterators-modern-cpp
+      class iterator {
+      public:
+         using value_type = ElementType;
 
-      iterator(const Collection& collection, size_t index): collection(collection), index(index) { }
+         iterator(const Collection& collection, size_t index): collection(collection), index(index) { }
 
-      [[nodiscard]] ElementType operator*() const {
-         return this->collection.dereference_iterator(this->index);
-      }
+         [[nodiscard]] ElementType operator*() const {
+            return this->collection.dereference_iterator(this->index);
+         }
 
-      iterator& operator++() {
-         this->collection.increment_iterator(this->index);
-         return *this;
-      }
+         iterator& operator++() {
+            this->collection.increment_iterator(this->index);
+            return *this;
+         }
 
-      friend bool operator!=(const iterator& a, const iterator& b) {
-         return &a.collection != &b.collection || a.index != b.index;
-      }
+         friend bool operator!=(const iterator& a, const iterator& b) {
+            return &a.collection != &b.collection || a.index != b.index;
+         }
 
-   protected:
-      const Collection& collection;
-      size_t index;
-   };
+      protected:
+         const Collection& collection;
+         size_t index;
+      };
 
-   using value_type = ElementType;
+      using value_type = ElementType;
 
-   explicit Collection() = default;
-   virtual ~Collection() = default;
+      explicit Collection() = default;
+      virtual ~Collection() = default;
 
-   [[nodiscard]] virtual size_t size() const = 0;
-   [[nodiscard]] bool is_empty() const;
+      [[nodiscard]] virtual size_t size() const = 0;
+      [[nodiscard]] bool is_empty() const;
 
-   [[nodiscard]] iterator begin() const { return iterator(*this, 0); }
-   [[nodiscard]] iterator end() const { return iterator(*this, this->size()); }
+      [[nodiscard]] iterator begin() const { return iterator(*this, 0); }
+      [[nodiscard]] iterator end() const { return iterator(*this, this->size()); }
 
-   [[nodiscard]] virtual ElementType dereference_iterator(size_t index) const = 0;
-   virtual void increment_iterator(size_t& index) const = 0;
-};
+      [[nodiscard]] virtual ElementType dereference_iterator(size_t index) const = 0;
+      virtual void increment_iterator(size_t& index) const = 0;
+   };
 
-template <typename ElementType>
-inline bool Collection<ElementType>::is_empty() const {
-   return (this->size() == 0);
-}
+   template <typename ElementType>
+   inline bool Collection<ElementType>::is_empty() const {
+      return (this->size() == 0);
+   }
+} // namespace
 
-#endif // UNO_COLLECTION_H
\ No newline at end of file
+#endif // UNO_COLLECTION_H
diff --git a/uno/symbolic/CollectionAdapter.hpp b/uno/symbolic/CollectionAdapter.hpp
index a9e9e8d..5aa64b4 100644
--- a/uno/symbolic/CollectionAdapter.hpp
+++ b/uno/symbolic/CollectionAdapter.hpp
@@ -6,44 +6,46 @@
 
 #include "Collection.hpp"
 
-// Adapter to adapt standard Arrays (e.g. std::vector) to the Collection interface
-template <typename Array>
-class CollectionAdapter: public Collection<typename std::remove_reference_t<Array>::value_type> {
-public:
-   explicit CollectionAdapter(Array&& array);
-   [[nodiscard]] size_t size() const override;
-
-   [[nodiscard]] typename CollectionAdapter::value_type dereference_iterator(size_t index) const override;
-   void increment_iterator(size_t& index) const override;
-
-protected:
-   Array array;
-};
-
-template <typename Array>
-CollectionAdapter<Array>::CollectionAdapter(Array&& array):
-   Collection<typename std::remove_reference_t<Array>::value_type>(), array(std::forward<Array>(array)) {
-}
-
-template <typename Array>
-size_t CollectionAdapter<Array>::size() const {
-   return this->array.size();
-}
-
-template <typename Array>
-typename CollectionAdapter<Array>::value_type CollectionAdapter<Array>::dereference_iterator(size_t index) const {
-   return this->array[index];
-}
-
-template <typename Array>
-void CollectionAdapter<Array>::increment_iterator(size_t& index) const {
-   index++;
-}
-
-// free function
-template <typename Array>
-CollectionAdapter<Array> adapt(Array&& array) {
-   return CollectionAdapter(std::forward<Array>(array));
-}
+namespace uno {
+   // Adapter to adapt standard Arrays (e.g. std::vector) to the Collection interface
+   template <typename Array>
+   class CollectionAdapter: public Collection<typename std::remove_reference_t<Array>::value_type> {
+   public:
+      explicit CollectionAdapter(Array&& array);
+      [[nodiscard]] size_t size() const override;
+
+      [[nodiscard]] typename CollectionAdapter::value_type dereference_iterator(size_t index) const override;
+      void increment_iterator(size_t& index) const override;
+
+   protected:
+      Array array;
+   };
+
+   template <typename Array>
+   CollectionAdapter<Array>::CollectionAdapter(Array&& array):
+      Collection<typename std::remove_reference_t<Array>::value_type>(), array(std::forward<Array>(array)) {
+   }
+
+   template <typename Array>
+   size_t CollectionAdapter<Array>::size() const {
+      return this->array.size();
+   }
+
+   template <typename Array>
+   typename CollectionAdapter<Array>::value_type CollectionAdapter<Array>::dereference_iterator(size_t index) const {
+      return this->array[index];
+   }
+
+   template <typename Array>
+   void CollectionAdapter<Array>::increment_iterator(size_t& index) const {
+      index++;
+   }
+
+   // free function
+   template <typename Array>
+   CollectionAdapter<Array> adapt(Array&& array) {
+      return CollectionAdapter(std::forward<Array>(array));
+   }
+} // namespace
 
 #endif // UNO_COLLECTIONADAPTER_H
diff --git a/uno/symbolic/Concatenation.hpp b/uno/symbolic/Concatenation.hpp
index 9d7905d..e462170 100644
--- a/uno/symbolic/Concatenation.hpp
+++ b/uno/symbolic/Concatenation.hpp
@@ -6,50 +6,52 @@
 
 #include "Collection.hpp"
 
-// Concatenation based on https://stackoverflow.com/questions/24175279/how-to-store-either-rvalue-or-lvalue-references-in-template
-// Aggregates two collections, not necessarily of the same type. However they must have the same value_type (the type of the elements)
-template <typename Collection1, typename Collection2>
-class Concatenation: public Collection<typename std::remove_reference_t<Collection1>::value_type> {
-public:
-   Concatenation(Collection1&& collection1, Collection2&& collection2);
-   [[nodiscard]] size_t size() const override;
-
-   typename Concatenation::value_type dereference_iterator(size_t index) const override {
-      const size_t collection1_size = this->collection1.size();
-      if (index < collection1_size) {
-         return collection1.dereference_iterator(index);
+namespace uno {
+   // Concatenation based on https://stackoverflow.com/questions/24175279/how-to-store-either-rvalue-or-lvalue-references-in-template
+   // Aggregates two collections, not necessarily of the same type. However they must have the same value_type (the type of the elements)
+   template <typename Collection1, typename Collection2>
+   class Concatenation: public Collection<typename std::remove_reference_t<Collection1>::value_type> {
+   public:
+      Concatenation(Collection1&& collection1, Collection2&& collection2);
+      [[nodiscard]] size_t size() const override;
+
+      typename Concatenation::value_type dereference_iterator(size_t index) const override {
+         const size_t collection1_size = this->collection1.size();
+         if (index < collection1_size) {
+            return collection1.dereference_iterator(index);
+         }
+         else {
+            return collection2.dereference_iterator(index - collection1_size);
+         }
       }
-      else {
-         return collection2.dereference_iterator(index - collection1_size);
+
+      void increment_iterator(size_t& index) const override {
+         index++;
       }
+
+   protected:
+      Collection1 collection1;
+      Collection2 collection2;
+   };
+
+   template <typename Collection1, typename Collection2>
+   Concatenation<Collection1, Collection2>::Concatenation(Collection1&& collection1, Collection2&& collection2):
+         Collection<typename Concatenation::value_type>(),
+         collection1(std::forward<Collection1>(collection1)), collection2(std::forward<Collection2>(collection2)) {
+      static_assert(std::is_same<typename std::remove_reference_t<Collection1>::value_type,
+            typename std::remove_reference_t<Collection2>::value_type>::value, "The iterators should contain the same type");
+   }
+
+   template <typename Collection1, typename Collection2>
+   size_t Concatenation<Collection1, Collection2>::size() const {
+      return this->collection1.size() + this->collection2.size();
    }
 
-   void increment_iterator(size_t& index) const override {
-      index++;
+   // free function
+   template <typename Collection1, typename Collection2>
+   Concatenation<Collection1, Collection2> concatenate(Collection1&& collection1, Collection2&& collection2) {
+      return Concatenation(std::forward<Collection1>(collection1), std::forward<Collection2>(collection2));
    }
+} // namespace
 
-protected:
-   Collection1 collection1;
-   Collection2 collection2;
-};
-
-template <typename Collection1, typename Collection2>
-Concatenation<Collection1, Collection2>::Concatenation(Collection1&& collection1, Collection2&& collection2):
-      Collection<typename Concatenation::value_type>(),
-      collection1(std::forward<Collection1>(collection1)), collection2(std::forward<Collection2>(collection2)) {
-   static_assert(std::is_same<typename std::remove_reference_t<Collection1>::value_type,
-         typename std::remove_reference_t<Collection2>::value_type>::value, "The iterators should contain the same type");
-}
-
-template <typename Collection1, typename Collection2>
-size_t Concatenation<Collection1, Collection2>::size() const {
-   return this->collection1.size() + this->collection2.size();
-}
-
-// free function
-template <typename Collection1, typename Collection2>
-Concatenation<Collection1, Collection2> concatenate(Collection1&& collection1, Collection2&& collection2) {
-   return Concatenation(std::forward<Collection1>(collection1), std::forward<Collection2>(collection2));
-}
-
-#endif // UNO_CHAINCOLLECTION_H
\ No newline at end of file
+#endif // UNO_CHAINCOLLECTION_H
diff --git a/uno/symbolic/Expression.hpp b/uno/symbolic/Expression.hpp
index 8c0cb0a..9a6deb6 100644
--- a/uno/symbolic/Expression.hpp
+++ b/uno/symbolic/Expression.hpp
@@ -7,8 +7,6 @@
 #include "ScalarMultiple.hpp"
 #include "Sum.hpp"
 #include "UnaryNegation.hpp"
-#include "Indicator.hpp"
-#include "Hadamard.hpp"
 #include "MatrixVectorProduct.hpp"
 
-#endif // UNO_EXPRESSION_H
\ No newline at end of file
+#endif // UNO_EXPRESSION_H
diff --git a/uno/symbolic/MatrixVectorProduct.hpp b/uno/symbolic/MatrixVectorProduct.hpp
index 42e345f..276b312 100644
--- a/uno/symbolic/MatrixVectorProduct.hpp
+++ b/uno/symbolic/MatrixVectorProduct.hpp
@@ -6,32 +6,34 @@
 
 #include "linear_algebra/SparseVector.hpp"
 
-// symbolic matrix-vector product
-template <typename Matrix, typename Vector>
-class MatrixVectorProduct {
-public:
-   using value_type = typename std::remove_reference_t<Vector>::value_type;
-
-   MatrixVectorProduct(Matrix&& matrix, Vector&& vector): matrix(std::forward<Matrix>(matrix)), vector(std::forward<Vector>(vector)) { }
-
-   [[nodiscard]] constexpr size_t size() const { return this->vector.size(); }
-
-   // product computed using row-major matrix
-   [[nodiscard]] typename MatrixVectorProduct::value_type operator[](size_t row_index) const {
-      return dot(this->vector, this->matrix[row_index]);
+namespace uno {
+   // symbolic matrix-vector product
+   template <typename Matrix, typename Vector>
+   class MatrixVectorProduct {
+   public:
+      using value_type = typename std::remove_reference_t<Vector>::value_type;
+
+      MatrixVectorProduct(Matrix&& matrix, Vector&& vector): matrix(std::forward<Matrix>(matrix)), vector(std::forward<Vector>(vector)) { }
+
+      [[nodiscard]] constexpr size_t size() const { return this->vector.size(); }
+
+      // product computed using row-major matrix
+      [[nodiscard]] typename MatrixVectorProduct::value_type operator[](size_t row_index) const {
+         return dot(this->vector, this->matrix[row_index]);
+      }
+
+   protected:
+      Matrix matrix;
+      Vector vector;
+   };
+
+   // free function
+   template <typename Matrix, typename Vector,
+         typename std::enable_if<std::is_same_v<typename std::remove_reference_t<Matrix>::value_type,
+                                                typename std::remove_reference_t<Vector>::value_type>, int>::type = 0>
+   inline MatrixVectorProduct<Matrix, Vector> operator*(Matrix&& matrix, Vector&& vector) {
+      return {std::forward<Matrix>(matrix), std::forward<Vector>(vector)};
    }
+} // namespace
 
-protected:
-   Matrix matrix;
-   Vector vector;
-};
-
-// free function
-template <typename Matrix, typename Vector,
-      typename std::enable_if<std::is_same_v<typename std::remove_reference_t<Matrix>::value_type,
-                                             typename std::remove_reference_t<Vector>::value_type>, int>::type = 0>
-inline MatrixVectorProduct<Matrix, Vector> operator*(Matrix&& matrix, Vector&& vector) {
-   return {std::forward<Matrix>(matrix), std::forward<Vector>(vector)};
-}
-
-#endif // UNO_MATRIXVECTORPRODUCT_H
\ No newline at end of file
+#endif // UNO_MATRIXVECTORPRODUCT_H
diff --git a/uno/symbolic/Range.hpp b/uno/symbolic/Range.hpp
index b42e4f1..8410e2e 100644
--- a/uno/symbolic/Range.hpp
+++ b/uno/symbolic/Range.hpp
@@ -7,72 +7,74 @@
 #include <stdexcept>
 #include "Collection.hpp"
 
-// direction of the range (FORWARD = increasing or BACKWARD = decreasing)
-enum RangeDirection {
-   FORWARD, BACKWARD
-};
+namespace uno {
+   // direction of the range (FORWARD = increasing or BACKWARD = decreasing)
+   enum RangeDirection {
+      FORWARD, BACKWARD
+   };
 
-// Default direction is FORWARD (increasing)
-template <RangeDirection direction = FORWARD>
-class Range: public Collection<size_t> {
-public:
-   using value_type = size_t;
+   // Default direction is FORWARD (increasing)
+   template <RangeDirection direction = FORWARD>
+   class Range: public Collection<size_t> {
+   public:
+      using value_type = size_t;
 
-   explicit Range(size_t end_index);
-   Range(size_t start_value, size_t end_value);
+      explicit Range(size_t end_index);
+      Range(size_t start_value, size_t end_value);
 
-   // iterable functions
-   [[nodiscard]] size_t size() const override;
+      // iterable functions
+      [[nodiscard]] size_t size() const override;
 
-   [[nodiscard]] size_t dereference_iterator(size_t index) const override;
-   void increment_iterator(size_t& index) const override;
+      [[nodiscard]] size_t dereference_iterator(size_t index) const override;
+      void increment_iterator(size_t& index) const override;
 
-protected:
-   const size_t start_value;
-   const size_t end_value;
-};
+   protected:
+      const size_t start_value;
+      const size_t end_value;
+   };
 
-template <RangeDirection direction>
-inline Range<direction>::Range(size_t end_index): Range(0, end_index) {
-   static_assert(direction == FORWARD);
-}
-
-template <RangeDirection direction>
-inline Range<direction>::Range(size_t start_value, size_t end_value): Collection<size_t>(), start_value(start_value), end_value(end_value) {
-   if (direction == FORWARD && end_value < start_value) {
-      throw std::runtime_error("Forward range: end index is smaller than start index\n");
-   }
-   else if (direction == BACKWARD && end_value > start_value) {
-      throw std::runtime_error("Backward range: end index is larger than start index\n");
+   template <RangeDirection direction>
+   inline Range<direction>::Range(size_t end_index): Range(0, end_index) {
+      static_assert(direction == FORWARD);
    }
-}
 
-template <RangeDirection direction>
-inline size_t Range<direction>::size() const {
-   if constexpr (direction == FORWARD) {
-      return this->end_value - this->start_value;
-   }
-   else {
-      return this->start_value - this->end_value;
+   template <RangeDirection direction>
+   inline Range<direction>::Range(size_t start_value, size_t end_value): Collection<size_t>(), start_value(start_value), end_value(end_value) {
+      if (direction == FORWARD && end_value < start_value) {
+         throw std::runtime_error("Forward range: end index is smaller than start index\n");
+      }
+      else if (direction == BACKWARD && end_value > start_value) {
+         throw std::runtime_error("Backward range: end index is larger than start index\n");
+      }
    }
-}
 
-template <RangeDirection direction>
-size_t Range<direction>::dereference_iterator(size_t index) const {
-   if constexpr (direction == FORWARD) {
-      return this->start_value + index;
+   template <RangeDirection direction>
+   inline size_t Range<direction>::size() const {
+      if constexpr (direction == FORWARD) {
+         return this->end_value - this->start_value;
+      }
+      else {
+         return this->start_value - this->end_value;
+      }
    }
-   else {
-      return this->start_value - index;
+
+   template <RangeDirection direction>
+   size_t Range<direction>::dereference_iterator(size_t index) const {
+      if constexpr (direction == FORWARD) {
+         return this->start_value + index;
+      }
+      else {
+         return this->start_value - index;
+      }
    }
-}
 
-template <RangeDirection direction>
-void Range<direction>::increment_iterator(size_t& index) const {
-   index++;
-}
+   template <RangeDirection direction>
+   void Range<direction>::increment_iterator(size_t& index) const {
+      index++;
+   }
 
-using ForwardRange = Range<FORWARD>;
-using BackwardRange = Range<BACKWARD>;
+   using ForwardRange = Range<FORWARD>;
+   using BackwardRange = Range<BACKWARD>;
+} // namespace
 
-#endif // UNO_RANGE_H
\ No newline at end of file
+#endif // UNO_RANGE_H
diff --git a/uno/symbolic/ScalarMultiple.hpp b/uno/symbolic/ScalarMultiple.hpp
index bf2e400..c39a174 100644
--- a/uno/symbolic/ScalarMultiple.hpp
+++ b/uno/symbolic/ScalarMultiple.hpp
@@ -4,29 +4,31 @@
 #ifndef UNO_SCALARMULTIPLE_H
 #define UNO_SCALARMULTIPLE_H
 
-// stores the expression (factor * expression) symbolically
-template <typename Expression>
-class ScalarMultiple {
-public:
-   using value_type = typename std::remove_reference_t<Expression>::value_type;
+namespace uno {
+   // stores the expression (factor * expression) symbolically
+   template <typename Expression>
+   class ScalarMultiple {
+   public:
+      using value_type = typename std::remove_reference_t<Expression>::value_type;
 
-   ScalarMultiple(value_type factor, Expression&& expression): factor(factor), expression(std::forward<Expression>(expression)) { }
+      ScalarMultiple(value_type factor, Expression&& expression): factor(factor), expression(std::forward<Expression>(expression)) { }
 
-   [[nodiscard]] constexpr size_t size() const { return this->expression.size(); }
-   [[nodiscard]] value_type operator[](size_t index) const {
-      return (this->factor == value_type(0)) ? value_type(0) : this->factor * this->expression[index];
-   }
+      [[nodiscard]] constexpr size_t size() const { return this->expression.size(); }
+      [[nodiscard]] value_type operator[](size_t index) const {
+         return (this->factor == value_type(0)) ? value_type(0) : this->factor * this->expression[index];
+      }
 
-protected:
-   const value_type factor;
-   Expression expression;
-};
+   protected:
+      const value_type factor;
+      Expression expression;
+   };
 
-// free function
-template <typename Expression, typename ElementType = typename Expression::value_type,
-      typename std::enable_if<std::is_arithmetic_v<ElementType>, int>::type = 0>
-inline ScalarMultiple<Expression> operator*(ElementType factor, Expression&& expression) {
-   return ScalarMultiple<Expression>(factor, std::forward<Expression>(expression));
-}
+   // free function
+   template <typename Expression, typename ElementType = typename Expression::value_type,
+         typename std::enable_if<std::is_arithmetic_v<ElementType>, int>::type = 0>
+   inline ScalarMultiple<Expression> operator*(ElementType factor, Expression&& expression) {
+      return ScalarMultiple<Expression>(factor, std::forward<Expression>(expression));
+   }
+} // namespace
 
-#endif // UNO_SCALARMULTIPLE_H
\ No newline at end of file
+#endif // UNO_SCALARMULTIPLE_H
diff --git a/uno/symbolic/Sum.hpp b/uno/symbolic/Sum.hpp
index c6ad125..1635a8b 100644
--- a/uno/symbolic/Sum.hpp
+++ b/uno/symbolic/Sum.hpp
@@ -4,29 +4,31 @@
 #ifndef UNO_SUM_H
 #define UNO_SUM_H
 
-// stores the expression (expression1 + expression2) symbolically
-// limited to types that possess value_type
-// https://stackoverflow.com/questions/11055923/stdenable-if-parameter-vs-template-parameter
-template <typename E1, typename E2,
-   typename std::enable_if_t<std::is_same_v<typename std::remove_reference_t<E1>::value_type, typename std::remove_reference_t<E2>::value_type>, int> = 0>
-class Sum {
-public:
-   using value_type = typename std::remove_reference_t<E1>::value_type;
+namespace uno {
+   // stores the expression (expression1 + expression2) symbolically
+   // limited to types that possess value_type
+   // https://stackoverflow.com/questions/11055923/stdenable-if-parameter-vs-template-parameter
+   template <typename E1, typename E2,
+      typename std::enable_if_t<std::is_same_v<typename std::remove_reference_t<E1>::value_type, typename std::remove_reference_t<E2>::value_type>, int> = 0>
+   class Sum {
+   public:
+      using value_type = typename std::remove_reference_t<E1>::value_type;
 
-   Sum(E1&& expression1, E2&& expression2): expression1(std::forward<E1>(expression1)), expression2(std::forward<E2>(expression2)) { }
+      Sum(E1&& expression1, E2&& expression2): expression1(std::forward<E1>(expression1)), expression2(std::forward<E2>(expression2)) { }
 
-   [[nodiscard]] constexpr size_t size() const { return this->expression1.size(); }
-   [[nodiscard]] typename Sum::value_type operator[](size_t index) const { return this->expression1[index] + this->expression2[index]; }
+      [[nodiscard]] constexpr size_t size() const { return this->expression1.size(); }
+      [[nodiscard]] typename Sum::value_type operator[](size_t index) const { return this->expression1[index] + this->expression2[index]; }
 
-protected:
-   E1 expression1;
-   E2 expression2;
-};
+   protected:
+      E1 expression1;
+      E2 expression2;
+   };
 
-// free function
-template <typename E1, typename E2>
-inline Sum<E1, E2> operator+(E1&& expression1, E2&& expression2) {
-   return {std::forward<E1>(expression1), std::forward<E2>(expression2)};
-}
+   // free function
+   template <typename E1, typename E2>
+   inline Sum<E1, E2> operator+(E1&& expression1, E2&& expression2) {
+      return {std::forward<E1>(expression1), std::forward<E2>(expression2)};
+   }
+} // namespace
 
-#endif // UNO_SUM_H
\ No newline at end of file
+#endif // UNO_SUM_H
diff --git a/uno/symbolic/UnaryNegation.hpp b/uno/symbolic/UnaryNegation.hpp
index a77e2a8..1068464 100644
--- a/uno/symbolic/UnaryNegation.hpp
+++ b/uno/symbolic/UnaryNegation.hpp
@@ -4,27 +4,29 @@
 #ifndef UNO_UNARYNEGATION_H
 #define UNO_UNARYNEGATION_H
 
-// stores the expression -expression symbolically
-// limited to types that possess value_type
-// https://stackoverflow.com/questions/11055923/stdenable-if-parameter-vs-template-parameter
-template <typename Expression>
-class UnaryNegation {
-public:
-   using value_type = typename std::remove_reference_t<Expression>::value_type;
+namespace uno {
+   // stores the expression -expression symbolically
+   // limited to types that possess value_type
+   // https://stackoverflow.com/questions/11055923/stdenable-if-parameter-vs-template-parameter
+   template <typename Expression>
+   class UnaryNegation {
+   public:
+      using value_type = typename std::remove_reference_t<Expression>::value_type;
 
-   explicit UnaryNegation(Expression&& expression): expression(std::forward<Expression>(expression)) { }
+      explicit UnaryNegation(Expression&& expression): expression(std::forward<Expression>(expression)) { }
 
-   [[nodiscard]] constexpr size_t size() const { return this->expression.size(); }
-   [[nodiscard]] typename UnaryNegation::value_type operator[](size_t index) const { return -this->expression[index]; }
+      [[nodiscard]] constexpr size_t size() const { return this->expression.size(); }
+      [[nodiscard]] typename UnaryNegation::value_type operator[](size_t index) const { return -this->expression[index]; }
 
-protected:
-   Expression expression;
-};
+   protected:
+      Expression expression;
+   };
 
-// free function
-template <typename Expression>
-inline UnaryNegation<Expression> operator-(Expression&& expression) {
-   return UnaryNegation<Expression>(std::forward<Expression>(expression));
-}
+   // free function
+   template <typename Expression>
+   inline UnaryNegation<Expression> operator-(Expression&& expression) {
+      return UnaryNegation<Expression>(std::forward<Expression>(expression));
+   }
+} // namespace
 
-#endif // UNO_UNARYNEGATION_H
\ No newline at end of file
+#endif // UNO_UNARYNEGATION_H
diff --git a/uno/symbolic/VectorExpression.hpp b/uno/symbolic/VectorExpression.hpp
index deab4fd..fbd0717 100644
--- a/uno/symbolic/VectorExpression.hpp
+++ b/uno/symbolic/VectorExpression.hpp
@@ -7,57 +7,59 @@
 #include <functional>
 #include "Collection.hpp"
 
-template <typename Indices, typename Callable>
-class VectorExpression {
-public:
-   class iterator {
+namespace uno {
+   template <typename Indices, typename Callable>
+   class VectorExpression {
    public:
-      using value_type = std::pair<size_t, double>;
+      class iterator {
+      public:
+         using value_type = std::pair<size_t, double>;
 
-      iterator(const VectorExpression& expression, size_t index): expression(expression), index(index) { }
+         iterator(const VectorExpression& expression, size_t index): expression(expression), index(index) { }
 
-      [[nodiscard]] std::pair<size_t, double> operator*() const {
-         const auto [_, expression_index] = expression.indices.dereference_iterator(this->index);
-         return {expression_index, expression.component_function[index]};
-      }
+         [[nodiscard]] std::pair<size_t, double> operator*() const {
+            const auto [_, expression_index] = expression.indices.dereference_iterator(this->index);
+            return {expression_index, expression.component_function[index]};
+         }
 
-      iterator& operator++() {
-         this->index++;
-         return *this;
-      }
+         iterator& operator++() {
+            this->index++;
+            return *this;
+         }
 
-      friend bool operator!=(const iterator& a, const iterator& b) {
-         return &a.expression != &b.expression || a.index != b.index;
-      }
+         friend bool operator!=(const iterator& a, const iterator& b) {
+            return &a.expression != &b.expression || a.index != b.index;
+         }
 
-   protected:
-      const VectorExpression& expression;
-      size_t index;
-   };
+      protected:
+         const VectorExpression& expression;
+         size_t index;
+      };
 
-   // compatible with algorithms that query the type of the elements
-   using value_type = double;
+      // compatible with algorithms that query the type of the elements
+      using value_type = double;
 
-   VectorExpression(Indices&& indices, Callable&& component_function);
-   [[nodiscard]] size_t size() const { return this->indices.size(); }
-   [[nodiscard]] double operator[](size_t index) const;
+      VectorExpression(Indices&& indices, Callable&& component_function);
+      [[nodiscard]] size_t size() const { return this->indices.size(); }
+      [[nodiscard]] double operator[](size_t index) const;
 
-   iterator begin() const { return iterator(*this, 0); }
-   iterator end() const { return iterator(*this, this->size()); }
+      iterator begin() const { return iterator(*this, 0); }
+      iterator end() const { return iterator(*this, this->size()); }
 
-protected:
-   Indices indices; // store const reference or rvalue (temporary)
-   Callable component_function;
-};
+   protected:
+      const Indices indices; // store const reference or rvalue (temporary)
+      const Callable component_function;
+   };
 
-template <typename Indices, typename Callable>
-VectorExpression<Indices, Callable>::VectorExpression(Indices&& indices, Callable&& component_function):
-      indices(std::forward<Indices>(indices)), component_function(std::forward<Callable>(component_function)) {
-}
+   template <typename Indices, typename Callable>
+   VectorExpression<Indices, Callable>::VectorExpression(Indices&& indices, Callable&& component_function):
+         indices(std::forward<Indices>(indices)), component_function(std::forward<Callable>(component_function)) {
+   }
 
-template <typename Indices, typename Callable>
-double VectorExpression<Indices, Callable>::operator[](size_t index) const {
-   return this->component_function(index);
-}
+   template <typename Indices, typename Callable>
+   double VectorExpression<Indices, Callable>::operator[](size_t index) const {
+      return this->component_function(index);
+   }
+} // namespace
 
-#endif // UNO_VECTOREXPRESSION_H
\ No newline at end of file
+#endif // UNO_VECTOREXPRESSION_H
diff --git a/uno/symbolic/VectorView.hpp b/uno/symbolic/VectorView.hpp
index c876e84..c55a0bb 100644
--- a/uno/symbolic/VectorView.hpp
+++ b/uno/symbolic/VectorView.hpp
@@ -4,58 +4,60 @@
 #ifndef UNO_VECTORVIEW_H
 #define UNO_VECTORVIEW_H
 
-// span of an arbitrary container: allocation-free view of a certain length
-template <typename Expression>
-class VectorView {
-public:
-   /*
-   class iterator {
+namespace uno {
+   // span of an arbitrary container: allocation-free view of a certain length
+   template <typename Expression>
+   class VectorView {
    public:
-      using value_type = std::pair<size_t, typename std::remove_reference_t<Expression>::value_type>;
-
-      iterator(const VectorView<Expression>& view, size_t index) : view(view), index(index) { }
-
-      value_type operator*() {
-         // copy the element in the pair. Cheap only for trivial types
-         return {this->index, this->view[this->index]};
+      /*
+      class iterator {
+      public:
+         using value_type = std::pair<size_t, typename std::remove_reference_t<Expression>::value_type>;
+
+         iterator(const VectorView<Expression>& view, size_t index) : view(view), index(index) { }
+
+         value_type operator*() {
+            // copy the element in the pair. Cheap only for trivial types
+            return {this->index, this->view[this->index]};
+         }
+         
+         iterator& operator++() { this->index++; return *this; }
+
+         friend bool operator!=(const iterator& a, const iterator& b) { return &a.view != &b.view || a.index != b.index; };
+
+      private:
+         const VectorView<Expression>& view;
+         size_t index;
+      };
+   */
+      using value_type = typename std::remove_reference_t<Expression>::value_type;
+
+      VectorView(Expression&& expression, size_t start, size_t end):
+            expression(std::forward<Expression>(expression)), start(start), end(std::min(end, expression.size())) {
+         if (end < start) {
+            throw std::runtime_error("The view ends before its starting point.");
+         }
       }
-      
-      iterator& operator++() { this->index++; return *this; }
 
-      friend bool operator!=(const iterator& a, const iterator& b) { return &a.view != &b.view || a.index != b.index; };
+      // preconditions: expression != nullptr, i < length
+      [[nodiscard]] value_type& operator[](size_t index) noexcept { return this->expression[this->start + index]; }
+      [[nodiscard]] value_type operator[](size_t index) const noexcept { return this->expression[this->start + index]; }
+      [[nodiscard]] size_t size() const noexcept { return this->end - this->start; }
 
-   private:
-      const VectorView<Expression>& view;
-      size_t index;
+      // [[nodiscard]] iterator begin() const noexcept { return iterator(*this, 0); }
+      // [[nodiscard]] iterator end() const noexcept { return iterator(*this, this->length); }
+
+   protected:
+      Expression expression;
+      const size_t start;
+      const size_t end;
    };
-*/
-   using value_type = typename std::remove_reference_t<Expression>::value_type;
 
-   VectorView(Expression&& expression, size_t start, size_t end):
-         expression(std::forward<Expression>(expression)), start(start), end(std::min(end, expression.size())) {
-      if (end < start) {
-         throw std::runtime_error("The view ends before its starting point.");
-      }
+   // free function
+   template <typename Expression>
+   VectorView<Expression> view(Expression&& expression, size_t start, size_t end) {
+      return {std::forward<Expression>(expression), start, end};
    }
+} // namespace
 
-   // preconditions: expression != nullptr, i < length
-   [[nodiscard]] value_type& operator[](size_t index) noexcept { return this->expression[this->start + index]; }
-   [[nodiscard]] value_type operator[](size_t index) const noexcept { return this->expression[this->start + index]; }
-   [[nodiscard]] size_t size() const noexcept { return this->end - this->start; }
-
-   // [[nodiscard]] iterator begin() const noexcept { return iterator(*this, 0); }
-   // [[nodiscard]] iterator end() const noexcept { return iterator(*this, this->length); }
-
-protected:
-   Expression expression;
-   const size_t start;
-   const size_t end;
-};
-
-// free function
-template <typename Expression>
-VectorView<Expression> view(Expression&& expression, size_t start, size_t end) {
-   return {std::forward<Expression>(expression), start, end};
-}
-
-#endif //UNO_VECTORVIEW_H
\ No newline at end of file
+#endif //UNO_VECTORVIEW_H
diff --git a/uno/tools/Infinity.hpp b/uno/tools/Infinity.hpp
index 1992d7e..50bce2a 100644
--- a/uno/tools/Infinity.hpp
+++ b/uno/tools/Infinity.hpp
@@ -7,12 +7,14 @@
 #include <cmath>
 #include <limits>
 
-template <typename NumericalType>
-const double INF = std::numeric_limits<NumericalType>::infinity();
+namespace uno {
+   template <typename NumericalType>
+   const double INF = std::numeric_limits<NumericalType>::infinity();
 
-template <typename NumericalType>
-inline bool is_finite(NumericalType value) {
-   return std::abs(value) < INF<NumericalType>;
-}
+   template <typename NumericalType>
+   inline bool is_finite(NumericalType value) {
+      return std::abs(value) < INF<NumericalType>;
+   }
+} // namespace
 
-#endif // UNO_INFINITY_H
\ No newline at end of file
+#endif // UNO_INFINITY_H
diff --git a/uno/tools/Logger.hpp b/uno/tools/Logger.hpp
index d1f9790..249a427 100644
--- a/uno/tools/Logger.hpp
+++ b/uno/tools/Logger.hpp
@@ -6,63 +6,65 @@
 
 #include <iostream>
 
-#define RED    "\x1B[31m"
-// #define GREEN   "\x1B[32m"
-#define YELLOW  "\x1B[33m"
-// #define BLUE    "\x1B[34m"
-// #define MAGENTA "\x1B[35m"
-// #define CYAN    "\x1B[36m"
-// #define WHITE   "\x1B[37m"
-#define RESET "\x1B[0m"
+namespace uno {
+   #define RED    "\x1B[31m"
+   // #define GREEN   "\x1B[32m"
+   #define YELLOW  "\x1B[33m"
+   // #define BLUE    "\x1B[34m"
+   // #define MAGENTA "\x1B[35m"
+   // #define CYAN    "\x1B[36m"
+   // #define WHITE   "\x1B[37m"
+   #define RESET "\x1B[0m"
 
-enum Level {
-    ERROR = 0, WARNING, INFO, DEBUG, DEBUG2, DEBUG3
-};
+   enum Level {
+       ERROR = 0, WARNING, INFO, DEBUG, DEBUG2, DEBUG3
+   };
 
-class Logger {
-public:
-    static Level level;
-    static void set_logger(const std::string& logger_level);
-};
+   class Logger {
+   public:
+       static Level level;
+       static void set_logger(const std::string& logger_level);
+   };
 
-template <typename T>
-const Level& operator<<(const Level& level, T& element) {
-    if (level <= Logger::level) {
-        std::cout << element;
-    }
-    return level;
-}
-
-template <typename T>
-const Level& operator<<(const Level& level, const T& element) {
-    if (level <= Logger::level) {
-        std::cout << element;
-    }
-    return level;
-}
-
-inline void Logger::set_logger(const std::string& logger_level) {
-   if (logger_level == "ERROR") {
-      Logger::level = ERROR;
-   }
-   else if (logger_level == "WARNING") {
-      Logger::level = WARNING;
-   }
-   else if (logger_level == "INFO") {
-      Logger::level = INFO;
+   template <typename T>
+   const Level& operator<<(const Level& level, T& element) {
+       if (level <= Logger::level) {
+           std::cout << element;
+       }
+       return level;
    }
-   else if (logger_level == "DEBUG") {
-      Logger::level = DEBUG;
-   }
-   else if (logger_level == "DEBUG2") {
-      Logger::level = DEBUG2;
-   }
-   else if (logger_level == "DEBUG3") {
-      Logger::level = DEBUG3;
+
+   template <typename T>
+   const Level& operator<<(const Level& level, const T& element) {
+       if (level <= Logger::level) {
+           std::cout << element;
+       }
+       return level;
    }
-   else {
-      throw std::out_of_range("The logger level " + logger_level + " was not found");
+
+   inline void Logger::set_logger(const std::string& logger_level) {
+      if (logger_level == "ERROR") {
+         Logger::level = ERROR;
+      }
+      else if (logger_level == "WARNING") {
+         Logger::level = WARNING;
+      }
+      else if (logger_level == "INFO") {
+         Logger::level = INFO;
+      }
+      else if (logger_level == "DEBUG") {
+         Logger::level = DEBUG;
+      }
+      else if (logger_level == "DEBUG2") {
+         Logger::level = DEBUG2;
+      }
+      else if (logger_level == "DEBUG3") {
+         Logger::level = DEBUG3;
+      }
+      else {
+         throw std::out_of_range("The logger level " + logger_level + " was not found");
+      }
    }
-}
+} // namespace
 
-#endif // UNO_LOGGER_H
\ No newline at end of file
+#endif // UNO_LOGGER_H
diff --git a/uno/tools/Options.cpp b/uno/tools/Options.cpp
index 1fe5622..5ea18ba 100644
--- a/uno/tools/Options.cpp
+++ b/uno/tools/Options.cpp
@@ -7,175 +7,201 @@
 #include "Options.hpp"
 #include "Logger.hpp"
 
-std::string& Options::operator[](const std::string& key) {
-   return this->options[key];
-}
-
-const std::string& Options::at(const std::string& key) const {
-   try {
-      const std::string& value = this->options.at(key);
-      this->is_used[key] = true;
-      return value;
+namespace uno {
+   std::string& Options::operator[](const std::string& key) {
+      return this->options[key];
    }
-   catch(const std::out_of_range&) {
-      throw std::out_of_range("The option " + key + " was not found");
+
+   const std::string& Options::at(const std::string& key) const {
+      try {
+         const std::string& value = this->options.at(key);
+         this->is_used[key] = true;
+         return value;
+      }
+      catch(const std::out_of_range&) {
+         throw std::out_of_range("The option " + key + " was not found");
+      }
    }
-}
 
-const std::string& Options::get_string(const std::string& key) const {
-   return this->at(key);
-}
+   const std::string& Options::get_string(const std::string& key) const {
+      return this->at(key);
+   }
 
-double Options::get_double(const std::string& key) const {
-   const std::string& entry = this->at(key);
-   return std::stod(entry);
-}
+   double Options::get_double(const std::string& key) const {
+      const std::string& entry = this->at(key);
+      return std::stod(entry);
+   }
 
-int Options::get_int(const std::string& key) const {
-   const std::string& entry = this->at(key);
-   return std::stoi(entry);
-}
+   int Options::get_int(const std::string& key) const {
+      const std::string& entry = this->at(key);
+      return std::stoi(entry);
+   }
 
-size_t Options::get_unsigned_int(const std::string& key) const {
-   const std::string& entry = this->at(key);
-   return std::stoul(entry);
-}
+   size_t Options::get_unsigned_int(const std::string& key) const {
+      const std::string& entry = this->at(key);
+      return std::stoul(entry);
+   }
 
-bool Options::get_bool(const std::string& key) const {
-   const std::string& entry = this->at(key);
-   return entry == "yes";
-}
+   bool Options::get_bool(const std::string& key) const {
+      const std::string& entry = this->at(key);
+      return entry == "yes";
+   }
 
-void Options::print(bool only_used) const {
-   std::cout << "Options:\n";
-   for (const auto& [key, value]: this->options) {
-      if (not only_used || this->is_used[key]) {
-         std::cout << "- " << key << " = " << value << '\n';
+   void Options::print(bool only_used) const {
+      std::cout << "Options:\n";
+      for (const auto& [key, value]: this->options) {
+         if (not only_used || this->is_used[key]) {
+            std::cout << "- " << key << " = " << value << '\n';
+         }
       }
    }
-}
 
-Options get_default_options(const std::string& file_name) {
-   std::ifstream file;
-   file.open(file_name);
-   if (!file) {
-      throw std::invalid_argument("The option file " + file_name + " was not found");
-   }
-   else {
-      // register the default options
-      Options options;
-      std::string key, value;
-      std::string line;
-      while (std::getline(file, line)) {
-         if (not line.empty() && line.find('#') != 0) {
-            std::istringstream iss;
-            iss.str(line);
-            iss >> key >> value;
-            options[key] = value;
+   Options Options::get_default_options(const std::string& file_name) {
+      std::ifstream file;
+      file.open(file_name);
+      if (!file) {
+         throw std::invalid_argument("The option file " + file_name + " was not found");
+      }
+      else {
+         // register the default options
+         Options options;
+         std::string key, value;
+         std::string line;
+         while (std::getline(file, line)) {
+            if (not line.empty() && line.find('#') != 0) {
+               std::istringstream iss;
+               iss.str(line);
+               iss >> key >> value;
+               options[key] = value;
+            }
          }
+         file.close();
+         return options;
       }
-      file.close();
-      return options;
    }
-}
 
-void find_preset(const std::string& preset_name, Options& options) {
-   // shortcuts for state-of-the-art combinations
-   if (preset_name == "ipopt") {
-      options["constraint_relaxation_strategy"] = "feasibility_restoration";
-      options["subproblem"] = "primal_dual_interior_point";
-      options["globalization_mechanism"] = "LS";
-      options["globalization_strategy"] = "waechter_filter_method";
-      options["filter_type"] = "standard";
-      options["filter_beta"] = "0.99999";
-      options["filter_gamma"] = "1e-8";
-      options["filter_delta"] = "1";
-      options["filter_ubd"] = "1e4";
-      options["filter_fact"] = "1e4";
-      options["filter_switching_infeasibility_exponent"] = "1.1";
-      options["armijo_decrease_fraction"] = "1e-8";
-      options["LS_backtracking_ratio"] = "0.5";
-      options["LS_min_step_length"] = "5e-7";
-      options["barrier_tau_min"] = "0.99";
-      options["barrier_damping_factor"] = "1e-5";
-      options["l1_constraint_violation_coefficient"] = "1000.";
-      options["progress_norm"] = "L1";
-      options["residual_norm"] = "INF";
-      options["scale_functions"] = "yes";
-      options["sparse_format"] = "COO";
-      options["tolerance"] = "1e-8";
-      options["loose_tolerance"] = "1e-6";
-      options["loose_tolerance_consecutive_iteration_threshold"] = "15";
-      options["switch_to_optimality_requires_acceptance"] = "yes";
-      options["switch_to_optimality_requires_linearized_feasibility"] = "no";
-      options["LS_scale_duals_with_step_length"] = "yes";
-      options["protect_actual_reduction_against_roundoff"] = "yes";
-   }
-   else if (preset_name == "filtersqp") {
-      options["constraint_relaxation_strategy"] = "feasibility_restoration";
-      options["subproblem"] = "QP";
-      options["globalization_mechanism"] = "TR";
-      options["globalization_strategy"] = "fletcher_filter_method";
-      options["filter_type"] = "standard";
-      options["progress_norm"] = "L1";
-      options["residual_norm"] = "L2";
-      options["sparse_format"] = "CSC";
-      options["TR_radius"] = "10";
-      options["l1_constraint_violation_coefficient"] = "1.";
-      options["enforce_linear_constraints"] = "yes";
-      options["tolerance"] = "1e-6";
-      options["loose_tolerance"] = "1e-6";
-      options["TR_min_radius"] = "1e-8";
-      options["switch_to_optimality_requires_acceptance"] = "no";
-      options["switch_to_optimality_requires_linearized_feasibility"] = "yes";
-      options["protect_actual_reduction_against_roundoff"] = "no";
-   }
-   else if (preset_name == "byrd") {
-      options["constraint_relaxation_strategy"] = "l1_relaxation";
-      options["subproblem"] = "QP";
-      options["globalization_mechanism"] = "LS";
-      options["globalization_strategy"] = "l1_merit";
-      options["l1_relaxation_initial_parameter"] = "1";
-      options["LS_backtracking_ratio"] = "0.5";
-      options["armijo_decrease_fraction"] = "1e-8";
-      options["l1_relaxation_epsilon1"] = "0.1";
-      options["l1_relaxation_epsilon2"] = "0.1";
-      options["l1_constraint_violation_coefficient"] = "1.";
-      options["tolerance"] = "1e-6";
-      options["loose_tolerance"] = "1e-6";
-      options["progress_norm"] = "L1";
-      options["residual_norm"] = "L1";
-      options["sparse_format"] = "CSC";
-      options["LS_scale_duals_with_step_length"] = "no";
-      options["protect_actual_reduction_against_roundoff"] = "no";
-   }
-   else {
-      throw std::runtime_error("The preset " + preset_name + " is not known.");
+   void find_preset(const std::string& preset_name, Options& options) {
+      // shortcuts for state-of-the-art combinations
+      if (preset_name == "ipopt") {
+         options["constraint_relaxation_strategy"] = "feasibility_restoration";
+         options["subproblem"] = "primal_dual_interior_point";
+         options["globalization_mechanism"] = "LS";
+         options["globalization_strategy"] = "waechter_filter_method";
+         options["filter_type"] = "standard";
+         options["filter_beta"] = "0.99999";
+         options["filter_gamma"] = "1e-8";
+         options["switching_delta"] = "1";
+         options["filter_ubd"] = "1e4";
+         options["filter_fact"] = "1e4";
+         options["filter_switching_infeasibility_exponent"] = "1.1";
+         options["armijo_decrease_fraction"] = "1e-8";
+         options["LS_backtracking_ratio"] = "0.5";
+         options["LS_min_step_length"] = "5e-7";
+         options["barrier_tau_min"] = "0.99";
+         options["barrier_damping_factor"] = "1e-5";
+         options["l1_constraint_violation_coefficient"] = "1000.";
+         options["progress_norm"] = "L1";
+         options["residual_norm"] = "INF";
+         options["scale_functions"] = "yes";
+         options["sparse_format"] = "COO";
+         options["tolerance"] = "1e-8";
+         options["loose_tolerance"] = "1e-6";
+         options["loose_tolerance_consecutive_iteration_threshold"] = "15";
+         options["switch_to_optimality_requires_linearized_feasibility"] = "no";
+         options["LS_scale_duals_with_step_length"] = "yes";
+         options["protect_actual_reduction_against_roundoff"] = "yes";
+      }
+      else if (preset_name == "filtersqp") {
+         options["constraint_relaxation_strategy"] = "feasibility_restoration";
+         options["subproblem"] = "QP";
+         options["globalization_mechanism"] = "TR";
+         options["globalization_strategy"] = "fletcher_filter_method";
+         options["filter_type"] = "standard";
+         options["progress_norm"] = "L1";
+         options["residual_norm"] = "L2";
+         options["sparse_format"] = "CSC";
+         options["TR_radius"] = "10";
+         options["l1_constraint_violation_coefficient"] = "1.";
+         options["enforce_linear_constraints"] = "yes";
+         options["tolerance"] = "1e-6";
+         options["loose_tolerance"] = "1e-6";
+         options["TR_min_radius"] = "1e-8";
+         options["switch_to_optimality_requires_linearized_feasibility"] = "yes";
+         options["protect_actual_reduction_against_roundoff"] = "no";
+      }
+      else if (preset_name == "byrd") {
+         options["constraint_relaxation_strategy"] = "l1_relaxation";
+         options["subproblem"] = "QP";
+         options["globalization_mechanism"] = "LS";
+         options["globalization_strategy"] = "l1_merit";
+         options["l1_relaxation_initial_parameter"] = "1";
+         options["LS_backtracking_ratio"] = "0.5";
+         options["armijo_decrease_fraction"] = "1e-8";
+         options["l1_relaxation_epsilon1"] = "0.1";
+         options["l1_relaxation_epsilon2"] = "0.1";
+         options["l1_constraint_violation_coefficient"] = "1.";
+         options["tolerance"] = "1e-6";
+         options["loose_tolerance"] = "1e-6";
+         options["progress_norm"] = "L1";
+         options["residual_norm"] = "L1";
+         options["sparse_format"] = "CSC";
+         options["LS_scale_duals_with_step_length"] = "no";
+         options["protect_actual_reduction_against_roundoff"] = "no";
+      }
+      else if (preset_name == "funnelsqp") {
+         options["constraint_relaxation_strategy"] = "feasibility_restoration";
+         options["subproblem"] = "QP";
+         options["globalization_mechanism"] = "TR";
+         options["globalization_strategy"] = "funnel_method";
+         options["progress_norm"] = "L1";
+         options["residual_norm"] = "L2";
+         options["sparse_format"] = "CSC";
+         options["TR_radius"] = "10";
+         options["l1_constraint_violation_coefficient"] = "1.";
+         options["enforce_linear_constraints"] = "yes";
+         options["tolerance"] = "1e-6";
+         options["loose_tolerance"] = "1e-6";
+         options["TR_min_radius"] = "1e-8";
+         options["switch_to_optimality_requires_acceptance"] = "no";
+         options["switch_to_optimality_requires_linearized_feasibility"] = "yes";
+
+         options["funnel_beta"] = "0.9999";
+         options["funnel_gamma"] = "0.001";
+         options["switching_delta"] = "0.999";
+         options["funnel_kappa"] = "0.5";
+         options["funnel_ubd"] = "1.0";
+         options["funnel_fact"] = "1.5";
+         options["funnel_switching_infeasibility_exponent"] = "2";
+         options["funnel_update_strategy"] = "2";
+      }
+      else {
+         throw std::runtime_error("The preset " + preset_name + " is not known.");
+      }
    }
-}
 
-void get_command_line_arguments(int argc, char* argv[], Options& options) {
-   // build the (name, value) map
-   int i = 1;
-   while (i < argc - 1) {
-      std::string argument = std::string(argv[i]);
-      if (argument[0] == '-') {
-         if (i < argc - 1) {
-            // remove the '-'
-            const std::string name = argument.substr(1);
-            const std::string value = std::string(argv[i + 1]);
-            if (name == "preset") {
-               find_preset(value, options);
-            }
-            else {
-               options[name] = value;
+   void Options::get_command_line_arguments(int argc, char* argv[]) {
+      // build the (name, value) map
+      int i = 1;
+      while (i < argc - 1) {
+         std::string argument = std::string(argv[i]);
+         if (argument[0] == '-') {
+            if (i < argc - 1) {
+               // remove the '-'
+               const std::string name = argument.substr(1);
+               const std::string value = std::string(argv[i + 1]);
+               if (name == "preset") {
+                  find_preset(value, *this);
+               }
+               else {
+                  this->operator[](name) = value;
+               }
+               i += 2;
             }
-            i += 2;
          }
-      }
-      else {
-         WARNING << "Argument " << argument << " was ignored\n";
-         i++;
+         else {
+            WARNING << "Argument " << argument << " was ignored\n";
+            i++;
+         }
       }
    }
-}
+} // namespace
diff --git a/uno/tools/Options.hpp b/uno/tools/Options.hpp
index 1de8f16..8e96b0a 100644
--- a/uno/tools/Options.hpp
+++ b/uno/tools/Options.hpp
@@ -7,28 +7,32 @@
 #include <map>
 #include <string>
 
-class Options {
-public:
-   Options() = default;
+namespace uno {
+   class Options {
+   public:
+      Options() = default;
 
-   std::string& operator[](const std::string& key);
+      std::string& operator[](const std::string& key);
 
-   [[nodiscard]] const std::string& get_string(const std::string& key) const;
-   [[nodiscard]] double get_double(const std::string& key) const;
-   [[nodiscard]] int get_int(const std::string& key) const;
-   [[nodiscard]] size_t get_unsigned_int(const std::string& key) const;
-   [[nodiscard]] bool get_bool(const std::string& key) const;
-   void print(bool only_used) const;
+      [[nodiscard]] const std::string& get_string(const std::string& key) const;
+      [[nodiscard]] double get_double(const std::string& key) const;
+      [[nodiscard]] int get_int(const std::string& key) const;
+      [[nodiscard]] size_t get_unsigned_int(const std::string& key) const;
+      [[nodiscard]] bool get_bool(const std::string& key) const;
 
-private:
-   std::map<std::string, std::string> options{};
-   // keep track of the options that are used
-   mutable std::map<std::string, bool> is_used{};
+      void get_command_line_arguments(int argc, char* argv[]);
 
-   [[nodiscard]] const std::string& at(const std::string& key) const;
-};
+      void print(bool only_used) const;
 
-Options get_default_options(const std::string& file_name);
-void get_command_line_arguments(int argc, char* argv[], Options& options);
+      static Options get_default_options(const std::string& file_name);
 
-#endif // UNO_OPTIONS_H
\ No newline at end of file
+   private:
+      std::map<std::string, std::string> options{};
+      // keep track of the options that are used
+      mutable std::map<std::string, bool> is_used{};
+
+      [[nodiscard]] const std::string& at(const std::string& key) const;
+   };
+} // namespace
+
+#endif // UNO_OPTIONS_H
diff --git a/uno/tools/Statistics.cpp b/uno/tools/Statistics.cpp
index 5215636..1745b1c 100644
--- a/uno/tools/Statistics.cpp
+++ b/uno/tools/Statistics.cpp
@@ -6,154 +6,157 @@
 #include "Statistics.hpp"
 #include "Options.hpp"
 
-// TODO move this to the option file
-int Statistics::int_width = 7;
-int Statistics::double_width = 17;
-int Statistics::string_width = 26;
+namespace uno {
+   // TODO move this to the option file
+   int Statistics::int_width = 7;
+   int Statistics::double_width = 17;
+   int Statistics::string_width = 26;
+   int Statistics::numerical_format_size = 6;
 
-Statistics::Statistics(const Options& options): print_header_every_iterations(options.get_unsigned_int("statistics_print_header_every_iterations")) {
-}
-
-void Statistics::add_column(std::string_view name, int width, int order) {
-   this->columns[order] = name;
-   this->widths[name] = width;
-}
-
-void Statistics::set(std::string_view name, std::string value) {
-   this->current_line[name] = std::move(value);
-}
-
-void Statistics::set(std::string_view name, int value) {
-   this->set(name, std::to_string(value));
-}
+   Statistics::Statistics(const Options& options): print_header_every_iterations(options.get_unsigned_int("statistics_print_header_every_iterations")) {
+   }
 
-void Statistics::set(std::string_view name, size_t value) {
-   this->set(name, std::to_string(value));
-}
+   void Statistics::add_column(std::string_view name, int width, int order) {
+      this->columns[order] = name;
+      this->widths[name] = width;
+   }
 
-void Statistics::set(std::string_view name, double value) {
-   std::ostringstream stream;
-   stream << std::defaultfloat << std::setprecision(7) << value;
-   this->set(name, stream.str());
-}
+   void Statistics::set(std::string_view name, std::string value) {
+      this->current_line[name] = std::move(value);
+   }
 
-void Statistics::print_header(bool first_occurrence) {
-   /* line above */
-   std::cout << (first_occurrence ? Statistics::symbol("top-left") : Statistics::symbol("left-mid"));
-   int k = 0;
-   for (const auto& element: this->columns) {
-      if (0 < k) {
-         std::cout << (first_occurrence ? Statistics::symbol("top-mid") : Statistics::symbol("mid-mid"));
-      }
-      const std::string& header = element.second;
-      for (int j = 0; j < this->widths[header]; j++) {
-         std::cout << Statistics::symbol("top");
-      }
-      k++;
+   void Statistics::set(std::string_view name, int value) {
+      this->set(name, std::to_string(value));
    }
-   std::cout << (first_occurrence ? Statistics::symbol("top-right") : Statistics::symbol("right-mid")) << '\n';
-   /* headers */
-   std::cout << Statistics::symbol("left");
-   k = 0;
-   for (const auto& element: this->columns) {
-      if (0 < k) {
-         std::cout << Statistics::symbol("middle");
-      }
-      const std::string& header = element.second;
-      std::cout << " " << header;
-      for (int j = 0; j < this->widths[header] - static_cast<int>(header.size()) - 1; j++) {
-         std::cout << " ";
-      }
-      k++;
+
+   void Statistics::set(std::string_view name, size_t value) {
+      this->set(name, std::to_string(value));
    }
-   std::cout << Statistics::symbol("right") << '\n';
-}
 
-void Statistics::print_current_line() {
-   if (this->iteration % this->print_header_every_iterations == 0) {
-      this->print_header(this->iteration == 0);
+   void Statistics::set(std::string_view name, double value) {
+      std::ostringstream stream;
+      stream << std::defaultfloat << std::setprecision(Statistics::numerical_format_size) << value;
+      this->set(name, stream.str());
    }
-   std::cout << Statistics::symbol("left-mid");
-   int k = 0;
-   for (const auto& element: this->columns) {
-      if (0 < k) {
-         std::cout << Statistics::symbol("mid-mid");
+
+   void Statistics::print_header(bool first_occurrence) {
+      /* line above */
+      std::cout << (first_occurrence ? Statistics::symbol("top-left") : Statistics::symbol("left-mid"));
+      int k = 0;
+      for (const auto& element: this->columns) {
+         if (0 < k) {
+            std::cout << (first_occurrence ? Statistics::symbol("top-mid") : Statistics::symbol("mid-mid"));
+         }
+         const std::string& header = element.second;
+         for (int j = 0; j < this->widths[header]; j++) {
+            std::cout << Statistics::symbol("top");
+         }
+         k++;
       }
-      std::string header = element.second;
-      for (int j = 0; j < this->widths[header]; j++) {
-         std::cout << Statistics::symbol("bottom");
+      std::cout << (first_occurrence ? Statistics::symbol("top-right") : Statistics::symbol("right-mid")) << '\n';
+      /* headers */
+      std::cout << Statistics::symbol("left");
+      k = 0;
+      for (const auto& element: this->columns) {
+         if (0 < k) {
+            std::cout << Statistics::symbol("middle");
+         }
+         const std::string& header = element.second;
+         std::cout << " " << header;
+         for (int j = 0; j < this->widths[header] - static_cast<int>(header.size()) - 1; j++) {
+            std::cout << " ";
+         }
+         k++;
       }
-      k++;
+      std::cout << Statistics::symbol("right") << '\n';
    }
-   std::cout << Statistics::symbol("right-mid") << '\n';
-   /* headers */
-   std::cout << Statistics::symbol("left");
-   k = 0;
-   for (const auto& element: this->columns) {
-      if (0 < k) {
-         std::cout << Statistics::symbol("middle");
-      }
-      const std::string& header = element.second;
-      int size;
-      try {
-         std::string value = this->current_line.at(header);
-         std::cout << " " << value;
-         size = 1 + static_cast<int>(value.size());
+
+   void Statistics::print_current_line() {
+      if (this->iteration % this->print_header_every_iterations == 0) {
+         this->print_header(this->iteration == 0);
       }
-      catch (const std::out_of_range&) {
-         std::cout << " -";
-         size = 2;
+      std::cout << Statistics::symbol("left-mid");
+      int k = 0;
+      for (const auto& element: this->columns) {
+         if (0 < k) {
+            std::cout << Statistics::symbol("mid-mid");
+         }
+         std::string header = element.second;
+         for (int j = 0; j < this->widths[header]; j++) {
+            std::cout << Statistics::symbol("bottom");
+         }
+         k++;
       }
-      int number_spaces = (size <= this->widths[header]) ? this->widths[header] - size : 0;
-      for (int j = 0; j < number_spaces; j++) {
-         std::cout << " ";
+      std::cout << Statistics::symbol("right-mid") << '\n';
+      /* headers */
+      std::cout << Statistics::symbol("left");
+      k = 0;
+      for (const auto& element: this->columns) {
+         if (0 < k) {
+            std::cout << Statistics::symbol("middle");
+         }
+         const std::string& header = element.second;
+         int size;
+         try {
+            std::string value = this->current_line.at(header);
+            std::cout << " " << value;
+            size = 1 + static_cast<int>(value.size());
+         }
+         catch (const std::out_of_range&) {
+            std::cout << " -";
+            size = 2;
+         }
+         int number_spaces = (size <= this->widths[header]) ? this->widths[header] - size : 0;
+         for (int j = 0; j < number_spaces; j++) {
+            std::cout << " ";
+         }
+         k++;
       }
-      k++;
+      std::cout << Statistics::symbol("right") << '\n';
+      this->iteration++;
    }
-   std::cout << Statistics::symbol("right") << '\n';
-   this->iteration++;
-}
 
-void Statistics::print_footer() {
-   std::cout << Statistics::symbol("bottom-left");
-   int k = 0;
-   for (const auto& element: this->columns) {
-      if (0 < k) {
-         std::cout << Statistics::symbol("bottom-mid");
-      }
-      std::string header = element.second;
-      for (int j = 0; j < this->widths[header]; j++) {
-         std::cout << Statistics::symbol("bottom");
+   void Statistics::print_footer() {
+      std::cout << Statistics::symbol("bottom-left");
+      int k = 0;
+      for (const auto& element: this->columns) {
+         if (0 < k) {
+            std::cout << Statistics::symbol("bottom-mid");
+         }
+         std::string header = element.second;
+         for (int j = 0; j < this->widths[header]; j++) {
+            std::cout << Statistics::symbol("bottom");
+         }
+         k++;
       }
-      k++;
+      std::cout << Statistics::symbol("bottom-right") << '\n';
    }
-   std::cout << Statistics::symbol("bottom-right") << '\n';
-}
 
-void Statistics::start_new_line() {
-   //this->current_line.clear();
-   for (const auto& column: this->columns) {
-      this->current_line[column.second] = "-";
+   void Statistics::start_new_line() {
+      //this->current_line.clear();
+      for (const auto& column: this->columns) {
+         this->current_line[column.second] = "-";
+      }
    }
-}
 
-std::string_view Statistics::symbol(std::string_view value) {
-   static std::map<std::string_view, std::string_view> symbols = {
-         {"top", "─"},
-         {"top-mid", "┬"},
-         {"top-left", "┌"},
-         {"top-right", "┐"},
-         {"bottom", "─"},
-         {"bottom-mid", "â”´"},
-         {"bottom-left", "â””"},
-         {"bottom-right", "┘"},
-         {"left", "│"},
-         {"left-mid", "├"},
-         {"mid", "─"},
-         {"mid-mid", "┼"},
-         {"right", "│"},
-         {"right-mid", "┤"},
-         {"middle", "│"}
-   };
-   return symbols[value];
-}
\ No newline at end of file
+   std::string_view Statistics::symbol(std::string_view value) {
+      static std::map<std::string_view, std::string_view> symbols = {
+            {"top", "─"},
+            {"top-mid", "┬"},
+            {"top-left", "┌"},
+            {"top-right", "┐"},
+            {"bottom", "─"},
+            {"bottom-mid", "â”´"},
+            {"bottom-left", "â””"},
+            {"bottom-right", "┘"},
+            {"left", "│"},
+            {"left-mid", "├"},
+            {"mid", "─"},
+            {"mid-mid", "┼"},
+            {"right", "│"},
+            {"right-mid", "┤"},
+            {"middle", "│"}
+      };
+      return symbols[value];
+   }
+} // namespace
diff --git a/uno/tools/Statistics.hpp b/uno/tools/Statistics.hpp
index c978e86..94421e7 100644
--- a/uno/tools/Statistics.hpp
+++ b/uno/tools/Statistics.hpp
@@ -7,35 +7,38 @@
 #include <string_view>
 #include <map>
 
-// forward declaration
-class Options;
-
-class Statistics {
-public:
-   explicit Statistics(const Options& options);
-
-   static int int_width;
-   static int double_width;
-   static int string_width;
-
-   void add_column(std::string_view name, int width, int order);
-   void set(std::string_view name, std::string value);
-   void set(std::string_view name, int value);
-   void set(std::string_view name, size_t value);
-   void set(std::string_view name, double value);
-   void print_header(bool first_occurrence);
-   void print_current_line();
-   void print_footer();
-   void start_new_line();
-
-private:
-   size_t iteration{0};
-   std::map<int, std::string> columns{};
-   std::map<std::string_view, int> widths{};
-   std::map<std::string_view, std::string> current_line{};
-
-   size_t print_header_every_iterations{};
-   static std::string_view symbol(std::string_view value);
-};
-
-#endif // UNO_STATISTICS_H
\ No newline at end of file
+namespace uno {
+   // forward declaration
+   class Options;
+
+   class Statistics {
+   public:
+      explicit Statistics(const Options& options);
+
+      static int int_width;
+      static int double_width;
+      static int string_width;
+      static int numerical_format_size;
+
+      void add_column(std::string_view name, int width, int order);
+      void set(std::string_view name, std::string value);
+      void set(std::string_view name, int value);
+      void set(std::string_view name, size_t value);
+      void set(std::string_view name, double value);
+      void print_header(bool first_occurrence);
+      void print_current_line();
+      void print_footer();
+      void start_new_line();
+
+   private:
+      size_t iteration{0};
+      std::map<int, std::string> columns{};
+      std::map<std::string_view, int> widths{};
+      std::map<std::string_view, std::string> current_line{};
+
+      size_t print_header_every_iterations{};
+      static std::string_view symbol(std::string_view value);
+   };
+} // namespace
+
+#endif // UNO_STATISTICS_H
diff --git a/uno/tools/Timer.cpp b/uno/tools/Timer.cpp
index 04d82a7..41bd594 100644
--- a/uno/tools/Timer.cpp
+++ b/uno/tools/Timer.cpp
@@ -5,15 +5,17 @@
 #include <chrono>
 #include <ctime>
 
-Timer::Timer(): start_time(std::clock()) {
-}
+namespace uno {
+   Timer::Timer(): start_time(std::clock()) {
+   }
 
-double Timer::get_duration() const {
-   return static_cast<double>(std::clock() - this->start_time) / static_cast<double>(CLOCKS_PER_SEC);
-}
+   double Timer::get_duration() const {
+      return static_cast<double>(std::clock() - this->start_time) / static_cast<double>(CLOCKS_PER_SEC);
+   }
 
-char* Timer::get_current_date() {
-   const auto current_time = std::chrono::system_clock::now();
-   const auto formatted_current_time = std::chrono::system_clock::to_time_t(current_time);
-   return std::ctime(&formatted_current_time);
-}
\ No newline at end of file
+   char* Timer::get_current_date() {
+      const auto current_time = std::chrono::system_clock::now();
+      const auto formatted_current_time = std::chrono::system_clock::to_time_t(current_time);
+      return std::ctime(&formatted_current_time);
+   }
+} // namespace
diff --git a/uno/tools/Timer.hpp b/uno/tools/Timer.hpp
index c6c62de..d5eabff 100644
--- a/uno/tools/Timer.hpp
+++ b/uno/tools/Timer.hpp
@@ -6,15 +6,17 @@
 
 #include <ctime>
 
-// timer starts upon creation
-class Timer {
-public:
-   Timer();
-   [[nodiscard]] double get_duration() const;
-   [[nodiscard]] static char* get_current_date();
+namespace uno {
+   // timer starts upon creation
+   class Timer {
+   public:
+      Timer();
+      [[nodiscard]] double get_duration() const;
+      [[nodiscard]] static char* get_current_date();
 
-private:
-   std::clock_t start_time;
-};
+   private:
+      std::clock_t start_time;
+   };
+} // namespace
 
 #endif //UNO_TIMER_H
diff --git a/uno_ampl-completion.bash b/uno_ampl-completion.bash
index bbae30d..e05fa65 100755
--- a/uno_ampl-completion.bash
+++ b/uno_ampl-completion.bash
@@ -16,7 +16,7 @@ _uno_ampl_completions()
             return 0
             ;;
         -globalization_strategy)
-			local globalization_strategies="l1_merit fletcher_filter_method waechter_filter_method"
+			local globalization_strategies="l1_merit fletcher_filter_method waechter_filter_method funnel_method"
             COMPREPLY=( $(compgen -W "${globalization_strategies}" -- ${cur}) )
             return 0
             ;;
@@ -31,7 +31,7 @@ _uno_ampl_completions()
             return 0
             ;;
         -preset)
-			local presets="filtersqp ipopt byrd"
+			local presets="filtersqp ipopt byrd funnelsqp"
             COMPREPLY=( $(compgen -W "${presets}" -- ${cur}) )
             return 0
             ;;
diff --git a/unotest/COOSymmetricMatrixTests.cpp b/unotest/COOSymmetricMatrixTests.cpp
index ef6bea6..b753beb 100644
--- a/unotest/COOSymmetricMatrixTests.cpp
+++ b/unotest/COOSymmetricMatrixTests.cpp
@@ -4,11 +4,13 @@
 #include <gtest/gtest.h>
 #include "linear_algebra/COOSymmetricMatrix.hpp"
 
+using namespace uno;
+
 const size_t n = 5;
 const size_t nnz = 7;
 
-COOSymmetricMatrix<double> create_COO_matrix() {
-   COOSymmetricMatrix<double> matrix(n, nnz, false);
+COOSymmetricMatrix<size_t, double> create_COO_matrix() {
+   COOSymmetricMatrix<size_t, double> matrix(n, nnz, false);
    matrix.insert(2., 0, 0);
    matrix.insert(3., 0, 1);
    matrix.insert(4., 1, 2);
@@ -20,15 +22,15 @@ COOSymmetricMatrix<double> create_COO_matrix() {
 }
 
 TEST(COOSymmetricMatrix, NNZ) {
-   const COOSymmetricMatrix<double> matrix = create_COO_matrix();
+   const COOSymmetricMatrix<size_t, double> matrix = create_COO_matrix();
    ASSERT_EQ(matrix.number_nonzeros, nnz);
 }
 
 /*
 TEST(COOSymmetricMatrix, Iterator) {
-   const COOSymmetricMatrix<double> matrix = create_COO_matrix();
+   const COOSymmetricMatrix<size_t, double> matrix = create_COO_matrix();
    for (const auto [i, j, Mij]: matrix) {
       std::cout << "COO(" << i << ", " << j << ") = " << Mij << '\n';
    }
 }
-*/
\ No newline at end of file
+*/
diff --git a/unotest/CSCSymmetricMatrixTests.cpp b/unotest/CSCSymmetricMatrixTests.cpp
index 55a7f85..1b57f5e 100644
--- a/unotest/CSCSymmetricMatrixTests.cpp
+++ b/unotest/CSCSymmetricMatrixTests.cpp
@@ -4,11 +4,13 @@
 #include <gtest/gtest.h>
 #include "linear_algebra/CSCSymmetricMatrix.hpp"
 
+using namespace uno;
+
 const size_t n = 5;
 const size_t nnz = 4;
 
-CSCSymmetricMatrix<double> create_CSC_matrix() {
-   CSCSymmetricMatrix<double> matrix(n, nnz, false);
+CSCSymmetricMatrix<size_t, double> create_CSC_matrix() {
+   CSCSymmetricMatrix<size_t, double> matrix(n, nnz, false);
    matrix.insert(2., 0, 0);
    matrix.finalize_column(0);
    matrix.finalize_column(1);
@@ -24,15 +26,15 @@ CSCSymmetricMatrix<double> create_CSC_matrix() {
 }
 
 TEST(CSCSymmetricMatrix, NNZ) {
-   const CSCSymmetricMatrix<double> matrix = create_CSC_matrix();
+   const CSCSymmetricMatrix<size_t, double> matrix = create_CSC_matrix();
    ASSERT_EQ(matrix.number_nonzeros, nnz);
 }
 
 /*
 TEST(CSCSymmetricMatrix, Iterator) {
-   const CSCSymmetricMatrix<double> matrix = create_CSC_matrix();
+   const CSCSymmetricMatrix<size_t, double> matrix = create_CSC_matrix();
    for (const auto [i, j, Mij]: matrix) {
       std::cout << "CSC(" << i << ", " << j << ") = " << Mij << '\n';
    }
 }
-*/
\ No newline at end of file
+*/
diff --git a/unotest/CollectionAdapterTests.cpp b/unotest/CollectionAdapterTests.cpp
index 7c9fb71..d192ec8 100644
--- a/unotest/CollectionAdapterTests.cpp
+++ b/unotest/CollectionAdapterTests.cpp
@@ -5,6 +5,8 @@
 #include "symbolic/CollectionAdapter.hpp"
 #include <vector>
 
+using namespace uno;
+
 TEST(CollectionAdapter, Size) {
    const std::vector<int> x{1, 2, 3};
    const auto y = CollectionAdapter(x);
@@ -19,4 +21,4 @@ TEST(CollectionAdapter, Iterator) {
       ASSERT_EQ(value, x[index]);
       index++;
    }
-}
\ No newline at end of file
+}
diff --git a/unotest/ConcatenationTests.cpp b/unotest/ConcatenationTests.cpp
index f2fb47d..db60634 100644
--- a/unotest/ConcatenationTests.cpp
+++ b/unotest/ConcatenationTests.cpp
@@ -7,6 +7,8 @@
 #include "symbolic/Range.hpp"
 #include <vector>
 
+using namespace uno;
+
 TEST(Concatenation, Size) {
    const std::vector<int> x{1, 2, 3};
    const std::vector<int> y{4, 5, 6};
@@ -36,4 +38,4 @@ TEST(Concatenation, Iterator) {
       index++;
    }
    std::cout << "\n";
-}
\ No newline at end of file
+}
diff --git a/unotest/MA57SolverTests.cpp b/unotest/MA57SolverTests.cpp
index 6d9ce76..e512874 100644
--- a/unotest/MA57SolverTests.cpp
+++ b/unotest/MA57SolverTests.cpp
@@ -5,12 +5,14 @@
 #include "linear_algebra/COOSymmetricMatrix.hpp"
 #include "solvers/linear/MA57Solver.hpp"
 
+using namespace uno;
+
 const size_t n = 5;
 const size_t nnz = 7;
 const std::array<double, n> reference{1., 2., 3., 4., 5.};
 
-COOSymmetricMatrix<double> create_matrix() {
-   COOSymmetricMatrix<double> matrix(n, nnz, false);
+COOSymmetricMatrix<size_t, double> create_matrix() {
+   COOSymmetricMatrix<size_t, double> matrix(n, nnz, false);
    matrix.insert(2., 0, 0);
    matrix.insert(3., 0, 1);
    matrix.insert(4., 1, 2);
@@ -27,7 +29,7 @@ Vector<double> create_rhs() {
 }
 
 TEST(MA57Solver, SystemSize5) {
-   const COOSymmetricMatrix<double> matrix = create_matrix();
+   const COOSymmetricMatrix<size_t, double> matrix = create_matrix();
    const Vector<double> rhs = create_rhs();
    Vector<double> result(n);
    result.fill(0.);
@@ -40,5 +42,21 @@ TEST(MA57Solver, SystemSize5) {
    for (size_t index: Range(n)) {
       EXPECT_DOUBLE_EQ(result[index], reference[index]);
    }
+}
+
+TEST(MA57Solver, Inertia) {
+   const COOSymmetricMatrix<size_t, double> matrix = create_matrix();
+   const Vector<double> rhs = create_rhs();
+   Vector<double> result(n);
+   result.fill(0.);
+
+   MA57Solver solver(n, nnz);
+   solver.do_symbolic_factorization(matrix);
+   solver.do_numerical_factorization(matrix);
+   solver.solve_indefinite_system(matrix, rhs, result);
 
+   const auto [number_positive, number_negative, number_zero] = solver.get_inertia();
+   ASSERT_EQ(number_positive, 3);
+   ASSERT_EQ(number_negative, 2);
+   ASSERT_EQ(number_zero, 0);
 }
\ No newline at end of file
diff --git a/unotest/MUMPSSolverTests.cpp b/unotest/MUMPSSolverTests.cpp
new file mode 100644
index 0000000..0ffc093
--- /dev/null
+++ b/unotest/MUMPSSolverTests.cpp
@@ -0,0 +1,80 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include <gtest/gtest.h>
+#include "linear_algebra/COOSymmetricMatrix.hpp"
+#include "solvers/linear/MUMPSSolver.hpp"
+
+using namespace uno;
+
+const size_t n = 5;
+const size_t nnz = 7;
+const std::array<double, n> reference{1., 2., 3., 4., 5.};
+const double tolerance = 1e-8;
+
+COOSymmetricMatrix<size_t, double> create_my_matrix() {
+   COOSymmetricMatrix<size_t, double> matrix(n, nnz, false);
+   matrix.insert(2., 0, 0);
+   matrix.insert(3., 0, 1);
+   matrix.insert(4., 1, 2);
+   matrix.insert(6., 1, 4);
+   matrix.insert(1., 2, 2);
+   matrix.insert(5., 2, 3);
+   matrix.insert(1., 4, 4);
+   return matrix;
+}
+
+Vector<double> create_my_rhs() {
+   Vector<double> rhs{8., 45., 31., 15., 17.};
+   return rhs;
+}
+
+TEST(MUMPSSolver, SystemSize5) {
+   const COOSymmetricMatrix<size_t, double> matrix = create_my_matrix();
+   const Vector<double> rhs = create_my_rhs();
+   Vector<double> result(n);
+   result.fill(0.);
+
+   MUMPSSolver solver(n, nnz);
+   solver.do_symbolic_factorization(matrix);
+   solver.do_numerical_factorization(matrix);
+   solver.solve_indefinite_system(matrix, rhs, result);
+
+   for (size_t index: Range(n)) {
+      EXPECT_NEAR(result[index], reference[index], tolerance);
+   }
+}
+
+/*
+>>> import numpy as np
+>>> from numpy import linalg as LA
+>>>
+>>> M = np.array([
+...    [2., 3., 0., 0., 0.],
+...    [3., 0., 4., 0., 6.],
+...    [0., 4., 1., 5., 0.],
+...    [0., 0., 5., 0., 0.],
+...    [0., 6., 0., 0., 1.]
+... ])
+>>>
+>>> eigenvalues, eigenvectors = LA.eig(M)
+>>> eigenvalues
+array([-7.83039207,  8.94059148, -3.50815575,  1.7888887 ,  4.60906763])
+*/
+
+TEST(MUMPSSolver, Inertia) {
+   const COOSymmetricMatrix<size_t, double> matrix = create_my_matrix();
+   const Vector<double> rhs = create_my_rhs();
+   Vector<double> result(n);
+   result.fill(0.);
+
+   MUMPSSolver solver(n, nnz);
+   solver.do_symbolic_factorization(matrix);
+   solver.do_numerical_factorization(matrix);
+   solver.solve_indefinite_system(matrix, rhs, result);
+
+   const auto [number_positive, number_negative, number_zero] = solver.get_inertia();
+   ASSERT_EQ(number_positive, 3);
+   ASSERT_EQ(number_negative, 2);
+   ASSERT_EQ(number_zero, 0);
+}
\ No newline at end of file
diff --git a/unotest/MatrixVectorProductTests.cpp b/unotest/MatrixVectorProductTests.cpp
index c709b79..d1ee3a7 100644
--- a/unotest/MatrixVectorProductTests.cpp
+++ b/unotest/MatrixVectorProductTests.cpp
@@ -6,6 +6,8 @@
 #include "linear_algebra/RectangularMatrix.hpp"
 #include "symbolic/MatrixVectorProduct.hpp"
 
+using namespace uno;
+
 TEST(MatrixVectorProduct, Test) {
    // (3, 7)
    // (7, 11)
diff --git a/unotest/RangeTests.cpp b/unotest/RangeTests.cpp
index c039c98..ea714ae 100644
--- a/unotest/RangeTests.cpp
+++ b/unotest/RangeTests.cpp
@@ -4,6 +4,8 @@
 #include <gtest/gtest.h>
 #include "symbolic/Range.hpp"
 
+using namespace uno;
+
 TEST(Range, Size) {
    const size_t size = 5;
    const Range range(size);
@@ -46,4 +48,4 @@ TEST(Range, BackwardSumLoop) {
       sum += element;
    }
    ASSERT_EQ(sum, 12);
-}
\ No newline at end of file
+}
diff --git a/unotest/ScalarMultipleTests.cpp b/unotest/ScalarMultipleTests.cpp
index cf5afd1..d1c2a5e 100644
--- a/unotest/ScalarMultipleTests.cpp
+++ b/unotest/ScalarMultipleTests.cpp
@@ -6,6 +6,8 @@
 #include "symbolic/ScalarMultiple.hpp"
 #include "symbolic/Range.hpp"
 
+using namespace uno;
+
 TEST(ScalarMultiple, TimesThree) {
    const std::vector<double> x{1., 2., 3.};
    const std::vector<double> reference_result{3., 6., 9.};
@@ -13,4 +15,4 @@ TEST(ScalarMultiple, TimesThree) {
    for (size_t i: Range(y.size())) {
       ASSERT_EQ(y[i], reference_result[i]);
    }
-}
\ No newline at end of file
+}
diff --git a/unotest/SparseVectorTests.cpp b/unotest/SparseVectorTests.cpp
index e8c43eb..fd19375 100644
--- a/unotest/SparseVectorTests.cpp
+++ b/unotest/SparseVectorTests.cpp
@@ -4,6 +4,8 @@
 #include <gtest/gtest.h>
 #include "linear_algebra/SparseVector.hpp"
 
+using namespace uno;
+
 TEST(SparseVector, Empty) {
    SparseVector<double> x(2);
    // x is empty: its size (number of elements) is 0
@@ -53,4 +55,4 @@ TEST(SparseVector, InsertAfterClear) {
    // insert an element
    x.insert(7, 3.);
    ASSERT_EQ(x.size(), 1);
-}
\ No newline at end of file
+}
diff --git a/unotest/VectorTests.cpp b/unotest/VectorTests.cpp
new file mode 100644
index 0000000..cb2f649
--- /dev/null
+++ b/unotest/VectorTests.cpp
@@ -0,0 +1,62 @@
+// Copyright (c) 2018-2024 Charlie Vanaret
+// Licensed under the MIT license. See LICENSE file in the project directory for details.
+
+#include <gtest/gtest.h>
+#include "linear_algebra/Vector.hpp"
+
+using namespace uno;
+
+TEST(Vector, Empty) {
+   Vector<double> x(0);
+   // x is empty: its size (number of elements) is 0
+   ASSERT_EQ(x.size(), 0);
+}
+
+TEST(Vector, Size) {
+   const size_t capacity = 3;
+   Vector<double> x{1., 1., 1.};
+   ASSERT_EQ(x.size(), capacity);
+}
+
+TEST(Vector, AllIdentical) {
+   const double constant_term = 1.;
+   Vector<double> x{constant_term, constant_term, constant_term};
+   for (const auto element: x) {
+      ASSERT_EQ(element, constant_term);
+   }
+}
+
+TEST(Vector, MoveConstructor) {
+   const double constant_term = 1.;
+   Vector<double> x{constant_term, constant_term, constant_term};
+   Vector<double> y = std::move(x);
+
+   for (const auto element: y) {
+      ASSERT_EQ(element, constant_term);
+   }
+}
+
+TEST(Vector, MoveAssignment) {
+   const double constant_term = 1.;
+   Vector<double> x{constant_term, constant_term, constant_term};
+   Vector<double> y{2.*constant_term, 2.*constant_term, 2.*constant_term};
+   y = std::move(x);
+
+   for (const auto element: y) {
+      ASSERT_EQ(element, constant_term);
+   }
+}
+
+TEST(Vector, Swap) {
+   const double constant_term = 1.;
+   Vector<double> x{constant_term, constant_term, constant_term};
+   Vector<double> y{2.*constant_term, 2.*constant_term, 2.*constant_term};
+   std::swap(x, y);
+
+   for (const auto element: x) {
+      ASSERT_EQ(element, 2.*constant_term);
+   }
+   for (const auto element: y) {
+      ASSERT_EQ(element, constant_term);
+   }
+}
diff --git a/unotest/unotest.cpp b/unotest/unotest.cpp
index 3b65ece..920ce13 100644
--- a/unotest/unotest.cpp
+++ b/unotest/unotest.cpp
@@ -1,15 +1,31 @@
 // Copyright (c) 2018-2024 Charlie Vanaret
 // Licensed under the MIT license. See LICENSE file in the project directory for details.
 
+#if defined(HAS_MPI) && defined(MUMPS_PARALLEL)
+#include "mpi.h"
+#endif
 #include <gtest/gtest.h>
 #include "tools/Logger.hpp"
 
-Level Logger::level = INFO;
+namespace uno {
+   Level Logger::level = INFO;
+} // namespace
 
 // https://www.eriksmistad.no/getting-started-with-google-test-on-ubuntu/
 int main(int argc, char **argv) {
+#if defined(HAS_MPI) && defined(MUMPS_PARALLEL)
+   int myid , ierr;
+   ierr = MPI_Init(&argc , &argv);
+   ierr = MPI_Comm_rank(MPI_COMM_WORLD, &myid);
+#endif
+
     testing::InitGoogleTest(&argc, argv);
-    return RUN_ALL_TESTS();
+    auto result = RUN_ALL_TESTS();
+
+#if defined(HAS_MPI) && defined(MUMPS_PARALLEL)
+   ierr = MPI_Finalize() ;
+#endif
+   return result;
 }
 
 //void test_mask_matrix() {
-- 
GitLab