Goal: Demonstrate the use of std::complex with CoDiPack.
Prerequisite: Tutorial 2 - Reverse mode AD, Example 2 - Custom adjoint vector evaluation
Function:
template<typename Type>
Type func(Type const& v) {
Type t = v + v.real();
return 2 * v * t;
}
Full code:
#include <codi.hpp>
#include <iostream>
template<typename Real>
void func(const Real* x, size_t l, Real* y) {
y[0] = 0.0;
y[1] = 1.0;
for(size_t i = 0; i < l; ++i) {
y[0] += x[i];
y[1] *= x[i];
}
}
int main(int nargs, char** args) {
Real x[5];
Real y[2];
x[0] = 1.0;
x[1] = 2.0;
x[2] = 3.0;
x[3] = 4.0;
x[4] = 5.0;
tape.setActive();
for(size_t i = 0; i < 5; ++i) {
tape.registerInput(x[i]);
}
func(x, 5, y);
tape.registerOutput(y[0]);
tape.registerOutput(y[1]);
tape.setPassive();
Identifier xIds[5];
Identifier yIds[2];
for(int i = 0; i < 5; i += 1) {
}
for(int i = 0; i < 2; i += 1) {
}
auto iterX = [&xIds](auto&& func) {
for(size_t i = 0; i < 5; ++i) {
func(xIds[i]);
}
};
auto iterY = [&yIds](auto&& func) {
for(size_t i = 0; i < 2; ++i) {
func(yIds[i]);
}
};
for(size_t curY = 0; curY < 2; curY += 1) {
tape.gradient(yIds[curY]) = 1.0;
tape.evaluate();
for(size_t curX = 0; curX < 5; curX += 1) {
jacobian(curY,curX) = tape.gradient(xIds[curX]);
tape.gradient(xIds[curX]) = 0.0;
}
}
std::cout << "Reverse Jacobian:" << std::endl;
std::cout << "f(1 .. 5) = (" << y[0] << ", " << y[1] << ")" << std::endl;
std::cout << "df/dx (1 .. 5) = \n" << jacobian << std::endl;
tape.reset();
return 0;
}
Additional information: The cache optimizer performs a lifetime analysis of the identifiers on the tape. The identifiers are redistributed for more cache performance during the reverse evaluation.
Since identifiers are redistributed, identifiers from values like x should not be used. Instead, the identifier of x should be stored and then the stored value should be given to the optimizer.
The cache optimization is only meaningfully if the tape is evaluated at least 10 times, e.g., as in a reverse accumulation process.