Last active
September 7, 2023 09:53
-
-
Save geoffreygarrett/019d65059f0a2966f0db3158dfd2d03d to your computer and use it in GitHub Desktop.
example_autodiff_stts, accompanied by this readout: https://gist.github.com/geoffreygarrett/5316dbfc5f6849cd3dfaae79272d4404
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// include for size_t | |
#include <Eigen/Core> | |
#include <tuple> | |
//#include <unsupported/Eigen/CXX11/Tensor> | |
//#ifdef ODIN_AUTODIFF | |
//#define ODIN_CONST | |
#include <autodiff/forward/dual.hpp> | |
#include <autodiff/forward/dual/eigen.hpp> | |
#include <autodiff/forward/real.hpp> | |
#include <autodiff/forward/real/eigen.hpp> | |
using namespace autodiff; | |
//#else | |
////#define ODIN_CONST const | |
//#endif | |
#include <Eigen/LU> | |
#include <Eigen/QR> | |
#include <iomanip> | |
#include <unsupported/Eigen/CXX11/Tensor> | |
//// FOR PRINTING TENSORS | |
template<typename Scalar, int Dimensions> | |
std::ostream &operator<<(std::ostream &os, const Eigen::Tensor<Scalar, Dimensions> &tensor) { | |
if (Dimensions < 1) return os;// Invalid for tensors with less than one dimension | |
Eigen::array<Eigen::Index, Dimensions> dims; | |
for (int i = 0; i < Dimensions; ++i) { dims[i] = tensor.dimension(i); } | |
std::vector<Eigen::Index> index(Dimensions, 0);// Initialize index vector to zeros | |
if (Dimensions == 1) { | |
// Specific case for 1D tensor (vector) | |
for (index[0] = 0; index[0] < dims[0]; ++index[0]) { | |
Eigen::array<Eigen::Index, Dimensions> index_array = {index[0]}; | |
os << std::setw(10) << tensor.coeff(index_array); | |
} | |
os << "\n"; | |
return os; | |
} | |
auto print_recursive | |
= [&](auto &self, int dim, std::vector<Eigen::Index> index_so_far) -> void { | |
if (dim == Dimensions - 1) { | |
for (index[dim] = 0; index[dim] < dims[dim]; ++index[dim]) { | |
Eigen::array<Eigen::Index, Dimensions> index_array; | |
std::copy(index.begin(), index.end(), index_array.begin()); | |
os << std::setw(10) << tensor.coeff(index_array); | |
} | |
os << "\n"; | |
} else { | |
for (index[dim] = 0; index[dim] < dims[dim]; ++index[dim]) { | |
index_so_far.push_back(index[dim]); | |
if (Dimensions > 2 | |
&& dim == Dimensions - 3) {// Only print "Slice" for 3D and higher | |
std::string einstein_notation = "Slice ("; | |
for (size_t i = 0; i < index_so_far.size(); ++i) { | |
einstein_notation += std::to_string(index_so_far[i]); | |
if (i < index_so_far.size() - 1) { einstein_notation += ", "; } | |
} | |
einstein_notation += ", :, ..., :)"; | |
os << einstein_notation << ":\n"; | |
} | |
self(self, dim + 1, index_so_far); | |
index_so_far.pop_back();// Remove the last element to revert the state | |
} | |
} | |
}; | |
print_recursive(print_recursive, 0, {}); | |
return os; | |
} | |
// Function to fill 1st-order tensor | |
template<typename Fun, typename... Vars, typename... Args, typename U, typename T2> | |
void sst1(const Fun &eom, const Wrt<Vars...> &wrt, const At<Args...> &at, U &u, T2 &t2) { | |
long n = wrt_total_length(wrt); | |
size_t m = 0; | |
ForEachWrtVar(wrt, [&](auto &&j, auto &&xj) constexpr { | |
u = eval(eom, at, detail::wrt(xj));// Seed xi, xj, xk and evaluate u | |
if (m == 0) { | |
m = u.size(); | |
t2.resize(m, n); | |
}; | |
auto du = derivative<1>(u); | |
for (size_t i = 0; i < m; ++i) t2(i, j) = du[i]; | |
}); | |
} | |
// Function to fill 2nd-order state transition tensor | |
template<typename Fun, typename... Vars, typename... Args, typename U, typename T3, typename T2> | |
void sst2(const Fun &eom, const Wrt<Vars...> &wrt, const At<Args...> &at, U &u, T3 &t3, T2 &t2) { | |
using namespace autodiff::detail; | |
long n = wrt_total_length(wrt); | |
size_t m = 0; | |
ForEachWrtVar(wrt, [&](auto &&j, auto &&xj) constexpr { | |
ForEachWrtVar(wrt, [&](auto &&k, auto &&xk) constexpr { | |
static_assert(!isConst<decltype(xj)> && !isConst<decltype(xk)>, | |
"Expecting non-const autodiff numbers in wrt list because these need to be " | |
"seeded, and thus altered!"); | |
if (j < j) return;// this take advantage of the fact the Hessian matrix is symmetric | |
// evaluate u with xi and xj seeded to produce u0, du/dxi, d2u/dxidxj | |
u = eval(eom, at, detail::wrt(xj, xk)); | |
if (m == 0) { | |
m = u.size(); | |
t3.resize(m, n, n); | |
t2.resize(m, n); | |
}; | |
if (j == k) { | |
auto du = derivative<1>(u); | |
for (size_t i = 0; i < m; ++i) t2(i, j) = du[i]; | |
} | |
auto d2u = derivative<2>(u); | |
for (size_t i = 0; i < m; ++i) t3(i, j, k) = t3(i, k, j) = d2u[i]; | |
}); | |
}); | |
} | |
// Function to fill 3rd-order state transition tensor | |
template<typename Fun, typename... Vars, typename... Args, typename U, typename T4> | |
void sst3(const Fun &eom, const Wrt<Vars...> &wrt, const At<Args...> &at, U &u, T4 &t4) { | |
using namespace autodiff::detail; | |
long n = wrt_total_length(wrt); | |
size_t m = 0; | |
ForEachWrtVar(wrt, [&](auto &&j, auto &&xj) constexpr { | |
ForEachWrtVar(wrt, [&](auto &&k, auto &&xk) constexpr { | |
ForEachWrtVar(wrt, [&](auto &&l, auto &&xl) constexpr { | |
// TODO: Improve efficiency with symmetry | |
static_assert( | |
!isConst<decltype(xj)> && !isConst<decltype(xk)> && !isConst<decltype(xl)>, | |
"Expecting non-const autodiff numbers in wrt list because these need to be " | |
"seeded, and thus altered!"); | |
if (j < k || k < l) | |
return;// this take advantage of the fact the Hessian matrix is symmetric | |
u = eval(eom, at, detail::wrt(xj, xk, xl)); | |
if (m == 0) { | |
m = u.size(); | |
t4.resize(m, n, n, n); | |
}; | |
auto d3u = derivative<3>(u); | |
// Fill symmetric entries | |
for (int i = 0; i < m; ++i) | |
t4(i, j, k, l) = t4(i, k, j, l) = t4(i, j, l, k) = t4(i, l, j, k) | |
= t4(i, k, l, j) = t4(i, l, k, j) = d3u[i]; | |
}); | |
}); | |
}); | |
} | |
// Function to fill 4th-order state transition tensor | |
template<typename Fun, typename... Vars, typename... Args, typename U, typename T5> | |
void sst4(const Fun &eom, const Wrt<Vars...> &wrt, const At<Args...> &at, U &u, T5 &t5) { | |
long dim_n = wrt_total_length(wrt); | |
size_t dim_m = 0; | |
ForEachWrtVar(wrt, [&](auto &&j, auto &&xj) constexpr { | |
ForEachWrtVar(wrt, [&](auto &&k, auto &&xk) constexpr { | |
ForEachWrtVar(wrt, [&](auto &&l, auto &&xl) constexpr { | |
ForEachWrtVar(wrt, [&](auto &&m, auto &&xm) constexpr { | |
// TODO: Improve efficiency with symmetry | |
if (j < k || k < l || l < m) | |
return;// this take advantage of the fact the Hessian matrix is symmetric | |
u = eval(eom, at, detail::wrt(xj, xk, xl, xm)); | |
if (dim_m == 0) { | |
dim_m = u.size(); | |
t5.resize(dim_m, dim_n, dim_n, dim_n, dim_n); | |
}; | |
auto d4u = derivative<4>(u); | |
for (int i = 0; i < dim_m; ++i) | |
t5(i, j, k, l, m) = t5(i, j, k, m, l) = t5(i, j, l, k, m) | |
= t5(i, j, l, m, k) = t5(i, j, m, k, l) = t5(i, j, m, l, k) | |
= t5(i, k, j, l, m) = t5(i, k, j, m, l) = t5(i, k, l, j, m) | |
= t5(i, k, l, m, j) = t5(i, k, m, j, l) = t5(i, k, m, l, j) | |
= t5(i, l, j, k, m) = t5(i, l, j, m, k) = t5(i, l, k, j, m) | |
= t5(i, l, k, m, j) = t5(i, l, m, j, k) = t5(i, l, m, k, j) | |
= t5(i, m, j, k, l) = t5(i, m, j, l, k) = t5(i, m, k, j, l) | |
= t5(i, m, k, l, j) = t5(i, m, l, j, k) = t5(i, m, l, k, j) | |
= d4u[i]; | |
}); | |
}); | |
}); | |
}); | |
} | |
// Function to retrieve the Nth-order state transition tensor series | |
template<int N, typename Fun, typename... Vars, typename... Args, typename... Tensors, typename U> | |
auto stts(const Fun &eom, | |
const Wrt<Vars...> &wrt, | |
const At<Args...> &at, | |
U &u, | |
std::tuple<Tensors &...> tensors_tuple) { | |
using namespace autodiff::detail; | |
static_assert(sizeof...(Vars) >= 1, "Expecting at least one wrt variable"); | |
static_assert(sizeof...(Args) >= 1, "Expecting at least one at variable"); | |
static_assert(sizeof...(Tensors) >= 1, "Expecting at least one tensor"); | |
static_assert(N >= 1 && N <= 4, "Expecting N to be between 1 and 3"); | |
size_t n = wrt_total_length(wrt); | |
u.resize(n); | |
// For STM (1st order) | |
// sst1(eom, wrt, at, u, std::get<0>(tensors_tuple)); | |
// For 2nd order | |
if constexpr (N >= 2) { | |
sst2(eom, wrt, at, u, std::get<1>(tensors_tuple), std::get<0>(tensors_tuple)); | |
} | |
// For 3rd order | |
if constexpr (N >= 3) { sst3(eom, wrt, at, u, std::get<2>(tensors_tuple)); } | |
// For 4th order | |
if constexpr (N >= 4) { sst4(eom, wrt, at, u, std::get<3>(tensors_tuple)); } | |
} | |
template<typename Scalar> | |
struct ScalarDivision { | |
Scalar divisor; | |
explicit ScalarDivision(const Scalar &divisor) | |
: divisor(divisor) {} | |
const Scalar operator()(const Scalar &x) const { | |
return x / divisor; | |
} | |
}; | |
int main() { | |
// Define types | |
using scalar_type = HigherOrderDual<4, double>;// dual1st | |
using vector_type = Eigen::Matrix<scalar_type, 3, 1>; | |
using state_type = Eigen::Matrix<scalar_type, 6, 1>; | |
using tensor1_type = Eigen::Tensor<scalar_type, 1>; | |
using tensor2_type = Eigen::Tensor<scalar_type, 2>; | |
using tensor3_type = Eigen::Tensor<scalar_type, 3>; | |
using tensor4_type = Eigen::Tensor<scalar_type, 4>; | |
using tensor5_type = Eigen::Tensor<scalar_type, 5>; | |
// Initial conditions & parameters | |
scalar_type gravity_parameter = 1.0; | |
vector_type central_position(0.0, 0.0, 0.0); | |
// scalar_type dt = 1.0; | |
state_type state; | |
state << 1.0, 0.0, 0.0, 0.0, 1.0, 0.0; | |
Eigen::Vector<scalar_type, 4> params; | |
params << central_position(0), central_position(1), central_position(2), gravity_parameter; | |
// Counter for eom evaluations | |
int fevals = 0; | |
// Arbitrary point mass acceleration | |
auto point_mass_acceleration = [](auto &state, auto ¶ms) -> vector_type { | |
auto r = state.template segment<3>(0) - params.template segment<3>(0); | |
return -params(3) * r / r.norm() / r.norm() / r.norm(); | |
}; | |
// Arbitrary drag acceleration | |
scalar_type drag_coefficient = 1.0; | |
scalar_type air_density = 1.0; | |
scalar_type sc_area = 1.0; | |
scalar_type sc_mass = 1.0; | |
auto drag_acceleration = [&drag_coefficient, &sc_mass, &sc_area, &air_density]( | |
auto &state, auto ¶ms) -> vector_type { | |
auto v = state.template segment<3>(3); | |
auto partial_dynamic_pressure = 0.5 * air_density * v.norm(); | |
return -drag_coefficient * partial_dynamic_pressure * sc_area / sc_mass * v; | |
}; | |
// EOM (point mass + drag) | |
auto eom = [&](auto &state, auto ¶ms) -> state_type { | |
state_type result; | |
result.template segment<3>(0) = state.template segment<3>(3); | |
result.template segment<3>(3) | |
= (point_mass_acceleration(state, params) + drag_acceleration(state, params)); | |
fevals++; | |
return result; | |
}; | |
// State transition tensors | |
tensor2_type stt1; | |
tensor3_type stt2; | |
tensor4_type stt3; | |
tensor5_type stt4; | |
// State derivative | |
state_type state_derivative; | |
// Perform differentiation for 1st, 2nd, 3rd, and 4th order | |
stts<4>(eom, wrt(state), at(state, params), state_derivative, std::tie(stt1, stt2, stt3, stt4)); | |
// State derivative | |
std::cout << "State derivative: \n" << state_derivative << std::endl; | |
// Perturb state | |
tensor1_type perturbation; | |
perturbation.resize(6); | |
perturbation.setZero(); | |
perturbation(0) = 0.1; | |
perturbation(1) = 0.1; | |
perturbation(2) = 0.1; | |
perturbation(3) = 0.1; | |
perturbation(4) = 0.1; | |
perturbation(5) = 0.1; | |
// Define contraction dimensions for each tensor | |
std::cout << "\nSS1: dx/dx0 " << stt1.dimensions() << "\n" << stt1 << std::endl; | |
std::cout << "\nSS2: d2x/dx0dx0 " << stt2.dimensions() << "\n" << stt2 << std::endl; | |
std::cout << "\nSS3: d3x/dx0dx0dx0 " << stt3.dimensions() << "\n" << stt3 << std::endl; | |
// Perform the contractions for terms 2, 3, and 4 (Contraction is general multiplication between tensors) | |
// I'm not convinced the indexing is 100% correct yet. | |
constexpr auto contraction_1_0 | |
= Eigen::array<Eigen::IndexPair<int>, 1>{Eigen::IndexPair<int>(1, 0)}; | |
constexpr auto contraction_2_0 | |
= Eigen::array<Eigen::IndexPair<int>, 1>{Eigen::IndexPair<int>(2, 0)}; | |
constexpr auto contraction_3_0 | |
= Eigen::array<Eigen::IndexPair<int>, 1>{Eigen::IndexPair<int>(3, 0)}; | |
constexpr auto contraction_4_0 | |
= Eigen::array<Eigen::IndexPair<int>, 1>{Eigen::IndexPair<int>(4, 0)}; | |
// [6, 6] -> [m, n] -> [i, j] | |
tensor1_type term1 = stt1.contract(perturbation, contraction_1_0); | |
// [6, 6, 6] -> [m, n, n] -> [i, j, k] | |
tensor1_type term2 = stt2.contract(perturbation, contraction_2_0) | |
.contract(perturbation, contraction_1_0) | |
.unaryExpr(ScalarDivision<scalar_type>(2.0)); | |
// [6, 6, 6, 6] -> [m, n, n, n] -> [i, j, j, k] | |
tensor1_type term3 = stt3.contract(perturbation, contraction_3_0) | |
.contract(perturbation, contraction_2_0) | |
.contract(perturbation, contraction_1_0) | |
.unaryExpr(ScalarDivision<scalar_type>(6.0)); | |
// [6, 6, 6, 6, 6] -> [m, n, n, n, n] -> [i, j, k, l, m] | |
tensor1_type term4 = stt4.contract(perturbation, contraction_4_0) | |
.contract(perturbation, contraction_3_0) | |
.contract(perturbation, contraction_2_0) | |
.contract(perturbation, contraction_1_0) | |
.unaryExpr(ScalarDivision<scalar_type>(24.0)); | |
// Print the terms | |
std::cout << "term1: \n" << term1 << std::endl; | |
std::cout << "term2: \n" << term2 << std::endl; | |
std::cout << "term3: \n" << term3 << std::endl; | |
std::cout << "term4: \n" << term4 << std::endl; | |
// Dx = dx/dx0 * Dx0 + d2x/dx0dx0 * Dx0 * Dx0 / 2! + d3x/dx0dx0dx0 * Dx0 * Dx0 * Dx0 / 3! + d4x/dx0dx0dx0dx0 * Dx0 * Dx0 * Dx0 * Dx0 / 4! | |
tensor1_type delta = term1 + term2 + term3 + term4; | |
std::cout << "delta (sum of terms): \n" << delta << std::endl; | |
// TODO: can be reduced with symmetry checks in the ss1,ss2 derivative functions | |
std::cout << "\nfevals: " << fevals << std::endl; | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment