Squashed 'third_party/osqp-cpp/' content from commit 8cd904e2b4

Change-Id: Ic67c163724b33720d49822debd467a344121342e
git-subtree-dir: third_party/osqp-cpp
git-subtree-split: 8cd904e2b49c24dd41d11f8c6e0adb113dd5e26c
Signed-off-by: Austin Schuh <austin.linux@gmail.com>
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..9255ada
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,27 @@
+name: build
+on:
+  push:
+  pull_request:
+  release:
+
+jobs:
+  build:
+    name: Ubuntu latest
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Install dependencies
+        run: |
+          sudo apt-get update
+          sudo apt-get install cmake libeigen3-dev
+          cmake --version
+
+      - name: Build and test
+        run: |
+          mkdir -p build
+          cd build
+          cmake ..
+          make
+          make test
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..16af8a8
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,96 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#    https://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.
+
+cmake_minimum_required(VERSION 3.14)
+
+project(osqp-cpp)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED True)
+
+# Options.
+option(OSQP-CPP_BUILD_TESTS
+  "Whether to build tests." ON
+)
+
+include(FetchContent)
+
+# ABSL
+if (NOT TARGET absl::strings OR NOT TARGET absl::status OR NOT TARGET absl::span)
+  message(STATUS "osqp-cpp: `absl` targets not found. Attempting to fetch contents...")
+  FetchContent_Declare(
+    abseil-cpp
+    GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git
+    GIT_TAG        origin/master
+  )
+  FetchContent_MakeAvailable(abseil-cpp)
+else()
+  message(STATUS "osqp-cpp: `absl` targets found.")
+endif()
+
+# EIGEN
+# Eigen is usually available as a system package, so we use find_package.
+if (NOT TARGET Eigen3::Eigen)
+  message(STATUS "osqp-cpp: `Eigen3` targets not found. Attempting to find package...")
+  find_package(Eigen3 3.3.7 REQUIRED NO_MODULE)
+else()
+  message(STATUS "osqp-cpp: `Eigen3` targets found.")
+endif()
+
+# OSQP
+if (NOT TARGET osqpstatic)
+  message(STATUS "osqp-cpp: `osqp` targets not found. Attempting to fetch contents...")
+  FetchContent_Declare(
+    osqp
+    GIT_REPOSITORY https://github.com/oxfordcontrol/osqp.git
+    GIT_TAG        origin/master
+  )
+  FetchContent_MakeAvailable(osqp)
+else()
+  message(STATUS "osqp-cpp: `osqp` targets found.")
+endif()
+
+# Googletest
+if (OSQP-CPP_BUILD_TESTS)
+  enable_testing()
+  if (NOT TARGET gtest OR NOT TARGET gmock OR NOT TARGET gtest_main)
+    message(STATUS "osqp-cpp: `googletest` targets not found. Attempting to fetch contents...")
+    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+    FetchContent_Declare(
+      googletest
+      GIT_REPOSITORY https://github.com/google/googletest.git
+      GIT_TAG        origin/main
+    )
+    FetchContent_MakeAvailable(googletest)
+    include(GoogleTest)
+  else()
+    message(STATUS "osqp-cpp: `googletest` targets found.")
+  endif()
+endif()
+
+message(STATUS "osqp-cpp: Adding osqp-cpp library...")
+add_library(osqp-cpp src/osqp++.cc)
+target_link_libraries(osqp-cpp PUBLIC absl::strings absl::status absl::statusor Eigen3::Eigen PRIVATE osqpstatic ${CMAKE_DL_LIBS})
+target_include_directories(osqp-cpp PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>")
+message(STATUS "osqp-cpp: Added osqp-cpp library.")
+
+# Build and register tests.
+if (OSQP-CPP_BUILD_TESTS)
+  message(STATUS "osqp-cpp: Adding osqp-cpp tests...")
+  add_executable(osqp_test test/osqp++_test.cc)
+  target_link_libraries(osqp_test gtest gmock gtest_main absl::status absl::span osqp-cpp)
+  gtest_discover_tests(osqp_test)
+  message(STATUS "osqp-cpp: Added osqp-cpp tests.")
+endif()
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..22b241c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,29 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement (CLA). You (or your employer) retain the copyright to your
+contribution; this simply gives us permission to use and redistribute your
+contributions as part of the project. Head over to
+<https://cla.developers.google.com/> to see your current agreements on file or
+to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows
+[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8e4c71a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,152 @@
+# osqp-cpp: A C++ wrapper for [OSQP](https://osqp.org/)
+
+A C++ wrapper for [OSQP](https://github.com/oxfordcontrol/osqp), an
+[ADMM](http://stanford.edu/~boyd/admm.html)-based solver for
+[quadratic programming](https://en.wikipedia.org/wiki/Quadratic_programming).
+
+Compared with OSQP's native C interface, the wrapper provides a more convenient
+input format using Eigen sparse matrices and handles the lifetime of the
+`OSQPWorkspace` struct. This package has similar functionality to
+[osqp-eigen](https://github.com/robotology/osqp-eigen).
+
+The full API is documented in-line in `osqp++.h`. We describe only the input
+format in this README.
+
+Note: OSQP uses looser default tolerances than other similar solvers. We
+recommend looking at the description of the convergence tolerances in Section
+3.4 of the OSQP [paper](https://arxiv.org/abs/1711.08013) and adjusting
+tolerances via the `OsqpSettings` struct as appropriate.
+
+This is not an officially supported Google product.
+
+## `OsqpInstance` format
+
+OSQP solves the convex quadratic optimization problem:
+
+```
+min_x 0.5 * x'Px + q'x
+s.t.  l <= Ax <= u
+```
+
+where `P` is a symmetric positive semi-definite matrix.
+
+The inequalities are component-wise, and equalities may be enforced by setting
+`l[i] == u[i]` for some row `i`. Single-sided inequalities can be enforced by
+setting the lower or upper bounds to negative or positive infinity
+(`std::numeric_limits<double>::infinity()`), respectively.
+
+This maps to the `OsqpInstance` struct in `osqp++.h` as follows.
+
+-   `objective_matrix` is `P`.
+-   `objective_vector` is `q`.
+-   `constraint_matrix` is `A`.
+-   `lower_bounds` is `l`.
+-   `upper_bounds` is `u`.
+
+## Example usage
+
+The code below formulates and solves the following 2-dimensional optimization
+problem:
+
+```
+min_(x,y) x^2 + 0.5 * x * y + y^2 + x
+s.t.      x >= 1
+```
+
+```C++
+const double kInfinity = std::numeric_limits<double>::infinity();
+SparseMatrix<double> objective_matrix(2, 2);
+const Triplet<double> kTripletsP[] = {
+    {0, 0, 2.0}, {1, 0, 0.5}, {0, 1, 0.5}, {1, 1, 2.0}};
+objective_matrix.setFromTriplets(std::begin(kTripletsP),
+                                   std::end(kTripletsP));
+
+SparseMatrix<double> constraint_matrix(1, 2);
+const Triplet<double> kTripletsA[] = {{0, 0, 1.0}};
+constraint_matrix.setFromTriplets(std::begin(kTripletsA),
+                                      std::end(kTripletsA));
+
+OsqpInstance instance;
+instance.objective_matrix = objective_matrix;
+instance.objective_vector.resize(2);
+instance.objective_vector << 1.0, 0.0;
+instance.constraint_matrix = constraint_matrix;
+instance.lower_bounds.resize(1);
+instance.lower_bounds << 1.0;
+instance.upper_bounds.resize(1);
+instance.upper_bounds << kInfinity;
+
+OsqpSolver solver;
+OsqpSettings settings;
+// Edit settings if appropriate.
+auto status = solver.Init(instance, settings);
+// Assuming status.ok().
+OsqpExitCode exit_code = solver.Solve();
+// Assuming exit_code == OsqpExitCode::kOptimal.
+double optimal_objective = solver.objective_value();
+Eigen::VectorXd optimal_solution = solver.primal_solution();
+```
+
+## Installation (Unix)
+
+osqp-cpp requires CMake, a C++17 compiler, and the following packages:
+
+- [OSQP](https://github.com/oxfordcontrol/osqp) (compiled with 64-bit integers)
+- [abseil-cpp](https://github.com/abseil/abseil-cpp)
+- [Eigen](http://eigen.tuxfamily.org/index.php?title=Main_Page)
+- [googletest](https://github.com/google/googletest) (for testing only)
+
+On Debian/Ubuntu systems you may install Eigen via the `libeigen3-dev` package.
+
+osqp-cpp will attempt to automatically detect if the necessary targets exist as
+part of the same project. If the necessary `OSQP`, `abseil-cpp`, or `googletest`
+targets are not found, osqp-cpp will attempt to download the sources from their
+GitHub repositories through the use of CMake's `FetchContent` functionality. If
+the `Eigen3` targets are not found, osqp-cpp will attempt to find Eigen3 as a
+system package. To prevent osqp-cpp from unnecessarily downloading target
+dependencies, please ensure that any target dependencies that are already
+available are included before osqp-cpp.
+
+To build osqp-cpp, run the following from the `osqp-cpp` directory:
+
+```sh
+$ mkdir build; cd build
+$ cmake ..
+$ make
+$ make test
+```
+
+The interface is regularly tested only on Linux. Contributions to support and
+automatically test additional platforms are welcome.
+
+## Installation (Windows)
+
+*These instructions are maintained by the community.*
+
+Install prerequisite packages:
+
+```sh
+$ vcpkg install eigen3:x64-windows
+$ vcpkg install abseil:x64-windows
+$ vcpkg install gtest:x64-windows
+```
+
+Then, run the following from the `osqp-cpp` directory:
+
+```sh
+$ mkdir build; cd build
+$ cmake ..
+$ cmake --build .
+$ cd Debug
+```
+
+## FAQ
+
+-   Is OSQP deterministic?
+    -   No, not in its default configuration. Section 5.2 of the OSQP
+        [paper](https://arxiv.org/abs/1711.08013) describes that the update rule
+        for the step size rho depends on the ratio between the runtime of the
+        iterations and the runtime of the numerical factorization. Setting
+        `adaptive_rho` to `false` disables this update rule and makes OSQP
+        deterministic, but this could significantly slow down OSQP's
+        convergence.
diff --git a/include/osqp++.h b/include/osqp++.h
new file mode 100644
index 0000000..3f79fa9
--- /dev/null
+++ b/include/osqp++.h
@@ -0,0 +1,443 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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 OSQP_CPP_H_
+#define OSQP_CPP_H_
+
+// A C++ wrapper for OSQP (https://osqp.org/). See README.md for an overview.
+
+#include <memory>
+#include <string>
+
+#include "absl/base/attributes.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "Eigen/Core"
+#include "Eigen/SparseCore"
+
+namespace osqp {
+
+// Must match the typedef in osqp/include/glob_opts.h (if not, it will trigger
+// a static_assert failure in osqp++.cc).
+using c_int = long long;  // NOLINT
+
+// A memory-safe mirror of the OSQPData struct defined in osqp/include/types.h.
+// The number of variables and constraints is implied by the shape of
+// constraint_matrix. The format of the struct is further discussed in
+// README.md. See also osqp++_test.cc for example usage.
+struct OsqpInstance {
+  c_int num_variables() const { return constraint_matrix.cols(); }
+  c_int num_constraints() const { return constraint_matrix.rows(); }
+
+  // Only the upper triangle of the objective matrix is read. The lower triangle
+  // is ignored.
+  Eigen::SparseMatrix<double, Eigen::ColMajor, c_int> objective_matrix;
+  Eigen::VectorXd objective_vector;
+  Eigen::SparseMatrix<double, Eigen::ColMajor, c_int> constraint_matrix;
+  Eigen::VectorXd lower_bounds;
+  Eigen::VectorXd upper_bounds;
+};
+
+// This is a mirror of the OSQPSettings struct defined in
+// osqp/include/types.h and documented at
+// http://osqp.readthedocs.io/en/latest/interfaces/solver_settings.html. The
+// names are unchanged and (hence) violate Google naming conventions. The
+// default values are defined in osqp/include/constants.h. Note, OSQP's default
+// settings are looser than other QP solvers. Do choose appropriate values of
+// eps_abs and eps_rel for your application.
+struct OsqpSettings {
+  OsqpSettings();  // Sets default values.
+
+  double rho;
+  double sigma;
+  c_int scaling;
+  bool adaptive_rho;
+  c_int adaptive_rho_interval;
+  double adaptive_rho_tolerance;
+  double adaptive_rho_fraction;
+  c_int max_iter;
+  double eps_abs;
+  double eps_rel;
+  double eps_prim_inf;
+  double eps_dual_inf;
+  double alpha;
+  // linsys_solver is omitted. We don't change this.
+  double delta;
+  bool polish;
+  c_int polish_refine_iter;
+  bool verbose;
+  bool scaled_termination;
+  c_int check_termination;
+  bool warm_start;
+  double time_limit;
+};
+
+// Type-safe wrapper for OSQP's status codes that are defined at
+// osqp/include/constants.h.
+enum class OsqpExitCode {
+  kOptimal,            // Optimal solution found.
+  kPrimalInfeasible,   // Certificate of primal infeasibility found.
+  kDualInfeasible,     // Certificate of dual infeasibility found.
+  kOptimalInaccurate,  // Optimal solution found subject to reduced tolerances
+  kPrimalInfeasibleInaccurate,  // Certificate of primal infeasibility found
+                                // subject to reduced tolerances.
+  kDualInfeasibleInaccurate,    // Certificate of dual infeasibility found
+                                // subject to reduced tolerances.
+  kMaxIterations,               // Maximum number of iterations reached.
+  kInterrupted,                 // Interrupted by signal or CTRL-C.
+  kTimeLimitReached,            // Ran out of time.
+  kNonConvex,                   // The problem was found to be non-convex.
+  kUnknown,                     // Unknown problem in solver.
+};
+
+std::string ToString(OsqpExitCode exitcode);
+
+// This is a workaround to avoid including OSQP's header file. We can't directly
+// forward-declare OSQPWorkspace because it is defined as a typedef of an
+// anonymous struct.
+struct OSQPWorkspaceHelper;
+
+// This class is the main interface for calling OSQP. See example usage in
+// README.md.
+class OsqpSolver {
+ public:
+  OsqpSolver() = default;
+  // Move-only.
+  OsqpSolver(OsqpSolver&& rhs) = default;
+  OsqpSolver& operator=(OsqpSolver&& rhs) = default;
+  OsqpSolver(const OsqpSolver&) = delete;
+  OsqpSolver& operator=(const OsqpSolver&) = delete;
+
+  // Creates the internal OSQP workspace given the instance data and settings.
+  // It is valid to call Init() multiple times.
+  absl::Status Init(const OsqpInstance& instance, const OsqpSettings& settings);
+
+  // Updates the elements of matrix the objective matrix P (upper triangular).
+  // The new matrix should have the same sparsity structure.
+  //
+  // The solve will start from the previous optimal solution, which might not be
+  // a good starting point given the new objective matrix. If that's the
+  // case, one can call SetWarmStart with zero vectors to reset the state of the
+  // solver.
+  absl::Status UpdateObjectiveMatrix(
+      const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+          objective_matrix);
+
+  // Updates the elements of matrix the constraint matrix A.
+  // The new matrix should have the same sparsity structure.
+  absl::Status UpdateConstraintMatrix(
+      const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+          constraint_matrix);
+
+  // Combines call of UpdateObjectiveMatrix and UpdateConstraintMatrix.
+  absl::Status UpdateObjectiveAndConstraintMatrices(
+      const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+          objective_matrix,
+      const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+          constraint_matrix);
+
+  // Returns true if Init() has been called successfully.
+  bool IsInitialized() const { return workspace_ != nullptr; }
+
+  // Solves the instance by calling osqp_solve(). CHECK-fails if IsInitialized()
+  // is false.
+  ABSL_MUST_USE_RESULT OsqpExitCode Solve();
+
+  // The number of iterations taken. CHECK-fails if IsInitialized() is false.
+  c_int iterations() const;
+
+  // The objective value of the primal solution. CHECK-fails if IsInitialized()
+  // is false.
+  double objective_value() const;
+
+  // The primal solution, i.e., x. The Map is valid only for the lifetime of
+  // the OSQP workspace. It will be invalidated by a call to Init() or if the
+  // OsqpSolver is deleted. CHECK-fails if IsInitialized() is false.
+  // Implementation details (do not depend on these): The underlying memory is
+  // overwritten by SetPrimalWarmStart(). Modification of the problem data does
+  // not destroy the solution.
+  Eigen::Map<const Eigen::VectorXd> primal_solution() const;
+
+  // The vector of lagrange multipliers on the linear constraints. The Map is
+  // valid only for the lifetime of the OSQP workspace. It will be invalidated
+  // by a call to Init() or if the OsqpSolver is deleted. CHECK-fails if
+  // IsInitialized() is false. Implementation details (do not depend on these):
+  // The underlying memory is overwritten by SetDualWarmStart(). Modification of
+  // the problem data does not destroy the solution.
+  Eigen::Map<const Eigen::VectorXd> dual_solution() const;
+
+  // The primal infeasibility certificate. It is valid to query this only if
+  // Solve() returns kPrimalInfeasible or kPrimalInfeasibleInaccurate. The
+  // Map is valid only for the lifetime of the OSQP workspace. It will be
+  // invalidated by a call to Init() or of the OsqpSolver is deleted.
+  Eigen::Map<const Eigen::VectorXd> primal_infeasibility_certificate() const;
+
+  // TODO(ml): Implement dual_infeasibility_certificate.
+
+  // Sets a primal and dual warm-start for the next solve. Equivalent to
+  // SetPrimalWarmStart(primal_vector) and SetDualWarmStart(dual_vector).
+  // Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if the vectors do not have expected dimensions
+  // - UnknownError if the internal OSQP call fails
+  // - OkStatus on success
+  absl::Status SetWarmStart(
+      const Eigen::Ref<const Eigen::VectorXd>& primal_vector,
+      const Eigen::Ref<const Eigen::VectorXd>& dual_vector);
+
+  // Sets a warm-start for the primal iterate for the next solve. Use a vector
+  // of zeros to reset to the default initialization.
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if the vector does not have expected dimensions
+  // - UnknownError if the internal OSQP call fails
+  // - OkStatus on success
+  absl::Status SetPrimalWarmStart(
+      const Eigen::Ref<const Eigen::VectorXd>& primal_vector);
+
+  // Sets a warm-start for the dual iterate for the next solve. Use a vector
+  // of zeros to reset to the default initialization.
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if the vector does not have expected dimensions
+  // - UnknownError if the internal OSQP call fails
+  // - OkStatus on success
+  absl::Status SetDualWarmStart(
+      const Eigen::Ref<const Eigen::VectorXd>& dual_vector);
+
+  // Sets the objective vector for the next solve. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if the vectors do not have expected dimensions
+  // - UnknownError if the internal OSQP call fails
+  // - OkStatus on success
+  absl::Status SetObjectiveVector(
+      const Eigen::Ref<const Eigen::VectorXd>& objective_vector);
+
+  // Sets the lower_bounds and upper_bounds vectors for the next solve. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if the vectors do not have expected dimensions
+  // - InvalidArgumentError if lower_bounds[i] > upper_bounds[i] for some i
+  // - UnknownError if the internal OSQP call fails
+  // - OkStatus on success
+  absl::Status SetBounds(const Eigen::Ref<const Eigen::VectorXd>& lower_bounds,
+                         const Eigen::Ref<const Eigen::VectorXd>& upper_bounds);
+
+  // Gets the current value of the rho setting, i.e., the ADMM rho step. Returns
+  // a FailedPreconditionError if IsInitialized() is false.
+  absl::StatusOr<double> GetRho() const;
+
+  // Gets the current value of the sigma setting, i.e., the ADMM sigma step.
+  // Returns a FailedPreconditionError if IsInitialized() is false.
+  absl::StatusOr<double> GetSigma() const;
+
+  // Gets the current value of the scaling setting, i.e., the number of
+  // heuristic scaling iterations. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<c_int> GetScaling() const;
+
+  // Gets the current value of the adaptive_rho setting, i.e., whether the rho
+  // step size is adaptively set. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<bool> GetAdaptiveRho() const;
+
+  // Gets the current value of the adaptive_rho_interval setting, i.e., the
+  // number of iterations between rho adaptations. Returns a
+  // FailedPreconditionError if IsInitialized() is false.
+  absl::StatusOr<c_int> GetAdaptiveRhoInterval() const;
+
+  // Gets the current value of the adaptive_rho_tolerance setting, i.e., the
+  // tolerance X for adapting rho (the new value must be X times larger, or 1/X
+  // times smaller, than the current value). Returns a FailedPreconditionError
+  // if IsInitialized() is false.
+  absl::StatusOr<double> GetAdaptiveRhoTolerance() const;
+
+  // Gets the current value of the adaptive_rho_fraction setting, i.e., in
+  // automatic mode (adaptive_rho_interval = 0), what fraction of setup time is
+  // spent on selecting rho. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<double> GetAdaptiveRhoFraction() const;
+
+  // Gets the current value of the max_iter setting, i.e., the maximum number of
+  // iterations. Returns a FailedPreconditionError if IsInitialized() is false.
+  absl::StatusOr<c_int> GetMaxIter() const;
+
+  // Gets the current value of the eps_abs setting, i.e., the absolute error
+  // tolerance for convergence. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<double> GetEpsAbs() const;
+
+  // Gets the current value of the eps_rel setting, i.e., the relative error
+  // tolerance for convergence. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<double> GetEpsRel() const;
+
+  // Gets the current value of the eps_prim_inf setting, i.e., the absolute
+  // error tolerance for primal infeasibility. Returns a FailedPreconditionError
+  // if IsInitialized() is false.
+  absl::StatusOr<double> GetEpsPrimInf() const;
+
+  // Gets the current value of the eps_dual_inf setting, i.e., the absolute
+  // error tolerance for dual infeasibility. Returns a FailedPreconditionError
+  // if IsInitialized() is false.
+  absl::StatusOr<double> GetEpsDualInf() const;
+
+  // Gets the current value of the alpha setting, i.e., the ADMM overrelaxation
+  // parameter. Returns a FailedPreconditionError if IsInitialized() is false.
+  absl::StatusOr<double> GetAlpha() const;
+
+  // Gets the current value of the delta setting, i.e., the polishing
+  // regularization parameter. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<double> GetDelta() const;
+
+  // Gets the current value of the polish setting, i.e., whether polishing is
+  // performed. Returns a FailedPreconditionError if IsInitialized() is false.
+  absl::StatusOr<bool> GetPolish() const;
+
+  // Gets the current value of the polish_refine_iter setting, i.e., the number
+  // of refinement iterations in polishing. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<c_int> GetPolishRefineIter() const;
+
+  // Gets the current value of the verbose setting, i.e., whether solver output
+  // is printed. Returns a FailedPreconditionError if IsInitialized() is false.
+  absl::StatusOr<bool> GetVerbose() const;
+
+  // Gets the current value of the scaled_termination setting, i.e., whether
+  // scaled termination criteria is used. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<bool> GetScaledTermination() const;
+
+  // Gets the current value of the check_termination setting, i.e., the interval
+  // for checking termination. Returns a FailedPreconditionError if
+  // IsInitialized() is false.
+  absl::StatusOr<c_int> GetCheckTermination() const;
+
+  // Gets the current value of the warm_start setting, i.e., if warm starting is
+  // performed. Returns a FailedPreconditionError if IsInitialized() is false.
+  absl::StatusOr<bool> GetWarmStart() const;
+
+  // Gets the current value of the time_limit setting, i.e., the time limit as
+  // expressed in seconds. Returns a FailedPreconditionError if IsInitialized()
+  // is false.
+  absl::StatusOr<double> GetTimeLimit() const;
+
+  // Updates the rho setting, i.e., the ADMM rho step. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if rho_new <= 0.0
+  // - OkStatus on success
+  absl::Status UpdateRho(double rho_new);
+
+  // Updates the max_iter setting, i.e., the maximum number of iterations.
+  // Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if max_iter_new <= 0
+  // - OkStatus on success
+  absl::Status UpdateMaxIter(int max_iter_new);
+
+  // Updates the eps_abs setting, i.e., the absolute error tolerance for
+  // convergence. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if eps_abs_new < 0.0
+  // - OkStatus on success
+  absl::Status UpdateEpsAbs(double eps_abs_new);
+
+  // Updates the eps_rel setting, i.e., the relative error tolerance for
+  // convergence. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if eps_rel_new < 0.0
+  // - OkStatus on success
+  absl::Status UpdateEpsRel(double eps_rel_new);
+
+  // Updates the eps_prim_inf setting, i.e., the absolute error tolerance for
+  // primal infeasibility. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if eps_prim_inf_new < 0.0
+  // - OkStatus on success
+  absl::Status UpdateEpsPrimInf(double eps_prim_inf_new);
+
+  // Updates the eps_dual_inf setting, i.e., the absolute error tolerance for
+  // dual infeasibility. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if eps_dual_inf_new < 0.0
+  // - OkStatus on success
+  absl::Status UpdateEpsDualInf(double eps_dual_inf_new);
+
+  // Updates the alpha setting, i.e., the ADMM overrelaxation parameter.
+  // Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if !(0 < alpha_new < 2)
+  // - OkStatus on success
+  absl::Status UpdateAlpha(double alpha_new);
+
+  // Updates the delta setting, i.e., the polishing regularization parameter.
+  // Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if delta_new <= 0.0
+  // - OkStatus on success
+  absl::Status UpdateDelta(double delta_new);
+
+  // Updates the polish setting, i.e., whether polishing is performed. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - OkStatus on success
+  absl::Status UpdatePolish(bool polish_new);
+
+  // Updates the polish_refine_iter setting, i.e., the number of refinement
+  // iterations in polishing. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if polish_refine_iter_new <= 0.0
+  // - OkStatus on success
+  absl::Status UpdatePolishRefineIter(int polish_refine_iter_new);
+
+  // Updates the verbose setting, i.e., whether solver output is printed.
+  // Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - OkStatus on success
+  absl::Status UpdateVerbose(bool verbose_new);
+
+  // Updates the scaled_termination setting, i.e., whether scaled termination
+  // criteria is used. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - OkStatus on success
+  absl::Status UpdateScaledTermination(bool scaled_termination_new);
+
+  // Updates the check_termination setting, i.e., the interval for checking
+  // termination. Setting to zero disables termination checking. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if check_termination_new < 0.0
+  // - OkStatus on success
+  absl::Status UpdateCheckTermination(c_int check_termination_new);
+
+  // Updates the warm_start setting, i.e., whether warm starting is performed.
+  // Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - OkStatus on success
+  absl::Status UpdateWarmStart(bool warm_start_new);
+
+  // Updates the time_limit setting, i.e., the time limit as expressed in
+  // seconds. Setting the time limit to zero disables time-limiting. Returns:
+  // - FailedPreconditionError if IsInitialized() is false
+  // - InvalidArgumentError if time_limit_new < 0.0
+  // - OkStatus on success
+  absl::Status UpdateTimeLimit(double time_limit_new);
+
+ private:
+  struct OsqpDeleter {
+    void operator()(OSQPWorkspaceHelper* workspace) const;
+  };
+
+  std::unique_ptr<OSQPWorkspaceHelper, OsqpDeleter> workspace_;
+};
+
+}  // namespace osqp
+
+#endif  // OSQP_CPP_H_
diff --git a/src/osqp++.cc b/src/osqp++.cc
new file mode 100644
index 0000000..5fa119e
--- /dev/null
+++ b/src/osqp++.cc
@@ -0,0 +1,992 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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 <algorithm>
+#include <cassert>
+#include <string>
+#include <type_traits>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/string_view.h"
+#include "Eigen/Core"
+#include "Eigen/SparseCore"
+#include "ctrlc.h"
+#include "osqp.h"
+#include "osqp++.h"
+
+// Fails to compile if OSQP's typedefs change. This lets us avoid including
+// osqp.h in osqp++.h.
+static_assert(
+    std::is_same_v<osqp::c_int, ::c_int>,
+    "OSQP's c_int typedef does not match the definition in osqp++.h.");
+static_assert(std::is_same_v<c_float, double>,
+              "OSQP's c_float typedef is unexpectedly not the same as double");
+
+static_assert(sizeof(OSQPSettings) == 176,
+              "The size of OSQPSettings has changed unexpectedly. Make sure "
+              "that the map between ::OSQPSettings and osqp::OsqpSettings "
+              "remains up to date.");
+
+namespace osqp {
+
+namespace {
+
+using ::Eigen::Map;
+using ::Eigen::Ref;
+using ::Eigen::VectorXd;
+
+// The mapping between ::OSQPSettings and osqp::OsqpSettings is maintained
+// manually and may need to be updated for new releases of OSQP.
+
+void CopyFromInternalSettings(const ::OSQPSettings& osqp_settings,
+                              OsqpSettings* settings) {
+  settings->rho = osqp_settings.rho;
+  settings->sigma = osqp_settings.sigma;
+  settings->scaling = osqp_settings.scaling;
+  settings->adaptive_rho = osqp_settings.adaptive_rho;
+  settings->adaptive_rho_interval = osqp_settings.adaptive_rho_interval;
+  settings->adaptive_rho_tolerance = osqp_settings.adaptive_rho_tolerance;
+  settings->adaptive_rho_fraction = osqp_settings.adaptive_rho_fraction;
+  settings->max_iter = osqp_settings.max_iter;
+  settings->eps_abs = osqp_settings.eps_abs;
+  settings->eps_rel = osqp_settings.eps_rel;
+  settings->eps_prim_inf = osqp_settings.eps_prim_inf;
+  settings->eps_dual_inf = osqp_settings.eps_dual_inf;
+  settings->alpha = osqp_settings.alpha;
+  settings->delta = osqp_settings.delta;
+  settings->polish = osqp_settings.polish;
+  settings->polish_refine_iter = osqp_settings.polish_refine_iter;
+  settings->verbose = osqp_settings.verbose;
+  settings->scaled_termination = osqp_settings.scaled_termination;
+  settings->check_termination = osqp_settings.check_termination;
+  settings->warm_start = osqp_settings.warm_start;
+  settings->time_limit = osqp_settings.time_limit;
+}
+
+::OSQPSettings ToInternalSettings(const OsqpSettings& settings) {
+  OSQPSettings osqp_settings;
+  osqp_settings.rho = settings.rho;
+  osqp_settings.sigma = settings.sigma;
+  osqp_settings.scaling = settings.scaling;
+  osqp_settings.adaptive_rho = settings.adaptive_rho;
+  osqp_settings.adaptive_rho_interval = settings.adaptive_rho_interval;
+  osqp_settings.adaptive_rho_tolerance = settings.adaptive_rho_tolerance;
+  osqp_settings.adaptive_rho_fraction = settings.adaptive_rho_fraction;
+  osqp_settings.max_iter = settings.max_iter;
+  osqp_settings.eps_abs = settings.eps_abs;
+  osqp_settings.eps_rel = settings.eps_rel;
+  osqp_settings.eps_prim_inf = settings.eps_prim_inf;
+  osqp_settings.eps_dual_inf = settings.eps_dual_inf;
+  osqp_settings.alpha = settings.alpha;
+  osqp_settings.delta = settings.delta;
+  osqp_settings.polish = settings.polish;
+  osqp_settings.polish_refine_iter = settings.polish_refine_iter;
+  osqp_settings.verbose = settings.verbose;
+  osqp_settings.scaled_termination = settings.scaled_termination;
+  osqp_settings.check_termination = settings.check_termination;
+  osqp_settings.warm_start = settings.warm_start;
+  osqp_settings.time_limit = settings.time_limit;
+  osqp_settings.linsys_solver = ::QDLDL_SOLVER;
+  return osqp_settings;
+}
+
+}  // namespace
+
+OsqpSettings::OsqpSettings() {
+  ::OSQPSettings osqp_settings;
+  osqp_set_default_settings(&osqp_settings);
+  CopyFromInternalSettings(osqp_settings, this);
+}
+
+struct OSQPWorkspaceHelper : public ::OSQPWorkspace {};
+
+void OsqpSolver::OsqpDeleter::operator()(OSQPWorkspaceHelper* workspace) const {
+  osqp_cleanup(workspace);
+}
+
+// OSQP_HANDLE_EXITCODE(x) expands to 'case x: return "x"'. Using this macro
+// prevents typos in the strings.
+#define OSQP_HANDLE_EXITCODE(x) \
+  case x:                       \
+    return #x
+
+std::string ToString(OsqpExitCode exitcode) {
+  switch (exitcode) {
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kOptimal);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kPrimalInfeasible);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kDualInfeasible);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kOptimalInaccurate);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kPrimalInfeasibleInaccurate);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kDualInfeasibleInaccurate);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kMaxIterations);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kInterrupted);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kTimeLimitReached);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kNonConvex);
+    OSQP_HANDLE_EXITCODE(OsqpExitCode::kUnknown);
+  }
+  return "Unknown exit code";
+}
+
+#undef OSQP_HANDLE_EXITCODE
+
+namespace {
+
+absl::Status CheckDimensions(const int left_value, const int right_value,
+                             absl::string_view left_name,
+                             absl::string_view right_name) {
+  if (left_value != right_value) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Dimension mismatch: ", left_name, " (= ", left_value,
+                     ") must equal ", right_name, " (= ", right_value, ")."));
+  } else {
+    return absl::OkStatus();
+  }
+}
+
+}  // namespace
+
+#define OSQP_CHECK_DIMENSIONS(left_value, right_value) \
+  CheckDimensions((left_value), (right_value), #left_value, #right_value)
+
+#define OSQP_RETURN_IF_ERROR(expr)    \
+  {                                   \
+    const absl::Status result = expr; \
+    if (!result.ok()) return result;  \
+  }
+
+#define OSQP_CHECK(expr) assert(expr)
+
+absl::Status OsqpSolver::Init(const OsqpInstance& instance,
+                              const OsqpSettings& settings) {
+  if (!instance.objective_matrix.isCompressed()) {
+    return absl::InvalidArgumentError(
+        "objective_matrix must be compressed (call makeCompressed()).");
+  }
+  if (!instance.constraint_matrix.isCompressed()) {
+    return absl::InvalidArgumentError(
+        "constraint_matrix must be compressed (call makeCompressed()).");
+  }
+  const c_int num_variables = instance.num_variables();
+  const c_int num_constraints = instance.num_constraints();
+
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(instance.objective_matrix.cols(), num_variables));
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(instance.objective_matrix.rows(), num_variables));
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(instance.objective_vector.size(), num_variables));
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(instance.lower_bounds.size(), num_constraints));
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(instance.upper_bounds.size(), num_constraints));
+
+  // Clip bounds using OSQP_INFTY. Failing to do this causes subtle convergence
+  // issues instead of producing explicit errors (e.g., the
+  // DetectsPrimalInfeasible test fails). The osqp-python interface also clips
+  // the bounds in the same way.
+  VectorXd clipped_lower_bounds = instance.lower_bounds.cwiseMax(-OSQP_INFTY);
+  VectorXd clipped_upper_bounds = instance.upper_bounds.cwiseMin(OSQP_INFTY);
+
+  // OSQP copies all the data, so it's okay to discard this struct after
+  // osqp_setup. It also does not modify the input data (note osqp_setup takes a
+  // const OSQPData*). const_cast is needed here to fill in the input data
+  // structures.
+  OSQPData data;
+  data.n = num_variables;
+  data.m = num_constraints;
+
+  // TODO(ml): This copy could be avoided if the matrix is already upper
+  // triangular.
+  Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>
+      objective_matrix_upper_triangle =
+          instance.objective_matrix.triangularView<Eigen::Upper>();
+
+  // OSQP's csc struct represents sparse matrices in compressed sparse column
+  // (CSC) format and (confusingly) triplet format. osqp_setup() assumes the
+  // input is provided in CSC format. The mapping from Eigen::SparseMatrix's
+  // outerIndexPtr(), innerIndexPtr(), and valuePtr() is direct because we
+  // require the sparse matrices to be compressed and follow the Eigen::ColMajor
+  // storage scheme. For further background, see
+  // https://eigen.tuxfamily.org/dox/group__TutorialSparse.html for the
+  // description of CSC format in Eigen and osqp/include/types.h for the
+  // definition of OSQP's csc struct.
+  ::csc objective_matrix = {
+      objective_matrix_upper_triangle.outerIndexPtr()[num_variables],
+      num_variables,
+      num_variables,
+      const_cast<c_int*>(objective_matrix_upper_triangle.outerIndexPtr()),
+      const_cast<c_int*>(objective_matrix_upper_triangle.innerIndexPtr()),
+      const_cast<double*>(objective_matrix_upper_triangle.valuePtr()),
+      -1};
+  data.P = &objective_matrix;
+
+  ::csc constraint_matrix = {
+      instance.constraint_matrix.outerIndexPtr()[num_variables],
+      num_constraints,
+      num_variables,
+      const_cast<c_int*>(instance.constraint_matrix.outerIndexPtr()),
+      const_cast<c_int*>(instance.constraint_matrix.innerIndexPtr()),
+      const_cast<double*>(instance.constraint_matrix.valuePtr()),
+      -1};
+  data.A = &constraint_matrix;
+
+  data.q = const_cast<double*>(instance.objective_vector.data());
+  data.l = clipped_lower_bounds.data();
+  data.u = clipped_upper_bounds.data();
+
+  ::OSQPSettings osqp_settings = ToInternalSettings(settings);
+
+  OSQPWorkspace* workspace = nullptr;
+  const int return_code = osqp_setup(&workspace, &data, &osqp_settings);
+  workspace_.reset(static_cast<OSQPWorkspaceHelper*>(workspace));
+  if (return_code == 0) {
+    return absl::OkStatus();
+  }
+  switch (static_cast<osqp_error_type>(return_code)) {
+    case OSQP_DATA_VALIDATION_ERROR:
+      return absl::InvalidArgumentError(
+          "Unable to initialize OSQP: data validation error.");
+    case OSQP_SETTINGS_VALIDATION_ERROR:
+      return absl::InvalidArgumentError(
+          "Unable to initialize OSQP: invalid settings.");
+    case OSQP_LINSYS_SOLVER_LOAD_ERROR:
+      // This should never happen because qdldl is statically linked in.
+      return absl::UnknownError(
+          "Unable to initialize OSQP: unable to load linear solver.");
+    case OSQP_LINSYS_SOLVER_INIT_ERROR:
+      return absl::UnknownError(
+          "Unable to initialize OSQP: unable to initialize linear solver.");
+    case OSQP_NONCVX_ERROR:
+      return absl::InvalidArgumentError(
+          "Unable to initialize OSQP: the problem appears non-convex.");
+    case OSQP_MEM_ALLOC_ERROR:
+      return absl::UnknownError(
+          "Unable to initialize OSQP: memory allocation error.");
+    case OSQP_WORKSPACE_NOT_INIT_ERROR:
+      return absl::UnknownError(
+          "Unable to initialize OSQP: workspace not initialized.");
+  }
+  return absl::UnknownError(
+      "Unable to initialize OSQP: unrecognized error code.");
+}
+
+namespace {
+
+absl::Status VerifySameSparsity(
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>& new_matrix,
+    const csc* ref_matrix, size_t num_variables) {
+  if (new_matrix.nonZeros() != ref_matrix->p[num_variables]) {
+    return absl::InvalidArgumentError(
+        "The new new matrix should have the same number of non-zero "
+        "elements.");
+  }
+
+  for (size_t i = 0; i < num_variables; ++i) {
+    if (ref_matrix->p[i] != new_matrix.outerIndexPtr()[i]) {
+      return absl::InvalidArgumentError(
+          "Sparsity of the new matrix differs from the previously "
+          "defined matrix.");
+    }
+  }
+
+  for (size_t i = 0; i < new_matrix.nonZeros(); ++i) {
+    if (ref_matrix->i[i] != new_matrix.innerIndexPtr()[i]) {
+      return absl::InvalidArgumentError(
+          "Sparsity of the new matrix differs from the previously "
+          "defined matrix.");
+    }
+  }
+
+  return absl::OkStatus();
+}
+
+// Helper function for calling osqp_update_P with an upper triangular objective
+// matrix. Assumes objective_matrix_upper_triangle is always upper triangular.
+absl::Status UpdateUpperTriangularObjectiveMatrix(
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+        objective_matrix_upper_triangle,
+    OSQPWorkspaceHelper* workspace) {
+  const c_int num_variables = workspace->data->n;
+
+  if (objective_matrix_upper_triangle.rows() !=
+          objective_matrix_upper_triangle.cols() ||
+      objective_matrix_upper_triangle.rows() != num_variables) {
+    return absl::InvalidArgumentError(absl::StrFormat(
+        "The new objective matrix should be square with dimension equal to the "
+        "number of variables. Matrix dimensions: %d x %d, num_variables=%d",
+        objective_matrix_upper_triangle.rows(),
+        objective_matrix_upper_triangle.cols(), num_variables));
+  }
+
+  OSQP_RETURN_IF_ERROR(VerifySameSparsity(objective_matrix_upper_triangle,
+                                          workspace->data->P, num_variables));
+
+  c_int nnzP = objective_matrix_upper_triangle.nonZeros();
+
+  const int return_code = osqp_update_P(
+      workspace, objective_matrix_upper_triangle.valuePtr(), OSQP_NULL, nnzP);
+  if (return_code == 0) {
+    return absl::OkStatus();
+  }
+  return absl::UnknownError(
+      "Unable to update OSQP P matrix: unrecognized error code.");
+}
+
+// Helper function for calling osqp_update_P_A with an upper triangular
+// objective matrix. Assumes objective_matrix_upper_triangle is always upper
+// triangular.
+absl::Status UpdateUpperTriangularObjectiveMatrixAndConstraintMatrix(
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+        objective_matrix_upper_triangle,
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+        constraint_matrix,
+    OSQPWorkspaceHelper* workspace) {
+  const c_int num_variables = workspace->data->n;
+
+  if (objective_matrix_upper_triangle.rows() !=
+          objective_matrix_upper_triangle.cols() ||
+      objective_matrix_upper_triangle.rows() != num_variables) {
+    return absl::InvalidArgumentError(absl::StrFormat(
+        "The new objective matrix should be square with dimension equal to the "
+        "number of variables. Matrix dimensions: %d x %d, num_variables=%d",
+        objective_matrix_upper_triangle.rows(),
+        objective_matrix_upper_triangle.cols(), num_variables));
+  }
+  if (constraint_matrix.cols() != num_variables) {
+    return absl::InvalidArgumentError(absl::StrFormat(
+        "The new constraint matrix should column size equal to the "
+        "number of variables. Matrix dimensions: %d x %d, num_variables=%d",
+        constraint_matrix.rows(), constraint_matrix.cols(), num_variables));
+  }
+
+  OSQP_RETURN_IF_ERROR(VerifySameSparsity(objective_matrix_upper_triangle,
+                                          workspace->data->P, num_variables));
+
+  c_int nnzP = objective_matrix_upper_triangle.nonZeros();
+
+  OSQP_RETURN_IF_ERROR(
+      VerifySameSparsity(constraint_matrix, workspace->data->A, num_variables));
+
+  c_int nnzA = constraint_matrix.nonZeros();
+
+  const int return_code = osqp_update_P_A(
+      workspace, objective_matrix_upper_triangle.valuePtr(), OSQP_NULL, nnzP,
+      constraint_matrix.valuePtr(), OSQP_NULL, nnzA);
+  if (return_code == 0) {
+    return absl::OkStatus();
+  }
+  return absl::UnknownError(
+      "Unable to update OSQP P and A matrix: unrecognized error code.");
+}
+
+// Returns true if the sparse matrix 'matrix' is upper triangular.
+bool IsUpperTriangular(
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>& matrix) {
+  // Iterate through all non-zero elements, and ensure that their indices are
+  // only on the upper-right triangle, including the diagonal.
+  for (int i = 0; i < matrix.outerSize(); ++i) {
+    for (Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>::InnerIterator it(
+             matrix, i);
+         it; ++it) {
+      if (it.col() < it.row()) {
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+}  // namespace
+
+absl::Status OsqpSolver::UpdateObjectiveMatrix(
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+        objective_matrix) {
+  // If the objective matrix is already upper triangular, we can skip the
+  // temporary.
+  if (IsUpperTriangular(objective_matrix)) {
+    return UpdateUpperTriangularObjectiveMatrix(objective_matrix,
+                                                workspace_.get());
+  }
+
+  // If not upper triangular, make a temporary.
+  Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>
+      objective_matrix_upper_triangle =
+          objective_matrix.triangularView<Eigen::Upper>();
+  return UpdateUpperTriangularObjectiveMatrix(objective_matrix_upper_triangle,
+                                              workspace_.get());
+}
+
+absl::Status OsqpSolver::UpdateConstraintMatrix(
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+        constraint_matrix) {
+  const c_int num_variables = workspace_->data->n;
+
+  if (constraint_matrix.cols() != num_variables) {
+    return absl::InvalidArgumentError(absl::StrFormat(
+        "The new constraint matrix should column size equal to the "
+        "number of variables. Matrix dimensions: %d x %d, num_variables=%d",
+        constraint_matrix.rows(), constraint_matrix.cols(), num_variables));
+  }
+
+  OSQP_RETURN_IF_ERROR(VerifySameSparsity(constraint_matrix,
+                                          workspace_->data->A, num_variables));
+
+  c_int nnzA = constraint_matrix.nonZeros();
+
+  const int return_code = osqp_update_A(
+      workspace_.get(), constraint_matrix.valuePtr(), OSQP_NULL, nnzA);
+  if (return_code == 0) {
+    return absl::OkStatus();
+  }
+  return absl::UnknownError(
+      "Unable to update OSQP A matrix: unrecognized error code.");
+}
+
+absl::Status OsqpSolver::UpdateObjectiveAndConstraintMatrices(
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>& objective_matrix,
+    const Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>&
+        constraint_matrix) {
+  // If the objective matrix is already upper triangular, we can skip the
+  // temporary.
+  if (IsUpperTriangular(objective_matrix)) {
+    return UpdateUpperTriangularObjectiveMatrixAndConstraintMatrix(
+        objective_matrix, constraint_matrix, workspace_.get());
+  }
+
+  // If not upper triangular, make a temporary.
+  Eigen::SparseMatrix<double, Eigen::ColMajor, c_int>
+      objective_matrix_upper_triangle =
+          objective_matrix.triangularView<Eigen::Upper>();
+  return UpdateUpperTriangularObjectiveMatrixAndConstraintMatrix(
+      objective_matrix_upper_triangle, constraint_matrix, workspace_.get());
+}
+
+namespace {
+OsqpExitCode StatusToExitCode(const c_int status_val) {
+  switch (status_val) {
+    case OSQP_SOLVED:
+      return OsqpExitCode::kOptimal;
+    case OSQP_SOLVED_INACCURATE:
+      return OsqpExitCode::kOptimalInaccurate;
+    case OSQP_PRIMAL_INFEASIBLE:
+      return OsqpExitCode::kPrimalInfeasible;
+    case OSQP_PRIMAL_INFEASIBLE_INACCURATE:
+      return OsqpExitCode::kPrimalInfeasibleInaccurate;
+    case OSQP_DUAL_INFEASIBLE:
+      return OsqpExitCode::kDualInfeasible;
+    case OSQP_DUAL_INFEASIBLE_INACCURATE:
+      return OsqpExitCode::kDualInfeasibleInaccurate;
+    case OSQP_MAX_ITER_REACHED:
+      return OsqpExitCode::kMaxIterations;
+    case OSQP_SIGINT:
+      return OsqpExitCode::kInterrupted;
+    case OSQP_TIME_LIMIT_REACHED:
+      return OsqpExitCode::kTimeLimitReached;
+    case OSQP_NON_CVX:
+      return OsqpExitCode::kNonConvex;
+    default:
+      return OsqpExitCode::kUnknown;
+  }
+}
+
+}  // namespace
+
+OsqpExitCode OsqpSolver::Solve() {
+  OSQP_CHECK(IsInitialized());
+  if (osqp_solve(workspace_.get()) != 0) {
+    // From looking at the code, this can happen if the solve is interrupted
+    // with ctrl-c or if updating "rho" fails.
+    if (osqp_is_interrupted()) {
+      return OsqpExitCode::kInterrupted;
+    }
+    return OsqpExitCode::kUnknown;
+  }
+  return StatusToExitCode(workspace_->info->status_val);
+}
+
+c_int OsqpSolver::iterations() const {
+  OSQP_CHECK(IsInitialized());
+  return workspace_->info->iter;
+}
+
+double OsqpSolver::objective_value() const {
+  OSQP_CHECK(IsInitialized());
+  return workspace_->info->obj_val;
+}
+
+Map<const VectorXd> OsqpSolver::primal_solution() const {
+  OSQP_CHECK(IsInitialized());
+  return Map<const VectorXd>(workspace_->solution->x, workspace_->data->n);
+}
+
+Map<const VectorXd> OsqpSolver::dual_solution() const {
+  OSQP_CHECK(IsInitialized());
+  return Map<const VectorXd>(workspace_->solution->y, workspace_->data->m);
+}
+
+Map<const VectorXd> OsqpSolver::primal_infeasibility_certificate() const {
+  OSQP_CHECK(IsInitialized());
+  const OsqpExitCode exit_code = StatusToExitCode(workspace_->info->status_val);
+  OSQP_CHECK(exit_code == OsqpExitCode::kPrimalInfeasible ||
+             exit_code == OsqpExitCode::kPrimalInfeasibleInaccurate);
+  return Map<const VectorXd>(workspace_->delta_y, workspace_->data->m);
+}
+
+absl::Status OsqpSolver::SetWarmStart(const Ref<const VectorXd>& primal_vector,
+                                      const Ref<const VectorXd>& dual_vector) {
+  // This is identical to calling osqp_warm_start with both vectors at once.
+  OSQP_RETURN_IF_ERROR(SetPrimalWarmStart(primal_vector));
+  OSQP_RETURN_IF_ERROR(SetDualWarmStart(dual_vector));
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::SetPrimalWarmStart(
+    const Ref<const VectorXd>& primal_vector) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  const c_int num_variables = workspace_->data->n;
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(primal_vector.size(), num_variables));
+
+  const int return_code =
+      osqp_warm_start_x(workspace_.get(), primal_vector.data());
+  if (return_code != 0) {
+    return absl::UnknownError("osqp_warm_start_x unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::SetDualWarmStart(
+    const Ref<const VectorXd>& dual_vector) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  const c_int num_constraints = workspace_->data->m;
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(dual_vector.size(), num_constraints));
+
+  const int return_code =
+      osqp_warm_start_y(workspace_.get(), dual_vector.data());
+  if (return_code != 0) {
+    return absl::UnknownError("osqp_warm_start_y unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::SetObjectiveVector(
+    const Ref<const VectorXd>& objective_vector) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  const c_int num_variables = workspace_->data->n;
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(objective_vector.size(), num_variables));
+
+  const int return_code =
+      osqp_update_lin_cost(workspace_.get(), objective_vector.data());
+  if (return_code != 0) {
+    return absl::UnknownError("osqp_update_lin_cost unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+// NOTE(ml): osqp_update_lower_bound and osqp_update_upper_bound are not
+// exposed because they have confusing semantics. They immediately error if a
+// new set of bounds is inconsistent with the existing bounds on the other side.
+absl::Status OsqpSolver::SetBounds(const Ref<const VectorXd>& lower_bounds,
+                                   const Ref<const VectorXd>& upper_bounds) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  const c_int num_constraints = workspace_->data->m;
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(lower_bounds.size(), num_constraints));
+  OSQP_RETURN_IF_ERROR(
+      OSQP_CHECK_DIMENSIONS(upper_bounds.size(), num_constraints));
+  // OSQP does this check internally, but we can return a better error message.
+  for (int i = 0; i < num_constraints; i++) {
+    if (lower_bounds[i] > upper_bounds[i]) {
+      return absl::InvalidArgumentError(
+          absl::StrCat("Inconsistent bounds at index ", i, ", ",
+                       lower_bounds[i], " must be <= ", upper_bounds[i], "."));
+    }
+  }
+
+  const int return_code = osqp_update_bounds(
+      workspace_.get(), lower_bounds.data(), upper_bounds.data());
+  if (return_code != 0) {
+    return absl::UnknownError("osqp_update_bounds unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::StatusOr<double> OsqpSolver::GetRho() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->rho;
+}
+
+absl::StatusOr<double> OsqpSolver::GetSigma() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->sigma;
+}
+
+absl::StatusOr<c_int> OsqpSolver::GetScaling() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->scaling;
+}
+
+absl::StatusOr<bool> OsqpSolver::GetAdaptiveRho() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->adaptive_rho;
+}
+
+absl::StatusOr<c_int> OsqpSolver::GetAdaptiveRhoInterval() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->adaptive_rho_interval;
+}
+
+absl::StatusOr<double> OsqpSolver::GetAdaptiveRhoTolerance() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->adaptive_rho_tolerance;
+}
+
+absl::StatusOr<double> OsqpSolver::GetAdaptiveRhoFraction() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->adaptive_rho_fraction;
+}
+
+absl::StatusOr<c_int> OsqpSolver::GetMaxIter() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->max_iter;
+}
+
+absl::StatusOr<double> OsqpSolver::GetEpsAbs() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->eps_abs;
+}
+
+absl::StatusOr<double> OsqpSolver::GetEpsRel() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->eps_rel;
+}
+
+absl::StatusOr<double> OsqpSolver::GetEpsPrimInf() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->eps_prim_inf;
+}
+
+absl::StatusOr<double> OsqpSolver::GetEpsDualInf() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->eps_dual_inf;
+}
+
+absl::StatusOr<double> OsqpSolver::GetAlpha() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->alpha;
+}
+
+absl::StatusOr<double> OsqpSolver::GetDelta() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->delta;
+}
+
+absl::StatusOr<bool> OsqpSolver::GetPolish() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->polish;
+}
+
+absl::StatusOr<c_int> OsqpSolver::GetPolishRefineIter() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->polish_refine_iter;
+}
+
+absl::StatusOr<bool> OsqpSolver::GetVerbose() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->verbose;
+}
+
+absl::StatusOr<bool> OsqpSolver::GetScaledTermination() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->scaled_termination;
+}
+
+absl::StatusOr<c_int> OsqpSolver::GetCheckTermination() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->check_termination;
+}
+
+absl::StatusOr<bool> OsqpSolver::GetWarmStart() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->warm_start;
+}
+
+absl::StatusOr<double> OsqpSolver::GetTimeLimit() const {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  return workspace_->settings->time_limit;
+}
+
+absl::Status OsqpSolver::UpdateRho(const double rho_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (rho_new <= 0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid rho value: ", rho_new));
+  }
+  if (osqp_update_rho(workspace_.get(), rho_new) != 0) {
+    return absl::UnknownError("osqp_update_rho unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateMaxIter(const int max_iter_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (max_iter_new <= 0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid max_iter value: ", max_iter_new));
+  }
+  if (osqp_update_max_iter(workspace_.get(), max_iter_new) != 0) {
+    return absl::UnknownError("osqp_update_max_iter unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateEpsAbs(const double eps_abs_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (eps_abs_new < 0.0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid eps_abs value: ", eps_abs_new));
+  }
+  if (osqp_update_eps_abs(workspace_.get(), eps_abs_new) != 0) {
+    return absl::UnknownError("osqp_update_eps_abs unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateEpsRel(const double eps_rel_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (eps_rel_new < 0.0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid eps_rel value: ", eps_rel_new));
+  }
+  if (osqp_update_eps_rel(workspace_.get(), eps_rel_new) != 0) {
+    return absl::UnknownError("osqp_update_eps_rel unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateEpsPrimInf(const double eps_prim_inf_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (eps_prim_inf_new < 0.0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid eps_prim_inf value: ", eps_prim_inf_new));
+  }
+  if (osqp_update_eps_prim_inf(workspace_.get(), eps_prim_inf_new) != 0) {
+    return absl::UnknownError("osqp_update_eps_prim_inf unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateEpsDualInf(const double eps_dual_inf_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (eps_dual_inf_new < 0.0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid eps_dual_inf value: ", eps_dual_inf_new));
+  }
+  if (osqp_update_eps_dual_inf(workspace_.get(), eps_dual_inf_new) != 0) {
+    return absl::UnknownError("osqp_update_eps_dual_inf unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateAlpha(const double alpha_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (alpha_new <= 0.0 || alpha_new >= 2) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid alpha value: ", alpha_new));
+  }
+  if (osqp_update_alpha(workspace_.get(), alpha_new) != 0) {
+    return absl::UnknownError("osqp_update_alpha unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateDelta(const double delta_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (delta_new <= 0.0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid delta value: ", delta_new));
+  }
+  if (osqp_update_delta(workspace_.get(), delta_new) != 0) {
+    return absl::UnknownError("osqp_update_delta unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdatePolish(const bool polish_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (osqp_update_polish(workspace_.get(), polish_new) != 0) {
+    return absl::UnknownError("osqp_update_polish unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdatePolishRefineIter(
+    const int polish_refine_iter_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (polish_refine_iter_new <= 0.0) {
+    return absl::InvalidArgumentError(absl::StrCat(
+        "Invalid polish_refine_iter value: ", polish_refine_iter_new));
+  }
+  if (osqp_update_polish_refine_iter(workspace_.get(),
+                                     polish_refine_iter_new) != 0) {
+    return absl::UnknownError(
+        "osqp_update_polish_refine_iter unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateVerbose(const bool verbose_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (osqp_update_verbose(workspace_.get(), verbose_new) != 0) {
+    return absl::UnknownError("osqp_update_verbose unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateScaledTermination(
+    const bool scaled_termination_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (osqp_update_scaled_termination(workspace_.get(),
+                                     scaled_termination_new) != 0) {
+    return absl::UnknownError(
+        "osqp_update_scaled_termination unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateCheckTermination(
+    const c_int check_termination_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (check_termination_new < 0.0) {
+    return absl::InvalidArgumentError(absl::StrCat(
+        "Invalid check_termination value: ", check_termination_new));
+  }
+  if (osqp_update_check_termination(workspace_.get(), check_termination_new) !=
+      0) {
+    return absl::UnknownError(
+        "osqp_update_check_termination unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateWarmStart(const bool warm_start_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (osqp_update_warm_start(workspace_.get(), warm_start_new) != 0) {
+    return absl::UnknownError("osqp_update_warm_start unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+absl::Status OsqpSolver::UpdateTimeLimit(const double time_limit_new) {
+  if (!IsInitialized()) {
+    return absl::FailedPreconditionError("OsqpSolver is not initialized.");
+  }
+  if (time_limit_new < 0.0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid time_limit value: ", time_limit_new));
+  }
+  if (osqp_update_time_limit(workspace_.get(), time_limit_new) != 0) {
+    return absl::UnknownError("osqp_update_time_limit unexpectedly failed.");
+  }
+  return absl::OkStatus();
+}
+
+#undef OSQP_CHECK_DIMENSIONS
+#undef OSQP_RETURN_IF_ERROR
+#undef OSQP_CHECK
+
+}  // namespace osqp
diff --git a/test/osqp++_test.cc b/test/osqp++_test.cc
new file mode 100644
index 0000000..205a74e
--- /dev/null
+++ b/test/osqp++_test.cc
@@ -0,0 +1,967 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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 <iterator>
+#include <limits>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/types/span.h"
+#include "Eigen/SparseCore"
+#include "osqp++.h"
+
+namespace osqp {
+namespace {
+
+using ::Eigen::SparseMatrix;
+using ::Eigen::Triplet;
+using ::Eigen::VectorXd;
+using ::testing::DoubleNear;
+
+constexpr double kTolerance = 1.0e-5;
+constexpr double kInfinity = std::numeric_limits<double>::infinity();
+
+void ExpectElementsAre(Eigen::Map<const Eigen::VectorXd> vec,
+                       absl::Span<const double> expected,
+                       const double tolerance) {
+  EXPECT_EQ(vec.size(), expected.size());
+  for (int i = 0; i < vec.size(); ++i) {
+    EXPECT_THAT(vec[i], DoubleNear(expected[i], tolerance))
+        << "violation at index =" << i;
+  }
+}
+
+TEST(OsqpTest, DimensionMismatchObjectiveMatrix) {
+  OsqpInstance instance;
+  instance.objective_matrix = SparseMatrix<double>(1, 1);
+  instance.objective_vector.resize(1);
+  instance.objective_vector << -1.0;
+  instance.constraint_matrix = SparseMatrix<double>(2, 2);
+  instance.lower_bounds.resize(2);
+  instance.lower_bounds << 0.0, 0.0;
+  instance.upper_bounds.resize(2);
+  instance.upper_bounds << 1.0, 1.0;
+  OsqpSolver solver;
+  OsqpSettings settings;
+  EXPECT_EQ(solver.Init(instance, settings).code(),
+            absl::StatusCode::kInvalidArgument);
+  EXPECT_FALSE(solver.IsInitialized());
+}
+
+TEST(OsqpTest, DimensionMismatchObjectiveVector) {
+  OsqpInstance instance;
+  instance.objective_matrix = SparseMatrix<double>(1, 1);
+  instance.objective_vector.resize(2);
+  instance.objective_vector << 1.0, 1.0;
+  instance.constraint_matrix = SparseMatrix<double>(0, 1);
+  // instance.lower_bounds not set.
+  // instance.upper_bounds not set.
+  OsqpSolver solver;
+  OsqpSettings settings;
+  EXPECT_EQ(solver.Init(instance, settings).code(),
+            absl::StatusCode::kInvalidArgument);
+  EXPECT_FALSE(solver.IsInitialized());
+}
+
+TEST(OsqpTest, DimensionMismatchLowerBounds) {
+  OsqpInstance instance;
+  instance.objective_matrix = SparseMatrix<double>(1, 1);
+  instance.objective_vector.resize(1);
+  instance.objective_vector << 1.0;
+  instance.constraint_matrix = SparseMatrix<double>(0, 1);
+  instance.lower_bounds.resize(1);
+  instance.lower_bounds << 1.0;
+  // instance.upper_bounds not set.
+  OsqpSolver solver;
+  OsqpSettings settings;
+  EXPECT_EQ(solver.Init(instance, settings).code(),
+            absl::StatusCode::kInvalidArgument);
+  EXPECT_FALSE(solver.IsInitialized());
+}
+
+TEST(OsqpTest, DimensionMismatchUpperBounds) {
+  OsqpInstance instance;
+  instance.objective_matrix = SparseMatrix<double>(1, 1);
+  instance.objective_vector.resize(1);
+  instance.objective_vector << 1.0;
+  instance.constraint_matrix = SparseMatrix<double>(0, 1);
+  // instance.lower_bounds not set.
+  instance.upper_bounds.resize(1);
+  instance.upper_bounds << 1.0;
+  OsqpSolver solver;
+  OsqpSettings settings;
+  EXPECT_EQ(solver.Init(instance, settings).code(),
+            absl::StatusCode::kInvalidArgument);
+  EXPECT_FALSE(solver.IsInitialized());
+}
+
+// A lower bound greater than an upper bound triggers an OSQP initialization
+// error.
+TEST(OsqpTest, InvalidData) {
+  OsqpInstance instance;
+  instance.objective_matrix = SparseMatrix<double>(1, 1);
+  instance.objective_vector.resize(1);
+  instance.objective_vector << 1.0;
+  instance.constraint_matrix = SparseMatrix<double>(1, 1);
+  instance.lower_bounds.resize(1);
+  instance.lower_bounds << 1.0;
+  instance.upper_bounds.resize(1);
+  instance.upper_bounds << 0.0;
+  OsqpSolver solver;
+  OsqpSettings settings;
+  EXPECT_EQ(solver.Init(instance, settings).code(),
+            absl::StatusCode::kInvalidArgument);
+  EXPECT_FALSE(solver.IsInitialized());
+}
+
+TEST(OsqpTest, TwoDimLP) {
+  // Minimize -x subject to:
+  // x + y <= 1, x >= 0, y >= 0.
+
+  SparseMatrix<double> constraint_matrix(3, 2);
+  const Triplet<double> kTripletsA[] = {
+      {0, 0, 1.0}, {0, 1, 1.0}, {1, 0, 1.0}, {2, 1, 1.0}};
+  constraint_matrix.setFromTriplets(std::begin(kTripletsA),
+                                    std::end(kTripletsA));
+
+  OsqpInstance instance;
+  instance.objective_matrix = SparseMatrix<double>(2, 2);
+  instance.objective_vector.resize(2);
+  instance.objective_vector << -1.0, 0.0;
+  instance.constraint_matrix = constraint_matrix;
+  instance.lower_bounds.resize(3);
+  instance.lower_bounds << -kInfinity, 0.0, 0.0;
+  instance.upper_bounds.resize(3);
+  instance.upper_bounds << 1.0, kInfinity, kInfinity;
+
+  OsqpSolver solver;
+  OsqpSettings settings;
+  // absolute_convergence_tolerance (eps_abs) is an l_2 tolerance on the
+  // residual vector, so this is safe given we use kTolerance
+  // as an l_infty tolerance.
+  settings.eps_abs = kTolerance;
+  settings.eps_rel = 0.0;
+  EXPECT_FALSE(solver.IsInitialized());
+  ASSERT_TRUE(solver.Init(instance, settings).ok());
+  EXPECT_TRUE(solver.IsInitialized());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  EXPECT_NEAR(solver.objective_value(), -1.0, kTolerance);
+  ExpectElementsAre(solver.primal_solution(), {1.0, 0.0}, kTolerance);
+  ExpectElementsAre(solver.dual_solution(), {1.0, 0.0, -1.0}, kTolerance);
+}
+
+TEST(OsqpTest, TwoDimLPWithEquality) {
+  // Minimize -x subject to:
+  // x + y <= 1, x >= 0, y == 0.5.
+
+  SparseMatrix<double> constraint_matrix(3, 2);
+  const Triplet<double> kTripletsA[] = {
+      {0, 0, 1.0}, {0, 1, 1.0}, {1, 0, 1.0}, {2, 1, 1.0}};
+  constraint_matrix.setFromTriplets(std::begin(kTripletsA),
+                                    std::end(kTripletsA));
+
+  OsqpInstance instance;
+  instance.objective_matrix = SparseMatrix<double>(2, 2);
+  instance.objective_vector.resize(2);
+  instance.objective_vector << -1.0, 0.0;
+  instance.constraint_matrix = constraint_matrix;
+  instance.lower_bounds.resize(3);
+  instance.lower_bounds << -kInfinity, 0.0, 0.5;
+  instance.upper_bounds.resize(3);
+  instance.upper_bounds << 1.0, kInfinity, 0.5;
+
+  OsqpSolver solver;
+  OsqpSettings settings;
+  settings.eps_abs = kTolerance;
+  settings.eps_rel = 0.0;
+  ASSERT_TRUE(solver.Init(instance, settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  EXPECT_NEAR(solver.objective_value(), -0.5, kTolerance);
+  ExpectElementsAre(solver.primal_solution(), {0.5, 0.5}, kTolerance);
+  ExpectElementsAre(solver.dual_solution(), {1.0, 0.0, -1.0}, kTolerance);
+}
+
+TEST(OsqpTest, DetectsPrimalInfeasible) {
+  // Minimize 0 subject to:
+  // x == 2, x >= 3.
+  SparseMatrix<double> objective_matrix(1, 1);
+
+  SparseMatrix<double> constraint_matrix(2, 1);
+  const Triplet<double> kTripletsQ[] = {{0, 0, 1.0}, {1, 0, 1.0}};
+  constraint_matrix.setFromTriplets(std::begin(kTripletsQ),
+                                    std::end(kTripletsQ));
+
+  OsqpInstance instance;
+  instance.objective_matrix = objective_matrix;
+  instance.objective_vector.resize(1);
+  instance.objective_vector << 0.0;
+  instance.constraint_matrix = constraint_matrix;
+  instance.lower_bounds.resize(2);
+  instance.lower_bounds << 2, 3;
+  instance.upper_bounds.resize(2);
+  instance.upper_bounds << 2, kInfinity;
+
+  OsqpSolver solver;
+  OsqpSettings settings;
+  ASSERT_TRUE(solver.Init(instance, settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kPrimalInfeasible);
+  Eigen::Map<const VectorXd> cert = solver.primal_infeasibility_certificate();
+  // The infeasibility certificate satisfies cert[0] + cert[1] == 0,
+  // cert[0]* 2 + cert[1] * 3 < 0, and cert[1] <= 0. See the OSQP documentation
+  // for the general definition of the certificate.
+  EXPECT_THAT(cert[0] / cert[1], DoubleNear(-1.0, kTolerance));
+  EXPECT_LE(cert[1], 0.0);
+  EXPECT_LT(cert[0] * 2 + cert[1] * 3, 0.0);
+}
+
+TEST(OsqpTest, NewConstraintMatrixInTwoDimLP) {
+  // Minimize -x subject to:
+  // x + y <= 1, x >= 1, y >= 0.
+
+  SparseMatrix<double> constraint_matrix(3, 2);
+  const Triplet<double> kTripletsA[] = {
+      {0, 0, 1.0}, {0, 1, 1.0}, {1, 0, 1.0}, {2, 1, 1.0}};
+  constraint_matrix.setFromTriplets(std::begin(kTripletsA),
+                                    std::end(kTripletsA));
+
+  OsqpInstance instance;
+  instance.objective_matrix = SparseMatrix<double>(2, 2);
+  instance.objective_vector.resize(2);
+  instance.objective_vector << -1.0, 0.0;
+  instance.constraint_matrix = constraint_matrix;
+  instance.lower_bounds.resize(3);
+  instance.lower_bounds << -kInfinity, 1.0, 0.0;
+  instance.upper_bounds.resize(3);
+  instance.upper_bounds << 1.0, kInfinity, kInfinity;
+
+  OsqpSolver solver;
+  OsqpSettings settings;
+  // absolute_convergence_tolerance (eps_abs) is an l_2 tolerance on the
+  // residual vector, so this is safe given we use kTolerance
+  // as an l_infty tolerance.
+  settings.eps_abs = kTolerance;
+  settings.eps_rel = 0.0;
+  EXPECT_FALSE(solver.IsInitialized());
+  ASSERT_TRUE(solver.Init(instance, settings).ok());
+  EXPECT_TRUE(solver.IsInitialized());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  double x_exp = 1.0;
+  double y_exp = 0.0;
+
+  EXPECT_NEAR(solver.objective_value(), -x_exp, kTolerance);
+  ExpectElementsAre(solver.primal_solution(), {x_exp, y_exp}, kTolerance);
+
+  // Change to Minimize -x subject to:
+  // 2*x + y <= 1.5, 2*x >= 1, y >= 0.25
+  SparseMatrix<double> new_constraint_matrix = constraint_matrix;
+  new_constraint_matrix.coeffRef(0, 0) = 2;
+  new_constraint_matrix.coeffRef(1, 0) = 2;
+
+  VectorXd new_lower_bounds(3);
+  new_lower_bounds << -kInfinity, 1, 0.25;
+  VectorXd new_upper_bounds(3);
+  new_upper_bounds << 1.5, kInfinity, kInfinity;
+
+  ASSERT_TRUE(solver.SetBounds(new_lower_bounds, new_upper_bounds).ok());
+
+  ASSERT_TRUE(solver.UpdateConstraintMatrix(new_constraint_matrix).ok());
+  EXPECT_TRUE(solver.IsInitialized());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  x_exp = 0.624997;
+  y_exp = 0.250002;
+
+  EXPECT_NEAR(solver.objective_value(), -x_exp, 2 * kTolerance);
+  ExpectElementsAre(solver.primal_solution(), {x_exp, y_exp}, kTolerance);
+}
+
+class TwoDimensionalQpTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    // Minimize x^2 + (1/2)xy + y^2 subject to x >= 1.
+    SparseMatrix<double> objective_matrix(2, 2);
+    const Triplet<double> kTripletsP[] = {
+        {0, 0, 2.0}, {1, 0, 0.5}, {0, 1, 0.5}, {1, 1, 2.0}};
+    objective_matrix.setFromTriplets(std::begin(kTripletsP),
+                                     std::end(kTripletsP));
+
+    SparseMatrix<double> constraint_matrix(1, 2);
+    const Triplet<double> kTripletsA[] = {{0, 0, 1.0}};
+    constraint_matrix.setFromTriplets(std::begin(kTripletsA),
+                                      std::end(kTripletsA));
+
+    OsqpInstance instance;
+    instance.objective_matrix = objective_matrix;
+    instance.objective_vector.resize(2);
+    instance.objective_vector << 0.0, 0.0;
+    instance.constraint_matrix = constraint_matrix;
+    instance.lower_bounds.resize(1);
+    instance.lower_bounds << 1.0;
+    instance.upper_bounds.resize(1);
+    instance.upper_bounds << kInfinity;
+
+    OsqpSettings settings;
+    settings.eps_abs = kTolerance / 10;
+    settings.eps_rel = 0.0;
+    // check_termination is set so that the warm-started instance converges
+    // immediately.
+    settings.check_termination = 1;
+    ASSERT_TRUE(solver_.Init(instance, settings).ok());
+  }
+
+  OsqpSolver solver_;
+};
+
+TEST_F(TwoDimensionalQpTest, Solves) {
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+
+  // NOTE(ml): The convergence guarantees don't imply that the error in
+  // objective_value() is within kTolerance. To be more precise, we could
+  // compute the worst error bound in a kTolerance box around the optimal
+  // solution, but 2 * kTolerance works here.
+  EXPECT_NEAR(solver_.objective_value(), 1.0 - 0.5 * 0.25 + 0.25 * 0.25,
+              2 * kTolerance);
+  ExpectElementsAre(solver_.primal_solution(), {1.0, -0.25}, kTolerance);
+  ExpectElementsAre(solver_.dual_solution(), {-15.0 / 8.0}, 2 * kTolerance);
+}
+
+TEST_F(TwoDimensionalQpTest, UsesJointWarmStart) {
+  VectorXd primal_warm_start(2);
+  primal_warm_start << 1.0, -0.25;
+  VectorXd dual_warm_start(1);
+  dual_warm_start << -15.0 / 8.0;
+  ASSERT_TRUE(solver_.SetWarmStart(primal_warm_start, dual_warm_start).ok());
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+  EXPECT_EQ(solver_.iterations(), 1);
+}
+
+TEST_F(TwoDimensionalQpTest, UsesSeparateWarmStart) {
+  VectorXd primal_warm_start(2);
+  primal_warm_start << 1.0, -0.25;
+  VectorXd dual_warm_start(1);
+  dual_warm_start << -15.0 / 8.0;
+  ASSERT_TRUE(solver_.SetPrimalWarmStart(primal_warm_start).ok());
+  ASSERT_TRUE(solver_.SetDualWarmStart(dual_warm_start).ok());
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+  EXPECT_EQ(solver_.iterations(), 1);
+}
+
+TEST_F(TwoDimensionalQpTest, SolvesWithNewObjectiveVector) {
+  // Changes the objective to x^2 + (1/2)xy + y^2 + x
+  VectorXd objective_vector(2);
+  objective_vector << 1.0, 0.0;
+  ASSERT_TRUE(solver_.SetObjectiveVector(objective_vector).ok());
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+
+  EXPECT_NEAR(solver_.objective_value(), 1.0 - 0.5 * 0.25 + 0.25 * 0.25 + 1.0,
+              3 * kTolerance);
+  ExpectElementsAre(solver_.primal_solution(), {1.0, -0.25}, kTolerance);
+}
+
+TEST_F(TwoDimensionalQpTest, SolvesWithNewBounds) {
+  // Fixes x to 2.0.
+  VectorXd bound_vector(1);
+  bound_vector << 2.0;
+  ASSERT_TRUE(solver_.SetBounds(bound_vector, bound_vector).ok());
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+  EXPECT_NEAR(solver_.objective_value(), 4.0 - 0.5 + 0.5 * 0.5, 2 * kTolerance);
+  ExpectElementsAre(solver_.primal_solution(), {2.0, -0.5}, kTolerance);
+}
+
+// These tests are instantiated twice, once with a non-upper triangular
+// objective matrix, and once with an upper triangular objective matrix.
+class ParameterizedTwoDimensionalQpTest
+    : public testing::WithParamInterface<bool>,
+      public TwoDimensionalQpTest {};
+TEST_P(ParameterizedTwoDimensionalQpTest, SolvesWithNewObjectiveMatrix) {
+  // Minimize x^2 + (1/2)xy + y^2 subject to x >= 1.
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+
+  double x_exp = 1.0;
+  double y_exp = -0.25;
+
+  EXPECT_NEAR(solver_.objective_value(),
+              x_exp * x_exp + 0.5 * x_exp * y_exp + y_exp * y_exp,
+              2 * kTolerance);
+  ExpectElementsAre(solver_.primal_solution(), {x_exp, y_exp}, kTolerance);
+  ExpectElementsAre(solver_.dual_solution(), {-15.0 / 8.0}, 2 * kTolerance);
+
+  // Minimize x^2 + (1/2)xy + (1/2)y^2 subject to x >= 1.
+  SparseMatrix<double> new_objective_matrix(2, 2);
+  const Triplet<double> kTripletsP[] = {
+      {0, 0, 2.0}, {1, 0, 0.5}, {0, 1, 0.5}, {1, 1, 1}};
+  new_objective_matrix.setFromTriplets(std::begin(kTripletsP),
+                                       std::end(kTripletsP));
+  if (GetParam()) {
+    new_objective_matrix = new_objective_matrix.triangularView<Eigen::Upper>();
+  }
+
+  ASSERT_TRUE(solver_.UpdateObjectiveMatrix(new_objective_matrix).ok());
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+
+  x_exp = 1.0;
+  y_exp = -0.5;
+  EXPECT_NEAR(solver_.objective_value(),
+              x_exp * x_exp + 0.5 * x_exp * y_exp + 0.5 * y_exp * y_exp,
+              2 * kTolerance);
+  ExpectElementsAre(solver_.primal_solution(), {x_exp, y_exp}, kTolerance);
+}
+TEST_P(ParameterizedTwoDimensionalQpTest,
+       SolvesWithNewObjectiveAndConstraintMatrices) {
+  // Minimize x^2 + (1/2)xy + y^2 subject to x >= 1.
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+
+  double x_exp = 1.0;
+  double y_exp = -0.25;
+
+  EXPECT_NEAR(solver_.objective_value(),
+              x_exp * x_exp + 0.5 * x_exp * y_exp + y_exp * y_exp,
+              2 * kTolerance);
+  ExpectElementsAre(solver_.primal_solution(), {x_exp, y_exp}, kTolerance);
+  ExpectElementsAre(solver_.dual_solution(), {-15.0 / 8.0}, 2 * kTolerance);
+
+  // Change to minimize x^2 + (1/2)xy + (1/2)y^2 subject to 0.5*x >= 1.
+  SparseMatrix<double> new_objective_matrix(2, 2);
+  const Triplet<double> kTripletsP[] = {
+      {0, 0, 2.0}, {1, 0, 0.5}, {0, 1, 0.5}, {1, 1, 1}};
+  new_objective_matrix.setFromTriplets(std::begin(kTripletsP),
+                                       std::end(kTripletsP));
+  if (GetParam()) {
+    new_objective_matrix = new_objective_matrix.triangularView<Eigen::Upper>();
+  }
+
+  SparseMatrix<double> new_constraint_matrix(1, 2);
+  const Triplet<double> kTripletsA[] = {{0, 0, 0.5}};
+  new_constraint_matrix.setFromTriplets(std::begin(kTripletsA),
+                                        std::end(kTripletsA));
+
+  const absl::Status result = solver_.UpdateObjectiveAndConstraintMatrices(
+      new_objective_matrix, new_constraint_matrix);
+  ASSERT_TRUE(result.ok());
+  ASSERT_EQ(solver_.Solve(), OsqpExitCode::kOptimal);
+
+  x_exp = 2.0;
+  y_exp = -1.0;
+  EXPECT_NEAR(solver_.objective_value(),
+              x_exp * x_exp + 0.5 * x_exp * y_exp + 0.5 * y_exp * y_exp,
+              2 * kTolerance);
+  ExpectElementsAre(solver_.primal_solution(), {x_exp, y_exp}, kTolerance);
+}
+INSTANTIATE_TEST_SUITE_P(UseUpperTriangularObjectiveMatrix,
+                         ParameterizedTwoDimensionalQpTest,
+                         ::testing::ValuesIn({false, true}));
+
+TEST(OsqpTest, ErrorIfNotPsd) {
+  // Minimize -x^2.
+
+  SparseMatrix<double> objective_matrix(1, 1);
+  const Triplet<double> kTripletsP[] = {{0, 0, -2.0}};
+  objective_matrix.setFromTriplets(std::begin(kTripletsP),
+                                   std::end(kTripletsP));
+
+  SparseMatrix<double> constraint_matrix(0, 1);
+
+  OsqpInstance instance;
+  instance.objective_matrix = objective_matrix;
+  instance.objective_vector.resize(1);
+  instance.objective_vector << 0.0;
+  instance.constraint_matrix = constraint_matrix;
+  // instance.lower_bounds not set.
+  // instance.upper_bounds not set.
+
+  OsqpSolver solver;
+  OsqpSettings settings;
+  settings.eps_abs = kTolerance;
+  settings.eps_rel = 0.0;
+  settings.verbose = true;
+  EXPECT_EQ(solver.Init(instance, settings).code(),
+            absl::StatusCode::kInvalidArgument);
+}
+
+// This returns an OsqpInstance that corresponds to minimizing
+//   (x-1)^2 + (y-1)^2 + c
+// within the bounds -100 <= x, y <= 100.
+OsqpInstance GetToyProblem() {
+  OsqpInstance instance;
+
+  instance.objective_matrix = SparseMatrix<double>(2, 2);
+  const Triplet<double> triplets[] = {{0, 0, 2.0}, {1, 1, 2.0}};
+  instance.objective_matrix.setFromTriplets(std::begin(triplets),
+                                            std::end(triplets));
+
+  instance.objective_vector.resize(2);
+  instance.objective_vector << -2.0, -2.0;
+
+  instance.constraint_matrix = instance.objective_matrix;
+
+  instance.lower_bounds.resize(2);
+  instance.lower_bounds << -100.0, -100.0;
+
+  instance.upper_bounds.resize(2);
+  instance.upper_bounds << 100.0, 100.0;
+
+  return instance;
+}
+
+// Returns the solution to the problem returned by GetToyProblem().
+VectorXd GetToySolution() {
+  VectorXd soln(2);
+  soln << 1.0, 1.0;
+  return soln;
+}
+
+TEST(OsqpTest, GetAndUpdateRho) {
+  OsqpSettings settings;
+  settings.rho = 3.0e-3;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto rho = solver.GetRho();
+    ASSERT_TRUE(rho.ok());
+    EXPECT_EQ(*rho, 3.0e-3);
+  }
+
+  ASSERT_TRUE(solver.UpdateRho(4.0e-4).ok());
+  {
+    const auto rho = solver.GetRho();
+    ASSERT_TRUE(rho.ok());
+    EXPECT_EQ(*rho, 4.0e-4);
+  }
+}
+
+TEST(OsqpTest, GetSigma) {
+  OsqpSettings settings;
+  settings.sigma = 3.0e-3;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  const auto sigma = solver.GetSigma();
+  ASSERT_TRUE(sigma.ok());
+  EXPECT_EQ(*sigma, 3.0e-3);
+}
+
+TEST(OsqpTest, GetScaling) {
+  OsqpSettings settings;
+  settings.scaling = 23;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  const auto scaling = solver.GetScaling();
+  ASSERT_TRUE(scaling.ok());
+  EXPECT_EQ(*scaling, 23);
+}
+
+TEST(OsqpTest, GetAdaptiveRho) {
+  OsqpSettings settings;
+  settings.adaptive_rho = false;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  const auto adaptive_rho = solver.GetAdaptiveRho();
+  ASSERT_TRUE(adaptive_rho.ok());
+  EXPECT_FALSE(*adaptive_rho);
+}
+
+TEST(OsqpTest, GetAdaptiveRhoInterval) {
+  OsqpSettings settings;
+  settings.adaptive_rho_interval = 12;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  const auto adaptive_rho_interval = solver.GetAdaptiveRhoInterval();
+  ASSERT_TRUE(adaptive_rho_interval.ok());
+  EXPECT_EQ(*adaptive_rho_interval, 12);
+}
+
+TEST(OsqpTest, GetAdaptiveRhoTolerance) {
+  OsqpSettings settings;
+  settings.adaptive_rho_tolerance = 17.0;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  const auto adaptive_rho_tolerance = solver.GetAdaptiveRhoTolerance();
+  ASSERT_TRUE(adaptive_rho_tolerance.ok());
+  EXPECT_EQ(*adaptive_rho_tolerance, 17);
+}
+
+TEST(OsqpTest, GetAdaptiveRhoFraction) {
+  OsqpSettings settings;
+  settings.adaptive_rho_fraction = 3.4;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  const auto adaptive_rho_fraction = solver.GetAdaptiveRhoFraction();
+  ASSERT_TRUE(adaptive_rho_fraction.ok());
+  EXPECT_EQ(*adaptive_rho_fraction, 3.4);
+}
+
+TEST(OsqpTest, GetAndUpdateEpsAbs) {
+  OsqpSettings settings;
+  settings.eps_abs = 3.0e-3;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto eps_abs = solver.GetEpsAbs();
+    ASSERT_TRUE(eps_abs.ok());
+    EXPECT_EQ(*eps_abs, 3.0e-3);
+  }
+
+  ASSERT_TRUE(solver.UpdateEpsAbs(4.0e-4).ok());
+  {
+    const auto eps_abs = solver.GetEpsAbs();
+    ASSERT_TRUE(eps_abs.ok());
+    EXPECT_EQ(*eps_abs, 4.0e-4);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateEpsRel) {
+  OsqpSettings settings;
+  settings.eps_rel = 3.0e-3;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto eps_rel = solver.GetEpsRel();
+    ASSERT_TRUE(eps_rel.ok());
+    EXPECT_EQ(*eps_rel, 3.0e-3);
+  }
+
+  ASSERT_TRUE(solver.UpdateEpsRel(4.0e-4).ok());
+  {
+    const auto eps_rel = solver.GetEpsRel();
+    ASSERT_TRUE(eps_rel.ok());
+    EXPECT_EQ(*eps_rel, 4.0e-4);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateEpsPrimInf) {
+  OsqpSettings settings;
+  settings.eps_prim_inf = 3.0e-3;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto eps_prim_inf = solver.GetEpsPrimInf();
+    ASSERT_TRUE(eps_prim_inf.ok());
+    EXPECT_EQ(*eps_prim_inf, 3.0e-3);
+  }
+
+  ASSERT_TRUE(solver.UpdateEpsPrimInf(4.0e-4).ok());
+  {
+    const auto eps_prim_inf = solver.GetEpsPrimInf();
+    ASSERT_TRUE(eps_prim_inf.ok());
+    EXPECT_EQ(*eps_prim_inf, 4.0e-4);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateEpsDualInf) {
+  OsqpSettings settings;
+  settings.eps_dual_inf = 3.0e-3;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto eps_dual_inf = solver.GetEpsDualInf();
+    ASSERT_TRUE(eps_dual_inf.ok());
+    EXPECT_EQ(*eps_dual_inf, 3.0e-3);
+  }
+  ASSERT_TRUE(solver.UpdateEpsDualInf(4.0e-4).ok());
+  {
+    const auto eps_dual_inf = solver.GetEpsDualInf();
+    ASSERT_TRUE(eps_dual_inf.ok());
+    EXPECT_EQ(*eps_dual_inf, 4.0e-4);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateAlpha) {
+  OsqpSettings settings;
+  settings.alpha = 3.0e-3;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto alpha = solver.GetAlpha();
+    ASSERT_TRUE(alpha.ok());
+    EXPECT_EQ(*alpha, 3.0e-3);
+  }
+
+  ASSERT_TRUE(solver.UpdateAlpha(4.0e-4).ok());
+  {
+    const auto alpha = solver.GetAlpha();
+    ASSERT_TRUE(alpha.ok());
+    EXPECT_EQ(*alpha, 4.0e-4);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateDelta) {
+  OsqpSettings settings;
+  settings.delta = 3.0e-3;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto delta = solver.GetDelta();
+    ASSERT_TRUE(delta.ok());
+    EXPECT_EQ(*delta, 3.0e-3);
+  }
+
+  ASSERT_TRUE(solver.UpdateDelta(4.0e-4).ok());
+  {
+    const auto delta = solver.GetDelta();
+    ASSERT_TRUE(delta.ok());
+    EXPECT_EQ(*delta, 4.0e-4);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdatePolish) {
+  OsqpSettings settings;
+  settings.polish = true;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto polish = solver.GetPolish();
+    ASSERT_TRUE(polish.ok());
+    EXPECT_TRUE(*polish);
+  }
+
+  ASSERT_TRUE(solver.UpdatePolish(false).ok());
+  {
+    const auto polish = solver.GetPolish();
+    ASSERT_TRUE(polish.ok());
+    EXPECT_FALSE(*polish);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdatePolishRefineIter) {
+  OsqpSettings settings;
+  settings.polish_refine_iter = 12;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto polish_refine_iter = solver.GetPolishRefineIter();
+    ASSERT_TRUE(polish_refine_iter.ok());
+    EXPECT_EQ(*polish_refine_iter, 12);
+  }
+
+  ASSERT_TRUE(solver.UpdatePolishRefineIter(13).ok());
+  {
+    const auto polish_refine_iter = solver.GetPolishRefineIter();
+    ASSERT_TRUE(polish_refine_iter.ok());
+    EXPECT_EQ(*polish_refine_iter, 13);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateVerbose) {
+  OsqpSettings settings;
+  settings.verbose = false;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto verbose = solver.GetVerbose();
+    ASSERT_TRUE(verbose.ok());
+    EXPECT_FALSE(*verbose);
+  }
+
+  ASSERT_TRUE(solver.UpdateVerbose(true).ok());
+  {
+    const auto verbose = solver.GetVerbose();
+    ASSERT_TRUE(verbose.ok());
+    EXPECT_TRUE(*verbose);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateScaledTermination) {
+  OsqpSettings settings;
+  settings.scaled_termination = true;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto scaled_termination = solver.GetScaledTermination();
+    ASSERT_TRUE(scaled_termination.ok());
+    EXPECT_TRUE(*scaled_termination);
+  }
+
+  ASSERT_TRUE(solver.UpdateScaledTermination(false).ok());
+  {
+    const auto scaled_termination = solver.GetScaledTermination();
+    ASSERT_TRUE(scaled_termination.ok());
+    EXPECT_FALSE(*scaled_termination);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateCheckTermination) {
+  OsqpSettings settings;
+  settings.check_termination = 12;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto check_termination = solver.GetCheckTermination();
+    ASSERT_TRUE(check_termination.ok());
+    EXPECT_EQ(*check_termination, 12);
+  }
+
+  ASSERT_TRUE(solver.UpdateCheckTermination(13).ok());
+  {
+    const auto check_termination = solver.GetCheckTermination();
+    ASSERT_TRUE(check_termination.ok());
+    EXPECT_EQ(*check_termination, 13);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateWarmStart) {
+  OsqpSettings settings;
+  settings.warm_start = false;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto warm_start = solver.GetWarmStart();
+    ASSERT_TRUE(warm_start.ok());
+    EXPECT_FALSE(*warm_start);
+  }
+
+  ASSERT_TRUE(solver.UpdateWarmStart(true).ok());
+  {
+    const auto warm_start = solver.GetWarmStart();
+    ASSERT_TRUE(warm_start.ok());
+    EXPECT_TRUE(*warm_start);
+  }
+}
+
+TEST(OsqpTest, GetAndUpdateTimeLimit) {
+  OsqpSettings settings;
+  settings.time_limit = 20.0;
+
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+
+  {
+    const auto time_limit = solver.GetTimeLimit();
+    ASSERT_TRUE(time_limit.ok());
+    EXPECT_EQ(*time_limit, 20.0);
+  }
+
+  ASSERT_TRUE(solver.UpdateTimeLimit(35.0).ok());
+  {
+    const auto time_limit = solver.GetTimeLimit();
+    ASSERT_TRUE(time_limit.ok());
+    EXPECT_EQ(*time_limit, 35.0);
+  }
+}
+
+TEST(OsqpTest, UpdateMaxIter) {
+  OsqpSettings settings;
+  settings.eps_abs = 1.0e-6;
+  settings.max_iter = 1;
+
+  // Try solving an easy problem and verify that we only used one iteration.
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kMaxIterations);
+  EXPECT_EQ(solver.iterations(), 1);
+
+  // Now allow for more iterations and check that they are actually used.
+  ASSERT_TRUE(solver.UpdateMaxIter(2).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kMaxIterations);
+  EXPECT_EQ(solver.iterations(), 2);
+
+  // Setting a large number of iterations allows us to actually solve the
+  // problem.
+  ASSERT_TRUE(solver.UpdateMaxIter(1000).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+}
+
+TEST(OsqpTest, UpdateEpsAbs) {
+  OsqpSettings settings;
+  // Check for termination after every iteration.
+  settings.check_termination = 1;
+  // Disable eps_rel to isolate the effect of eps_abs.
+  settings.eps_rel = 0.0;
+  // Set a very loose eps_abs value.
+  settings.eps_abs = 1;
+
+  // Solve and make a note of the error in our solution.
+  OsqpSolver solver;
+  ASSERT_TRUE(solver.Init(GetToyProblem(), settings).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+  const double err1 = (solver.primal_solution() - GetToySolution()).norm();
+
+  // Now set a much tighter eps_abs and solve again.
+  ASSERT_TRUE(solver.UpdateEpsAbs(1.0e-7).ok());
+  ASSERT_EQ(solver.Solve(), OsqpExitCode::kOptimal);
+  const double err2 = (solver.primal_solution() - GetToySolution()).norm();
+
+  // We should have ended up with a significantly smaller error the second time.
+  EXPECT_LT(100 * err2, err1);
+  // And we should be nearly optimal.
+  EXPECT_LT(err2, 1.0e-5);
+}
+
+// TODO(ml): Missing tests:
+// - Dual infeasible
+// - Dimension mismatches
+
+}  // namespace
+}  // namespace osqp