Repository: ipc-sim/ipc-toolkit (language: python)

Issue:
Average body mass in IPC-toolkit vs average lumped nodal mass in IPC
I have a question about the arguments in adaptive barrier stiffness function called `ipc::initial_barrier_stiffness`, it has one argument called `const double average_mass` which indicates as average mass of all bodies while in `IPC` function called `suggestKappa(kappa)` and according to Supplement of IPC paper, it should be average of lumped nodal mass. Does it the same as `IPC`?
And what types of CCD `IPC-toolkit` does use for `ipc::compute_collision_free_stepsize`, it seems that it is `TIGHT_INCLUSION` while it seems that `IPC` uses `FLOATING_POINT_ROOT_FINDER` as default. Can I use this with `IPC-toolkit`?



Discussion / hints:
The `average_mass` indeed refers to the "average lumped nodal mass" as described in the supplemental document of our original IPC paper. 

By default, the IPC Toolkit uses the [Tight-Inclusion](https://github.com/Continuous-Collision-Detection/Tight-Inclusion) CCD algorithm. To use the same CCD as in the original IPC codebase you can set the CMake option `IPC_TOOLKIT_WITH_CORRECT_CCD` to `OFF`.
Thanks for your clarification, I assume that `ipc::compute_collision_free_stepsize` doesn't compute the `CFL-INSPIRED CULLING OF CCD`. I also want to implement this part using `IPC-toolkit`, but I am not sure what is the equivalent functions for `sh.build(result, searchDir, alpha, result.avgEdgeLen / 3.0);` and what is the best way to do it using `IPC-toolkit`?

Fix patch:
diff --git a/python/src/candidates/candidates.cpp b/python/src/candidates/candidates.cpp
index 1d899d3d9..82549e3df 100644
--- a/python/src/candidates/candidates.cpp
+++ b/python/src/candidates/candidates.cpp
@@ -104,6 +104,38 @@ void define_candidates(py::module_& m)
             py::arg("min_distance") = 0.0,
             py::arg("tolerance") = DEFAULT_CCD_TOLERANCE,
             py::arg("max_iterations") = DEFAULT_CCD_MAX_ITERATIONS)
+        .def(
+            "compute_noncandidate_conservative_stepsize",
+            &Candidates::compute_noncandidate_conservative_stepsize,
+            R"ipc_Qu8mg5v7(
+            Computes a conservative bound on the largest-feasible step size for surface primitives not in contact.
+
+            Parameters:
+                mesh: The collision mesh.
+                displacements: Surface vertex displacements (rowwise).
+                dhat: Barrier activation distance.
+            )ipc_Qu8mg5v7",
+            py::arg("mesh"), py::arg("displacements"), py::arg("dhat"))
+        .def(
+            "compute_cfl_stepsize", &Candidates::compute_cfl_stepsize,
+            R"ipc_Qu8mg5v7(
+            Computes a CFL-inspired CCD maximum step step size.
+
+            Parameters:
+                mesh: The collision mesh.
+                vertices_t0: Surface vertex starting positions (rowwise).
+                vertices_t1: Surface vertex ending positions (rowwise).
+                dhat: Barrier activation distance.
+                min_distance: The minimum distance allowable between any two elements.
+                tolerance: The tolerance for the CCD algorithm.
+                max_iterations: The maximum number of iterations for the CCD algorithm.
+            )ipc_Qu8mg5v7",
+            py::arg("mesh"), py::arg("vertices_t0"), py::arg("vertices_t1"),
+            py::arg("dhat"),
+            py::arg("broad_phase_method") = DEFAULT_BROAD_PHASE_METHOD,
+            py::arg("min_distance") = 0.0,
+            py::arg("tolerance") = DEFAULT_CCD_TOLERANCE,
+            py::arg("max_iterations") = DEFAULT_CCD_MAX_ITERATIONS)
         .def(
             "save_obj", &Candidates::save_obj, "", py::arg("filename"),
             py::arg("vertices"), py::arg("edges"), py::arg("faces"))
