diff --git a/include/teqp/algorithms/VLE.hpp b/include/teqp/algorithms/VLE.hpp
index 346a05b021e4b20e5c31f60ec588c402ca39a1b9..25f9f690550051cdd2a40ae4e25a7cc8c2858794 100644
--- a/include/teqp/algorithms/VLE.hpp
+++ b/include/teqp/algorithms/VLE.hpp
@@ -1,8 +1,14 @@
 #pragma once
 
+#include <optional>
 #include "teqp/derivs.hpp"
 #include <Eigen/Dense>
 
+// Imports from boost for numerical integration
+#include <boost/numeric/odeint/stepper/controlled_runge_kutta.hpp>
+#include <boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp>
+#include <boost/numeric/odeint/stepper/euler.hpp>
+
 template<typename Model, typename TYPE = double>
 class IsothermPureVLEResiduals  {
     typedef Eigen::Array<TYPE, 2, 1> EigenArray;
@@ -207,4 +213,189 @@ auto get_drhovecdp_Tsat(const Model& model, const Scalar &T, const VecType& rhov
         drhodp_vap = linsolve(PSIVstar, PSILstar*drhodp_liq);
     }
     return std::make_tuple(drhodp_liq, drhodp_vap);
+}
+
+struct TVLEOptions {
+    double init_dt = 1e-5, abs_err = 1e-8, rel_err = 1e-8, max_dt = 100000;
+    int max_steps = 1000, integration_order = 5;
+    bool polish = false;
+};
+
+/***
+* \brief Trace an isotherm with parametric tracing
+*/
+template<typename Model, typename Scalar, typename VecType>
+auto trace_VLE_isotherm_binary(const Model &model, Scalar T, VecType rhovecL0, VecType rhovecV0, const std::optional<TVLEOptions>& options = std::nullopt) 
+{
+    // Get the options, or the default values if not provided
+    TVLEOptions opt = options.value_or(TVLEOptions{});
+    auto N = rhovecL0.size();
+    if (N != 2) {
+        std::invalid_argument("Size must be 2");
+    }
+    if (rhovecL0.size() != rhovecV0.size()) {
+        std::invalid_argument("Both molar concentration arrays must be of the same size");
+    }
+
+    auto norm = [](const auto& v) { return sqrt((v * v).sum()); };
+
+    // Define datatypes and functions for tracing tools
+    auto JSONdata = nlohmann::json::array();
+
+    // Typedefs for the types
+    using namespace boost::numeric::odeint;
+    using state_type = std::vector<double>;
+
+    // Class for simple Euler integration
+    euler<state_type> eul;
+    typedef runge_kutta_cash_karp54< state_type > error_stepper_type;
+    typedef controlled_runge_kutta< error_stepper_type > controlled_stepper_type;
+
+    // Define the tolerances
+    double abs_err = opt.abs_err, rel_err = opt.rel_err, a_x = 1.0, a_dxdt = 1.0;
+    controlled_stepper_type controlled_stepper(default_error_checker< double, range_algebra, default_operations >(abs_err, rel_err, a_x, a_dxdt));
+
+
+    double c = 1.0;
+
+    // The function to be integrated by odeint
+    auto xprime = [&](const state_type& X, state_type& Xprime, double /*t*/) {
+        // Memory maps into the state vector for inputs and their derivatives
+        // These are views, not copies!
+        auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(X[0]), N);
+        auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(X[0]) + N, N);
+        auto drhovecdtL = Eigen::Map<Eigen::ArrayXd>(&(Xprime[0]), N);
+        auto drhovecdtV = Eigen::Map<Eigen::ArrayXd>(&(Xprime[0]) + N, N);
+        // Get the derivatives with respect to pressure along the isotherm of the phase envelope
+        auto [drhovecdpL, drhovecdpV] = get_drhovecdp_Tsat(model, T, rhovecL, rhovecV);
+        // Get the derivative of p w.r.t. parameter
+        auto dpdt = 1.0/sqrt(norm(drhovecdpL.array()) + norm(drhovecdpV.array()));
+        // And finally the derivatives with respect to the tracing variable
+        drhovecdtL = c*drhovecdpL*dpdt;
+        drhovecdtV = c*drhovecdpV*dpdt;
+    };
+
+    // Set up the initial state vector
+    state_type x0(2*N), last_drhodt(2*N);
+    auto set_init_state = [&](state_type& X) {
+        auto rhovecL = Eigen::Map<Eigen::ArrayXd>(&(X[0]), N);
+        auto rhovecV = Eigen::Map<Eigen::ArrayXd>(&(X[0]) + N, N);
+        rhovecL = rhovecL0;
+        rhovecV = rhovecV0;
+    };
+    set_init_state(x0);
+    
+    // Figure out which direction to trace initially
+    double t = 0, dt = opt.init_dt;
+    {
+        auto dxdt = x0;
+        xprime(x0, dxdt, -1.0);
+        const auto dXdt = Eigen::Map<const Eigen::ArrayXd>(&(dxdt[0]), dxdt.size());
+        const auto X = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), x0.size());
+
+        const Eigen::ArrayXd step = X + dXdt * dt;
+        Eigen::ArrayX<bool> negativestepvals = (step < 0).eval();
+        // Flip the sign if the first step would yield any negative concentrations
+        if (negativestepvals.any()) {
+            c *= -1;
+        }
+    }
+    std::string termination_reason;
+    
+    // Then trace...
+    int retry_count = 0;
+    for (auto istep = 0; istep < opt.max_steps; ++istep) {
+
+        auto store_point = [&]() {
+            //// Calculate some other parameters, for debugging
+            auto N = x0.size() / 2;
+            auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), N);
+            auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]) + N, N);
+            auto rhotot = rhovecL.sum();
+            using id = IsochoricDerivatives<decltype(model), Scalar, VecType>;
+            double pL = rhotot * model.R(rhovecL / rhovecL.sum())*T + id::get_pr(model, T, rhovecL);
+
+            // Store the data in a JSON structure
+            nlohmann::json point = {
+                {"t", t},
+                {"T / K", T},
+                {"pL / Pa", pL},
+                {"c", c},
+                {"rhoL / mol/m^3", rhovecL},
+                {"rhoV / mol/m^3", rhovecV},
+                
+            };
+            JSONdata.push_back(point);
+        };
+        if (istep == 0 && retry_count == 0) {
+            store_point();
+        }
+
+        double dtold = dt;
+        auto x0_previous = x0;
+
+        // Call the derivative function to get drho/dt in order 
+        // to cache it at the *beginning* of the step
+        auto dxdt = x0;
+        try {
+            xprime(x0, dxdt, -1.0);
+        }
+        catch (...) {
+            break;
+        }
+        last_drhodt = dxdt;
+
+        if (opt.integration_order == 5) {
+            controlled_step_result res = controlled_step_result::fail;
+            try {
+                res = controlled_stepper.try_step(xprime, x0, t, dt);
+            }
+            catch (...) {
+                break;
+            }
+
+            if (res != controlled_step_result::success) {
+                // Try again, with a smaller step size
+                istep--;
+                retry_count++;
+                continue;
+            }
+            else {
+                retry_count = 0;
+            }
+            // Reduce step size if greater than the specified max step size
+            dt = std::min(dt, opt.max_dt);
+        }
+        else if (opt.integration_order == 1) {
+            try {
+                eul.do_step(xprime, x0, t, dt);
+            }
+            catch (...) {
+                break;
+            }
+        }
+        else {
+            throw std::invalid_argument("integration order is invalid:" + std::to_string(opt.integration_order));
+        }
+        auto stop_requested = [&]() {
+            //// Calculate some other parameters, for debugging
+            auto N = x0.size() / 2;
+            auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), N);
+            auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]) + N, N);
+            auto x = rhovecL / rhovecL.sum();
+            auto y = rhovecV / rhovecV.sum();
+            if ((x < 0).any() || (x > 1).any() || (y < 0).any() || (y > 1).any()) {
+                return true;
+            }
+            else {
+                return false;
+            }
+        };
+        if (stop_requested()) {
+            break;
+        }
+
+        store_point();
+    }
+    return JSONdata;
 }
