Skip to content

Commit 43e61e1

Browse files
committed
Enhance finite_jacobian functionality with tensor order support
- Follows the convention that even-order tensors (e.g., matrices) are stored in column-blocks, while odd-order tensors are stored in row-blocks. This is based on the tensor vectorization presented in "Dynamic Deformables" by Kim and Eberle [2022]. - Update CMake configuration to use C++17 - Add tests for size mismatch and tensor layouts
1 parent 5e84b8f commit 43e61e1

9 files changed

Lines changed: 148 additions & 20 deletions

CMakeLists.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ option(FINITE_DIFF_BUILD_UNIT_TESTS "Build unit-tests" ${FINITE_DIFF_TOPLEVEL_
2727

2828
# Set default minimum C++ standard
2929
if(FINITE_DIFF_TOPLEVEL_PROJECT)
30-
set(CMAKE_CXX_STANDARD 11)
30+
set(CMAKE_CXX_STANDARD 17)
3131
set(CMAKE_CXX_STANDARD_REQUIRED ON)
3232
set(CMAKE_CXX_EXTENSIONS OFF)
3333
endif()
@@ -76,8 +76,8 @@ target_link_libraries(finitediff_finitediff PUBLIC spdlog::spdlog)
7676
# Compiler options
7777
################################################################################
7878

79-
# Use C++11
80-
target_compile_features(finitediff_finitediff PUBLIC cxx_std_11)
79+
# Use C++17
80+
target_compile_features(finitediff_finitediff PUBLIC cxx_std_17)
8181

8282
################################################################################
8383
# Tests

src/finitediff.cpp

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ void finite_gradient(
9292
}
9393
}
9494

95+
template <bool IsTensorOrderEven>
9596
void finite_jacobian(
9697
const Eigen::Ref<const Eigen::VectorXd>& x,
9798
const std::function<Eigen::MatrixXd(const Eigen::VectorXd&)>& f,
@@ -113,18 +114,31 @@ void finite_jacobian(
113114
const Eigen::MatrixXd tmp = f(x);
114115
f_rows = tmp.rows(), f_cols = tmp.cols();
115116
}
116-
jac.setZero(f_rows, f_cols * x.size());
117+
if constexpr (IsTensorOrderEven) {
118+
jac.setZero(f_rows, f_cols * x.size());
119+
} else {
120+
jac.setZero(f_rows * x.size(), f_cols);
121+
}
117122

118123
// f: ℝ^n ↦ ℝ^{p×q} ⟹ ∇f: ℝ^n ↦ ℝ^{p×(qn)}
119124
Eigen::VectorXd x_mutable = x;
120125
for (size_t i = 0; i < x.size(); i++) {
121126
for (size_t ci = 0; ci < inner_steps; ci++) {
122127
x_mutable[i] += internal_coeffs[ci] * eps;
123-
jac.middleCols(f_cols * i, f_cols) +=
124-
external_coeffs[ci] * f(x_mutable);
128+
if constexpr (IsTensorOrderEven) {
129+
jac.middleCols(f_cols * i, f_cols) +=
130+
external_coeffs[ci] * f(x_mutable);
131+
} else {
132+
jac.middleRows(f_rows * i, f_rows) +=
133+
external_coeffs[ci] * f(x_mutable);
134+
}
125135
x_mutable[i] = x[i];
126136
}
127-
jac.middleCols(f_cols * i, f_cols) /= denom;
137+
if constexpr (IsTensorOrderEven) {
138+
jac.middleCols(f_cols * i, f_cols) /= denom;
139+
} else {
140+
jac.middleRows(f_rows * i, f_rows) /= denom;
141+
}
128142
}
129143
}
130144

@@ -172,7 +186,12 @@ bool compare_gradient(
172186
const double test_eps,
173187
const std::string& msg)
174188
{
175-
assert(x.rows() == y.rows());
189+
if (x.rows() != y.rows()) {
190+
spdlog::debug(
191+
"{} gradient size mismatch: x.rows()={} y.rows()={}", msg, x.rows(),
192+
y.rows());
193+
return false;
194+
}
176195

177196
bool same = true;
178197
for (long i = 0; i < x.rows(); i++) {
@@ -199,8 +218,13 @@ bool compare_jacobian(
199218
const double test_eps,
200219
const std::string& msg)
201220
{
202-
assert(x.rows() == y.rows());
203-
assert(x.cols() == y.cols());
221+
if (x.rows() != y.rows() || x.cols() != y.cols()) {
222+
spdlog::debug(
223+
"{} jacobian size mismatch: x.rows()={} x.cols()={} "
224+
"y.rows()={} y.cols()={}",
225+
msg, x.rows(), x.cols(), y.rows(), y.cols());
226+
return false;
227+
}
204228

205229
bool same = true;
206230
for (long i = 0; i < x.rows(); i++) {
@@ -256,4 +280,9 @@ Eigen::MatrixXd unflatten(const Eigen::Ref<const Eigen::VectorXd>& x, int dim)
256280
return X;
257281
}
258282

283+
// clang-format off
284+
template void finite_jacobian<false>(const Eigen::Ref<const Eigen::VectorXd>& x, const std::function<Eigen::MatrixXd(const Eigen::VectorXd&)>& f, Eigen::MatrixXd& jac, const AccuracyOrder accuracy, const double eps);
285+
template void finite_jacobian<true>(const Eigen::Ref<const Eigen::VectorXd>& x, const std::function<Eigen::MatrixXd(const Eigen::VectorXd&)>& f, Eigen::MatrixXd& jac, const AccuracyOrder accuracy, const double eps);
286+
// clang-format on
287+
259288
} // namespace fd

src/finitediff.hpp

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,48 @@ void finite_gradient(
4141
/**
4242
* @brief Compute the Jacobian of a function using finite differences.
4343
*
44+
* @param[in] x Point at which to compute the Jacobian.
45+
* @param[in] f Compute the Jacobian of this function.
46+
* @param[out] jac Computed Jacobian.
47+
* @param[in] accuracy Accuracy of the finite differences.
48+
* @param[in] eps Value of the finite difference step.
49+
* @tparam IsTensorOrderEven If true, the Jacobian is stored in column-blocks.
50+
* Otherwise, in row-blocks.
51+
*/
52+
template <bool IsTensorOrderEven = true>
53+
void finite_jacobian(
54+
const Eigen::Ref<const Eigen::VectorXd>& x,
55+
const std::function<Eigen::MatrixXd(const Eigen::VectorXd&)>& f,
56+
Eigen::MatrixXd& jac,
57+
const AccuracyOrder accuracy = SECOND,
58+
const double eps = 1.0e-8);
59+
60+
/**
61+
* @brief Compute the Jacobian of a function using finite differences with
62+
* explicit tensor order.
63+
*
64+
* Follows the convention that even-order tensors (e.g., matrices) are stored in
65+
* column-blocks, while odd-order tensors are stored in row-blocks. This is
66+
* based on the tensor vectorization presented in "Dynamic Deformables" by Kim
67+
* and Eberle [2022].
68+
*
4469
* @param[in] x Point at which to compute the Jacobian.
4570
* @param[in] f Compute the Jacobian of this function.
4671
* @param[out] jac Computed Jacobian.
4772
* @param[in] accuracy Accuracy of the finite differences.
4873
* @param[in] eps Value of the finite difference step.
74+
* @tparam TensorOrder Order of the output tensor of the function f.
4975
*/
50-
void finite_jacobian(
76+
template <int TensorOrder>
77+
inline void finite_jacobian_tensor(
5178
const Eigen::Ref<const Eigen::VectorXd>& x,
5279
const std::function<Eigen::MatrixXd(const Eigen::VectorXd&)>& f,
5380
Eigen::MatrixXd& jac,
5481
const AccuracyOrder accuracy = SECOND,
55-
const double eps = 1.0e-8);
82+
const double eps = 1.0e-8)
83+
{
84+
return finite_jacobian<TensorOrder % 2 == 0>(x, f, jac, accuracy, eps);
85+
}
5686

5787
/**
5888
* @brief Compute the Hessian of a function using finite differences.

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
add_executable(finitediff_tests
66
test_gradient.cpp
77
test_jacobian.cpp
8+
test_compare_size_mismatch.cpp
9+
test_jacobian_tensor.cpp
810
test_hessian.cpp
911
test_flatten.cpp
1012
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
3+
#include <Eigen/Core>
4+
5+
#include <finitediff.hpp>
6+
7+
using namespace fd;
8+
9+
TEST_CASE("compare_jacobian returns false for different sizes", "[compare]")
10+
{
11+
Eigen::MatrixXd A = Eigen::MatrixXd::Random(2, 3);
12+
Eigen::MatrixXd B = Eigen::MatrixXd::Random(3, 2);
13+
14+
CHECK_FALSE(compare_jacobian(A, B));
15+
}

tests/test_gradient.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
using namespace fd;
1111

12-
TEST_CASE("Test finite difference gradient of quadratic", "[gradient]")
12+
TEST_CASE("Finite difference gradient of quadratic", "[gradient]")
1313
{
1414
int n = GENERATE(1, 2, 4, 10, 100);
1515

@@ -33,7 +33,7 @@ TEST_CASE("Test finite difference gradient of quadratic", "[gradient]")
3333
CHECK(compare_gradient(grad, fgrad));
3434
}
3535

36-
TEST_CASE("Test finite difference gradient of Rosenbrock", "[gradient]")
36+
TEST_CASE("Finite difference gradient of Rosenbrock", "[gradient]")
3737
{
3838
const auto f = [](const Eigen::VectorXd& x) {
3939
double t1 = 1 - x[0];
@@ -59,7 +59,7 @@ TEST_CASE("Test finite difference gradient of Rosenbrock", "[gradient]")
5959
CHECK(compare_gradient(grad, fgrad));
6060
}
6161

62-
TEST_CASE("Test finite difference gradient of trig", "[gradient]")
62+
TEST_CASE("Finite difference gradient of trig", "[gradient]")
6363
{
6464
int n = GENERATE(1, 2, 4, 10, 100);
6565

tests/test_hessian.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
using namespace fd;
1212

13-
TEST_CASE("Test finite difference hessian of quadratic", "[hessian]")
13+
TEST_CASE("Finite difference hessian of quadratic", "[hessian]")
1414
{
1515
AccuracyOrder accuracy = GENERATE(SECOND, FOURTH, SIXTH, EIGHTH);
1616

@@ -35,7 +35,7 @@ TEST_CASE("Test finite difference hessian of quadratic", "[hessian]")
3535
CHECK(compare_hessian(hess, fhess));
3636
}
3737

38-
TEST_CASE("Test finite difference hessian of Rosenbrock", "[hessian]")
38+
TEST_CASE("Finite difference hessian of Rosenbrock", "[hessian]")
3939
{
4040
AccuracyOrder accuracy = GENERATE(SECOND, FOURTH, SIXTH, EIGHTH);
4141
const auto f = [](const Eigen::VectorXd& x) {
@@ -58,7 +58,7 @@ TEST_CASE("Test finite difference hessian of Rosenbrock", "[hessian]")
5858
CHECK(compare_hessian(hess, fhess));
5959
}
6060

61-
TEST_CASE("Test finite difference hessian of trig", "[hessian]")
61+
TEST_CASE("Finite difference hessian of trig", "[hessian]")
6262
{
6363
AccuracyOrder accuracy = GENERATE(SECOND, FOURTH, SIXTH, EIGHTH);
6464
int n = GENERATE(1, 2, 4, 10, 25);

tests/test_jacobian.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
using namespace fd;
1111

12-
TEST_CASE("Test finite difference jacobian of linear", "[jacobian]")
12+
TEST_CASE("Finite difference jacobian of linear", "[jacobian]")
1313
{
1414
int n = GENERATE(1, 2, 4, 10, 100);
1515

@@ -32,7 +32,7 @@ TEST_CASE("Test finite difference jacobian of linear", "[jacobian]")
3232
CHECK(compare_jacobian(jac, fjac));
3333
}
3434

35-
TEST_CASE("Test finite difference jacobian of trig", "[jacobian]")
35+
TEST_CASE("Finite difference jacobian of trig", "[jacobian]")
3636
{
3737
int n = GENERATE(1, 2, 4, 10, 100);
3838

tests/test_jacobian_tensor.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include <catch2/generators/catch_generators_all.hpp>
3+
4+
#include <Eigen/Core>
5+
6+
#include <finitediff.hpp>
7+
8+
using namespace fd;
9+
10+
TEST_CASE("finite_jacobian_tensor layouts", "[jacobian_tensor]")
11+
{
12+
int n = GENERATE(1, 2, 4, 10);
13+
14+
const int p = 2;
15+
const int q = 3;
16+
17+
// Construct a tensor T_k (p x q) for each input component k such that
18+
// f(x) = sum_k x_k * T_k. The analytic derivative w.r.t x_k is T_k.
19+
std::vector<Eigen::MatrixXd> T;
20+
T.reserve(n);
21+
for (int k = 0; k < n; ++k) {
22+
T.emplace_back(Eigen::MatrixXd::Random(p, q));
23+
}
24+
25+
const auto f = [&](const Eigen::VectorXd& x) -> Eigen::MatrixXd {
26+
Eigen::MatrixXd R = Eigen::MatrixXd::Zero(p, q);
27+
for (int k = 0; k < n; ++k) {
28+
R += x[k] * T[k];
29+
}
30+
return R;
31+
};
32+
33+
Eigen::VectorXd x = Eigen::VectorXd::Random(n);
34+
35+
// Analytic Jacobians for the two storage conventions
36+
Eigen::MatrixXd jac_even = Eigen::MatrixXd::Zero(p, q * n);
37+
Eigen::MatrixXd jac_odd = Eigen::MatrixXd::Zero(p * n, q);
38+
for (int k = 0; k < n; ++k) {
39+
jac_even.block(0, q * k, p, q) = T[k];
40+
jac_odd.block(p * k, 0, p, q) = T[k];
41+
}
42+
43+
AccuracyOrder accuracy = GENERATE(SECOND, FOURTH, SIXTH, EIGHTH);
44+
45+
Eigen::MatrixXd fjac_even;
46+
finite_jacobian_tensor<2>(x, f, fjac_even, accuracy);
47+
CHECK(compare_jacobian(jac_even, fjac_even));
48+
49+
Eigen::MatrixXd fjac_odd;
50+
finite_jacobian_tensor<1>(x, f, fjac_odd, accuracy);
51+
CHECK(compare_jacobian(jac_odd, fjac_odd));
52+
}

0 commit comments

Comments
 (0)