diff --git a/cmake/modules/FindAE108_PETSc.cmake b/cmake/modules/FindAE108_PETSc.cmake
index 75de1f8b256ec74ff0c53f87a0c5a9af557bc1da..1330a22935e777c5b0c59c33cee0148eb29086aa 100755
--- a/cmake/modules/FindAE108_PETSc.cmake
+++ b/cmake/modules/FindAE108_PETSc.cmake
@@ -51,6 +51,7 @@ if(AE108_PETSc_FOUND AND NOT TARGET ae108::external::petsc)
     endif()
 
     try_compile_with_petsc("-DAE108_PETSC_COMPLEX")
+    set(AE108_PETSC_COMPLEX ${AE108_PETSc_COMPILE_RESULT} CACHE BOOL "ae108: PETSc with complex scalar type")
     if(AE108_PETSc_COMPILE_RESULT)
         target_compile_definitions(ae108::external::petsc
             INTERFACE AE108_PETSC_COMPLEX=1
diff --git a/cpppetsc/src/include/ae108/cpppetsc/TAOSolver.h b/cpppetsc/src/include/ae108/cpppetsc/TAOSolver.h
index 3e343fc2739a01c1fdf16ad4361818065d139771..b44ad10b57333beafe32c28b1f96aa92c352d1dd 100644
--- a/cpppetsc/src/include/ae108/cpppetsc/TAOSolver.h
+++ b/cpppetsc/src/include/ae108/cpppetsc/TAOSolver.h
@@ -1,4 +1,4 @@
-// © 2020 ETH Zurich, Mechanics and Materials Lab
+// © 2020, 2022 ETH Zurich, Mechanics and Materials Lab
 // © 2020 California Institute of Technology
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,6 +25,7 @@
 #include "ae108/cpppetsc/UniqueEntity.h"
 #include "ae108/cpppetsc/Vector.h"
 #include <functional>
+#include <optional>
 #include <petscmat.h>
 #include <petscmath.h>
 #include <petscsys.h>
@@ -71,12 +72,17 @@ public:
     gpcg,
     nm,
     pounders,
+    brgn,
     lcl,
     ssils,
     ssfls,
     asils,
     asfls,
-    ipm
+    ipm,
+    pdipm,
+    shell,
+    admm,
+    almm,
   };
 
   /**
@@ -132,6 +138,29 @@ public:
   void setBounds(const distributed<vector_type> &lowerBounds,
                  const distributed<vector_type> &upperBounds);
 
+  struct EqualityConstraints {
+    /**
+     * @brief The solver attempts to reach a residual of zero.
+     */
+    GradientFunctor residual;
+
+    /**
+     * @brief The jacobian of the residual.
+     */
+    HessianFunctor jacobian;
+
+    /**
+     * @brief Used to store residuals and jacobians internally.
+     */
+    std::pair<distributed<vector_type>, matrix_type> buffer;
+  };
+
+  /**
+   * @brief Set the equality constraints of the problem.
+   * @note Not all solvers consider these constraints.
+   */
+  void setConstraints(EqualityConstraints constraints);
+
   /**
    * @brief Minimizes the objective function using optimization.
    *
@@ -164,6 +193,8 @@ private:
   UniqueEntity<Tao> _tao;
 
   matrix_type _buffer_matrix;
+
+  std::optional<EqualityConstraints> _constraints;
 };
 
 template <class Policy>
@@ -288,6 +319,10 @@ template <class Policy> void TAOSolver<Policy>::setType(const Type type) {
     taoType = TAOPOUNDERS;
     break;
   }
+  case Type::brgn: {
+    taoType = TAOBRGN;
+    break;
+  }
   case Type::lcl: {
     taoType = TAOLCL;
     break;
@@ -312,6 +347,22 @@ template <class Policy> void TAOSolver<Policy>::setType(const Type type) {
     taoType = TAOIPM;
     break;
   }
+  case Type::pdipm: {
+    taoType = TAOPDIPM;
+    break;
+  }
+  case Type::shell: {
+    taoType = TAOSHELL;
+    break;
+  }
+  case Type::admm: {
+    taoType = TAOADMM;
+    break;
+  }
+  case Type::almm: {
+    taoType = TAOALMM;
+    break;
+  }
   }
   Policy::handleError(TaoSetType(_tao.get(), taoType));
 }
@@ -323,6 +374,19 @@ void TAOSolver<Policy>::setBounds(const distributed<vector_type> &lowerBounds,
       _tao.get(), lowerBounds.unwrap().data(), upperBounds.unwrap().data()));
 }
 
+template <class Policy>
+void TAOSolver<Policy>::setConstraints(EqualityConstraints constraints) {
+  _constraints.emplace(std::move(constraints));
+
+  Policy::handleError(TaoSetEqualityConstraintsRoutine(
+      _tao.get(), _constraints->buffer.first.unwrap().data(),
+      &TAOSolver::gradientAdapter, &_constraints->residual));
+  Policy::handleError(TaoSetJacobianEqualityRoutine(
+      _tao.get(), _constraints->buffer.second.data(),
+      _constraints->buffer.second.data(), &TAOSolver::hessianAdapter,
+      &_constraints->jacobian));
+}
+
 template <class Policy> void TAOSolver<Policy>::checkConvergence() const {
   const auto errorCode = [this]() {
     auto reason = TaoConvergedReason{};
diff --git a/cpppetsc/test/TAOSolver_Test.cc b/cpppetsc/test/TAOSolver_Test.cc
index e54a1275a29f81012b9203dbe8554789ef966ad3..107e656dbb8eae244caa68e8ee138d3e0d56174c 100644
--- a/cpppetsc/test/TAOSolver_Test.cc
+++ b/cpppetsc/test/TAOSolver_Test.cc
@@ -1,4 +1,4 @@
-// © 2020 ETH Zurich, Mechanics and Materials Lab
+// © 2020, 2022 ETH Zurich, Mechanics and Materials Lab
 // © 2020 California Institute of Technology
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,6 +26,7 @@
 
 using ae108::cppptest::isLocal;
 using ae108::cppptest::ScalarEqIfLocal;
+using ae108::cppptest::ScalarNearIfLocal;
 using testing::Test;
 using testing::Types;
 
@@ -140,6 +141,54 @@ TYPED_TEST(TAOSolver_Test, raises_exception_on_nonconvergence) {
   EXPECT_THROW(callSolve(), TAOSolverDivergedException);
 }
 
+TYPED_TEST(TAOSolver_Test, minimizes_with_equality_constraints) {
+  using real_type = typename TestFixture::real_type;
+  using vector_type = typename TestFixture::vector_type;
+  using matrix_type = typename TestFixture::matrix_type;
+
+  typename TestFixture::solver_type solver(
+      matrix_type(2, 2), TestFixture::solver_type::Type::almm);
+
+  const auto value = 5.;
+
+  solver.setConstraints({
+      [&](const distributed<vector_type> &input,
+          distributed<vector_type> *const output) {
+        const auto full = vector_type::fromDistributed(input);
+        output->unwrap().setZero();
+        output->unwrap().replace().element(0, full(0) - value);
+      },
+      [](const distributed<vector_type> &, matrix_type *const output) {
+        output->setZero();
+        const auto replacer =
+            output->assemblyView().replace().element(0, 0, 1.);
+      },
+      {tag<DistributedTag>(vector_type(1)), matrix_type(1, 2)},
+  });
+
+  const auto solution = solver.solve(
+      [](const distributed<vector_type> &input, real_type *const output) {
+        const auto full = vector_type::fromDistributed(input);
+        *output = std::pow(full(0) - 1., 2.) + std::pow(full(1) - 2., 2.);
+      },
+      [](const distributed<vector_type> &input,
+         distributed<vector_type> *const output) {
+        const auto full = vector_type::fromDistributed(input);
+        const auto replacer = output->unwrap().replace();
+        replacer(0) = 2. * (full(0) - 1.);
+        replacer(1) = 2. * (full(1) - 2.);
+      },
+      [](const distributed<vector_type> &, matrix_type *const output) {
+        const auto replacer = output->assemblyView().replace();
+        replacer(0, 0) = 2.;
+        replacer(1, 1) = 2.;
+      },
+      tag<DistributedTag>(vector_type::fromList({3., 4.})));
+
+  EXPECT_THAT(solution.unwrap(), ScalarNearIfLocal(0, value, 1e-7));
+  EXPECT_THAT(solution.unwrap(), ScalarEqIfLocal(1, 2.));
+}
+
 template <class Policy> struct TAOSolver_BoundsTest : Test {
   using solver_type = TAOSolver<Policy>;
   using value_type = typename solver_type::value_type;
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 921a4ac2e90a8eff8fd471945d77b2d1fcd9fa50..356c5b08e104bf4fb0efa6b7ad774ee77bb8c936 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -126,4 +126,13 @@ target_link_libraries(${PROJECT_NAME}-Bar
                       PRIVATE ae108::solve
                       PRIVATE ae108::assembly
                       PRIVATE ae108::elements
-)
\ No newline at end of file
+)
+
+if (NOT ${AE108_PETSC_COMPLEX})
+    add_executable(${PROJECT_NAME}-NonlinearBC NonlinearBC.cc)
+    target_link_libraries(${PROJECT_NAME}-NonlinearBC
+                          PRIVATE ae108::solve
+                          PRIVATE ae108::assembly
+                          PRIVATE ae108::elements
+    )
+endif()
\ No newline at end of file
diff --git a/examples/NonlinearBC.cc b/examples/NonlinearBC.cc
new file mode 100644
index 0000000000000000000000000000000000000000..a0acf5c0bf701b8719f347bfa0a29f1474184e28
--- /dev/null
+++ b/examples/NonlinearBC.cc
@@ -0,0 +1,271 @@
+// © 2020, 2022 ETH Zurich, Mechanics and Materials Lab
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "ae108/assembly/Assembler.h"
+#include "ae108/cpppetsc/Context.h"
+#include "ae108/cpppetsc/Mesh.h"
+#include "ae108/cpppetsc/ParallelComputePolicy.h"
+#include "ae108/cpppetsc/Vector.h"
+#include "ae108/elements/CoreElement.h"
+#include "ae108/elements/embedding/IsoparametricEmbedding.h"
+#include "ae108/elements/integrator/IsoparametricIntegrator.h"
+#include "ae108/elements/materialmodels/Hookean.h"
+#include "ae108/elements/quadrature/Quadrature.h"
+#include "ae108/elements/shape/Quad4.h"
+#include "ae108/solve/GeneralizedTAOSolver.h"
+#include <array>
+#include <unordered_map>
+
+using namespace ae108;
+
+using Policy = cpppetsc::ParallelComputePolicy;
+using Context = cpppetsc::Context<Policy>;
+using Mesh = cpppetsc::Mesh<Policy>;
+using Vector = cpppetsc::Vector<Policy>;
+using Matrix = cpppetsc::Matrix<Policy>;
+
+namespace embedding = ae108::elements::embedding;
+namespace integrator = ae108::elements::integrator;
+namespace materialmodels = ae108::elements::materialmodels;
+namespace quadrature = ae108::elements::quadrature;
+namespace shape = ae108::elements::shape;
+
+// In this example we will simulate two elements, A and B.
+// Each of these elements has four vertices.
+
+//  3 ------- 2 ------- 5
+//  |         |         |
+//  |    A    |    B    |
+//  0---------1---------4
+
+// Let's specify the parameters of this mesh.
+
+constexpr auto number_of_vertices_per_element = Mesh::size_type{4};
+constexpr auto number_of_elements = Mesh::size_type{2};
+constexpr auto number_of_vertices = Mesh::size_type{6};
+constexpr auto dimension = Mesh::size_type{2};
+constexpr auto dof_per_vertex = Mesh::size_type{2};
+
+// The connectivity specifies the vertex indices per Element.
+
+using Connectivity =
+    std::array<std::array<Mesh::size_type, number_of_vertices_per_element>,
+               number_of_elements>;
+constexpr auto connectivity = Connectivity{{
+    {{0, 1, 2, 3}}, // vertices of element A
+    {{1, 4, 5, 2}}, // vertices of element B
+}};
+
+// Each of the vertices is located at the following coordinates in physical
+// space.
+
+using VertexPositions =
+    std::array<std::array<Mesh::real_type, dimension>, number_of_vertices>;
+constexpr auto vertex_positions = VertexPositions{{
+    {{0., 0.}},
+    {{1., 0.}},
+    {{1., 1.}},
+    {{0., 1.}},
+    {{2., 0.}},
+    {{2., 1.}},
+}};
+
+// Let's specify how the elements behave when deformed.
+
+// First, we select a Hookean (linear elastic) material model in 2D.
+
+using MaterialModel =
+    materialmodels::Hookean<dimension, Vector::value_type, Vector::real_type>;
+
+// We'll choose the Quad4 shape functions (4 shape functions) that are defined
+// in 2D reference space.
+
+using Shape = shape::Quad4;
+
+// Since also our physical space is 2D we may use the isoparametric embedding.
+
+using Embedding = embedding::IsoparametricEmbedding<Shape>;
+
+// Let's choose a simple quadrature rule in reference space.
+
+using Quadrature = quadrature::Quadrature<quadrature::QuadratureType::Cube,
+                                          dimension, 1 /* order */>;
+
+// We use an "Integrator" to integrate in physical space. Of course, it depends
+// on the selected quadrature rule and the embedding. In the case of the
+// isoparametric embedding, this embedding is specified by the "Shape".
+
+using Integrator =
+    integrator::IsoparametricIntegrator<Shape, Quadrature, Vector::value_type,
+                                        Vector::real_type>;
+
+// Now we are ready to select our element type.
+
+using Element = elements::CoreElement<MaterialModel, Integrator,
+                                      Vector::value_type, Vector::real_type>;
+
+// We will assemble e.g. energy using a collection of elements. This is done by
+// the assembler. (The list DefaultFeaturePlugins contain the features (e.g.
+// energy) that most elements support.)
+
+using Assembler =
+    assembly::Assembler<Element, assembly::DefaultFeaturePlugins, Policy>;
+
+// Our goals is to minimize the energy. This is done by the solver.
+
+using Solver = solve::GeneralizedTAOSolver<Assembler>;
+using BoundaryCondition = Solver::BoundaryConditionContainer;
+
+int main(int argc, char **argv) {
+  // MPI/PETSc/cpppetsc must be initialized before using it.
+
+  const auto context = Context(&argc, &argv);
+
+  // Let's create the mesh, an assembler, and a material model.
+
+  const auto mesh = Mesh::fromConnectivity(dimension, connectivity,
+                                           number_of_vertices, dof_per_vertex);
+  auto assembler = Assembler();
+  const auto model = MaterialModel(1.0, 0.);
+
+  // Depending on whether we use MPI, our mesh may be distributed and not all
+  // elements are present on this computational node.
+
+  // Let's add those elements that are "local" to the assembler.
+
+  for (const auto &element : mesh.localElements()) {
+    assembler.emplaceElement(
+        element, model,
+        Integrator(Embedding(Embedding::Collection<Embedding::PhysicalPoint>{{
+            vertex_positions.at(connectivity.at(element.index()).at(0)),
+            vertex_positions.at(connectivity.at(element.index()).at(1)),
+            vertex_positions.at(connectivity.at(element.index()).at(2)),
+            vertex_positions.at(connectivity.at(element.index()).at(3)),
+        }})));
+  }
+
+  // We need to create a solver. We do not use the time, so we can set it to
+  // zero.
+
+  const auto solver = Solver(&mesh);
+  const auto time = Element::Time{0.};
+
+  // Before we can produce meaningful results, we need to specify boundary
+  // conditions. Let's fix the nodes at x=0 and pull on the nodes at x=2.
+
+  // For the sake of this example we will specify those boundary conditions
+  // via nonlinear equations. More precisely, we will ask the solver to minimize
+  // the sin of the difference.
+
+  // We will create a residual function with 8-dimensional output, each
+  // representing the sin of the difference for one degree of freedom. Let's
+  // decide on a first row in this output for each vertex.
+
+  const auto vertex_to_row = [](const auto vertex) -> Mesh::size_type {
+    switch (vertex.index()) {
+    case 0:
+      return 0;
+    case 3:
+      return 2;
+    case 4:
+      return 4;
+    case 5:
+      return 6;
+    default:
+      throw std::out_of_range("The vertex index does not correspond to a row.");
+    }
+  };
+
+  // To simplify writing the residual function, we create two helper functions
+  // that identify unconstrained and pulled vertices.
+
+  const auto is_unconstrained_vertex = [](const auto vertex) {
+    return vertex.index() == 1 || vertex.index() == 2;
+  };
+
+  const auto is_pulled_vertex = [](const auto vertex) {
+    return vertex.index() == 4 || vertex.index() == 5;
+  };
+
+  const auto boundary_conditions = BoundaryCondition{
+      8,
+      [&](const cpppetsc::distributed<Vector> &in,
+          cpppetsc::distributed<Vector> *const out) {
+        out->unwrap().setZero();
+        auto replacer = out->unwrap().replace();
+        const auto full = Vector::fromDistributed(in);
+        for (const auto &vertex : mesh.localVertices()) {
+          if (is_unconstrained_vertex(vertex))
+            continue;
+          // The rows corresponding to the vertex contain the sin of the
+          // difference between the degree of freedoms and the prescribed
+          // values.
+          replacer(vertex_to_row(vertex)) =
+              std::sin(full(vertex.globalDofLineRange().first) +
+                       (is_pulled_vertex(vertex) ? -.5 : 0.));
+          replacer(vertex_to_row(vertex) + 1) =
+              std::sin(full(vertex.globalDofLineRange().first + 1));
+        }
+      },
+      [&](const cpppetsc::distributed<Vector> &in, Matrix *const out) {
+        out->setZero();
+        const auto full = Vector::fromDistributed(in);
+        auto replacer = out->preallocatedAssemblyView(1).replace();
+        for (const auto &vertex : mesh.localVertices()) {
+          if (is_unconstrained_vertex(vertex))
+            continue;
+          // The rows corresponding to the vertex contain the cos of the
+          // difference between the degree of freedoms and the prescribed
+          // values.
+          replacer(vertex_to_row(vertex), vertex.globalDofLineRange().first) =
+              std::cos(full(vertex.globalDofLineRange().first) +
+                       (is_pulled_vertex(vertex) ? -.5 : 0.));
+          replacer(vertex_to_row(vertex) + 1,
+                   vertex.globalDofLineRange().first + 1) =
+              std::cos(full(vertex.globalDofLineRange().first + 1));
+        }
+      },
+  };
+
+  // We are ready to minimize the energy.
+
+  const auto result = solver.computeSolution(
+      boundary_conditions, Vector::fromGlobalMesh(mesh), time, &assembler);
+
+  // As a final step, we print the result.
+  // We could use: result.unwrap().print();
+
+  // However, the output of this command changes when running the application
+  // in MPI-parallel mode. This happens because the vector "result" is
+  // distributed between the ranks in this case.
+
+  // So we gather all the results locally in the canonical data format first:
+  // [ vertex-0-dof-0, vertex-0-dof-1, vertex-1-dof-0, vertex-1-dof-1, ...].
+
+  const auto global_result =
+      Vector::fromDistributedInCanonicalOrder(result, mesh);
+
+  // Then we print this global vector only on the primary rank.
+  // We expect the degrees of freedom in x direction to be between 0 and .5,
+  // and the degrees of freedom in y direction to be 0:
+  // Vertex 0: (0.0, 0.0)
+  // Vertex 1: (.25, 0.0)
+  // Vertex 2: (.25, 0.0)
+  // Vertex 3: (0.0, 0.0)
+  // Vertex 4: (0.5, 0.0)
+  // Vertex 5: (0.5, 0.0)
+
+  if (Policy::isPrimaryRank())
+    global_result.unwrap().print();
+}
diff --git a/solve/src/CMakeLists.txt b/solve/src/CMakeLists.txt
index 3d3f8eddf0ebfabd5c3e41ca5f550db67c886ce4..9c8e60841fc8863da8588d22ce98f2b64521af9b 100644
--- a/solve/src/CMakeLists.txt
+++ b/solve/src/CMakeLists.txt
@@ -21,6 +21,7 @@ add_library(${PROJECT_NAME}-solve
             ./DynamicSolver.cc
             ./DynamicState.cc
             ./GeneralizedNonlinearSolver.cc
+            ./GeneralizedTAOSolver.cc
             ./InconsistentBoundaryConditionsException.cc
             ./InvalidVertexException.cc
             ./LeastSquaresSolver.cc
diff --git a/solve/src/GeneralizedTAOSolver.cc b/solve/src/GeneralizedTAOSolver.cc
new file mode 100644
index 0000000000000000000000000000000000000000..45795ca19c7b5187ecf2b73739b0c8c63d4427ef
--- /dev/null
+++ b/solve/src/GeneralizedTAOSolver.cc
@@ -0,0 +1,15 @@
+// © 2022 ETH Zurich, Mechanics and Materials Lab
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "ae108/solve/GeneralizedTAOSolver.h"
\ No newline at end of file
diff --git a/solve/src/include/ae108/solve/GeneralizedTAOSolver.h b/solve/src/include/ae108/solve/GeneralizedTAOSolver.h
new file mode 100644
index 0000000000000000000000000000000000000000..073b34e8f2d053923b1695dcccf4ed8926a79868
--- /dev/null
+++ b/solve/src/include/ae108/solve/GeneralizedTAOSolver.h
@@ -0,0 +1,311 @@
+// © 2020, 2022 ETH Zurich, Mechanics and Materials Lab
+// © 2020 California Institute of Technology
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#ifndef AE108_PETSC_COMPLEX
+
+#include "ae108/assembly/AssemblerTypeTraits.h"
+#include "ae108/cpppetsc/MeshBoundaryCondition.h"
+#include "ae108/cpppetsc/TAOSolver.h"
+#include "ae108/cpppetsc/TaggedVector.h"
+#include <cassert>
+#include <functional>
+#include <mpi.h>
+#include <petscsys.h>
+#include <vector>
+
+namespace ae108 {
+namespace solve {
+
+template <class Assembler> class GeneralizedTAOSolver {
+public:
+  using policy_type = typename assembly::PolicyTypeTrait<Assembler>::type;
+  using mesh_type = typename assembly::MeshTypeTrait<Assembler>::type;
+  using size_type = typename mesh_type::size_type;
+  using value_type = typename mesh_type::value_type;
+  using real_type = typename mesh_type::real_type;
+  using vector_type = typename mesh_type::vector_type;
+  using matrix_type = typename mesh_type::matrix_type;
+
+  /**
+   * @param mesh A valid nonzero pointer to a mesh_type instance.
+   */
+  explicit GeneralizedTAOSolver(const mesh_type *mesh);
+
+  struct BoundaryConditionContainer {
+    /**
+     * @brief The dimension of the output of `residual`.
+     */
+    size_type size;
+
+    /**
+     * @brief The solver attempts to reach a residual of zero.
+     */
+    typename cpppetsc::TAOSolver<policy_type>::GradientFunctor residual;
+
+    /**
+     * @brief The jacobian of the residual.
+     */
+    typename cpppetsc::TAOSolver<policy_type>::HessianFunctor jacobian;
+  };
+
+  /**
+   * @brief Calls computeSolution with the default assembler calls (ie. calls
+   * assembleEnergy, assembleForceVector or assembleStiffnessMatrix). Passes on
+   * the rest of the arguments.
+   *
+   * @param boundaryConditions The nonlinear boundary conditions to apply.
+   * @param initialGuess The global vector to start iterating at.
+   * @param time This will be used to configure the assembler.
+   * @param assembler Valid nonzero pointer.
+   */
+  cpppetsc::distributed<vector_type>
+  computeSolution(const BoundaryConditionContainer &boundaryConditions,
+                  cpppetsc::distributed<vector_type> initialGuess,
+                  const double time, const Assembler *const assembler) const;
+
+  using LocalEnergyAssembler = std::function<void(
+      const cpppetsc::local<vector_type> &, double, real_type *)>;
+
+  using LocalForceVectorAssembler =
+      std::function<void(const cpppetsc::local<vector_type> &, double,
+                         cpppetsc::local<vector_type> *)>;
+
+  using LocalStiffnessMatrixAssembler = std::function<void(
+      const cpppetsc::local<vector_type> &, double, matrix_type *)>;
+
+  /**
+   * @brief Calls computeSolution wrapping up the local assembler calls into
+   * distributed assembler calls. Passes on the rest of the arguments.
+   *
+   * @param boundaryConditions The nonlinear boundary conditions to apply.
+   * Note that empty functions (residual, jacobian) are interpreted as functions
+   * that do not change the output.
+   * @param initialGuess The global vector to start iterating at.
+   * @param time This will be used to configure the assembler.
+   * @param assembleEnergy A valid callable. It will be called to assemble
+   * the local energy.
+   * @param assembleForceVector A valid callable. It will be called to assemble
+   * the local force vector.
+   * @param assembleStiffnessMatrix A valid callable. It will be called to
+   * assemble the stiffness matrix.
+   */
+  cpppetsc::distributed<vector_type>
+  computeSolution(const BoundaryConditionContainer &boundaryConditions,
+                  cpppetsc::distributed<vector_type> initialGuess,
+                  const double time, LocalEnergyAssembler assembleEnergy,
+                  LocalForceVectorAssembler assembleForceVector,
+                  LocalStiffnessMatrixAssembler assemblerStiffnessMatrix) const;
+
+  using DistributedEnergyAssembler = std::function<void(
+      const cpppetsc::distributed<vector_type> &, double, real_type *)>;
+
+  using DistributedForceVectorAssembler =
+      std::function<void(const cpppetsc::distributed<vector_type> &, double,
+                         cpppetsc::distributed<vector_type> *)>;
+
+  using DistributedStiffnessMatrixAssembler = std::function<void(
+      const cpppetsc::distributed<vector_type> &, double, matrix_type *)>;
+
+  /**
+   * @brief The goal of this function is to solve E(x) = min under the condition
+   * that the residual is equal to zero.
+   *
+   * @remark Does not update internal variables.
+   *
+   * @param boundaryConditions The nonlinear boundary conditions to apply.
+   * @param initialGuess The global vector to start iterating at.
+   * @param time This will be used to configure the assembler.
+   * @param assembleEnergy A valid callable. It will be called to assemble
+   * the distributed energy.
+   * @param assembleForceVector A valid callable. It will be called to assemble
+   * the distributed force vector.
+   * @param assembleStiffnessMatrix A valid callable. It will be called to
+   * assemble the stiffness matrix.
+   */
+  cpppetsc::distributed<vector_type> computeSolution(
+      const BoundaryConditionContainer &boundaryConditions,
+      cpppetsc::distributed<vector_type> initialGuess, const double time,
+      DistributedEnergyAssembler assembleEnergy,
+      DistributedForceVectorAssembler assembleForceVector,
+      DistributedStiffnessMatrixAssembler assemblerStiffnessMatrix) const;
+
+private:
+  const mesh_type *_mesh;
+}; // namespace solve
+} // namespace solve
+} // namespace ae108
+
+/********************************************************************
+ *  implementations
+ *******************************************************************/
+
+namespace ae108 {
+namespace solve {
+template <class Assembler>
+GeneralizedTAOSolver<Assembler>::GeneralizedTAOSolver(const mesh_type *mesh)
+    : _mesh(mesh) {}
+
+template <class Assembler>
+cpppetsc::distributed<typename GeneralizedTAOSolver<Assembler>::vector_type>
+GeneralizedTAOSolver<Assembler>::computeSolution(
+    const BoundaryConditionContainer &boundaryConditions,
+    cpppetsc::distributed<vector_type> initialGuess, const double time,
+    const Assembler *const assembler) const {
+  assert(assembler);
+  return computeSolution(
+      boundaryConditions, std::move(initialGuess), time,
+      LocalEnergyAssembler(
+          [assembler](const cpppetsc::local<vector_type> &localDisplacements,
+                      const double time, real_type *const localEnergy) {
+            assembler->assembleEnergy(localDisplacements, time, localEnergy);
+          }),
+      LocalForceVectorAssembler(
+          [assembler](const cpppetsc::local<vector_type> &localDisplacements,
+                      const double time,
+                      cpppetsc::local<vector_type> *const localForces) {
+            assembler->assembleForceVector(localDisplacements, time,
+                                           localForces);
+          }),
+      LocalStiffnessMatrixAssembler(
+          [assembler](const cpppetsc::local<vector_type> &localDisplacements,
+                      const double time, matrix_type *const output) {
+            assembler->assembleStiffnessMatrix(localDisplacements, time,
+                                               output);
+          }));
+}
+
+template <class Assembler>
+cpppetsc::distributed<typename GeneralizedTAOSolver<Assembler>::vector_type>
+GeneralizedTAOSolver<Assembler>::computeSolution(
+    const BoundaryConditionContainer &boundaryConditions,
+    cpppetsc::distributed<vector_type> initialGuess, const double time,
+    LocalEnergyAssembler assembleEnergy,
+    LocalForceVectorAssembler assembleForceVector,
+    LocalStiffnessMatrixAssembler assembleStiffnessMatrix) const {
+  assert(assembleEnergy);
+  assert(assembleForceVector);
+  assert(assembleStiffnessMatrix);
+
+  const auto &mesh = *_mesh;
+
+  auto localDisplacements = vector_type::fromLocalMesh(mesh);
+  auto localForces = vector_type::fromLocalMesh(mesh);
+
+  const auto distributedEnergyAssembler =
+      [&assembleEnergy, &mesh,
+       &localDisplacements](const cpppetsc::distributed<vector_type> &input,
+                            const double time, real_type *const output) {
+        mesh.copyToLocalVector(input, &localDisplacements);
+
+        *output = real_type{};
+        assembleEnergy(localDisplacements, time, output);
+
+        policy_type::handleError(MPI_Allreduce(MPI_IN_PLACE, output, 1,
+                                               MPIU_REAL, MPIU_SUM,
+                                               policy_type::communicator()));
+      };
+
+  const auto distributedForceVectorAssembler =
+      [&assembleForceVector, &mesh, &localDisplacements, &localForces](
+          const cpppetsc::distributed<vector_type> &input, const double time,
+          cpppetsc::distributed<vector_type> *const output) {
+        mesh.copyToLocalVector(input, &localDisplacements);
+
+        localForces.unwrap().setZero();
+        assembleForceVector(localDisplacements, time, &localForces);
+
+        mesh.addToGlobalVector(localForces, output);
+      };
+
+  const auto distributedStiffnessMatrixAssembler =
+      [&assembleStiffnessMatrix, &mesh,
+       &localDisplacements](const cpppetsc::distributed<vector_type> &input,
+                            const double time, matrix_type *const output) {
+        mesh.copyToLocalVector(input, &localDisplacements);
+
+        assembleStiffnessMatrix(localDisplacements, time, output);
+        output->finalize();
+      };
+
+  return computeSolution(boundaryConditions, std::move(initialGuess), time,
+                         distributedEnergyAssembler,
+                         distributedForceVectorAssembler,
+                         distributedStiffnessMatrixAssembler);
+}
+
+template <class Assembler>
+cpppetsc::distributed<typename GeneralizedTAOSolver<Assembler>::vector_type>
+GeneralizedTAOSolver<Assembler>::computeSolution(
+    const BoundaryConditionContainer &boundaryConditions,
+    cpppetsc::distributed<vector_type> initialGuess, const double time,
+    DistributedEnergyAssembler assembleEnergy,
+    DistributedForceVectorAssembler assembleForceVector,
+    DistributedStiffnessMatrixAssembler assembleStiffnessMatrix) const {
+  assert(assembleEnergy);
+  assert(assembleForceVector);
+  assert(assembleStiffnessMatrix);
+
+  using Solver = cpppetsc::TAOSolver<policy_type>;
+  auto matrix = matrix_type::fromMesh(*_mesh);
+
+  const auto global = matrix.size();
+  const auto local = matrix.localSize();
+
+  Solver solver{std::move(matrix), Solver::Type::almm};
+
+  solver.setConstraints(typename Solver::EqualityConstraints{
+      boundaryConditions.residual
+          ? boundaryConditions.residual
+          : [](const cpppetsc::distributed<vector_type> &,
+               cpppetsc::distributed<vector_type> *) {},
+      boundaryConditions.jacobian
+          ? boundaryConditions.jacobian
+          : [](const cpppetsc::distributed<vector_type> &, matrix_type *) {},
+      {
+          cpppetsc::distributed<vector_type>(boundaryConditions.size),
+          matrix_type(typename matrix_type::LocalRows(PETSC_DECIDE),
+                      typename matrix_type::LocalCols(local.second),
+                      typename matrix_type::GlobalRows(boundaryConditions.size),
+                      typename matrix_type::GlobalCols(global.second)),
+      },
+  });
+
+  return solver.solve(
+      [time, &assembleEnergy](const cpppetsc::distributed<vector_type> &input,
+                              double *const output) {
+        *output = real_type{};
+        assembleEnergy(input, time, output);
+      },
+      [time,
+       &assembleForceVector](const cpppetsc::distributed<vector_type> &input,
+                             cpppetsc::distributed<vector_type> *const output) {
+        output->unwrap().setZero();
+        assembleForceVector(input, time, output);
+      },
+      [time, &assembleStiffnessMatrix](
+          const cpppetsc::distributed<vector_type> &input,
+          matrix_type *const output) {
+        output->setZero();
+        assembleStiffnessMatrix(input, time, output);
+      },
+      std::move(initialGuess));
+}
+
+} // namespace solve
+} // namespace ae108
+
+#endif
\ No newline at end of file
diff --git a/solve/test/CMakeLists.txt b/solve/test/CMakeLists.txt
index d86645660457acbdc13d5194460e12ec1362436d..ff34b6e7b3e1f7af60e61d8b854069626897fc36 100644
--- a/solve/test/CMakeLists.txt
+++ b/solve/test/CMakeLists.txt
@@ -19,6 +19,7 @@ add_executable(${PROJECT_NAME}-SolveTests
                ./AffineTransform_Test.cc
                ./DynamicSolver_Test.cc
                ./GeneralizedNonlinearSolver_Test.cc
+               ./GeneralizedTAOSolver_Test.cc
                ./InconsistentBoundaryConditionsException_Test.cc
                ./InvalidVertexException_Test.cc
                ./LeastSquaresSolver_Test.cc
diff --git a/solve/test/GeneralizedTAOSolver_Test.cc b/solve/test/GeneralizedTAOSolver_Test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..9949dca1b64cd13adc45cf955b86247708e1efeb
--- /dev/null
+++ b/solve/test/GeneralizedTAOSolver_Test.cc
@@ -0,0 +1,193 @@
+// © 2022 ETH Zurich, Mechanics and Materials Lab
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef AE108_PETSC_COMPLEX
+
+#include "ae108/cpppetsc/ParallelComputePolicy.h"
+#include "ae108/cpppetsc/SequentialComputePolicy.h"
+#include "ae108/cppptest/Matchers.h"
+#include "ae108/solve/GeneralizedTAOSolver.h"
+#include "ae108/solve/test/Solver_Test.h"
+#include <gmock/gmock.h>
+
+using ae108::cppptest::ScalarEqIfLocal;
+using ae108::cppptest::ScalarNearIfLocal;
+using testing::Types;
+
+namespace ae108 {
+namespace solve {
+namespace {
+
+template <class Policy> struct Configuration {
+  using policy_type = Policy;
+  template <class T> using solver_type = GeneralizedTAOSolver<T>;
+};
+
+template <class Configuration>
+struct GeneralizedTAOSolver_Test : test::Solver_Test<Configuration> {};
+
+using Configurations = Types<Configuration<cpppetsc::SequentialComputePolicy>,
+                             Configuration<cpppetsc::ParallelComputePolicy>>;
+TYPED_TEST_CASE(GeneralizedTAOSolver_Test, Configurations);
+
+TYPED_TEST(GeneralizedTAOSolver_Test, solves_unconstrained_problem) {
+  using assembler_type = typename TestFixture::assembler_type;
+  using vector_type = typename TestFixture::vector_type;
+
+  const auto solution = this->solver.computeSolution(
+      {}, vector_type::fromGlobalMesh(this->mesh), assembler_type::constantTime,
+      &this->assembler);
+
+  EXPECT_THAT(solution.unwrap(), ScalarEqIfLocal(0, 1.));
+  EXPECT_THAT(solution.unwrap(), ScalarEqIfLocal(1, 2.));
+}
+
+TYPED_TEST(GeneralizedTAOSolver_Test, solves_partially_constrained_problem) {
+  using assembler_type = typename TestFixture::assembler_type;
+  using matrix_type = typename TestFixture::matrix_type;
+  using vector_type = typename TestFixture::vector_type;
+
+  const auto solution = this->solver.computeSolution(
+      {
+          1,
+          [](const cpppetsc::distributed<vector_type> &in,
+             cpppetsc::distributed<vector_type> *const out) {
+            const auto full = vector_type::fromDistributed(in);
+            out->unwrap().setZero();
+            out->unwrap().replace().element(0, full(0) - full(1));
+          },
+          [](const cpppetsc::distributed<vector_type> &,
+             matrix_type *const out) {
+            out->setZero();
+            out->preallocatedAssemblyView(2)
+                .replace()
+                .element(0, 0, 1.)
+                .element(0, 1, -1.);
+          },
+      },
+      vector_type::fromGlobalMesh(this->mesh), assembler_type::constantTime,
+      &this->assembler);
+
+  EXPECT_THAT(solution.unwrap(), ScalarNearIfLocal(0, 3. / 2., 1e-6));
+  EXPECT_THAT(solution.unwrap(), ScalarNearIfLocal(1, 3. / 2., 1e-6));
+}
+
+TYPED_TEST(GeneralizedTAOSolver_Test, solves_fully_constrained_problem) {
+  using assembler_type = typename TestFixture::assembler_type;
+  using matrix_type = typename TestFixture::matrix_type;
+  using vector_type = typename TestFixture::vector_type;
+
+  const auto solution = this->solver.computeSolution(
+      {
+          2,
+          [](const cpppetsc::distributed<vector_type> &in,
+             cpppetsc::distributed<vector_type> *const out) {
+            const auto full = vector_type::fromDistributed(in);
+            out->unwrap().setZero();
+            out->unwrap()
+                .replace()
+                .element(0, full(0) - 3.)
+                .element(1, full(1) - 4.);
+          },
+          [](const cpppetsc::distributed<vector_type> &,
+             matrix_type *const out) {
+            out->setZero();
+            out->preallocatedAssemblyView(1)
+                .replace()
+                .element(0, 0, 1.)
+                .element(1, 1, 1.);
+          },
+      },
+      vector_type::fromGlobalMesh(this->mesh), assembler_type::constantTime,
+      &this->assembler);
+
+  EXPECT_THAT(solution.unwrap(), ScalarNearIfLocal(0, 3., 1e-6));
+  EXPECT_THAT(solution.unwrap(), ScalarNearIfLocal(1, 4., 1e-6));
+}
+
+TYPED_TEST(GeneralizedTAOSolver_Test,
+           assembler_can_be_replaced_by_local_assembly_functions) {
+  using solver_type = typename TestFixture::solver_type;
+  using matrix_type = typename TestFixture::matrix_type;
+  using vector_type = typename TestFixture::vector_type;
+
+  const auto solution = this->solver.computeSolution(
+      {}, vector_type::fromGlobalMesh(this->mesh),
+      TestFixture::assembler_type::constantTime,
+      [assembler = &this->assembler](
+          const cpppetsc::local<vector_type> &in, const double time,
+          typename solver_type::real_type *const out) {
+        assembler->assembleEnergy(in, time, out);
+      },
+      [assembler = &this->assembler](const cpppetsc::local<vector_type> &in,
+                                     const double time,
+                                     cpppetsc::local<vector_type> *const out) {
+        assembler->assembleForceVector(in, time, out);
+      },
+      [assembler = &this->assembler](const cpppetsc::local<vector_type> &in,
+                                     const double time,
+                                     matrix_type *const out) {
+        assembler->assembleStiffnessMatrix(in, time, out);
+      });
+
+  EXPECT_THAT(solution.unwrap(), ScalarEqIfLocal(0, 1.));
+  EXPECT_THAT(solution.unwrap(), ScalarEqIfLocal(1, 2.));
+}
+
+TYPED_TEST(GeneralizedTAOSolver_Test,
+           assembler_can_be_replaced_by_distributed_assembly_functions) {
+  using policy_type = typename TestFixture::policy_type;
+  using vector_type = typename TestFixture::vector_type;
+  using matrix_type = typename TestFixture::matrix_type;
+  using real_type = typename TestFixture::solver_type::real_type;
+
+  const auto solution = this->solver.computeSolution(
+      {}, vector_type::fromGlobalMesh(this->mesh),
+      TestFixture::assembler_type::constantTime,
+      [assembler = &this->assembler,
+       mesh = &this->mesh](const cpppetsc::distributed<vector_type> &in,
+                           const double time, real_type *const out) {
+        auto local = vector_type::fromLocalMesh(*mesh);
+        mesh->copyToLocalVector(in, &local);
+        assembler->assembleEnergy(local, time, out);
+        policy_type::handleError(MPI_Allreduce(MPI_IN_PLACE, out, 1, MPIU_REAL,
+                                               MPIU_SUM,
+                                               policy_type::communicator()));
+      },
+      [assembler = &this->assembler, mesh = &this->mesh](
+          const cpppetsc::distributed<vector_type> &in, const double time,
+          cpppetsc::distributed<vector_type> *const out) {
+        auto localIn = vector_type::fromLocalMesh(*mesh);
+        auto localOut = vector_type::fromLocalMesh(*mesh);
+        mesh->copyToLocalVector(in, &localIn);
+        assembler->assembleForceVector(localIn, time, &localOut);
+        mesh->addToGlobalVector(localOut, out);
+      },
+      [assembler = &this->assembler,
+       mesh = &this->mesh](const cpppetsc::distributed<vector_type> &in,
+                           const double time, matrix_type *const out) {
+        auto local = vector_type::fromLocalMesh(*mesh);
+        mesh->copyToLocalVector(in, &local);
+        assembler->assembleStiffnessMatrix(local, time, out);
+        out->finalize();
+      });
+
+  EXPECT_THAT(solution.unwrap(), ScalarEqIfLocal(0, 1.));
+  EXPECT_THAT(solution.unwrap(), ScalarEqIfLocal(1, 2.));
+}
+} // namespace
+} // namespace solve
+} // namespace ae108
+
+#endif
\ No newline at end of file
diff --git a/tests/examples/nonlinearbc/definition.json b/tests/examples/nonlinearbc/definition.json
new file mode 100644
index 0000000000000000000000000000000000000000..e9f78d0ea60f26e8ec1870c1dde39e01cf925b85
--- /dev/null
+++ b/tests/examples/nonlinearbc/definition.json
@@ -0,0 +1,11 @@
+{
+  "executable": [
+    "examples",
+    "ae108-examples-NonlinearBC"
+  ],
+  "compare_stdout": "numeric",
+  "mpi_processes": [
+    1,
+    3
+  ]
+}
\ No newline at end of file
diff --git a/tests/examples/nonlinearbc/references/stdout.txt b/tests/examples/nonlinearbc/references/stdout.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b63e010232622ec6f46afed12a95e518d7c9a69a
--- /dev/null
+++ b/tests/examples/nonlinearbc/references/stdout.txt
@@ -0,0 +1,14 @@
+Vec Object: 1 MPI processes
+  type: seq
+5.47984e-07
+-6.7861e-19
+0.25
+-9.60278e-18
+0.25
+-9.60278e-18
+5.47984e-07
+-6.7861e-19
+0.499998
+-7.44116e-19
+0.499998
+-7.44116e-19
diff --git a/tests/run.py b/tests/run.py
index 3944fbd21399d096097f094af5c72fd94ccd16f5..bf2fd51446138c8f79770f163546da3d069cd695 100755
--- a/tests/run.py
+++ b/tests/run.py
@@ -394,26 +394,24 @@ def load_tests(
                     ROOT_DIRECTORY / "tests" / "schema.json", "r", encoding="utf-8"
                 ) as schema:
                     jsonschema.validate(instance=instance, schema=json.load(schema))
+                executable_path = pathlib.Path.cwd() / pathlib.Path(
+                    *instance["executable"]
+                )
+                if not executable_path.exists():
+                    print(
+                        f"Warning: Test '{path.parent}' will be skipped. "
+                        f"The executable '{executable_path}' does not exist.",
+                        file=sys.stderr,
+                    )
+                    continue
                 test_case_definitions = as_test_case_definitions(path.parent, instance)
             except jsonschema.ValidationError as error:
                 print(
-                    f"Warning: Test definition '{path}' is invalid. {error.message}.",
+                    f"Warning: Test '{path.parent}' will be skipped. "
+                    f"The test definition is invalid: {error.message}.",
                     file=sys.stderr,
                 )
-                test_case_definitions = (
-                    definition
-                    for definition in [
-                        TestCaseDefinition(
-                            executable=pathlib.Path(),
-                            references=pathlib.Path(),
-                            args=[],
-                            mpi_processes=1,
-                            compare_stdout=ComparisonType.NONE,
-                            ae108_output=[],
-                        )
-                    ]
-                )
-
+                continue
             testcase = type(
                 group_name,
                 (unittest.TestCase,),