\ No newline at end of file
diff --git a/interface/pybind11_wrapper.cpp b/interface/pybind11_wrapper.cpp
index 0750c84900c7e70283dc8dc57e8e62cbcc1f12b3..910b0b94cd77c4c50d0c7dd1f8e1e75d9a50700c 100644
--- a/interface/pybind11_wrapper.cpp
+++ b/interface/pybind11_wrapper.cpp
@@ -38,6 +38,17 @@ void init_teqp(py::module& m) {
         .def_readwrite("integration_order", &TCABOptions::integration_order)
         ;
 
+    // The options class for isotherm tracer, not tied to a particular model
+    py::class_<TVLEOptions>(m, "TVLEOptions")
+        .def(py::init<>())
+        .def_readwrite("abs_err", &TVLEOptions::abs_err)
+        .def_readwrite("rel_err", &TVLEOptions::rel_err)
+        .def_readwrite("init_dt", &TVLEOptions::init_dt)
+        .def_readwrite("max_dt", &TVLEOptions::max_dt)
+        .def_readwrite("max_steps", &TVLEOptions::max_steps)
+        .def_readwrite("integration_order", &TVLEOptions::integration_order)
+        ;
+
     // Some functions for timing overhead of interface
     m.def("___mysummer", [](const double &c, const Eigen::ArrayXd &x) { return c*x.sum(); });
     using RAX = Eigen::Ref<Eigen::ArrayXd>;
diff --git a/interface/pybind11_wrapper.hpp b/interface/pybind11_wrapper.hpp
index dff601d79b6ed1d02f3b900cc4c4c210bbad95af..cc11df5f76b4daa8073e245d208626b98774c421 100644
--- a/interface/pybind11_wrapper.hpp
+++ b/interface/pybind11_wrapper.hpp
@@ -51,6 +51,7 @@ void add_derivatives(py::module &m, Wrapper &cls) {
     m.def("extrapolate_from_critical", &extrapolate_from_critical<Model, double>);
     m.def("pure_VLE_T", &pure_VLE_T<Model, double>);
     m.def("get_drhovecdp_Tsat", &get_drhovecdp_Tsat<Model, double, RAX>, py::arg("model"), py::arg("T"), py::arg("rhovecL").noconvert(), py::arg("rhovecV").noconvert());
+    m.def("trace_VLE_isotherm_binary", &trace_VLE_isotherm_binary<Model, double, Eigen::ArrayXd>, py::arg("model"), py::arg("T"), py::arg("rhovecL0").noconvert(), py::arg("rhovecV0").noconvert(), py::arg_v("options", std::nullopt, "None"));
 
     //cls.def("get_Ar01", [](const Model& m, const double T, const Eigen::ArrayXd& rhovec) { return id::get_Ar01(m, T, rhovec); });
     //cls.def("get_Ar10", [](const Model& m, const double T, const Eigen::ArrayXd& rhovec) { return id::get_Ar10(m, T, rhovec); });
diff --git a/src/tests/catch_test_cubics.cxx b/src/tests/catch_test_cubics.cxx
index 7edd6a1a15728eba6f64a0934a5e5eebe8cfcb1d..737f9208dae1b4f3db69cb7cea42a106ad1fd357 100644
--- a/src/tests/catch_test_cubics.cxx
+++ b/src/tests/catch_test_cubics.cxx
@@ -1,10 +1,12 @@
+
+#include <fstream>
+
 #include "catch/catch.hpp"
 
 #include "teqp/models/cubics.hpp"
 #include "teqp/derivs.hpp"
 #include "teqp/algorithms/VLE.hpp"
 
-
 #include <boost/numeric/odeint/stepper/euler.hpp>
 #include <boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp>
 
@@ -88,32 +90,52 @@ TEST_CASE("Check manual integration of subcritical VLE isotherm for binary mixtu
         auto pfromderiv = rho * model.R(molefrac) * T + id::get_pr(model, T, rhovecL);
         return pfromderiv;
     };
-    
-    for (int i : { 0 }) {
-        state_type X0 = get_start(T, i); // Starting point; liquid, then vapor
-        double p0 = get_p(X0);
-        state_type Xfinal = get_start(T, 1-i); // Ending point; liquid, then vapor
-        double pfinal = get_p(Xfinal);
 
-        //euler<state_type> integrator;
-        runge_kutta_cash_karp54< state_type > integrator;
-        int Nstep = 10000;
-        double p = p0, pmax = pfinal, dp = (pmax - p0) / (Nstep - 1);
+    SECTION("Manual integration") {
+
+        for (int i : { 0 }) {
+            state_type X0 = get_start(T, i); // Starting point; liquid, then vapor
+            double p0 = get_p(X0);
+            state_type Xfinal = get_start(T, 1 - i); // Ending point; liquid, then vapor
+            double pfinal = get_p(Xfinal);
+
+            //euler<state_type> integrator;
+            runge_kutta_cash_karp54< state_type > integrator;
+            int Nstep = 10000;
+            double p = p0, pmax = pfinal, dp = (pmax - p0) / (Nstep - 1);
 
-        auto write = [&]() { 
-            //std::cout << p << " " << X0[0] << "," << X0[1] << std::endl;
-        };
-        for (auto i = 0; p < pmax; ++i) {
-            if (p + dp > pmax) { break; }
+            auto write = [&]() {
+                //std::cout << p << " " << X0[0] << "," << X0[1] << std::endl;
+            };
+            for (auto i = 0; p < pmax; ++i) {
+                if (p + dp > pmax) { break; }
+                write();
+                integrator.do_step(xprime, X0, p, dp);
+                p += dp;
+            }
+            double diffs = 0;
+            for (auto i = 0; i < X0.size(); ++i) {
+                diffs += std::abs(X0[i] - Xfinal[i]);
+            }
+            CHECK(diffs < 0.1);
             write();
-            integrator.do_step(xprime, X0, p, dp);
-            p += dp;
         }
-        double diffs = 0;
-        for (auto i = 0; i < X0.size(); ++i) {
-            diffs += std::abs(X0[i] - Xfinal[i]);
-        }
-        CHECK(diffs < 0.1);
-        write();
+    }
+    SECTION("Parametric integration of isotherm") {
+        int i = 0;
+        auto X = get_start(T, 0);
+        state_type Xfinal = get_start(T, 1 - i); // Ending point; liquid, then vapor
+        double pfinal_goal = get_p(Xfinal); 
+        
+        auto N = X.size() / 2;
+        Eigen::ArrayXd rhovecL0 = Eigen::Map<const Eigen::ArrayXd>(&(X[0]), N);
+        Eigen::ArrayXd rhovecV0 = Eigen::Map<const Eigen::ArrayXd>(&(X[0]) + N, N);
+        auto J = trace_VLE_isotherm_binary(model, T, rhovecL0, rhovecV0);
+        auto Nstep = J.size();
+
+        std::ofstream file("isoT.json"); file << J;
+
+        double pfinal = J.back().at("pL / Pa").back();
+        CHECK(std::abs(pfinal / pfinal_goal-1) < 1e-6);
     }
 }
\ No newline at end of file