diff --git a/src/ipc/candidates/CMakeLists.txt b/src/ipc/candidates/CMakeLists.txt
index 222da6166..daa314882 100644
--- a/src/ipc/candidates/CMakeLists.txt
+++ b/src/ipc/candidates/CMakeLists.txt
@@ -3,6 +3,7 @@ set(SOURCES
   candidates.hpp
   collision_stencil.hpp
   collision_stencil.cpp
+  continuous_collision_candidate.cpp
   continuous_collision_candidate.hpp
   edge_edge.cpp
   edge_edge.hpp
diff --git a/src/ipc/candidates/candidates.cpp b/src/ipc/candidates/candidates.cpp
index d1b5a8060..8590ade0d 100644
--- a/src/ipc/candidates/candidates.cpp
+++ b/src/ipc/candidates/candidates.cpp
@@ -1,5 +1,6 @@
 #include "candidates.hpp"
 
+#include <ipc/ipc.hpp>
 #include <ipc/utils/save_obj.hpp>
 
 #include <ipc/config.hpp>
@@ -125,6 +126,71 @@ double Candidates::compute_collision_free_stepsize(
     return earliest_toi;
 }
 
+double Candidates::compute_noncandidate_conservative_stepsize(
+    const CollisionMesh& mesh,
+    const Eigen::MatrixXd& displacements,
+    const double dhat) const
+{
+    assert(displacements.rows() == mesh.num_vertices());
+
+    if (empty()) {
+        return 1; // No possible collisions, so can take full step.
+    }
+
+    const auto& E = mesh.edges();
+    const auto& F = mesh.faces();
+
+    std::vector<bool> is_vertex_a_candidates(mesh.num_vertices(), false);
+    for (size_t i = 0; i < size(); i++) {
+        for (const long vid : (*this)[i].vertex_ids(E, F)) {
+            if (vid < 0) {
+                break;
+            }
+            is_vertex_a_candidates[vid] = true;
+        }
+    }
+
+    double max_displacement = 0;
+    for (size_t i = 0; i < displacements.rows(); i++) {
+        if (!is_vertex_a_candidates[i]) {
+            continue;
+        }
+        max_displacement =
+            std::max(max_displacement, displacements.row(i).norm());
+    }
+
+    return 0.5 * dhat / max_displacement;
+}
+
+double Candidates::compute_cfl_stepsize(
+    const CollisionMesh& mesh,
+    const Eigen::MatrixXd& vertices_t0,
+    const Eigen::MatrixXd& vertices_t1,
+    const double dhat,
+    const BroadPhaseMethod broad_phase_method,
+    const double min_distance,
+    const double tolerance,
+    const long max_iterations) const
+{
+    assert(vertices_t0.rows() == mesh.num_vertices());
+    assert(vertices_t1.rows() == mesh.num_vertices());
+
+    const double alpha_C = this->compute_collision_free_stepsize(
+        mesh, vertices_t0, vertices_t1, min_distance, tolerance,
+        max_iterations);
+
+    const double alpha_F = this->compute_noncandidate_conservative_stepsize(
+        mesh, vertices_t1 - vertices_t0, dhat);
+
+    // If alpha_F < 0.5 * alpha_C, then we should do full CCD.
+    if (alpha_F < 0.5 * alpha_C) {
+        return ipc::compute_collision_free_stepsize(
+            mesh, vertices_t0, vertices_t1, broad_phase_method, min_distance,
+            tolerance, max_iterations);
+    }
+    return std::min(alpha_C, alpha_F);
+}
+
 size_t Candidates::size() const
 {
     return ev_candidates.size() + ee_candidates.size() + fv_candidates.size();
diff --git a/src/ipc/candidates/candidates.hpp b/src/ipc/candidates/candidates.hpp
index 28a1cce6f..bbdc13511 100644
--- a/src/ipc/candidates/candidates.hpp
+++ b/src/ipc/candidates/candidates.hpp
@@ -83,6 +83,33 @@ class Candidates {
         const double tolerance = DEFAULT_CCD_TOLERANCE,
         const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS) const;
 
+    /// @brief Computes a conservative bound on the largest-feasible step size for surface primitives not in contact.
+    /// @param mesh The collision mesh.
+    /// @param displacements Surface vertex displacements (rowwise).
+    /// @param dhat Barrier activation distance.
+    double compute_noncandidate_conservative_stepsize(
+        const CollisionMesh& mesh,
+        const Eigen::MatrixXd& displacements,
+        const double dhat) const;
+
+    /// @brief Computes a CFL-inspired CCD maximum step step size.
+    /// @param mesh The collision mesh.
+    /// @param vertices_t0 Surface vertex starting positions (rowwise).
+    /// @param vertices_t1 Surface vertex ending positions (rowwise).
+    /// @param dhat Barrier activation distance.
+    /// @param min_distance The minimum distance allowable between any two elements.
+    /// @param tolerance The tolerance for the CCD algorithm.
+    /// @param max_iterations The maximum number of iterations for the CCD algorithm.
+    double compute_cfl_stepsize(
+        const CollisionMesh& mesh,
+        const Eigen::MatrixXd& vertices_t0,
+        const Eigen::MatrixXd& vertices_t1,
+        const double dhat,
+        const BroadPhaseMethod broad_phase_method = DEFAULT_BROAD_PHASE_METHOD,
+        const double min_distance = 0.0,
+        const double tolerance = DEFAULT_CCD_TOLERANCE,
+        const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS) const;
+
     bool save_obj(
         const std::string& filename,
         const Eigen::MatrixXd& vertices,
diff --git a/src/ipc/candidates/continuous_collision_candidate.cpp b/src/ipc/candidates/continuous_collision_candidate.cpp
new file mode 100644
index 000000000..d559cd539
--- /dev/null
+++ b/src/ipc/candidates/continuous_collision_candidate.cpp
@@ -0,0 +1,22 @@
+#include "continuous_collision_candidate.hpp"
+
+namespace ipc {
+
+bool ContinuousCollisionCandidate::ccd(
+    const Eigen::MatrixXd& vertices_t0,
+    const Eigen::MatrixXd& vertices_t1,
+    const Eigen::MatrixXi& edges,
+    const Eigen::MatrixXi& faces,
+    double& toi,
+    const double min_distance,
+    const double tmax,
+    const double tolerance,
+    const long max_iterations,
+    const double conservative_rescaling) const
+{
+    return ccd(
+        dof(vertices_t0, edges, faces), dof(vertices_t1, edges, faces), toi,
+        min_distance, tmax, tolerance, max_iterations, conservative_rescaling);
+}
+
+} // namespace ipc
diff --git a/src/ipc/candidates/continuous_collision_candidate.hpp b/src/ipc/candidates/continuous_collision_candidate.hpp
index e4d032ac2..03444a0dd 100644
--- a/src/ipc/candidates/continuous_collision_candidate.hpp
+++ b/src/ipc/candidates/continuous_collision_candidate.hpp
@@ -1,15 +1,14 @@
 #pragma once
 
 #include <ipc/ccd/ccd.hpp>
-
-#include <Eigen/Core>
+#include <ipc/candidates/collision_stencil.hpp>
 
 #include <vector>
 
 namespace ipc {
 
 /// Virtual class for candidates that support CCD.
-class ContinuousCollisionCandidate {
+class ContinuousCollisionCandidate : virtual public CollisionStencil {
 public:
     virtual ~ContinuousCollisionCandidate() { }
 
@@ -25,7 +24,7 @@ class ContinuousCollisionCandidate {
     /// @param[in] max_iterations Maximum iterations used by Tight-Inclusion CCD.
     /// @param[in] conservative_rescaling Conservative rescaling value used to avoid taking steps exactly to impact.
     /// @return If the candidate had a collision over the time interval.
-    virtual bool
+    bool
     ccd(const Eigen::MatrixXd& vertices_t0,
         const Eigen::MatrixXd& vertices_t1,
         const Eigen::MatrixXi& edges,
@@ -36,7 +35,7 @@ class ContinuousCollisionCandidate {
         const double tolerance = DEFAULT_CCD_TOLERANCE,
         const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS,
         const double conservative_rescaling =
-            DEFAULT_CCD_CONSERVATIVE_RESCALING) const = 0;
+            DEFAULT_CCD_CONSERVATIVE_RESCALING) const;
 
     // Print the vertices of the CCD query for debugging.
     virtual void print_ccd_query(
@@ -44,6 +43,18 @@ class ContinuousCollisionCandidate {
         const Eigen::MatrixXd& vertices_t1,
         const Eigen::MatrixXi& edges,
         const Eigen::MatrixXi& faces) const = 0;
+
+protected:
+    virtual bool
+    ccd(const VectorMax12d& vertices_t0,
+        const VectorMax12d& vertices_t1,
+        double& toi,
+        const double min_distance = 0.0,
+        const double tmax = 1.0,
+        const double tolerance = DEFAULT_CCD_TOLERANCE,
+        const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS,
+        const double conservative_rescaling =
+            DEFAULT_CCD_CONSERVATIVE_RESCALING) const = 0;
 };
 
 } // namespace ipc
diff --git a/src/ipc/candidates/edge_edge.cpp b/src/ipc/candidates/edge_edge.cpp
index c049e8d57..bccbac225 100644
--- a/src/ipc/candidates/edge_edge.cpp
+++ b/src/ipc/candidates/edge_edge.cpp
@@ -40,10 +40,8 @@ EdgeEdgeCandidate::compute_distance_hessian(const VectorMax12d& positions) const
 }
 
 bool EdgeEdgeCandidate::ccd(
-    const Eigen::MatrixXd& vertices_t0,
-    const Eigen::MatrixXd& vertices_t1,
-    const Eigen::MatrixXi& edges,
-    const Eigen::MatrixXi& faces,
+    const VectorMax12d& vertices_t0,
+    const VectorMax12d& vertices_t1,
     double& toi,
     const double min_distance,
     const double tmax,
@@ -51,19 +49,16 @@ bool EdgeEdgeCandidate::ccd(
     const long max_iterations,
     const double conservative_rescaling) const
 {
+    assert(vertices_t0.size() == 12 && vertices_t1.size() == 12);
     return edge_edge_ccd(
         // Edge 1 at t=0
-        vertices_t0.row(edges(edge0_id, 0)),
-        vertices_t0.row(edges(edge0_id, 1)),
+        vertices_t0.head<3>(), vertices_t0.segment<3>(3),
         // Edge 2 at t=0
-        vertices_t0.row(edges(edge1_id, 0)),
-        vertices_t0.row(edges(edge1_id, 1)),
+        vertices_t0.segment<3>(6), vertices_t0.tail<3>(),
         // Edge 1 at t=1
-        vertices_t1.row(edges(edge0_id, 0)),
-        vertices_t1.row(edges(edge0_id, 1)),
+        vertices_t1.head<3>(), vertices_t1.segment<3>(3),
         // Edge 2 at t=1
-        vertices_t1.row(edges(edge1_id, 0)),
-        vertices_t1.row(edges(edge1_id, 1)), //
+        vertices_t1.segment<3>(6), vertices_t1.tail<3>(), //
         toi, min_distance, tmax, tolerance, max_iterations,
         conservative_rescaling);
 }
diff --git a/src/ipc/candidates/edge_edge.hpp b/src/ipc/candidates/edge_edge.hpp
index 93e15aa70..e60d09a27 100644
--- a/src/ipc/candidates/edge_edge.hpp
+++ b/src/ipc/candidates/edge_edge.hpp
@@ -10,8 +10,7 @@
 
 namespace ipc {
 
-class EdgeEdgeCandidate : virtual public CollisionStencil,
-                          public ContinuousCollisionCandidate {
+class EdgeEdgeCandidate : public ContinuousCollisionCandidate {
 public:
     EdgeEdgeCandidate(long edge0_id, long edge1_id);
 
@@ -27,30 +26,6 @@ class EdgeEdgeCandidate : virtual public CollisionStencil,
 
     // ------------------------------------------------------------------------
 
-    /// Perform narrow-phase CCD on the candidate.
-    /// @param[in] vertices_t0 Mesh vertices at the start of the time step.
-    /// @param[in] vertices_t1 Mesh vertices at the end of the time step.
-    /// @param[in] edges Collision mesh edges as rows of indicies into vertices.
-    /// @param[in] faces Collision mesh triangular faces as rows of indicies into vertices.
-    /// @param[out] toi Computed time of impact (normalized).
-    /// @param[in] tmax Maximum time (normalized) to look for collisions. Should be in [0, 1].
-    /// @param[in] tolerance CCD tolerance used by Tight-Inclusion CCD.
-    /// @param[in] max_iterations Maximum iterations used by Tight-Inclusion CCD.
-    /// @param[in] conservative_rescaling Conservative rescaling value used to avoid taking steps exactly to impact.
-    /// @return If the candidate had a collision over the time interval.
-    bool
-    ccd(const Eigen::MatrixXd& vertices_t0,
-        const Eigen::MatrixXd& vertices_t1,
-        const Eigen::MatrixXi& edges,
-        const Eigen::MatrixXi& faces,
-        double& toi,
-        const double min_distance = 0.0,
-        const double tmax = 1.0,
-        const double tolerance = DEFAULT_CCD_TOLERANCE,
-        const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS,
-        const double conservative_rescaling =
-            DEFAULT_CCD_CONSERVATIVE_RESCALING) const override;
-
     void print_ccd_query(
         const Eigen::MatrixXd& vertices_t0,
         const Eigen::MatrixXd& vertices_t1,
@@ -80,6 +55,7 @@ class EdgeEdgeCandidate : virtual public CollisionStencil,
     using CollisionStencil::compute_distance;
     using CollisionStencil::compute_distance_gradient;
     using CollisionStencil::compute_distance_hessian;
+    using ContinuousCollisionCandidate::ccd;
 
 protected:
     double compute_distance(const VectorMax12d& positions) const override;
@@ -90,6 +66,17 @@ class EdgeEdgeCandidate : virtual public CollisionStencil,
     MatrixMax12d
     compute_distance_hessian(const VectorMax12d& positions) const override;
 
+    bool
+    ccd(const VectorMax12d& vertices_t0,
+        const VectorMax12d& vertices_t1,
+        double& toi,
+        const double min_distance = 0.0,
+        const double tmax = 1.0,
+        const double tolerance = DEFAULT_CCD_TOLERANCE,
+        const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS,
+        const double conservative_rescaling =
+            DEFAULT_CCD_CONSERVATIVE_RESCALING) const override;
+
     virtual EdgeEdgeDistanceType known_dtype() const
     {
         return EdgeEdgeDistanceType::AUTO;
diff --git a/src/ipc/candidates/edge_vertex.cpp b/src/ipc/candidates/edge_vertex.cpp
index 12a20b1d7..97fcfb29b 100644
--- a/src/ipc/candidates/edge_vertex.cpp
+++ b/src/ipc/candidates/edge_vertex.cpp
@@ -44,10 +44,8 @@ MatrixMax12d EdgeVertexCandidate::compute_distance_hessian(
 }
 
 bool EdgeVertexCandidate::ccd(
-    const Eigen::MatrixXd& vertices_t0,
-    const Eigen::MatrixXd& vertices_t1,
-    const Eigen::MatrixXi& edges,
-    const Eigen::MatrixXi& faces,
+    const VectorMax12d& vertices_t0,
+    const VectorMax12d& vertices_t1,
     double& toi,
     const double min_distance,
     const double tmax,
@@ -55,15 +53,18 @@ bool EdgeVertexCandidate::ccd(
     const long max_iterations,
     const double conservative_rescaling) const
 {
+    assert(vertices_t0.size() == 6 || vertices_t0.size() == 9);
+    assert(vertices_t0.size() == vertices_t1.size());
+    const int dim = vertices_t0.size() / 3;
     return point_edge_ccd(
         // Point at t=0
-        vertices_t0.row(vertex_id),
+        vertices_t0.head(dim),
         // Edge at t=0
-        vertices_t0.row(edges(edge_id, 0)), vertices_t0.row(edges(edge_id, 1)),
+        vertices_t0.segment(dim, dim), vertices_t0.tail(dim),
         // Point at t=1
-        vertices_t1.row(vertex_id),
+        vertices_t1.head(dim),
         // Edge at t=1
-        vertices_t1.row(edges(edge_id, 0)), vertices_t1.row(edges(edge_id, 1)),
+        vertices_t1.segment(dim, dim), vertices_t1.tail(dim), //
         toi, min_distance, tmax, tolerance, max_iterations,
         conservative_rescaling);
 }
diff --git a/src/ipc/candidates/edge_vertex.hpp b/src/ipc/candidates/edge_vertex.hpp
index f8c3f00de..22f08a046 100644
--- a/src/ipc/candidates/edge_vertex.hpp
+++ b/src/ipc/candidates/edge_vertex.hpp
@@ -10,8 +10,7 @@
 
 namespace ipc {
 
-class EdgeVertexCandidate : virtual public CollisionStencil,
-                            public ContinuousCollisionCandidate {
+class EdgeVertexCandidate : public ContinuousCollisionCandidate {
 public:
     EdgeVertexCandidate(long edge_id, long vertex_id);
 
@@ -26,30 +25,6 @@ class EdgeVertexCandidate : virtual public CollisionStencil,
 
     // ------------------------------------------------------------------------
 
-    /// Perform narrow-phase CCD on the candidate.
-    /// @param[in] vertices_t0 Mesh vertices at the start of the time step.
-    /// @param[in] vertices_t1 Mesh vertices at the end of the time step.
-    /// @param[in] edges Collision mesh edges as rows of indicies into vertices.
-    /// @param[in] faces Collision mesh triangular faces as rows of indicies into vertices.
-    /// @param[out] toi Computed time of impact (normalized).
-    /// @param[in] tmax Maximum time (normalized) to look for collisions. Should be in [0, 1].
-    /// @param[in] tolerance CCD tolerance used by Tight-Inclusion CCD.
-    /// @param[in] max_iterations Maximum iterations used by Tight-Inclusion CCD.
-    /// @param[in] conservative_rescaling Conservative rescaling value used to avoid taking steps exactly to impact.
-    /// @return If the candidate had a collision over the time interval.
-    bool
-    ccd(const Eigen::MatrixXd& vertices_t0,
-        const Eigen::MatrixXd& vertices_t1,
-        const Eigen::MatrixXi& edges,
-        const Eigen::MatrixXi& faces,
-        double& toi,
-        const double min_distance = 0.0,
-        const double tmax = 1.0,
-        const double tolerance = DEFAULT_CCD_TOLERANCE,
-        const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS,
-        const double conservative_rescaling =
-            DEFAULT_CCD_CONSERVATIVE_RESCALING) const override;
-
     void print_ccd_query(
         const Eigen::MatrixXd& vertices_t0,
         const Eigen::MatrixXd& vertices_t1,
@@ -75,6 +50,7 @@ class EdgeVertexCandidate : virtual public CollisionStencil,
     using CollisionStencil::compute_distance;
     using CollisionStencil::compute_distance_gradient;
     using CollisionStencil::compute_distance_hessian;
+    using ContinuousCollisionCandidate::ccd;
 
 protected:
     double compute_distance(const VectorMax12d& positions) const override;
@@ -85,6 +61,17 @@ class EdgeVertexCandidate : virtual public CollisionStencil,
     MatrixMax12d
     compute_distance_hessian(const VectorMax12d& positions) const override;
 
+    bool
+    ccd(const VectorMax12d& vertices_t0,
+        const VectorMax12d& vertices_t1,
+        double& toi,
+        const double min_distance = 0.0,
+        const double tmax = 1.0,
+        const double tolerance = DEFAULT_CCD_TOLERANCE,
+        const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS,
+        const double conservative_rescaling =
+            DEFAULT_CCD_CONSERVATIVE_RESCALING) const override;
+
     virtual PointEdgeDistanceType known_dtype() const
     {
         return PointEdgeDistanceType::AUTO;
diff --git a/src/ipc/candidates/face_vertex.cpp b/src/ipc/candidates/face_vertex.cpp
index 34e1cda04..4abff5a3b 100644
--- a/src/ipc/candidates/face_vertex.cpp
+++ b/src/ipc/candidates/face_vertex.cpp
@@ -41,10 +41,8 @@ MatrixMax12d FaceVertexCandidate::compute_distance_hessian(
 }
 
 bool FaceVertexCandidate::ccd(
-    const Eigen::MatrixXd& vertices_t0,
-    const Eigen::MatrixXd& vertices_t1,
-    const Eigen::MatrixXi& edges,
-    const Eigen::MatrixXi& faces,
+    const VectorMax12d& vertices_t0,
+    const VectorMax12d& vertices_t1,
     double& toi,
     const double min_distance,
     const double tmax,
@@ -52,17 +50,18 @@ bool FaceVertexCandidate::ccd(
     const long max_iterations,
     const double conservative_rescaling) const
 {
+    assert(vertices_t0.size() == 12 && vertices_t1.size() == 12);
     return point_triangle_ccd(
         // Point at t=0
-        vertices_t0.row(vertex_id),
+        vertices_t0.head<3>(),
         // Triangle at t=0
-        vertices_t0.row(faces(face_id, 0)), vertices_t0.row(faces(face_id, 1)),
-        vertices_t0.row(faces(face_id, 2)),
+        vertices_t0.segment<3>(3), vertices_t0.segment<3>(6),
+        vertices_t0.tail<3>(),
         // Point at t=1
-        vertices_t1.row(vertex_id),
+        vertices_t1.head<3>(),
         // Triangle at t=1
-        vertices_t1.row(faces(face_id, 0)), vertices_t1.row(faces(face_id, 1)),
-        vertices_t1.row(faces(face_id, 2)), //
+        vertices_t1.segment<3>(3), vertices_t1.segment<3>(6),
+        vertices_t1.tail<3>(), //
         toi, min_distance, tmax, tolerance, max_iterations,
         conservative_rescaling);
 }
diff --git a/src/ipc/candidates/face_vertex.hpp b/src/ipc/candidates/face_vertex.hpp
index 0d2597dee..5c92a29a1 100644
--- a/src/ipc/candidates/face_vertex.hpp
+++ b/src/ipc/candidates/face_vertex.hpp
@@ -10,8 +10,7 @@
 
 namespace ipc {
 
-class FaceVertexCandidate : virtual public CollisionStencil,
-                            public ContinuousCollisionCandidate {
+class FaceVertexCandidate : public ContinuousCollisionCandidate {
 public:
     FaceVertexCandidate(long face_id, long vertex_id);
 
@@ -27,19 +26,6 @@ class FaceVertexCandidate : virtual public CollisionStencil,
 
     // ------------------------------------------------------------------------
 
-    bool
-    ccd(const Eigen::MatrixXd& vertices_t0,
-        const Eigen::MatrixXd& vertices_t1,
-        const Eigen::MatrixXi& edges,
-        const Eigen::MatrixXi& faces,
-        double& toi,
-        const double min_distance = 0.0,
-        const double tmax = 1.0,
-        const double tolerance = DEFAULT_CCD_TOLERANCE,
-        const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS,
-        const double conservative_rescaling =
-            DEFAULT_CCD_CONSERVATIVE_RESCALING) const override;
-
     void print_ccd_query(
         const Eigen::MatrixXd& vertices_t0,
         const Eigen::MatrixXd& vertices_t1,
@@ -65,6 +51,7 @@ class FaceVertexCandidate : virtual public CollisionStencil,
     using CollisionStencil::compute_distance;
     using CollisionStencil::compute_distance_gradient;
     using CollisionStencil::compute_distance_hessian;
+    using ContinuousCollisionCandidate::ccd;
 
 protected:
     double compute_distance(const VectorMax12d& positions) const override;
@@ -75,6 +62,17 @@ class FaceVertexCandidate : virtual public CollisionStencil,
     MatrixMax12d
     compute_distance_hessian(const VectorMax12d& positions) const override;
 
+    bool
+    ccd(const VectorMax12d& vertices_t0,
+        const VectorMax12d& vertices_t1,
+        double& toi,
+        const double min_distance = 0.0,
+        const double tmax = 1.0,
+        const double tolerance = DEFAULT_CCD_TOLERANCE,
+        const long max_iterations = DEFAULT_CCD_MAX_ITERATIONS,
+        const double conservative_rescaling =
+            DEFAULT_CCD_CONSERVATIVE_RESCALING) const override;
+
     virtual PointTriangleDistanceType known_dtype() const
     {
         return PointTriangleDistanceType::AUTO;


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 939600bbb..71fac9fdd 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -17,6 +17,7 @@ add_executable(ipc_toolkit_tests
   ccd/benchmark_ccd.cpp
   ccd/collision_generator.cpp
   ccd/test_ccd.cpp
+  test_cfl.cpp
 
   # Test distances and dertivatives
   distance/test_distance_type.cpp
diff --git a/tests/data/two-cubes-intersecting.obj b/tests/data/two-cubes-intersecting.obj
new file mode 100644
index 000000000..71659a2ba
--- /dev/null
+++ b/tests/data/two-cubes-intersecting.obj
@@ -0,0 +1,743 @@
+# Created by meshio v5.3.4, 2023-07-23T10:13:55.680467
+v 0.0 0.15467947383035 0.15467947383035
+v -0.0206208850973656 -0.00381781127659854 -0.00244418501080484
+v 0.143308108716642 -0.00657505850867589 0.152440992597133
+v 0.143439883827856 0.15084333208134 -0.00585869087634158
+v 0.778221391243562 0.847618803473975 -0.0192798506121999
+v -0.0497461310505196 0.0034392284054536 -0.00598640750082274
+v 0.922848759446328 0.849549899895694 0.137087640850983
+v 0.779332744527734 1.00769308677809 0.13769094271757
+v 0.143253043286885 0.847067199355938 -0.00693074410027469
+v -0.0207572783840324 0.00253349954426728 -0.00385437447943628
+v 0.143817170470774 1.00528714046045 0.150311118730187
+v 0.0 0.84532052616965 0.15467947383035
+v 0.143489073619746 -0.00586028508714777 0.849430753396442
+v -0.0207533805533393 -0.00255922665767959 0.00391356672286398
+v 0.0 0.15467947383035 0.84532052616965
+v 0.142668195890447 0.15259569872271 1.00686055344273
+v 0.143614366447094 0.849238225588357 1.00574127113943
+v -0.0205586756315011 0.00389788827221101 0.0024312309765147
+v 0.0 0.84532052616965 0.84532052616965
+v 0.143424405132644 1.00665705216196 0.847509412581043
+v 0.78005990878925 0.143955464655101 -0.00993501365678144
+v -0.0494794216098879 -0.0053817376746601 -0.00243627607954393
+v 0.780509978137339 -0.0134056538955894 0.148727648122619
+v 0.924658374625098 0.144385472464882 0.148042943714966
+v 0.925821865915046 0.852793816908192 0.85036997664993
+v -0.049681008428237 0.00540548419204872 0.00266276764862462
+v 0.780727852722013 0.855405799286306 1.0085870652963
+v 0.78132234961306 1.01177600276505 0.84875837417071
+v 0.778589663841125 0.151997420835217 1.0188453975922
+v -0.0498124588565648 -0.00343151509593053 0.0064059453214452
+v 0.923489347501199 0.150492254554748 0.862475511788454
+v 0.780443126787852 -0.00779222711191864 0.861944256063881
+v -0.034426373539866 -3.47620929117939e-05 -6.41005775395754e-06
+v 0.463302555261065 0.497249374821749 -0.0112238443092495
+v 0.924990538741766 0.500308425624533 0.501761482127425
+v 0.464435887077223 1.01152696893403 0.496869014691551
+v 0.0 0.5 0.5
+v 0.464562331971133 -0.0114126990913052 0.502957289034411
+v 0.463417278637442 0.502727453057081 1.01150206172621
+v 0.0 0.70833333333333 0.29166666666667
+v 0.271490335781922 -0.0086678861054481 0.713395759641258
+v 0.654613870031688 0.283744445724632 -0.00995565588511506
+v 0.270860176755708 0.709747151809434 -0.00972281202556575
+v 0.271054323916343 -0.00941041572942707 0.289680871126122
+v 0.271214563273246 1.00955655720772 0.710250395699915
+v 0.654933578297918 0.716456886998932 1.0100276500449
+v 0.925692532394266 0.712384350662506 0.710870065920214
+v 0.27170939685449 0.713020486877646 1.00833420195897
+v 0.652952054816449 0.291086862056773 1.01544188157721
+v 0.271433248907797 0.287076349453635 -0.0085755114204295
+v 0.270195695936004 0.289913000960323 1.00970576257853
+v 0.925264960002149 0.285747170192688 0.288773578129722
+v 0.655550097254812 -0.00941913453246767 0.718913040525912
+v 0.924872724777003 0.712316348835984 0.280942013903524
+v 0.65486410959467 1.00978913448405 0.280753912890742
+v 0.0 1.0 0.25
+v 0.229316244432755 -0.0102288969293213 -0.00767738276319443
+v 0.229022250500578 1.00739777297892 -0.0109606560010155
+v 0.22870373174059 -0.00753883410170454 1.01079987612725
+v 0.922402522601539 0.241368807844931 -0.00851581598372768
+v 0.229409833892833 1.01039169157659 1.00779582472494
+v 0.918548182250225 0.251200019508661 1.0218536161502
+v 0.921541614402775 -0.00925185961088918 0.766917654565378
+v 0.693243139542691 1.01482499947896 1.00761792072389
+v 0.919576415463607 1.01183613314301 0.232305279503295
+v 0.0 0.0 0.25
+v 0.0 0.25 0.0
+v 0.0 0.75 0.0
+v 0.0 0.75 1.0
+v 0.69040318657779 1.00700003435451 -0.0191664039598756
+v 0.0 0.25 1.0
+v 0.923888146865409 1.00752188668329 0.749317638418288
+v 0.921291701394758 0.757961581981873 1.01039290828862
+v 0.0 0.0 0.75
+v 0.690893311704431 -0.00724855710617338 1.01836640420184
+v 0.918954887477893 0.74902542894085 -0.0224483196572322
+v 0.919506576313373 -0.0148475042624296 0.247116284605134
+v 0.691704206866536 -0.0145391609649996 -0.00908530405516754
+v 0.0 1.0 0.75
+v 0.271269881624821 1.00841612344967 0.286635014198831
+v 0.655752312629701 1.01221501285684 0.708884169724587
+v 0.0 0.29166666666667 0.70833333333333
+v 0.924979485508175 0.287967576987339 0.718792620285801
+v 0.0 0.29166666666667 0.29166666666667
+v 0.653195188937069 0.708480196962636 -0.0149581772179427
+v 0.655260480438444 -0.0123925711767978 0.289891345386612
+v 0.0 0.70833333333333 0.70833333333333
+v 0.462766233065267 1.00963785111037 0.210448171683798
+v 0.205775754222367 1.00791100124343 0.498770976106266
+v 0.720340821151606 0.503588993002459 1.01256614706867
+v 0.463056391102668 -0.00959112682473816 0.789353236526146
+v 0.46153264426918 0.216813682394294 1.01344909457763
+v 0.925897433972194 0.787268617648826 0.49439298053656
+v 0.462961124181116 0.211744889609974 -0.00952133846726139
+v 0.463278435421093 1.01251871127918 0.783045404592838
+v 0.722274915236781 -0.0113900190009552 0.504500873016267
+v 0.720511275840693 0.496208974335456 -0.0110392077784909
+v 0.204121992706 0.498577680220015 -0.00864760270719316
+v 0.205836091051138 -0.00790852711623427 0.501259945192102
+v 0.0 0.2202380952381 0.5
+v 0.462943437843719 -0.0124616705407894 0.216605228054477
+v 0.204839555613753 0.501616096399772 1.00826225553784
+v 0.926127779649238 0.213495350901844 0.503709472767385
+v 0.461903627300772 0.782859950076503 -0.0132110244080713
+v 0.0 0.5 0.2202380952381
+v 0.0 0.5 0.7797619047619
+v 0.0 0.7797619047619 0.5
+v 0.722111053598492 1.01223102297868 0.494494862507441
+v 0.925275064882146 0.498893292457204 0.21421706131254
+v 0.463263319606458 0.788669786814622 1.00963417527123
+v 0.925246482268937 0.500901952709948 0.787912564966553
+v 0.0 0.0 0.5
+v 0.461011763160346 -0.0132678386271163 -0.00864726469742404
+v 0.923690861557616 0.49760497225183 -0.0119812267985972
+v 0.923629336940129 1.01523064062333 0.494194975762246
+v 0.461753599152044 1.01356626716912 1.00814398071899
+v 0.0 1.0 0.5
+v 0.0 0.5 1.0
+v 0.460177417890609 -0.00823045965164033 1.01468323472919
+v 0.0 0.5 0.0
+v 0.923785480840332 0.502147277159417 1.01443294004035
+v 0.925192357467569 -0.0102995725490218 0.504094931335987
+v 0.460095474645511 1.00796425551457 -0.0149839859317959
+v 0.918383450415843 1.01493539617134 1.00622420624225
+v 0.0 1.0 1.0
+v 0.920622165268609 -0.00136224816839275 1.02795856786995
+v 0.918918258673754 1.0034965900962 -0.0246841466788183
+v 0.0 0.0 1.0
+v 0.916939864347425 -0.0132381038937096 -0.0185401970171117
+v 0.0 1.0 0.0
+v 0.0 0.0 0.0
+v 0.897316385908833 0.146857124136305 0.142805942477558
+v -0.529004943857591 -0.00339490057466124 -0.00616411281744019
+v 1.0315801661256698 -0.0107127629480741 0.130856820277053
+v 1.03105683711388 0.136241292477901 -0.0145810004898018
+v 1.82105263157895 0.15467947383035 0.15467947383035
+v -0.558190787923812 -0.00233447145603128 -0.00398622789350035
+v 1.6840852962728299 0.146042139811069 -0.00659879878405757
+v 1.6837882645963098 -0.00564747902582855 0.144290921886765
+v 1.68339440484866 0.144024812610622 1.00561245868544
+v -0.558225625760692 -0.00395217156716344 0.00251633357887401
+v 1.82105263157895 0.15467947383035 0.84532052616965
+v 1.6842262866951299 -0.00669424681420341 0.853865150063581
+v 1.6833563672941099 0.855702368667255 -0.00568172108874652
+v -0.558398210384429 0.00382232214204434 -0.00254646771355229
+v 1.82105263157895 0.84532052616965 0.15467947383035
+v 1.6839097681362099 1.00646126170402 0.14592839649607
+v 1.68402711624704 0.853910140671045 1.00666026975474
+v -0.558243969061645 0.00245438334900471 0.00392675099441872
+v 1.6840620234969998 1.00564755700143 0.855895573886306
+v 1.82105263157895 0.84532052616965 0.84532052616965
+v 1.04091580193558 -0.0173384209702504 0.84535754180399
+v -0.529364708716361 -0.0055428744891919 0.00241310544821618
+v 0.896179741281258 0.140351549001502 0.84571425375072
+v 1.02816962270124 0.126913961474031 1.00465008826693
+v 0.896148293236754 0.857855472193007 0.152819105628092
+v -0.529422301576095 0.0055634053893915 -0.00263031546982173
+v 1.03910253764177 0.859232210716414 -0.00632383475760729
+v 1.02973131241977 1.01597240534683 0.141664491391955
+v 1.04201727660339 0.852166834670715 1.01600596990383
+v -0.529207117525993 0.00336863488009298 0.00586593877960557
+v 0.898671037724355 0.854432175929993 0.858829399255194
+v 1.04283651853995 1.01154780782565 0.859236446356311
+v -0.544456305194095 3.45476207211632e-05 1.08360491901687e-05
+v 0.896073812384962 0.4996978643664 0.498976128365606
+v 1.3577362371042698 -0.0114612982555181 0.496971817451605
+v 1.3563657107169098 0.496958580633775 1.01129584031858
+v 1.3573270706371698 1.01150410241189 0.502934665885383
+v 1.3566635205017699 0.502726641712141 -0.0114750419810648
+v 1.82105263157895 0.5 0.5
+v 1.16689464851763 0.289374976912318 -0.0136491274226043
+v 1.55031881759894 1.00946355938402 0.289630949469467
+v 1.55019397936197 0.710109723361712 1.00971065955277
+v 1.549250729539 0.286457950271266 1.00851473737968
+v 1.55045427289777 0.28989156423375 -0.0097447244894987
+v 1.5496703561016698 -0.00838229427453478 0.286953985089862
+v 1.5489396164723799 0.713067543575188 -0.00854407425788305
+v 1.5502069749394198 1.00844036219876 0.713309370264208
+v 1.5506216438643499 -0.00966201948658741 0.710007287177809
+v 0.8953412466730181 0.284163090107485 0.708418532909235
+v 1.16675150061203 0.710225066447686 1.01356723725066
+v 1.1679065393036898 1.01119640144724 0.717943145979462
+v 1.16476504006318 0.717862357095659 -0.00873469399966154
+v 1.16741631459514 -0.0110767821939537 0.282935037079021
+v 1.1645028167893698 0.2813084111861 1.00790169533201
+v 1.1668041397787998 1.01358451323558 0.291980796716836
+v 1.5924228056388 -0.00786403745048897 -0.0105491733751473
+v 1.5924976459135098 1.00782624673061 1.01073867454524
+v 1.59157472697872 1.01034706362684 -0.00762640070189536
+v 1.5917539379397398 -0.0108291039214005 1.00733414865983
+v 1.13018322338316 -0.00977213483269596 -0.0160382601570317
+v 1.1311037146749 1.00992834261745 1.01662000867233
+v 0.9009994108680001 -0.0185362413326657 0.74544201939414
+v 0.899147063702044 0.763580995298564 -0.00950906829122397
+v 0.90253843865846 0.754327676770368 1.01768382885829
+v 0.9039338274255301 1.01478126006257 0.763259658921222
+v 1.82105263157895 0.25 0.0
+v 1.82105263157895 0.75 1.0
+v 1.82105263157895 0.0 0.25
+v 1.82105263157895 0.25 1.0
+v 1.82105263157895 1.0 0.25
+v 1.82105263157895 0.0 0.75
+v 1.82105263157895 1.0 0.75
+v 1.82105263157895 0.75 0.0
+v 1.82105263157895 0.70833333333333 0.29166666666667
+v 0.8963784069277331 0.714151773983525 0.716520347624303
+v 0.895877575782127 0.286675390561843 0.283670904255442
+v 1.1671250471767398 -0.0144422298677697 0.707673529278271
+v 0.895463146506501 0.715107033267223 0.290918152487949
+v 1.82105263157895 0.29166666666667 0.29166666666667
+v 1.82105263157895 0.70833333333333 0.70833333333333
+v 1.82105263157895 0.29166666666667 0.70833333333333
+v 0.895710970288164 0.787680709038706 0.504338801212743
+v 0.895257082665292 0.501245178792702 0.212673890565305
+v 0.895894714415795 0.212465331966275 0.495992021462393
+v 1.0989636430007599 0.503487083468969 -0.0113467112598067
+v 1.6151750611537998 0.498573203050349 1.00792965103689
+v 1.61584347211325 0.501633336338176 -0.00835886736378534
+v 1.3589657554905499 0.21639739109664 -0.0128711008070373
+v 1.35721556400829 0.210659132188595 1.00895384882164
+v 1.1006424458189898 -0.0124445320494594 0.49514519414483
+v 1.0985820616970199 0.495950680757374 1.01133839710915
+v 1.1001081804715298 1.01297385028382 0.505177175155088
+v 0.895306777386717 0.498551545916317 0.786117262151413
+v 1.3591044648467598 1.00991585786908 0.789081176993692
+v 1.82105263157895 0.2202380952381 0.5
+v 1.3587747316944598 -0.0131642510952202 0.782618085180973
+v 1.3584274068751399 1.01287615871895 0.216941483696452
+v 1.82105263157895 0.5 0.2202380952381
+v 1.3587249420777 -0.0100268642587493 0.210985139500558
+v 1.82105263157895 0.5 0.7797619047619
+v 1.82105263157895 0.7797619047619 0.5
+v 1.35882211890343 0.783260870566633 1.01286913549926
+v 1.6162676671616598 -0.00828303997763927 0.498363847312249
+v 1.3571770602279098 0.788905621212421 -0.00924100741257122
+v 1.6155515388134298 1.00783944767443 0.50117776763691
+v 0.9008900966319001 -0.0132487085474739 0.239296410382688
+v 0.90315380986188 0.245585910406302 -0.0201644922505634
+v 0.899745967319989 1.01373566578716 0.254364056931976
+v 1.12766112665827 1.0166524889322 -0.00653765067887031
+v 0.89793385618987 0.235347163126916 1.00521038170951
+v 1.12808698451528 -0.0175876283734913 1.00534439455491
+v 1.82105263157895 0.5 0.0
+v 0.8954765104319791 0.502081895474904 -0.00942343550890806
+v 1.82105263157895 0.0 0.5
+v 1.36134305889607 1.00868924267874 1.01422488558835
+v 0.896429123218072 1.01415634576833 0.506693730226038
+v 0.897475006333711 -0.0142245549114507 0.495124026246552
+v 1.3609516673640498 -0.00889088383511088 -0.0140412946899024
+v 1.35933516834125 1.01394036371169 -0.00793372971637753
+v 0.896131749164353 0.49728436587734 1.01017276501394
+v 1.35954938471675 -0.0144763742873952 1.00743312994483
+v 1.82105263157895 1.0 0.5
+v 1.82105263157895 0.5 1.0
+v 0.9010346043885201 1.01573729208042 1.01766619666575
+v 1.82105263157895 0.0 0.0
+v 1.82105263157895 1.0 1.0
+v 1.82105263157895 0.0 1.0
+v 1.82105263157895 1.0 0.0
+v 0.897973416765109 1.0234649690458 0.00138788186098191
+v 0.898158331105392 -0.0252849741430759 0.997131429156518
+v 0.90174427720724 -0.013474285725655 -0.0095788014677523
+f 40 56 12
+f 44 3 57
+f 9 58 43
+f 13 41 59
+f 42 60 21
+f 61 45 20
+f 29 62 49
+f 32 53 63
+f 27 64 46
+f 8 55 65
+f 3 44 66
+f 4 67 50
+f 68 9 43
+f 69 48 17
+f 70 55 8
+f 51 71 16
+f 25 47 72
+f 47 25 73
+f 13 74 41
+f 29 49 75
+f 7 54 76
+f 52 77 24
+f 78 42 21
+f 20 45 79
+f 19 87 69
+f 73 27 46
+f 28 81 64
+f 85 70 5
+f 81 28 72
+f 23 86 78
+f 48 61 17
+f 82 15 71
+f 82 74 15
+f 62 31 83
+f 31 63 83
+f 54 7 65
+f 76 85 5
+f 1 66 84
+f 4 50 57
+f 80 11 56
+f 59 51 16
+f 75 53 32
+f 24 60 52
+f 68 40 12
+f 84 67 1
+f 23 77 86
+f 11 80 58
+f 87 19 79
+f 103 52 35
+f 88 80 36
+f 82 106 37
+f 91 41 38
+f 100 82 37
+f 88 36 55
+f 55 36 108
+f 109 35 52
+f 80 89 36
+f 98 34 50
+f 102 39 48
+f 45 36 89
+f 110 48 39
+f 49 90 39
+f 46 39 90
+f 109 54 35
+f 86 38 101
+f 97 42 34
+f 98 43 34
+f 43 104 34
+f 99 44 38
+f 44 101 38
+f 34 94 50
+f 45 95 36
+f 53 38 96
+f 53 91 38
+f 92 49 39
+f 81 108 36
+f 46 110 39
+f 54 93 35
+f 86 96 38
+f 34 42 94
+f 84 37 105
+f 111 83 35
+f 51 39 102
+f 51 92 39
+f 99 38 41
+f 85 34 104
+f 105 37 40
+f 107 40 37
+f 37 84 100
+f 83 103 35
+f 93 47 35
+f 97 34 85
+f 95 81 36
+f 111 35 47
+f 106 87 37
+f 107 37 87
+f 81 72 108
+f 95 64 81
+f 86 77 96
+f 42 97 60
+f 41 74 99
+f 111 62 83
+f 53 96 63
+f 46 90 73
+f 55 108 65
+f 80 56 89
+f 84 105 67
+f 89 79 45
+f 44 57 101
+f 88 58 80
+f 110 61 48
+f 102 71 51
+f 106 69 87
+f 99 66 44
+f 92 51 59
+f 70 85 104
+f 78 86 101
+f 75 91 53
+f 105 40 68
+f 97 85 76
+f 40 107 56
+f 90 49 62
+f 67 98 50
+f 93 54 65
+f 59 41 91
+f 61 95 45
+f 78 94 42
+f 74 82 100
+f 87 79 107
+f 84 66 100
+f 110 46 64
+f 48 69 102
+f 43 98 68
+f 50 94 57
+f 58 104 43
+f 82 71 106
+f 75 49 92
+f 103 77 52
+f 47 73 111
+f 109 52 60
+f 103 83 63
+f 55 70 88
+f 47 93 72
+f 54 109 76
+f 100 112 74
+f 112 99 74
+f 113 94 78
+f 113 78 101
+f 76 114 97
+f 109 114 76
+f 115 65 108
+f 65 115 93
+f 61 116 95
+f 110 116 61
+f 117 107 79
+f 79 89 117
+f 102 118 71
+f 118 106 71
+f 75 119 91
+f 92 119 75
+f 114 109 60
+f 60 97 114
+f 67 120 98
+f 105 120 67
+f 120 68 98
+f 105 68 120
+f 113 57 94
+f 101 57 113
+f 117 89 56
+f 107 117 56
+f 66 112 100
+f 99 112 66
+f 59 91 119
+f 59 119 92
+f 115 72 93
+f 115 108 72
+f 90 62 121
+f 111 121 62
+f 116 110 64
+f 64 95 116
+f 111 73 121
+f 90 121 73
+f 122 63 96
+f 63 122 103
+f 122 77 103
+f 77 122 96
+f 69 118 102
+f 106 118 69
+f 123 104 58
+f 58 88 123
+f 123 88 70
+f 70 104 123
+f 58 130 11
+f 9 130 58
+f 77 129 24
+f 23 129 77
+f 16 128 59
+f 13 59 128
+f 124 73 25
+f 27 73 124
+f 64 124 28
+f 27 124 64
+f 8 127 70
+f 5 70 127
+f 127 76 5
+f 76 127 7
+f 20 125 61
+f 17 61 125
+f 11 130 56
+f 130 12 56
+f 15 128 71
+f 128 16 71
+f 63 126 32
+f 31 126 63
+f 126 31 62
+f 62 29 126
+f 124 25 72
+f 72 28 124
+f 127 8 65
+f 7 127 65
+f 60 129 21
+f 24 129 60
+f 125 69 17
+f 19 69 125
+f 4 131 67
+f 1 67 131
+f 66 1 131
+f 3 66 131
+f 57 131 4
+f 131 57 3
+f 130 68 12
+f 9 68 130
+f 125 20 79
+f 19 125 79
+f 74 128 15
+f 13 128 74
+f 75 126 29
+f 32 126 75
+f 129 23 78
+f 21 129 78
+f 187 175 138
+f 148 188 173
+f 189 172 147
+f 140 174 190
+f 171 191 135
+f 182 163 192
+f 154 180 193
+f 194 158 183
+f 195 181 160
+f 163 182 196
+f 175 197 138
+f 198 148 173
+f 176 139 199
+f 140 200 174
+f 147 172 201
+f 179 202 143
+f 150 203 178
+f 204 177 144
+f 134 184 237
+f 135 238 171
+f 239 186 159
+f 159 186 240
+f 185 241 155
+f 155 242 185
+f 150 178 188
+f 177 189 144
+f 190 179 143
+f 139 176 187
+f 134 191 184
+f 205 146 201
+f 198 211 151
+f 200 142 212
+f 199 136 210
+f 136 197 210
+f 142 202 212
+f 162 206 195
+f 180 154 241
+f 183 158 240
+f 194 209 156
+f 160 181 192
+f 242 152 208
+f 209 239 156
+f 207 238 132
+f 203 151 211
+f 146 205 204
+f 132 237 207
+f 152 193 208
+f 206 162 196
+f 210 170 226
+f 229 170 210
+f 212 170 231
+f 170 211 231
+f 170 212 226
+f 229 205 170
+f 211 170 232
+f 167 174 217
+f 214 207 165
+f 180 165 215
+f 169 218 175
+f 236 172 168
+f 206 165 224
+f 223 182 168
+f 168 182 225
+f 165 207 215
+f 165 180 224
+f 165 209 214
+f 233 167 173
+f 165 206 213
+f 222 167 181
+f 184 166 221
+f 219 171 169
+f 169 183 235
+f 209 165 213
+f 169 216 183
+f 219 169 175
+f 227 208 166
+f 225 178 168
+f 228 168 172
+f 230 166 184
+f 216 169 171
+f 167 185 220
+f 167 222 185
+f 169 235 177
+f 167 233 181
+f 168 228 186
+f 223 168 186
+f 176 166 230
+f 177 218 169
+f 179 227 166
+f 208 221 166
+f 236 168 178
+f 173 167 217
+f 234 179 166
+f 174 167 220
+f 205 232 170
+f 234 166 176
+f 242 220 185
+f 205 229 204
+f 217 198 173
+f 186 228 240
+f 180 241 224
+f 200 217 174
+f 178 203 236
+f 233 192 181
+f 209 213 239
+f 175 218 197
+f 210 226 199
+f 180 215 193
+f 196 213 206
+f 214 209 194
+f 172 236 201
+f 184 191 230
+f 179 234 202
+f 225 188 178
+f 207 214 238
+f 215 207 237
+f 190 227 179
+f 195 206 224
+f 228 172 189
+f 176 230 187
+f 242 208 227
+f 234 176 199
+f 192 225 182
+f 218 177 204
+f 220 190 174
+f 205 201 232
+f 232 203 211
+f 233 173 188
+f 196 182 223
+f 222 241 185
+f 183 240 235
+f 200 212 231
+f 219 191 171
+f 221 237 184
+f 238 216 171
+f 208 193 221
+f 175 187 219
+f 211 198 231
+f 189 177 235
+f 186 239 223
+f 195 222 181
+f 229 210 197
+f 226 212 202
+f 183 216 194
+f 243 204 229
+f 243 218 204
+f 243 229 197
+f 197 218 243
+f 244 214 194
+f 194 216 244
+f 245 202 234
+f 245 226 202
+f 246 233 188
+f 188 225 246
+f 238 244 216
+f 214 244 238
+f 213 196 247
+f 196 223 247
+f 215 237 248
+f 237 221 248
+f 192 246 225
+f 233 246 192
+f 248 193 215
+f 221 193 248
+f 187 230 249
+f 187 249 219
+f 240 250 235
+f 228 250 240
+f 224 241 251
+f 241 222 251
+f 252 227 190
+f 252 190 220
+f 252 220 242
+f 227 252 242
+f 250 189 235
+f 228 189 250
+f 191 219 249
+f 230 191 249
+f 253 203 232
+f 236 203 253
+f 247 239 213
+f 223 239 247
+f 224 251 195
+f 251 222 195
+f 217 200 254
+f 231 254 200
+f 236 253 201
+f 232 201 253
+f 231 198 254
+f 217 254 198
+f 245 234 199
+f 199 226 245
+f 163 255 192
+f 160 192 255
+f 193 261 154
+f 152 261 193
+f 156 260 194
+f 260 158 194
+f 255 195 160
+f 162 195 255
+f 255 163 196
+f 162 255 196
+f 199 256 136
+f 139 256 199
+f 258 142 200
+f 200 140 258
+f 257 198 151
+f 148 198 257
+f 257 151 203
+f 203 150 257
+f 259 147 201
+f 146 259 201
+f 204 259 146
+f 259 204 144
+f 197 256 138
+f 136 256 197
+f 202 258 143
+f 142 258 202
+f 188 257 150
+f 148 257 188
+f 190 258 140
+f 143 258 190
+f 256 139 187
+f 138 256 187
+f 147 259 189
+f 144 189 259
+f 155 261 242
+f 152 242 261
+f 135 262 238
+f 132 238 262
+f 260 239 159
+f 156 239 260
+f 237 132 262
+f 134 237 262
+f 240 260 159
+f 158 260 240
+f 154 261 241
+f 261 155 241
+f 262 191 134
+f 191 262 135
diff --git a/tests/test_cfl.cpp b/tests/test_cfl.cpp
new file mode 100644
index 000000000..7674c8aac
--- /dev/null
+++ b/tests/test_cfl.cpp
@@ -0,0 +1,45 @@
+#include <catch2/catch_all.hpp>
+
+#include "test_utils.hpp"
+
+#include <ipc/ipc.hpp>
+
+#include "test_utils.hpp"
+
+using namespace ipc;
+
+TEST_CASE("Test compute_cfl_stepsize()", "[ccd][cfl]")
+{
+    const double dhat = GENERATE(1e-6, 1e-3, 1e-1);
+
+    Eigen::MatrixXd V0, V1;
+    Eigen::MatrixXi E, F;
+    SECTION("Cube-Cube")
+    {
+        Eigen::MatrixXi E1, F1;
+        const bool success = load_mesh("two-cubes-close.obj", V0, E, F)
+            && load_mesh("two-cubes-intersecting.obj", V1, E1, F1);
+        REQUIRE(success);
+    }
+#ifdef NDEBUG
+    SECTION("Cloth-Ball")
+    {
+        const bool success = load_mesh("cloth_ball92.ply", V0, E, F)
+            && load_mesh("cloth_ball93.ply", V1, E, F);
+        REQUIRE(success);
+    }
+#endif
+
+    CollisionMesh mesh(V0, E, F);
+
+    Candidates candidates;
+    candidates.build(mesh, V0, dhat / 2);
+
+    const double cfl_step_size =
+        candidates.compute_cfl_stepsize(mesh, V0, V1, dhat);
+
+    const double ccd_step_size =
+        ipc::compute_collision_free_stepsize(mesh, V0, V1);
+
+    CHECK(cfl_step_size <= ccd_step_size);
+}